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/qprog/batch.py
CHANGED
|
@@ -2,36 +2,35 @@
|
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
+
import atexit
|
|
5
6
|
import traceback
|
|
7
|
+
import warnings
|
|
6
8
|
from abc import ABC, abstractmethod
|
|
7
|
-
from concurrent.futures import
|
|
8
|
-
from multiprocessing import Event, Manager
|
|
9
|
-
from multiprocessing.synchronize import Event as EventClass
|
|
9
|
+
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
|
|
10
10
|
from queue import Empty, Queue
|
|
11
|
-
from threading import Lock, Thread
|
|
11
|
+
from threading import Event, Lock, Thread
|
|
12
|
+
from typing import Any
|
|
12
13
|
from warnings import warn
|
|
13
14
|
|
|
14
15
|
from rich.console import Console
|
|
15
16
|
from rich.progress import Progress, TaskID
|
|
16
17
|
|
|
17
|
-
from divi.
|
|
18
|
-
from divi.interfaces import CircuitRunner
|
|
19
|
-
from divi.parallel_simulator import ParallelSimulator
|
|
20
|
-
from divi.qlogger import disable_logging
|
|
18
|
+
from divi.backends import CircuitRunner
|
|
21
19
|
from divi.qprog.quantum_program import QuantumProgram
|
|
20
|
+
from divi.reporting import disable_logging, make_progress_bar
|
|
22
21
|
|
|
23
22
|
|
|
24
|
-
def
|
|
23
|
+
def _queue_listener(
|
|
25
24
|
queue: Queue,
|
|
26
25
|
progress_bar: Progress,
|
|
27
26
|
pb_task_map: dict[QuantumProgram, TaskID],
|
|
28
|
-
done_event:
|
|
27
|
+
done_event: Event,
|
|
29
28
|
is_jupyter: bool,
|
|
30
29
|
lock: Lock,
|
|
31
30
|
):
|
|
32
31
|
while not done_event.is_set():
|
|
33
32
|
try:
|
|
34
|
-
msg = queue.get(timeout=0.1)
|
|
33
|
+
msg: dict[str, Any] = queue.get(timeout=0.1)
|
|
35
34
|
except Empty:
|
|
36
35
|
continue
|
|
37
36
|
except Exception as e:
|
|
@@ -41,14 +40,30 @@ def queue_listener(
|
|
|
41
40
|
with lock:
|
|
42
41
|
task_id = pb_task_map[msg["job_id"]]
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
# Prepare update arguments, starting with progress.
|
|
44
|
+
update_args = {"advance": msg["progress"]}
|
|
45
|
+
|
|
46
|
+
if "poll_attempt" in msg:
|
|
47
|
+
update_args["poll_attempt"] = msg.get("poll_attempt", 0)
|
|
48
|
+
if "max_retries" in msg:
|
|
49
|
+
update_args["max_retries"] = msg.get("max_retries")
|
|
50
|
+
if "service_job_id" in msg:
|
|
51
|
+
update_args["service_job_id"] = msg.get("service_job_id")
|
|
52
|
+
if "job_status" in msg:
|
|
53
|
+
update_args["job_status"] = msg.get("job_status")
|
|
54
|
+
if msg.get("message"):
|
|
55
|
+
update_args["message"] = msg.get("message")
|
|
56
|
+
if "final_status" in msg:
|
|
57
|
+
update_args["final_status"] = msg.get("final_status", "")
|
|
58
|
+
|
|
59
|
+
update_args["refresh"] = is_jupyter
|
|
60
|
+
|
|
61
|
+
progress_bar.update(task_id, **update_args)
|
|
62
|
+
queue.task_done()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _default_task_function(program: QuantumProgram):
|
|
66
|
+
return program.run()
|
|
52
67
|
|
|
53
68
|
|
|
54
69
|
class ProgramBatch(ABC):
|
|
@@ -74,12 +89,12 @@ class ProgramBatch(ABC):
|
|
|
74
89
|
|
|
75
90
|
self.backend = backend
|
|
76
91
|
self._executor = None
|
|
77
|
-
self.
|
|
92
|
+
self._task_fn = _default_task_function
|
|
93
|
+
self._programs = {}
|
|
78
94
|
|
|
79
95
|
self._total_circuit_count = 0
|
|
80
96
|
self._total_run_time = 0.0
|
|
81
97
|
|
|
82
|
-
self._is_local = isinstance(backend, ParallelSimulator)
|
|
83
98
|
self._is_jupyter = Console().is_jupyter
|
|
84
99
|
|
|
85
100
|
# Disable logging since we already have the bars to track progress
|
|
@@ -87,22 +102,100 @@ class ProgramBatch(ABC):
|
|
|
87
102
|
|
|
88
103
|
@property
|
|
89
104
|
def total_circuit_count(self):
|
|
105
|
+
"""
|
|
106
|
+
Get the total number of circuits executed across all programs in the batch.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
int: Cumulative count of circuits submitted by all programs.
|
|
110
|
+
"""
|
|
90
111
|
return self._total_circuit_count
|
|
91
112
|
|
|
92
113
|
@property
|
|
93
114
|
def total_run_time(self):
|
|
115
|
+
"""
|
|
116
|
+
Get the total runtime across all programs in the batch.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
float: Cumulative execution time in seconds across all programs.
|
|
120
|
+
"""
|
|
94
121
|
return self._total_run_time
|
|
95
122
|
|
|
123
|
+
@property
|
|
124
|
+
def programs(self) -> dict:
|
|
125
|
+
"""
|
|
126
|
+
Get a copy of the programs dictionary.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
dict: Copy of the programs dictionary mapping program IDs to
|
|
130
|
+
QuantumProgram instances. Modifications to this dict will not
|
|
131
|
+
affect the internal state.
|
|
132
|
+
"""
|
|
133
|
+
return self._programs.copy()
|
|
134
|
+
|
|
135
|
+
@programs.setter
|
|
136
|
+
def programs(self, value: dict):
|
|
137
|
+
"""Set the programs dictionary."""
|
|
138
|
+
self._programs = value
|
|
139
|
+
|
|
96
140
|
@abstractmethod
|
|
97
141
|
def create_programs(self):
|
|
98
|
-
|
|
99
|
-
|
|
142
|
+
"""Generate and populate the programs dictionary for batch execution.
|
|
143
|
+
|
|
144
|
+
This method must be implemented by subclasses to create the quantum programs
|
|
145
|
+
that will be executed as part of the batch. The method operates via side effects:
|
|
146
|
+
it populates `self._programs` (or `self.programs`) with a dictionary mapping
|
|
147
|
+
program identifiers to `QuantumProgram` instances.
|
|
148
|
+
|
|
149
|
+
Implementation Notes:
|
|
150
|
+
- Subclasses should call `super().create_programs()` first to initialize
|
|
151
|
+
internal state (queue, events, etc.) and validate that no programs
|
|
152
|
+
already exist.
|
|
153
|
+
- After calling super(), subclasses should populate `self.programs` or
|
|
154
|
+
`self._programs` with their program instances.
|
|
155
|
+
- Program identifiers can be any hashable type (e.g., strings, tuples).
|
|
156
|
+
Common patterns include strings like "program_1", "program_2" or tuples like
|
|
157
|
+
('A', 5) for partitioned problems.
|
|
158
|
+
|
|
159
|
+
Side Effects:
|
|
160
|
+
- Populates `self._programs` with program instances.
|
|
161
|
+
- Initializes `self._queue` for progress reporting.
|
|
162
|
+
- Initializes `self._done_event` if `max_iterations` attribute exists.
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
RuntimeError: If programs already exist (should call `reset()` first).
|
|
166
|
+
|
|
167
|
+
Example:
|
|
168
|
+
>>> def create_programs(self):
|
|
169
|
+
... super().create_programs()
|
|
170
|
+
... self.programs = {
|
|
171
|
+
... "prog1": QAOA(...),
|
|
172
|
+
... "prog2": QAOA(...),
|
|
173
|
+
... }
|
|
174
|
+
"""
|
|
175
|
+
if len(self._programs) > 0:
|
|
176
|
+
raise RuntimeError(
|
|
177
|
+
"Some programs already exist. "
|
|
178
|
+
"Clear the program dictionary before creating new ones by using batch.reset()."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
self._queue = Queue()
|
|
100
182
|
|
|
101
183
|
if hasattr(self, "max_iterations"):
|
|
102
184
|
self._done_event = Event()
|
|
103
185
|
|
|
104
186
|
def reset(self):
|
|
105
|
-
|
|
187
|
+
"""
|
|
188
|
+
Reset the batch to its initial state.
|
|
189
|
+
|
|
190
|
+
Clears all programs, stops any running executors, terminates listener threads,
|
|
191
|
+
and stops progress bars. This allows the batch to be reused for a new set of
|
|
192
|
+
programs.
|
|
193
|
+
|
|
194
|
+
Note:
|
|
195
|
+
Any running programs will be forcefully stopped. Results from incomplete
|
|
196
|
+
programs will be lost.
|
|
197
|
+
"""
|
|
198
|
+
self._programs.clear()
|
|
106
199
|
|
|
107
200
|
# Stop any active executor
|
|
108
201
|
if self._executor is not None:
|
|
@@ -118,66 +211,111 @@ class ProgramBatch(ABC):
|
|
|
118
211
|
if getattr(self, "_listener_thread", None) is not None:
|
|
119
212
|
self._listener_thread.join(timeout=1)
|
|
120
213
|
if self._listener_thread.is_alive():
|
|
121
|
-
warn(
|
|
122
|
-
|
|
123
|
-
)
|
|
124
|
-
else:
|
|
125
|
-
self._listener_thread = None
|
|
126
|
-
self._queue.close()
|
|
127
|
-
self._queue.join_thread()
|
|
128
|
-
self._queue = None
|
|
214
|
+
warn("Listener thread did not terminate within timeout.")
|
|
215
|
+
self._listener_thread = None
|
|
129
216
|
|
|
130
217
|
# Stop the progress bar if it's still active
|
|
131
218
|
if getattr(self, "_progress_bar", None) is not None:
|
|
132
219
|
try:
|
|
133
220
|
self._progress_bar.stop()
|
|
134
|
-
self._progress_bar = None
|
|
135
221
|
except Exception:
|
|
136
222
|
pass # Already stopped or not running
|
|
137
|
-
|
|
223
|
+
self._progress_bar = None
|
|
138
224
|
self._pb_task_map.clear()
|
|
139
225
|
|
|
140
|
-
def
|
|
141
|
-
|
|
226
|
+
def _atexit_cleanup_hook(self):
|
|
227
|
+
# This hook is only registered for non-blocking runs.
|
|
228
|
+
if self._executor is not None:
|
|
229
|
+
warn(
|
|
230
|
+
"A non-blocking ProgramBatch run was not explicitly closed with "
|
|
231
|
+
"'join()'. The batch was cleaned up automatically on exit.",
|
|
232
|
+
UserWarning,
|
|
233
|
+
)
|
|
234
|
+
self.reset()
|
|
235
|
+
|
|
236
|
+
def _add_program_to_executor(self, program: QuantumProgram) -> Future:
|
|
237
|
+
"""
|
|
238
|
+
Add a quantum program to the thread pool executor for execution.
|
|
239
|
+
|
|
240
|
+
Sets up the program with cancellation support and progress tracking, then
|
|
241
|
+
submits it for execution in a separate thread.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
program (QuantumProgram): The quantum program to execute.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
Future: A Future object representing the program's execution.
|
|
248
|
+
"""
|
|
249
|
+
if hasattr(program, "_set_cancellation_event"):
|
|
250
|
+
program._set_cancellation_event(self._cancellation_event)
|
|
142
251
|
|
|
143
252
|
if self._progress_bar is not None:
|
|
144
253
|
with self._pb_lock:
|
|
145
|
-
self._pb_task_map[program.
|
|
254
|
+
self._pb_task_map[program.program_id] = self._progress_bar.add_task(
|
|
146
255
|
"",
|
|
147
|
-
job_name=f"
|
|
256
|
+
job_name=f"Program {program.program_id}",
|
|
148
257
|
total=self.max_iterations,
|
|
149
258
|
completed=0,
|
|
150
|
-
poll_attempt=0,
|
|
151
259
|
message="",
|
|
152
|
-
final_status="",
|
|
153
|
-
mode=("simulation" if self._is_local else "network"),
|
|
154
260
|
)
|
|
155
261
|
|
|
156
|
-
|
|
262
|
+
return self._executor.submit(self._task_fn, program)
|
|
263
|
+
|
|
264
|
+
def run(self, blocking: bool = False):
|
|
265
|
+
"""
|
|
266
|
+
Execute all programs in the batch.
|
|
267
|
+
|
|
268
|
+
Starts all quantum programs in parallel using a thread pool. Can run in
|
|
269
|
+
blocking or non-blocking mode.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
blocking (bool, optional): If True, waits for all programs to complete
|
|
273
|
+
before returning. If False, returns immediately and programs run in
|
|
274
|
+
the background. Defaults to False.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
ProgramBatch: Returns self for method chaining.
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
RuntimeError: If a batch is already running or if no programs have been
|
|
281
|
+
created.
|
|
282
|
+
|
|
283
|
+
Note:
|
|
284
|
+
In non-blocking mode, call `join()` later to wait for completion and
|
|
285
|
+
collect results.
|
|
286
|
+
"""
|
|
157
287
|
if self._executor is not None:
|
|
158
288
|
raise RuntimeError("A batch is already being run.")
|
|
159
289
|
|
|
160
|
-
if len(self.
|
|
290
|
+
if len(self._programs) == 0:
|
|
161
291
|
raise RuntimeError("No programs to run.")
|
|
162
292
|
|
|
163
293
|
self._progress_bar = (
|
|
164
|
-
make_progress_bar(
|
|
165
|
-
max_retries=None if self._is_local else self.backend.max_retries,
|
|
166
|
-
is_jupyter=self._is_jupyter,
|
|
167
|
-
)
|
|
294
|
+
make_progress_bar(is_jupyter=self._is_jupyter)
|
|
168
295
|
if hasattr(self, "max_iterations")
|
|
169
296
|
else None
|
|
170
297
|
)
|
|
171
298
|
|
|
172
|
-
|
|
299
|
+
# Validate that all program instances are unique to prevent thread-safety issues
|
|
300
|
+
program_instances = list(self._programs.values())
|
|
301
|
+
if len(set(program_instances)) != len(program_instances):
|
|
302
|
+
raise RuntimeError(
|
|
303
|
+
"Duplicate program instances detected in batch. "
|
|
304
|
+
"QuantumProgram instances are stateful and NOT thread-safe. "
|
|
305
|
+
"You must provide a unique instance for each program ID."
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
self._executor = ThreadPoolExecutor()
|
|
309
|
+
self._cancellation_event = Event()
|
|
173
310
|
self.futures = []
|
|
311
|
+
self._future_to_program = {}
|
|
174
312
|
self._pb_task_map = {}
|
|
175
313
|
self._pb_lock = Lock()
|
|
176
314
|
|
|
177
315
|
if self._progress_bar is not None:
|
|
178
316
|
self._progress_bar.start()
|
|
179
317
|
self._listener_thread = Thread(
|
|
180
|
-
target=
|
|
318
|
+
target=_queue_listener,
|
|
181
319
|
args=(
|
|
182
320
|
self._queue,
|
|
183
321
|
self._progress_bar,
|
|
@@ -190,46 +328,218 @@ class ProgramBatch(ABC):
|
|
|
190
328
|
)
|
|
191
329
|
self._listener_thread.start()
|
|
192
330
|
|
|
193
|
-
for program in self.
|
|
194
|
-
self.
|
|
331
|
+
for program in self._programs.values():
|
|
332
|
+
future = self._add_program_to_executor(program)
|
|
333
|
+
self.futures.append(future)
|
|
334
|
+
self._future_to_program[future] = program
|
|
335
|
+
|
|
336
|
+
if not blocking:
|
|
337
|
+
# Arm safety net
|
|
338
|
+
atexit.register(self._atexit_cleanup_hook)
|
|
339
|
+
else:
|
|
340
|
+
self.join()
|
|
195
341
|
|
|
196
|
-
|
|
342
|
+
return self
|
|
343
|
+
|
|
344
|
+
def check_all_done(self) -> bool:
|
|
345
|
+
"""
|
|
346
|
+
Check if all programs in the batch have completed execution.
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
bool: True if all programs are finished (successfully or with errors),
|
|
350
|
+
False if any are still running.
|
|
351
|
+
"""
|
|
197
352
|
return all(future.done() for future in self.futures)
|
|
198
353
|
|
|
199
|
-
def
|
|
354
|
+
def _collect_completed_results(self, completed_futures: list):
|
|
355
|
+
"""
|
|
356
|
+
Collects results from any futures that have completed successfully.
|
|
357
|
+
Appends (circuit_count, run_time) tuples to the completed_futures list.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
completed_futures: List to append results to
|
|
361
|
+
"""
|
|
362
|
+
for future in self.futures:
|
|
363
|
+
if future.done() and not future.cancelled():
|
|
364
|
+
try:
|
|
365
|
+
completed_futures.append(future.result())
|
|
366
|
+
except Exception:
|
|
367
|
+
pass # Skip failed futures
|
|
368
|
+
|
|
369
|
+
def _handle_cancellation(self):
|
|
370
|
+
"""
|
|
371
|
+
Handles cancellation gracefully, providing accurate feedback by checking
|
|
372
|
+
the result of future.cancel().
|
|
373
|
+
"""
|
|
374
|
+
self._cancellation_event.set()
|
|
375
|
+
|
|
376
|
+
successfully_cancelled = []
|
|
377
|
+
unstoppable_futures = []
|
|
378
|
+
|
|
379
|
+
# --- Phase 1: Attempt to cancel all non-finished tasks ---
|
|
380
|
+
for future, program in self._future_to_program.items():
|
|
381
|
+
if future.done():
|
|
382
|
+
continue
|
|
383
|
+
|
|
384
|
+
task_id = self._pb_task_map.get(program.program_id)
|
|
385
|
+
if self._progress_bar and task_id is not None:
|
|
386
|
+
cancel_result = future.cancel()
|
|
387
|
+
if cancel_result:
|
|
388
|
+
# The task was pending and was successfully cancelled.
|
|
389
|
+
successfully_cancelled.append(program)
|
|
390
|
+
else:
|
|
391
|
+
# The task is already running and cannot be stopped.
|
|
392
|
+
# Attempt to cancel the cloud job to allow polling loop to exit.
|
|
393
|
+
program.cancel_unfinished_job()
|
|
394
|
+
unstoppable_futures.append(future)
|
|
395
|
+
self._progress_bar.update(
|
|
396
|
+
task_id,
|
|
397
|
+
message="Finishing... ⏳",
|
|
398
|
+
refresh=self._is_jupyter,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# --- Phase 2: Immediately mark the successfully cancelled tasks ---
|
|
402
|
+
for program in successfully_cancelled:
|
|
403
|
+
task_id = self._pb_task_map.get(program.program_id)
|
|
404
|
+
if self._progress_bar and task_id is not None:
|
|
405
|
+
self._progress_bar.update(
|
|
406
|
+
task_id,
|
|
407
|
+
final_status="Cancelled",
|
|
408
|
+
message="Cancelled by user",
|
|
409
|
+
refresh=self._is_jupyter,
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# --- Phase 3: Wait for the unstoppable tasks to finish ---
|
|
413
|
+
if unstoppable_futures:
|
|
414
|
+
for future in as_completed(unstoppable_futures):
|
|
415
|
+
program = self._future_to_program[future]
|
|
416
|
+
task_id = self._pb_task_map.get(program.program_id)
|
|
417
|
+
if self._progress_bar and task_id is not None:
|
|
418
|
+
self._progress_bar.update(
|
|
419
|
+
task_id,
|
|
420
|
+
final_status="Aborted",
|
|
421
|
+
message="Completed during cancellation",
|
|
422
|
+
refresh=self._is_jupyter,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
def join(self):
|
|
426
|
+
"""
|
|
427
|
+
Wait for all programs in the batch to complete and collect results.
|
|
428
|
+
|
|
429
|
+
Blocks until all programs finish execution, aggregating their circuit counts
|
|
430
|
+
and run times. Handles keyboard interrupts gracefully by attempting to cancel
|
|
431
|
+
remaining programs.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
bool or None: Returns False if interrupted by KeyboardInterrupt, None otherwise.
|
|
435
|
+
|
|
436
|
+
Raises:
|
|
437
|
+
RuntimeError: If any program fails with an exception, after cancelling
|
|
438
|
+
remaining programs.
|
|
439
|
+
|
|
440
|
+
Note:
|
|
441
|
+
This method should be called after `run(blocking=False)` to wait for
|
|
442
|
+
completion. It's automatically called when using `run(blocking=True)`.
|
|
443
|
+
"""
|
|
200
444
|
if self._executor is None:
|
|
201
445
|
return
|
|
202
446
|
|
|
203
|
-
|
|
447
|
+
completed_futures = []
|
|
204
448
|
try:
|
|
205
|
-
#
|
|
449
|
+
# The as_completed iterator will yield futures as they finish.
|
|
450
|
+
# If a task fails, future.result() will raise the exception immediately.
|
|
206
451
|
for future in as_completed(self.futures):
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
452
|
+
completed_futures.append(future.result())
|
|
453
|
+
|
|
454
|
+
except KeyboardInterrupt:
|
|
455
|
+
|
|
456
|
+
if self._progress_bar is not None:
|
|
457
|
+
self._progress_bar.console.print(
|
|
458
|
+
"[bold yellow]Shutdown signal received, waiting for programs to finish current iteration...[/bold yellow]"
|
|
459
|
+
)
|
|
460
|
+
self._handle_cancellation()
|
|
461
|
+
|
|
462
|
+
# Collect results from any futures that completed before/during cancellation
|
|
463
|
+
self._collect_completed_results(completed_futures)
|
|
464
|
+
|
|
465
|
+
return False
|
|
466
|
+
|
|
467
|
+
except Exception as e:
|
|
468
|
+
# A task has failed. Print the error and cancel the rest.
|
|
469
|
+
print(f"A task failed with an exception. Cancelling remaining tasks...")
|
|
470
|
+
traceback.print_exception(type(e), e, e.__traceback__)
|
|
471
|
+
|
|
472
|
+
# Collect results from any futures that completed before the failure
|
|
473
|
+
self._collect_completed_results(completed_futures)
|
|
474
|
+
|
|
475
|
+
# Cancel all other futures that have not yet completed.
|
|
476
|
+
for f in self.futures:
|
|
477
|
+
f.cancel()
|
|
478
|
+
|
|
479
|
+
# Re-raise a new error to indicate the batch failed.
|
|
480
|
+
raise RuntimeError("Batch execution failed and was cancelled.") from e
|
|
211
481
|
|
|
212
482
|
finally:
|
|
213
|
-
|
|
483
|
+
# Aggregate results from completed futures
|
|
484
|
+
if completed_futures:
|
|
485
|
+
self._total_circuit_count += sum(
|
|
486
|
+
result[0] for result in completed_futures
|
|
487
|
+
)
|
|
488
|
+
self._total_run_time += sum(result[1] for result in completed_futures)
|
|
489
|
+
self.futures.clear()
|
|
490
|
+
|
|
491
|
+
self._executor.shutdown(wait=False)
|
|
214
492
|
self._executor = None
|
|
215
493
|
|
|
216
494
|
if self._progress_bar is not None:
|
|
495
|
+
self._queue.join()
|
|
217
496
|
self._done_event.set()
|
|
218
497
|
self._listener_thread.join()
|
|
498
|
+
self._progress_bar.stop()
|
|
219
499
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if self._progress_bar is not None:
|
|
227
|
-
self._progress_bar.stop()
|
|
228
|
-
|
|
229
|
-
self._total_circuit_count += sum(future.result()[0] for future in self.futures)
|
|
230
|
-
self._total_run_time += sum(future.result()[1] for future in self.futures)
|
|
231
|
-
self.futures = []
|
|
500
|
+
# After successful cleanup, try to unregister the hook.
|
|
501
|
+
try:
|
|
502
|
+
atexit.unregister(self._atexit_cleanup_hook)
|
|
503
|
+
except TypeError:
|
|
504
|
+
pass
|
|
232
505
|
|
|
233
506
|
@abstractmethod
|
|
234
507
|
def aggregate_results(self):
|
|
235
|
-
|
|
508
|
+
"""
|
|
509
|
+
Aggregate results from all programs in the batch after execution.
|
|
510
|
+
|
|
511
|
+
This is an abstract method that must be implemented by subclasses. The base
|
|
512
|
+
implementation performs validation checks:
|
|
513
|
+
- Ensures programs have been created
|
|
514
|
+
- Waits for any running programs to complete (calls join() if needed)
|
|
515
|
+
- Verifies that all programs have completed execution (non-empty losses_history)
|
|
516
|
+
|
|
517
|
+
Subclasses should call super().aggregate_results() first, then implement
|
|
518
|
+
their own aggregation logic to combine results from all programs. The
|
|
519
|
+
aggregation should handle different result formats (counts dictionary,
|
|
520
|
+
expectation values, etc.) as appropriate for the specific use case.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
The aggregated result, format depends on the subclass implementation.
|
|
524
|
+
|
|
525
|
+
Raises:
|
|
526
|
+
RuntimeError: If no programs exist, or if programs haven't completed
|
|
527
|
+
execution (empty losses_history).
|
|
528
|
+
"""
|
|
529
|
+
if len(self._programs) == 0:
|
|
530
|
+
raise RuntimeError("No programs to aggregate. Run create_programs() first.")
|
|
531
|
+
|
|
532
|
+
if self._executor is not None:
|
|
533
|
+
self.join()
|
|
534
|
+
|
|
535
|
+
# Suppress warnings when checking for empty losses_history for cleanliness sake
|
|
536
|
+
with warnings.catch_warnings():
|
|
537
|
+
warnings.filterwarnings(
|
|
538
|
+
"ignore", category=UserWarning, message=".*losses_history is empty.*"
|
|
539
|
+
)
|
|
540
|
+
if any(
|
|
541
|
+
len(program.losses_history) == 0 for program in self._programs.values()
|
|
542
|
+
):
|
|
543
|
+
raise RuntimeError(
|
|
544
|
+
"Some/All programs have empty losses. Did you call run()?"
|
|
545
|
+
)
|