qoro-divi 0.3.3__py3-none-any.whl → 0.3.5__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.

Potentially problematic release.


This version of qoro-divi might be problematic. Click here for more details.

Files changed (74) hide show
  1. divi/__init__.py +1 -2
  2. divi/backends/__init__.py +7 -0
  3. divi/backends/_circuit_runner.py +46 -0
  4. divi/{parallel_simulator.py → backends/_parallel_simulator.py} +136 -53
  5. divi/backends/_qoro_service.py +531 -0
  6. divi/circuits/__init__.py +5 -0
  7. divi/circuits/_core.py +226 -0
  8. divi/{qasm.py → circuits/qasm.py} +21 -2
  9. divi/{exp → extern}/cirq/_validator.py +9 -7
  10. divi/qprog/__init__.py +18 -5
  11. divi/qprog/algorithms/__init__.py +14 -0
  12. divi/qprog/algorithms/_ansatze.py +311 -0
  13. divi/qprog/{_qaoa.py → algorithms/_qaoa.py} +69 -41
  14. divi/qprog/{_vqe.py → algorithms/_vqe.py} +79 -135
  15. divi/qprog/batch.py +239 -55
  16. divi/qprog/exceptions.py +9 -0
  17. divi/qprog/optimizers.py +219 -18
  18. divi/qprog/quantum_program.py +389 -57
  19. divi/qprog/workflows/__init__.py +10 -0
  20. divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +3 -34
  21. divi/qprog/{_qubo_partitioning.py → workflows/_qubo_partitioning.py} +42 -25
  22. divi/qprog/{_vqe_sweep.py → workflows/_vqe_sweep.py} +59 -26
  23. divi/reporting/__init__.py +7 -0
  24. divi/reporting/_pbar.py +112 -0
  25. divi/{qlogger.py → reporting/_qlogger.py} +37 -2
  26. divi/{reporter.py → reporting/_reporter.py} +8 -14
  27. divi/utils.py +49 -10
  28. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/METADATA +2 -1
  29. qoro_divi-0.3.5.dist-info/RECORD +69 -0
  30. divi/_pbar.py +0 -70
  31. divi/circuits.py +0 -139
  32. divi/interfaces.py +0 -25
  33. divi/qoro_service.py +0 -425
  34. qoro_divi-0.3.3.dist-info/RECORD +0 -62
  35. /divi/{qpu_system.py → backends/_qpu_system.py} +0 -0
  36. /divi/{qem.py → circuits/qem.py} +0 -0
  37. /divi/{exp → extern}/cirq/__init__.py +0 -0
  38. /divi/{exp → extern}/cirq/_lexer.py +0 -0
  39. /divi/{exp → extern}/cirq/_parser.py +0 -0
  40. /divi/{exp → extern}/cirq/_qasm_export.py +0 -0
  41. /divi/{exp → extern}/cirq/_qasm_import.py +0 -0
  42. /divi/{exp → extern}/cirq/exception.py +0 -0
  43. /divi/{exp → extern}/scipy/_cobyla.py +0 -0
  44. /divi/{exp → extern}/scipy/pyprima/LICENCE.txt +0 -0
  45. /divi/{exp → extern}/scipy/pyprima/__init__.py +0 -0
  46. /divi/{exp → extern}/scipy/pyprima/cobyla/__init__.py +0 -0
  47. /divi/{exp → extern}/scipy/pyprima/cobyla/cobyla.py +0 -0
  48. /divi/{exp → extern}/scipy/pyprima/cobyla/cobylb.py +0 -0
  49. /divi/{exp → extern}/scipy/pyprima/cobyla/geometry.py +0 -0
  50. /divi/{exp → extern}/scipy/pyprima/cobyla/initialize.py +0 -0
  51. /divi/{exp → extern}/scipy/pyprima/cobyla/trustregion.py +0 -0
  52. /divi/{exp → extern}/scipy/pyprima/cobyla/update.py +0 -0
  53. /divi/{exp → extern}/scipy/pyprima/common/__init__.py +0 -0
  54. /divi/{exp → extern}/scipy/pyprima/common/_bounds.py +0 -0
  55. /divi/{exp → extern}/scipy/pyprima/common/_linear_constraints.py +0 -0
  56. /divi/{exp → extern}/scipy/pyprima/common/_nonlinear_constraints.py +0 -0
  57. /divi/{exp → extern}/scipy/pyprima/common/_project.py +0 -0
  58. /divi/{exp → extern}/scipy/pyprima/common/checkbreak.py +0 -0
  59. /divi/{exp → extern}/scipy/pyprima/common/consts.py +0 -0
  60. /divi/{exp → extern}/scipy/pyprima/common/evaluate.py +0 -0
  61. /divi/{exp → extern}/scipy/pyprima/common/history.py +0 -0
  62. /divi/{exp → extern}/scipy/pyprima/common/infos.py +0 -0
  63. /divi/{exp → extern}/scipy/pyprima/common/linalg.py +0 -0
  64. /divi/{exp → extern}/scipy/pyprima/common/message.py +0 -0
  65. /divi/{exp → extern}/scipy/pyprima/common/powalg.py +0 -0
  66. /divi/{exp → extern}/scipy/pyprima/common/preproc.py +0 -0
  67. /divi/{exp → extern}/scipy/pyprima/common/present.py +0 -0
  68. /divi/{exp → extern}/scipy/pyprima/common/ratio.py +0 -0
  69. /divi/{exp → extern}/scipy/pyprima/common/redrho.py +0 -0
  70. /divi/{exp → extern}/scipy/pyprima/common/selectx.py +0 -0
  71. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSE +0 -0
  72. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/.license-header +0 -0
  73. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/Apache-2.0.txt +0 -0
  74. {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/WHEEL +0 -0
divi/qprog/batch.py CHANGED
@@ -5,29 +5,25 @@
5
5
  import atexit
6
6
  import traceback
7
7
  from abc import ABC, abstractmethod
8
- from concurrent.futures import ProcessPoolExecutor, as_completed
9
- from multiprocessing import Event, Manager
10
- from multiprocessing.synchronize import Event as EventClass
8
+ from concurrent.futures import Future, ThreadPoolExecutor, as_completed
11
9
  from queue import Empty, Queue
12
- from threading import Lock, Thread
10
+ from threading import Event, Lock, Thread
13
11
  from typing import Any
14
12
  from warnings import warn
15
13
 
16
14
  from rich.console import Console
17
15
  from rich.progress import Progress, TaskID
18
16
 
19
- from divi._pbar import make_progress_bar
20
- from divi.interfaces import CircuitRunner
21
- from divi.parallel_simulator import ParallelSimulator
22
- from divi.qlogger import disable_logging
17
+ from divi.backends import CircuitRunner, ParallelSimulator
23
18
  from divi.qprog.quantum_program import QuantumProgram
19
+ from divi.reporting import disable_logging, make_progress_bar
24
20
 
25
21
 
26
- def queue_listener(
22
+ def _queue_listener(
27
23
  queue: Queue,
28
24
  progress_bar: Progress,
29
25
  pb_task_map: dict[QuantumProgram, TaskID],
30
- done_event: EventClass,
26
+ done_event: Event,
31
27
  is_jupyter: bool,
32
28
  lock: Lock,
33
29
  ):
@@ -62,6 +58,7 @@ def queue_listener(
62
58
  update_args["refresh"] = is_jupyter
63
59
 
64
60
  progress_bar.update(task_id, **update_args)
61
+ queue.task_done()
65
62
 
66
63
 
67
64
  def _default_task_function(program: QuantumProgram):
@@ -92,7 +89,7 @@ class ProgramBatch(ABC):
92
89
  self.backend = backend
93
90
  self._executor = None
94
91
  self._task_fn = _default_task_function
95
- self.programs = {}
92
+ self._programs = {}
96
93
 
97
94
  self._total_circuit_count = 0
98
95
  self._total_run_time = 0.0
@@ -105,28 +102,67 @@ class ProgramBatch(ABC):
105
102
 
106
103
  @property
107
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
+ """
108
111
  return self._total_circuit_count
109
112
 
110
113
  @property
111
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
+ """
112
121
  return self._total_run_time
113
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
+
114
140
  @abstractmethod
115
141
  def create_programs(self):
116
- if len(self.programs) > 0:
142
+ if len(self._programs) > 0:
117
143
  raise RuntimeError(
118
144
  "Some programs already exist. "
119
145
  "Clear the program dictionary before creating new ones by using batch.reset()."
120
146
  )
121
147
 
122
- self._manager = Manager()
123
- self._queue = self._manager.Queue()
148
+ self._queue = Queue()
124
149
 
125
150
  if hasattr(self, "max_iterations"):
126
151
  self._done_event = Event()
127
152
 
128
153
  def reset(self):
129
- self.programs.clear()
154
+ """
155
+ Reset the batch to its initial state.
156
+
157
+ Clears all programs, stops any running executors, terminates listener threads,
158
+ and stops progress bars. This allows the batch to be reused for a new set of
159
+ programs.
160
+
161
+ Note:
162
+ Any running programs will be forcefully stopped. Results from incomplete
163
+ programs will be lost.
164
+ """
165
+ self._programs.clear()
130
166
 
131
167
  # Stop any active executor
132
168
  if self._executor is not None:
@@ -145,12 +181,6 @@ class ProgramBatch(ABC):
145
181
  warn("Listener thread did not terminate within timeout.")
146
182
  self._listener_thread = None
147
183
 
148
- # Shut down the manager process, which handles the queue cleanup.
149
- if hasattr(self, "_manager") and self._manager is not None:
150
- self._manager.shutdown()
151
- self._manager = None
152
- self._queue = None
153
-
154
184
  # Stop the progress bar if it's still active
155
185
  if getattr(self, "_progress_bar", None) is not None:
156
186
  try:
@@ -170,8 +200,21 @@ class ProgramBatch(ABC):
170
200
  )
171
201
  self.reset()
172
202
 
173
- def add_program_to_executor(self, program):
174
- self.futures.append(self._executor.submit(self._task_fn, program))
203
+ def _add_program_to_executor(self, program: QuantumProgram) -> Future:
204
+ """
205
+ Add a quantum program to the thread pool executor for execution.
206
+
207
+ Sets up the program with cancellation support and progress tracking, then
208
+ submits it for execution in a separate thread.
209
+
210
+ Args:
211
+ program (QuantumProgram): The quantum program to execute.
212
+
213
+ Returns:
214
+ Future: A Future object representing the program's execution.
215
+ """
216
+ if hasattr(program, "_set_cancellation_event"):
217
+ program._set_cancellation_event(self._cancellation_event)
175
218
 
176
219
  if self._progress_bar is not None:
177
220
  with self._pb_lock:
@@ -180,17 +223,39 @@ class ProgramBatch(ABC):
180
223
  job_name=f"Job {program.job_id}",
181
224
  total=self.max_iterations,
182
225
  completed=0,
183
- poll_attempt=0,
184
226
  message="",
185
- final_status="",
186
227
  mode=("simulation" if self._is_local else "network"),
187
228
  )
188
229
 
230
+ return self._executor.submit(self._task_fn, program)
231
+
189
232
  def run(self, blocking: bool = False):
233
+ """
234
+ Execute all programs in the batch.
235
+
236
+ Starts all quantum programs in parallel using a thread pool. Can run in
237
+ blocking or non-blocking mode.
238
+
239
+ Args:
240
+ blocking (bool, optional): If True, waits for all programs to complete
241
+ before returning. If False, returns immediately and programs run in
242
+ the background. Defaults to False.
243
+
244
+ Returns:
245
+ ProgramBatch: Returns self for method chaining.
246
+
247
+ Raises:
248
+ RuntimeError: If a batch is already running or if no programs have been
249
+ created.
250
+
251
+ Note:
252
+ In non-blocking mode, call `join()` later to wait for completion and
253
+ collect results.
254
+ """
190
255
  if self._executor is not None:
191
256
  raise RuntimeError("A batch is already being run.")
192
257
 
193
- if len(self.programs) == 0:
258
+ if len(self._programs) == 0:
194
259
  raise RuntimeError("No programs to run.")
195
260
 
196
261
  self._progress_bar = (
@@ -199,15 +264,17 @@ class ProgramBatch(ABC):
199
264
  else None
200
265
  )
201
266
 
202
- self._executor = ProcessPoolExecutor()
267
+ self._executor = ThreadPoolExecutor()
268
+ self._cancellation_event = Event()
203
269
  self.futures = []
270
+ self._future_to_program = {}
204
271
  self._pb_task_map = {}
205
272
  self._pb_lock = Lock()
206
273
 
207
274
  if self._progress_bar is not None:
208
275
  self._progress_bar.start()
209
276
  self._listener_thread = Thread(
210
- target=queue_listener,
277
+ target=_queue_listener,
211
278
  args=(
212
279
  self._queue,
213
280
  self._progress_bar,
@@ -220,8 +287,10 @@ class ProgramBatch(ABC):
220
287
  )
221
288
  self._listener_thread.start()
222
289
 
223
- for program in self.programs.values():
224
- self.add_program_to_executor(program)
290
+ for program in self._programs.values():
291
+ future = self._add_program_to_executor(program)
292
+ self.futures.append(future)
293
+ self._future_to_program[future] = program
225
294
 
226
295
  if not blocking:
227
296
  # Arm safety net
@@ -231,60 +300,175 @@ class ProgramBatch(ABC):
231
300
 
232
301
  return self
233
302
 
234
- def check_all_done(self):
303
+ def check_all_done(self) -> bool:
304
+ """
305
+ Check if all programs in the batch have completed execution.
306
+
307
+ Returns:
308
+ bool: True if all programs are finished (successfully or with errors),
309
+ False if any are still running.
310
+ """
235
311
  return all(future.done() for future in self.futures)
236
312
 
313
+ def _collect_completed_results(self, completed_futures: list):
314
+ """
315
+ Collects results from any futures that have completed successfully.
316
+ Appends (circuit_count, run_time) tuples to the completed_futures list.
317
+
318
+ Args:
319
+ completed_futures: List to append results to
320
+ """
321
+ for future in self.futures:
322
+ if future.done() and not future.cancelled():
323
+ try:
324
+ completed_futures.append(future.result())
325
+ except Exception:
326
+ pass # Skip failed futures
327
+
328
+ def _handle_cancellation(self):
329
+ """
330
+ Handles cancellation gracefully, providing accurate feedback by checking
331
+ the result of future.cancel().
332
+ """
333
+ self._cancellation_event.set()
334
+
335
+ successfully_cancelled = []
336
+ unstoppable_futures = []
337
+
338
+ # --- Phase 1: Attempt to cancel all non-finished tasks ---
339
+ for future, program in self._future_to_program.items():
340
+ if future.done():
341
+ continue
342
+
343
+ task_id = self._pb_task_map.get(program.job_id)
344
+ if self._progress_bar and task_id is not None:
345
+ cancel_result = future.cancel()
346
+ if cancel_result:
347
+ # The task was pending and was successfully cancelled.
348
+ successfully_cancelled.append(program)
349
+ else:
350
+ # The task is already running and cannot be stopped.
351
+ unstoppable_futures.append(future)
352
+ self._progress_bar.update(
353
+ task_id,
354
+ message="Finishing... ⏳",
355
+ refresh=self._is_jupyter,
356
+ )
357
+
358
+ # --- Phase 2: Immediately mark the successfully cancelled tasks ---
359
+ for program in successfully_cancelled:
360
+ task_id = self._pb_task_map.get(program.job_id)
361
+ if self._progress_bar and task_id is not None:
362
+ self._progress_bar.update(
363
+ task_id,
364
+ final_status="Cancelled",
365
+ message="Cancelled by user",
366
+ refresh=self._is_jupyter,
367
+ )
368
+
369
+ # --- Phase 3: Wait for the unstoppable tasks to finish ---
370
+ if unstoppable_futures:
371
+ for future in as_completed(unstoppable_futures):
372
+ program = self._future_to_program[future]
373
+ task_id = self._pb_task_map.get(program.job_id)
374
+ if self._progress_bar and task_id is not None:
375
+ self._progress_bar.update(
376
+ task_id,
377
+ final_status="Aborted",
378
+ message="Completed during cancellation",
379
+ refresh=self._is_jupyter,
380
+ )
381
+
237
382
  def join(self):
383
+ """
384
+ Wait for all programs in the batch to complete and collect results.
385
+
386
+ Blocks until all programs finish execution, aggregating their circuit counts
387
+ and run times. Handles keyboard interrupts gracefully by attempting to cancel
388
+ remaining programs.
389
+
390
+ Returns:
391
+ bool or None: Returns False if interrupted by KeyboardInterrupt, None otherwise.
392
+
393
+ Raises:
394
+ RuntimeError: If any program fails with an exception, after cancelling
395
+ remaining programs.
396
+
397
+ Note:
398
+ This method should be called after `run(blocking=False)` to wait for
399
+ completion. It's automatically called when using `run(blocking=True)`.
400
+ """
238
401
  if self._executor is None:
239
402
  return
240
403
 
241
- exceptions = []
404
+ completed_futures = []
242
405
  try:
243
- # Ensure all futures are completed and handle exceptions.
406
+ # The as_completed iterator will yield futures as they finish.
407
+ # If a task fails, future.result() will raise the exception immediately.
244
408
  for future in as_completed(self.futures):
245
- try:
246
- future.result() # Raises an exception if the task failed.
247
- except Exception as e:
248
- exceptions.append(e)
409
+ completed_futures.append(future.result())
410
+
411
+ except KeyboardInterrupt:
412
+
413
+ if self._progress_bar is not None:
414
+ self._progress_bar.console.print(
415
+ "[bold yellow]Shutdown signal received, waiting for programs to finish current iteration...[/bold yellow]"
416
+ )
417
+ self._handle_cancellation()
418
+
419
+ # Collect results from any futures that completed before/during cancellation
420
+ self._collect_completed_results(completed_futures)
421
+
422
+ return False
423
+
424
+ except Exception as e:
425
+ # A task has failed. Print the error and cancel the rest.
426
+ print(f"A task failed with an exception. Cancelling remaining tasks...")
427
+ traceback.print_exception(type(e), e, e.__traceback__)
428
+
429
+ # Collect results from any futures that completed before the failure
430
+ self._collect_completed_results(completed_futures)
431
+
432
+ # Cancel all other futures that have not yet completed.
433
+ for f in self.futures:
434
+ f.cancel()
435
+
436
+ # Re-raise a new error to indicate the batch failed.
437
+ raise RuntimeError("Batch execution failed and was cancelled.") from e
249
438
 
250
439
  finally:
251
- self._executor.shutdown(wait=True, cancel_futures=False)
440
+ # Aggregate results from completed futures
441
+ if completed_futures:
442
+ self._total_circuit_count += sum(
443
+ result[0] for result in completed_futures
444
+ )
445
+ self._total_run_time += sum(result[1] for result in completed_futures)
446
+ self.futures.clear()
447
+
448
+ self._executor.shutdown(wait=False)
252
449
  self._executor = None
253
450
 
254
451
  if self._progress_bar is not None:
452
+ self._queue.join()
255
453
  self._done_event.set()
256
454
  self._listener_thread.join()
257
-
258
- if exceptions:
259
- for i, exc in enumerate(exceptions, 1):
260
- print(f"Task {i} failed with exception:")
261
- traceback.print_exception(type(exc), exc, exc.__traceback__)
262
- raise RuntimeError("One or more tasks failed. Check logs for details.")
263
-
264
- if self._progress_bar is not None:
265
- self._progress_bar.stop()
266
-
267
- self._total_circuit_count += sum(future.result()[0] for future in self.futures)
268
- self._total_run_time += sum(future.result()[1] for future in self.futures)
269
- self.futures.clear()
455
+ self._progress_bar.stop()
270
456
 
271
457
  # After successful cleanup, try to unregister the hook.
272
- # This will only succeed if it was a non-blocking run.
273
458
  try:
274
459
  atexit.unregister(self._atexit_cleanup_hook)
275
460
  except TypeError:
276
- # This is expected for blocking runs where the hook was never registered.
277
461
  pass
278
462
 
279
463
  @abstractmethod
280
464
  def aggregate_results(self):
281
- if len(self.programs) == 0:
465
+ if len(self._programs) == 0:
282
466
  raise RuntimeError("No programs to aggregate. Run create_programs() first.")
283
467
 
284
468
  if self._executor is not None:
285
469
  self.join()
286
470
 
287
- if any(len(program.losses) == 0 for program in self.programs.values()):
471
+ if any(len(program.losses) == 0 for program in self._programs.values()):
288
472
  raise RuntimeError(
289
473
  "Some/All programs have empty losses. Did you call run()?"
290
474
  )
@@ -0,0 +1,9 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+
6
+ class _CancelledError(Exception):
7
+ """Internal exception to signal a task to stop due to cancellation."""
8
+
9
+ pass