qoro-divi 0.2.0b1__py3-none-any.whl → 0.5.0__py3-none-any.whl
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.
- divi/__init__.py +1 -2
- divi/backends/__init__.py +9 -0
- divi/backends/_circuit_runner.py +70 -0
- divi/backends/_execution_result.py +70 -0
- divi/backends/_parallel_simulator.py +486 -0
- divi/backends/_qoro_service.py +663 -0
- divi/backends/_qpu_system.py +101 -0
- divi/backends/_results_processing.py +133 -0
- divi/circuits/__init__.py +8 -0
- divi/{exp/cirq → circuits/_cirq}/__init__.py +1 -2
- divi/circuits/_cirq/_parser.py +110 -0
- divi/circuits/_cirq/_qasm_export.py +78 -0
- divi/circuits/_core.py +369 -0
- divi/{qasm.py → circuits/_qasm_conversion.py} +73 -14
- divi/circuits/_qasm_validation.py +694 -0
- divi/qprog/__init__.py +24 -6
- divi/qprog/_expectation.py +181 -0
- divi/qprog/_hamiltonians.py +281 -0
- divi/qprog/algorithms/__init__.py +14 -0
- divi/qprog/algorithms/_ansatze.py +356 -0
- divi/qprog/algorithms/_qaoa.py +572 -0
- divi/qprog/algorithms/_vqe.py +249 -0
- divi/qprog/batch.py +383 -73
- divi/qprog/checkpointing.py +556 -0
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +1014 -43
- divi/qprog/quantum_program.py +231 -413
- divi/qprog/variational_quantum_algorithm.py +995 -0
- divi/qprog/workflows/__init__.py +10 -0
- divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +139 -95
- divi/qprog/workflows/_qubo_partitioning.py +220 -0
- divi/qprog/workflows/_vqe_sweep.py +560 -0
- divi/reporting/__init__.py +7 -0
- divi/reporting/_pbar.py +127 -0
- divi/reporting/_qlogger.py +68 -0
- divi/reporting/_reporter.py +133 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info}/METADATA +43 -15
- qoro_divi-0.5.0.dist-info/RECORD +43 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info}/WHEEL +1 -1
- qoro_divi-0.5.0.dist-info/licenses/LICENSES/.license-header +3 -0
- divi/_pbar.py +0 -73
- divi/circuits.py +0 -139
- divi/exp/cirq/_lexer.py +0 -126
- divi/exp/cirq/_parser.py +0 -889
- divi/exp/cirq/_qasm_export.py +0 -37
- divi/exp/cirq/_qasm_import.py +0 -35
- divi/exp/cirq/exception.py +0 -21
- divi/exp/scipy/_cobyla.py +0 -342
- divi/exp/scipy/pyprima/LICENCE.txt +0 -28
- divi/exp/scipy/pyprima/__init__.py +0 -263
- divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
- divi/exp/scipy/pyprima/cobyla/cobyla.py +0 -599
- divi/exp/scipy/pyprima/cobyla/cobylb.py +0 -849
- divi/exp/scipy/pyprima/cobyla/geometry.py +0 -240
- divi/exp/scipy/pyprima/cobyla/initialize.py +0 -269
- divi/exp/scipy/pyprima/cobyla/trustregion.py +0 -540
- divi/exp/scipy/pyprima/cobyla/update.py +0 -331
- divi/exp/scipy/pyprima/common/__init__.py +0 -0
- divi/exp/scipy/pyprima/common/_bounds.py +0 -41
- divi/exp/scipy/pyprima/common/_linear_constraints.py +0 -46
- divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +0 -64
- divi/exp/scipy/pyprima/common/_project.py +0 -224
- divi/exp/scipy/pyprima/common/checkbreak.py +0 -107
- divi/exp/scipy/pyprima/common/consts.py +0 -48
- divi/exp/scipy/pyprima/common/evaluate.py +0 -101
- divi/exp/scipy/pyprima/common/history.py +0 -39
- divi/exp/scipy/pyprima/common/infos.py +0 -30
- divi/exp/scipy/pyprima/common/linalg.py +0 -452
- divi/exp/scipy/pyprima/common/message.py +0 -336
- divi/exp/scipy/pyprima/common/powalg.py +0 -131
- divi/exp/scipy/pyprima/common/preproc.py +0 -393
- divi/exp/scipy/pyprima/common/present.py +0 -5
- divi/exp/scipy/pyprima/common/ratio.py +0 -56
- divi/exp/scipy/pyprima/common/redrho.py +0 -49
- divi/exp/scipy/pyprima/common/selectx.py +0 -346
- divi/interfaces.py +0 -25
- divi/parallel_simulator.py +0 -258
- divi/qlogger.py +0 -119
- divi/qoro_service.py +0 -343
- divi/qprog/_mlae.py +0 -182
- divi/qprog/_qaoa.py +0 -440
- divi/qprog/_vqe.py +0 -275
- divi/qprog/_vqe_sweep.py +0 -144
- divi/utils.py +0 -116
- qoro_divi-0.2.0b1.dist-info/RECORD +0 -58
- /divi/{qem.py → circuits/qem.py} +0 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info/licenses}/LICENSE +0 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.5.0.dist-info/licenses}/LICENSES/Apache-2.0.txt +0 -0
divi/qlogger.py
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
-
#
|
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
|
|
5
|
-
import logging
|
|
6
|
-
import shutil
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def _is_jupyter():
|
|
11
|
-
"""
|
|
12
|
-
Checks if the code is running inside a Jupyter Notebook or IPython environment.
|
|
13
|
-
"""
|
|
14
|
-
try:
|
|
15
|
-
from IPython import get_ipython
|
|
16
|
-
|
|
17
|
-
# Check if get_ipython() returns a shell instance (not None)
|
|
18
|
-
# and if the shell class is 'ZMQInteractiveShell' for Jupyter notebooks/qtconsole
|
|
19
|
-
# or 'TerminalInteractiveShell' for IPython console.
|
|
20
|
-
shell = get_ipython().__class__.__name__
|
|
21
|
-
if shell == "ZMQInteractiveShell":
|
|
22
|
-
return True # Jupyter notebook or qtconsole
|
|
23
|
-
elif shell == "TerminalInteractiveShell":
|
|
24
|
-
return False # IPython terminal
|
|
25
|
-
else:
|
|
26
|
-
return False # Other IPython environment (less common for typical Jupyter detection)
|
|
27
|
-
except NameError:
|
|
28
|
-
return False # Not running in IPython
|
|
29
|
-
except ImportError:
|
|
30
|
-
return False # IPython is not installed
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
class OverwriteStreamHandler(logging.StreamHandler):
|
|
34
|
-
def __init__(self, stream=None):
|
|
35
|
-
super().__init__(stream)
|
|
36
|
-
|
|
37
|
-
self._last_record = ""
|
|
38
|
-
self._last_message = ""
|
|
39
|
-
|
|
40
|
-
# Worst case: 2 complex emojis (8 chars each) + buffer = 21 extra chars
|
|
41
|
-
self._emoji_buffer = 21
|
|
42
|
-
|
|
43
|
-
self._is_jupyter = _is_jupyter()
|
|
44
|
-
|
|
45
|
-
def emit(self, record):
|
|
46
|
-
msg = self.format(record)
|
|
47
|
-
|
|
48
|
-
if record.message.startswith(r"\c"):
|
|
49
|
-
sep = r"\c"
|
|
50
|
-
msg = f"{msg.split(sep)[0]}{self._last_record} [{record.message[2:-2]}]\r"
|
|
51
|
-
|
|
52
|
-
if msg.endswith("\r\n"):
|
|
53
|
-
overwrite_and_newline = True
|
|
54
|
-
clean_msg = msg[:-2]
|
|
55
|
-
|
|
56
|
-
if not record.message.startswith("\c"):
|
|
57
|
-
self._last_record = record.message[:-2]
|
|
58
|
-
elif msg.endswith("\r"):
|
|
59
|
-
overwrite_and_newline = False
|
|
60
|
-
clean_msg = msg[:-1]
|
|
61
|
-
|
|
62
|
-
if not record.message.startswith(r"\c"):
|
|
63
|
-
self._last_record = record.message[:-1]
|
|
64
|
-
else:
|
|
65
|
-
# Normal message - no overwriting
|
|
66
|
-
self.stream.write(msg + "\n")
|
|
67
|
-
self.stream.flush()
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
# Clear previous line if needed
|
|
71
|
-
if len(self._last_message) > 0:
|
|
72
|
-
if self._is_jupyter:
|
|
73
|
-
clear_length = len(self._last_message) + self._emoji_buffer + 50
|
|
74
|
-
else:
|
|
75
|
-
clear_length = min(
|
|
76
|
-
len(self._last_message) + self._emoji_buffer,
|
|
77
|
-
shutil.get_terminal_size().columns,
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
self.stream.write("\r" + " " * clear_length + "\r")
|
|
81
|
-
self.stream.flush()
|
|
82
|
-
|
|
83
|
-
# Write message with appropriate ending
|
|
84
|
-
if overwrite_and_newline:
|
|
85
|
-
self.stream.write(clean_msg + "\n")
|
|
86
|
-
self._last_message = ""
|
|
87
|
-
else:
|
|
88
|
-
self.stream.write(clean_msg + "\r")
|
|
89
|
-
self._last_message = self._strip_ansi(clean_msg)
|
|
90
|
-
|
|
91
|
-
self.stream.flush()
|
|
92
|
-
|
|
93
|
-
def _strip_ansi(self, text):
|
|
94
|
-
"""Remove ANSI escape sequences for accurate length calculation"""
|
|
95
|
-
import re
|
|
96
|
-
|
|
97
|
-
ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
98
|
-
return ansi_escape.sub("", text)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def enable_logging(level=logging.INFO):
|
|
102
|
-
root_logger = logging.getLogger(__name__.split(".")[0])
|
|
103
|
-
|
|
104
|
-
formatter = logging.Formatter(
|
|
105
|
-
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
handler = OverwriteStreamHandler(sys.stdout)
|
|
109
|
-
handler.setFormatter(formatter)
|
|
110
|
-
|
|
111
|
-
root_logger.setLevel(level)
|
|
112
|
-
root_logger.handlers.clear()
|
|
113
|
-
root_logger.addHandler(handler)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def disable_logging():
|
|
117
|
-
root_logger = logging.getLogger(__name__.split(".")[0])
|
|
118
|
-
root_logger.handlers.clear()
|
|
119
|
-
root_logger.setLevel(logging.CRITICAL + 1)
|
divi/qoro_service.py
DELETED
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
-
#
|
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
|
|
5
|
-
import base64
|
|
6
|
-
import gzip
|
|
7
|
-
import json
|
|
8
|
-
import logging
|
|
9
|
-
import time
|
|
10
|
-
from collections.abc import Callable
|
|
11
|
-
from enum import Enum
|
|
12
|
-
from http import HTTPStatus
|
|
13
|
-
from typing import Optional
|
|
14
|
-
|
|
15
|
-
import requests
|
|
16
|
-
from requests.adapters import HTTPAdapter, Retry
|
|
17
|
-
|
|
18
|
-
from divi.interfaces import CircuitRunner
|
|
19
|
-
|
|
20
|
-
API_URL = "https://app.qoroquantum.net/api"
|
|
21
|
-
MAX_PAYLOAD_SIZE_MB = 0.95
|
|
22
|
-
|
|
23
|
-
session = requests.Session()
|
|
24
|
-
retries = Retry(
|
|
25
|
-
total=5,
|
|
26
|
-
backoff_factor=0.1,
|
|
27
|
-
status_forcelist=[502],
|
|
28
|
-
allowed_methods=["GET", "POST", "DELETE"],
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
session.mount("http://", HTTPAdapter(max_retries=retries))
|
|
32
|
-
session.mount("https://", HTTPAdapter(max_retries=retries))
|
|
33
|
-
|
|
34
|
-
logger = logging.getLogger(__name__)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class JobStatus(Enum):
|
|
38
|
-
PENDING = "PENDING"
|
|
39
|
-
RUNNING = "RUNNING"
|
|
40
|
-
COMPLETED = "COMPLETED"
|
|
41
|
-
FAILED = "FAILED"
|
|
42
|
-
CANCELLED = "CANCELLED"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
class JobType(Enum):
|
|
46
|
-
EXECUTE = "EXECUTE"
|
|
47
|
-
SIMULATE = "SIMULATE"
|
|
48
|
-
ESTIMATE = "ESTIMATE"
|
|
49
|
-
CIRCUIT_CUT = "CIRCUIT_CUT"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class MaxRetriesReachedError(Exception):
|
|
53
|
-
"""Exception raised when the maximum number of retries is reached."""
|
|
54
|
-
|
|
55
|
-
def __init__(self, retries, message="Maximum retries reached"):
|
|
56
|
-
self.retries = retries
|
|
57
|
-
self.message = f"{message}: {retries} retries attempted"
|
|
58
|
-
super().__init__(self.message)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
class QoroService(CircuitRunner):
|
|
62
|
-
|
|
63
|
-
def __init__(
|
|
64
|
-
self,
|
|
65
|
-
auth_token: str,
|
|
66
|
-
polling_interval: float = 3.0,
|
|
67
|
-
max_retries: int = 5000,
|
|
68
|
-
shots: int = 1000,
|
|
69
|
-
use_circuit_packing: Optional[bool] = False,
|
|
70
|
-
):
|
|
71
|
-
super().__init__(shots=shots)
|
|
72
|
-
|
|
73
|
-
self.auth_token = "Bearer " + auth_token
|
|
74
|
-
self.polling_interval = polling_interval
|
|
75
|
-
self.max_retries = max_retries
|
|
76
|
-
self.use_circuit_packing = use_circuit_packing
|
|
77
|
-
|
|
78
|
-
def test_connection(self):
|
|
79
|
-
"""Test the connection to the Qoro API"""
|
|
80
|
-
response = session.get(
|
|
81
|
-
API_URL, headers={"Authorization": self.auth_token}, timeout=10
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
if response.status_code != HTTPStatus.OK:
|
|
85
|
-
raise requests.exceptions.HTTPError(
|
|
86
|
-
f"Connection failed with error: {response.status_code}: {response.reason}"
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
return response
|
|
90
|
-
|
|
91
|
-
def submit_circuits(
|
|
92
|
-
self,
|
|
93
|
-
circuits: dict[str, str],
|
|
94
|
-
tag: str = "default",
|
|
95
|
-
job_type: JobType = JobType.SIMULATE,
|
|
96
|
-
override_circuit_packing: bool | None = None,
|
|
97
|
-
):
|
|
98
|
-
"""
|
|
99
|
-
Submit quantum circuits to the Qoro API for execution.
|
|
100
|
-
|
|
101
|
-
Args:
|
|
102
|
-
circuits (dict[str, str]):
|
|
103
|
-
Dictionary mapping unique circuit IDs to QASM circuit strings.
|
|
104
|
-
tag (str, optional):
|
|
105
|
-
Tag to associate with the job for identification. Defaults to "default".
|
|
106
|
-
job_type (JobType, optional):
|
|
107
|
-
Type of job to execute (e.g., SIMULATE, EXECUTE, ESTIMATE, CIRCUIT_CUT). Defaults to JobType.SIMULATE.
|
|
108
|
-
use_packing (bool):
|
|
109
|
-
Whether to use circuit packing optimization. Defaults to False.
|
|
110
|
-
|
|
111
|
-
Raises:
|
|
112
|
-
ValueError: If more than one circuit is submitted for a CIRCUIT_CUT job.
|
|
113
|
-
|
|
114
|
-
Returns:
|
|
115
|
-
str or list[str]:
|
|
116
|
-
The job ID(s) of the created job(s). Returns a single job ID if only one job is created,
|
|
117
|
-
otherwise returns a list of job IDs if the circuits are split into multiple jobs due to payload size.
|
|
118
|
-
"""
|
|
119
|
-
|
|
120
|
-
if job_type == JobType.CIRCUIT_CUT and len(circuits) > 1:
|
|
121
|
-
raise ValueError("Only one circuit allowed for circuit-cutting jobs.")
|
|
122
|
-
|
|
123
|
-
def _compress_data(value) -> bytes:
|
|
124
|
-
return base64.b64encode(gzip.compress(value.encode("utf-8"))).decode(
|
|
125
|
-
"utf-8"
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
def _split_circuits(circuits: dict[str, str]) -> list[dict[str, str]]:
|
|
129
|
-
"""
|
|
130
|
-
Split circuits into smaller chunks if the payload size exceeds the maximum allowed size.
|
|
131
|
-
|
|
132
|
-
Args:
|
|
133
|
-
circuits: Dictionary of circuits to be sent
|
|
134
|
-
|
|
135
|
-
Returns:
|
|
136
|
-
List of circuit chunks
|
|
137
|
-
"""
|
|
138
|
-
|
|
139
|
-
def _estimate_size(data):
|
|
140
|
-
payload_json = json.dumps(data)
|
|
141
|
-
return len(payload_json.encode("utf-8")) / 1024 / 1024
|
|
142
|
-
|
|
143
|
-
circuit_chunks = []
|
|
144
|
-
current_chunk = {}
|
|
145
|
-
current_size = 0
|
|
146
|
-
|
|
147
|
-
for key, value in circuits.items():
|
|
148
|
-
compressed_value = _compress_data(value)
|
|
149
|
-
estimated_size = _estimate_size({key: compressed_value})
|
|
150
|
-
|
|
151
|
-
if current_size + estimated_size > MAX_PAYLOAD_SIZE_MB:
|
|
152
|
-
circuit_chunks.append(current_chunk)
|
|
153
|
-
current_chunk = {key: compressed_value}
|
|
154
|
-
current_size = estimated_size
|
|
155
|
-
else:
|
|
156
|
-
current_chunk[key] = compressed_value
|
|
157
|
-
current_size += estimated_size
|
|
158
|
-
|
|
159
|
-
if current_chunk:
|
|
160
|
-
circuit_chunks.append(current_chunk)
|
|
161
|
-
|
|
162
|
-
return circuit_chunks
|
|
163
|
-
|
|
164
|
-
circuit_chunks = _split_circuits(circuits)
|
|
165
|
-
|
|
166
|
-
job_ids = []
|
|
167
|
-
for chunk in circuit_chunks:
|
|
168
|
-
response = session.post(
|
|
169
|
-
API_URL + "/job/",
|
|
170
|
-
headers={
|
|
171
|
-
"Authorization": self.auth_token,
|
|
172
|
-
"Content-Type": "application/json",
|
|
173
|
-
},
|
|
174
|
-
json={
|
|
175
|
-
"circuits": chunk,
|
|
176
|
-
"shots": self.shots,
|
|
177
|
-
"tag": tag,
|
|
178
|
-
"job_type": job_type.value,
|
|
179
|
-
"use_packing": (
|
|
180
|
-
override_circuit_packing
|
|
181
|
-
if override_circuit_packing is not None
|
|
182
|
-
else self.use_circuit_packing
|
|
183
|
-
),
|
|
184
|
-
},
|
|
185
|
-
timeout=100,
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
if response.status_code == HTTPStatus.CREATED:
|
|
189
|
-
job_ids.append(response.json()["job_id"])
|
|
190
|
-
else:
|
|
191
|
-
raise requests.exceptions.HTTPError(
|
|
192
|
-
f"{response.status_code}: {response.reason}"
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
return job_ids if len(job_ids) > 1 else job_ids[0]
|
|
196
|
-
|
|
197
|
-
def delete_job(self, job_ids):
|
|
198
|
-
"""
|
|
199
|
-
Delete a job from the Qoro Database.
|
|
200
|
-
|
|
201
|
-
Args:
|
|
202
|
-
job_id: The ID of the jobs to be deleted
|
|
203
|
-
Returns:
|
|
204
|
-
response: The response from the API
|
|
205
|
-
"""
|
|
206
|
-
if not isinstance(job_ids, list):
|
|
207
|
-
job_ids = [job_ids]
|
|
208
|
-
|
|
209
|
-
responses = []
|
|
210
|
-
|
|
211
|
-
for job_id in job_ids:
|
|
212
|
-
response = session.delete(
|
|
213
|
-
API_URL + f"/job/{job_id}",
|
|
214
|
-
headers={"Authorization": self.auth_token},
|
|
215
|
-
timeout=50,
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
responses.append(response)
|
|
219
|
-
|
|
220
|
-
return responses if len(responses) > 1 else responses[0]
|
|
221
|
-
|
|
222
|
-
def get_job_results(self, job_ids):
|
|
223
|
-
"""
|
|
224
|
-
Get the results of a job from the Qoro Database.
|
|
225
|
-
|
|
226
|
-
Args:
|
|
227
|
-
job_id: The ID of the job to get results from
|
|
228
|
-
Returns:
|
|
229
|
-
results: The results of the job
|
|
230
|
-
"""
|
|
231
|
-
if not isinstance(job_ids, list):
|
|
232
|
-
job_ids = [job_ids]
|
|
233
|
-
|
|
234
|
-
responses = []
|
|
235
|
-
for job_id in job_ids:
|
|
236
|
-
response = session.get(
|
|
237
|
-
API_URL + f"/job/{job_id}/results",
|
|
238
|
-
headers={"Authorization": self.auth_token},
|
|
239
|
-
timeout=100,
|
|
240
|
-
)
|
|
241
|
-
responses.append(response)
|
|
242
|
-
|
|
243
|
-
if all(response.status_code == HTTPStatus.OK for response in responses):
|
|
244
|
-
responses = [response.json() for response in responses]
|
|
245
|
-
return sum(responses, [])
|
|
246
|
-
elif any(
|
|
247
|
-
response.status_code == HTTPStatus.BAD_REQUEST for response in responses
|
|
248
|
-
):
|
|
249
|
-
raise requests.exceptions.HTTPError(
|
|
250
|
-
"400 Bad Request: Job results not available, likely job is still running"
|
|
251
|
-
)
|
|
252
|
-
else:
|
|
253
|
-
for response in responses:
|
|
254
|
-
if response.status_code not in [HTTPStatus.OK, HTTPStatus.BAD_REQUEST]:
|
|
255
|
-
raise requests.exceptions.HTTPError(
|
|
256
|
-
f"{response.status_code}: {response.reason}"
|
|
257
|
-
)
|
|
258
|
-
|
|
259
|
-
def poll_job_status(
|
|
260
|
-
self,
|
|
261
|
-
job_ids: str | list[str],
|
|
262
|
-
loop_until_complete: bool = False,
|
|
263
|
-
on_complete: Optional[Callable] = None,
|
|
264
|
-
verbose: bool = True,
|
|
265
|
-
pbar_update_fn: Optional[Callable] = None,
|
|
266
|
-
):
|
|
267
|
-
"""
|
|
268
|
-
Get the status of a job and optionally execute function *on_complete* on the results
|
|
269
|
-
if the status is COMPLETE.
|
|
270
|
-
|
|
271
|
-
Args:
|
|
272
|
-
job_ids: The job id of the jobs to check
|
|
273
|
-
loop_until_complete (bool): A flag to loop until the job is completed
|
|
274
|
-
on_complete (optional): A function to be called when the job is completed
|
|
275
|
-
polling_interval (optional): The time to wait between retries
|
|
276
|
-
max_retries (optional): The maximum number of retries
|
|
277
|
-
verbose (optional): A flag to print the when retrying
|
|
278
|
-
pbar_update_fn (optional): A function for updating progress bars while polling.
|
|
279
|
-
Returns:
|
|
280
|
-
status: The status of the job
|
|
281
|
-
"""
|
|
282
|
-
if not isinstance(job_ids, list):
|
|
283
|
-
job_ids = [job_ids]
|
|
284
|
-
|
|
285
|
-
def _poll_job_status(job_id):
|
|
286
|
-
response = session.get(
|
|
287
|
-
API_URL + f"/job/{job_id}/status/",
|
|
288
|
-
headers={
|
|
289
|
-
"Authorization": self.auth_token,
|
|
290
|
-
"Content-Type": "application/json",
|
|
291
|
-
},
|
|
292
|
-
timeout=200,
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
if response.status_code == HTTPStatus.OK:
|
|
296
|
-
return response.json()["status"], response
|
|
297
|
-
else:
|
|
298
|
-
raise requests.exceptions.HTTPError(
|
|
299
|
-
f"{response.status_code}: {response.reason}"
|
|
300
|
-
)
|
|
301
|
-
|
|
302
|
-
if loop_until_complete:
|
|
303
|
-
retries = 0
|
|
304
|
-
completed = False
|
|
305
|
-
while True:
|
|
306
|
-
responses = []
|
|
307
|
-
statuses = []
|
|
308
|
-
|
|
309
|
-
for job_id in job_ids:
|
|
310
|
-
job_status, response = _poll_job_status(job_id)
|
|
311
|
-
statuses.append(job_status)
|
|
312
|
-
responses.append(response)
|
|
313
|
-
|
|
314
|
-
if all(status == JobStatus.COMPLETED.value for status in statuses):
|
|
315
|
-
responses = [response.json() for response in responses]
|
|
316
|
-
completed = True
|
|
317
|
-
break
|
|
318
|
-
|
|
319
|
-
if retries >= self.max_retries:
|
|
320
|
-
break
|
|
321
|
-
|
|
322
|
-
retries += 1
|
|
323
|
-
|
|
324
|
-
time.sleep(self.polling_interval)
|
|
325
|
-
|
|
326
|
-
if verbose:
|
|
327
|
-
if pbar_update_fn:
|
|
328
|
-
pbar_update_fn(retries)
|
|
329
|
-
else:
|
|
330
|
-
logger.info(
|
|
331
|
-
rf"\cPolling {retries} / {self.max_retries} retries\r"
|
|
332
|
-
)
|
|
333
|
-
|
|
334
|
-
if completed and on_complete:
|
|
335
|
-
on_complete(responses)
|
|
336
|
-
return JobStatus.COMPLETED
|
|
337
|
-
elif completed:
|
|
338
|
-
return JobStatus.COMPLETED
|
|
339
|
-
else:
|
|
340
|
-
raise MaxRetriesReachedError(retries)
|
|
341
|
-
else:
|
|
342
|
-
statuses = [_poll_job_status(job_id)[0] for job_id in job_ids]
|
|
343
|
-
return statuses if len(statuses) > 1 else statuses[0]
|
divi/qprog/_mlae.py
DELETED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
-
#
|
|
3
|
-
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
|
|
5
|
-
from functools import reduce
|
|
6
|
-
|
|
7
|
-
import numpy as np
|
|
8
|
-
import scipy.optimize as optimize
|
|
9
|
-
from qiskit import QuantumCircuit
|
|
10
|
-
from qiskit_algorithms import EstimationProblem, MaximumLikelihoodAmplitudeEstimation
|
|
11
|
-
|
|
12
|
-
from divi.circuits import Circuit
|
|
13
|
-
from divi.qprog.quantum_program import QuantumProgram
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class BernoulliA(QuantumCircuit):
|
|
17
|
-
"""A circuit representing the Bernoulli A operator."""
|
|
18
|
-
|
|
19
|
-
def __init__(self, probability):
|
|
20
|
-
super().__init__(1)
|
|
21
|
-
|
|
22
|
-
theta_p = 2 * np.arcsin(np.sqrt(probability))
|
|
23
|
-
self.ry(theta_p, 0)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
class BernoulliQ(QuantumCircuit):
|
|
27
|
-
"""A circuit representing the Bernoulli Q operator."""
|
|
28
|
-
|
|
29
|
-
def __init__(self, probability):
|
|
30
|
-
super().__init__(1)
|
|
31
|
-
|
|
32
|
-
self._theta_p = 2 * np.arcsin(np.sqrt(probability))
|
|
33
|
-
self.ry(2 * self._theta_p, 0)
|
|
34
|
-
|
|
35
|
-
def power(self, k):
|
|
36
|
-
# implement the efficient power of Q
|
|
37
|
-
q_k = QuantumCircuit(1)
|
|
38
|
-
q_k.ry(2 * k * self._theta_p, 0)
|
|
39
|
-
return q_k
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class MLAE(QuantumProgram):
|
|
43
|
-
"""
|
|
44
|
-
An implementation of the Maximum Likelihood Amplitude Estimateion described in
|
|
45
|
-
https://arxiv.org/pdf/1904.10246
|
|
46
|
-
"""
|
|
47
|
-
|
|
48
|
-
def __init__(
|
|
49
|
-
self,
|
|
50
|
-
grovers: list[int],
|
|
51
|
-
qubits_to_measure: list[int],
|
|
52
|
-
probability: float,
|
|
53
|
-
**kwargs,
|
|
54
|
-
):
|
|
55
|
-
"""
|
|
56
|
-
Initializes the MLAE problem.
|
|
57
|
-
args:
|
|
58
|
-
grovers (list): A list of non-negative integers corresponding to the powers of the Grover
|
|
59
|
-
operator for each iteration
|
|
60
|
-
qubits: An integer or list of integers containing the index of the qubits to measure
|
|
61
|
-
probability: The probability of being in the good state to estimate
|
|
62
|
-
shots: The number of shots to run for each circuit. Default set at 5000.
|
|
63
|
-
"""
|
|
64
|
-
|
|
65
|
-
super().__init__(**kwargs)
|
|
66
|
-
|
|
67
|
-
self.grovers = grovers
|
|
68
|
-
self.qubits_to_measure = qubits_to_measure
|
|
69
|
-
self.probability = probability
|
|
70
|
-
self.likelihood_functions = []
|
|
71
|
-
|
|
72
|
-
def _create_meta_circuits_dict(self):
|
|
73
|
-
return super()._create_meta_circuits_dict()
|
|
74
|
-
|
|
75
|
-
def _generate_circuits(self, params=None, **kwargs):
|
|
76
|
-
"""
|
|
77
|
-
Generates the circuits that perform step one of the MLAE algorithm,
|
|
78
|
-
the quantum amplitude amplification.
|
|
79
|
-
|
|
80
|
-
Inputs a selection of m values corresponding to the powers of the
|
|
81
|
-
Grover operatorfor each iteration.
|
|
82
|
-
|
|
83
|
-
Returns:
|
|
84
|
-
A list of QASM circuits to run on various devices
|
|
85
|
-
"""
|
|
86
|
-
self.circuits.clear()
|
|
87
|
-
|
|
88
|
-
A = BernoulliA(self.probability)
|
|
89
|
-
Q = BernoulliQ(self.probability)
|
|
90
|
-
|
|
91
|
-
problem = EstimationProblem(
|
|
92
|
-
state_preparation=A,
|
|
93
|
-
grover_operator=Q,
|
|
94
|
-
objective_qubits=self.qubits_to_measure,
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
qiskit_circuits = MaximumLikelihoodAmplitudeEstimation(
|
|
98
|
-
self.grovers
|
|
99
|
-
).construct_circuits(problem)
|
|
100
|
-
|
|
101
|
-
for circuit, grover in zip(qiskit_circuits, self.grovers):
|
|
102
|
-
circuit.measure_all()
|
|
103
|
-
self.circuits.append(Circuit(circuit, tags=[f"{grover}"]))
|
|
104
|
-
|
|
105
|
-
def run(self, store_data=False, data_file=None):
|
|
106
|
-
self._generate_circuits()
|
|
107
|
-
self._dispatch_circuits_and_process_results(
|
|
108
|
-
store_data=store_data, data_file=data_file
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
def _post_process_results(self, results):
|
|
112
|
-
"""
|
|
113
|
-
Generates the likelihood function for each circuit of the quantum
|
|
114
|
-
amplitude amplification. These likelihood functions will then
|
|
115
|
-
be combined to create a maximum likelihood function to analyze.
|
|
116
|
-
|
|
117
|
-
Returns:
|
|
118
|
-
A callable maximum likelihood function
|
|
119
|
-
"""
|
|
120
|
-
|
|
121
|
-
# Define the necessary variables Nk, Mk, Lk
|
|
122
|
-
for label, shots_dict in results.items():
|
|
123
|
-
mk = int(label)
|
|
124
|
-
Nk = 0
|
|
125
|
-
hk = 0
|
|
126
|
-
for key, shots in shots_dict.items():
|
|
127
|
-
Nk += shots
|
|
128
|
-
hk += shots if key.count("1") == len(key) else 0
|
|
129
|
-
|
|
130
|
-
def likelihood_function(theta, mk=mk, hk=hk, Nk=Nk):
|
|
131
|
-
as_theta = np.arcsin(np.sqrt(theta))
|
|
132
|
-
return ((np.sin((2 * mk + 1) * as_theta)) ** (2 * hk)) * (
|
|
133
|
-
(np.cos((2 * mk + 1) * as_theta)) ** (2 * (Nk - hk))
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
self.likelihood_functions.append(likelihood_function)
|
|
137
|
-
|
|
138
|
-
def generate_maximum_likelihood_function(self, factor=1.0):
|
|
139
|
-
"""
|
|
140
|
-
Post-processing takes in likelihood functions.
|
|
141
|
-
|
|
142
|
-
A large factor (e.g. 1e200) should be used for visualization purposes.
|
|
143
|
-
Returns:
|
|
144
|
-
The maximum likelihood function.
|
|
145
|
-
"""
|
|
146
|
-
|
|
147
|
-
def combined_likelihood_function(theta):
|
|
148
|
-
return (
|
|
149
|
-
reduce(
|
|
150
|
-
lambda result, f: result * f(theta), self.likelihood_functions, 1.0
|
|
151
|
-
)
|
|
152
|
-
* factor
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
self.maximum_likelihood_fn = combined_likelihood_function
|
|
156
|
-
|
|
157
|
-
return combined_likelihood_function
|
|
158
|
-
|
|
159
|
-
def estimate_amplitude(self, factor):
|
|
160
|
-
"""
|
|
161
|
-
Uses the maximum likelihood function to ascertain
|
|
162
|
-
a value for the amplitude.
|
|
163
|
-
|
|
164
|
-
Returns
|
|
165
|
-
Estimation of the amplitude
|
|
166
|
-
"""
|
|
167
|
-
|
|
168
|
-
def minimum_likelihood_function(theta):
|
|
169
|
-
# The factor to set to -10e30 in the older branch
|
|
170
|
-
return (
|
|
171
|
-
reduce(
|
|
172
|
-
lambda result, f: result * f(theta), self.likelihood_functions, 1.0
|
|
173
|
-
)
|
|
174
|
-
* factor
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
# create the range of possible amplitudes
|
|
178
|
-
amplitudes = np.linspace(0, 1, 100)
|
|
179
|
-
|
|
180
|
-
bounds = [(min(amplitudes), max(amplitudes))]
|
|
181
|
-
|
|
182
|
-
return optimize.differential_evolution(minimum_likelihood_function, bounds).x[0]
|