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
@@ -12,8 +12,8 @@ import numpy as np
12
12
  import scipy.sparse as sps
13
13
  from dimod import BinaryQuadraticModel
14
14
 
15
- from divi.interfaces import CircuitRunner
16
- from divi.qprog._qaoa import QAOA, QUBOProblemTypes
15
+ from divi.backends import CircuitRunner
16
+ from divi.qprog.algorithms import QAOA, QUBOProblemTypes
17
17
  from divi.qprog.batch import ProgramBatch
18
18
  from divi.qprog.optimizers import MonteCarloOptimizer, Optimizer
19
19
  from divi.qprog.quantum_program import QuantumProgram
@@ -49,14 +49,6 @@ def _sanitize_problem_input(qubo: T) -> tuple[T, BinaryQuadraticModel]:
49
49
  raise ValueError(f"Got an unsupported QUBO input format: {type(qubo)}")
50
50
 
51
51
 
52
- def _run_and_compute_solution(program: QuantumProgram):
53
- program.run()
54
-
55
- final_sol_circuit_count, final_sol_run_time = program.compute_final_solution()
56
-
57
- return final_sol_circuit_count, final_sol_run_time
58
-
59
-
60
52
  class QUBOPartitioningQAOA(ProgramBatch):
61
53
  def __init__(
62
54
  self,
@@ -94,10 +86,10 @@ class QUBOPartitioningQAOA(ProgramBatch):
94
86
  self._partitioning = hybrid.Unwind(decomposer)
95
87
  self._aggregating = hybrid.Reduce(hybrid.Lambda(_merge_substates)) | composer
96
88
 
97
- self._task_fn = _run_and_compute_solution
98
-
99
89
  self.max_iterations = max_iterations
100
90
 
91
+ self.trivial_program_ids = set()
92
+
101
93
  self._constructor = partial(
102
94
  QAOA,
103
95
  optimizer=optimizer if optimizer is not None else MonteCarloOptimizer(),
@@ -140,6 +132,12 @@ class QUBOPartitioningQAOA(ProgramBatch):
140
132
  del partition["problem"]
141
133
 
142
134
  prog_id = (string.ascii_uppercase[i], len(partition.subproblem))
135
+ self.prog_id_to_bqm_subproblem_states[prog_id] = partition
136
+
137
+ if partition.subproblem.num_interactions == 0:
138
+ # Skip creating a full QAOA program for this trivial case.
139
+ self.trivial_program_ids.add(prog_id)
140
+ continue
143
141
 
144
142
  ldata, (irow, icol, qdata), _ = partition.subproblem.to_numpy_vectors(
145
143
  partition.subproblem.variables
@@ -155,18 +153,30 @@ class QUBOPartitioningQAOA(ProgramBatch):
155
153
  ),
156
154
  shape=(len(ldata), len(ldata)),
157
155
  )
158
- self.prog_id_to_bqm_subproblem_states[prog_id] = partition
159
- self.programs[prog_id] = self._constructor(
156
+
157
+ self._programs[prog_id] = self._constructor(
160
158
  job_id=prog_id,
161
159
  problem=coo_mat,
162
- losses=self._manager.list(),
163
- probs=self._manager.dict(),
164
- final_params=self._manager.list(),
165
- solution_bitstring=self._manager.list(),
166
160
  progress_queue=self._queue,
167
161
  )
168
162
 
169
163
  def aggregate_results(self):
164
+ """
165
+ Aggregate results from all QUBO subproblems into a global solution.
166
+
167
+ Collects solutions from each partitioned subproblem (both QAOA-optimized and
168
+ trivial ones) and uses the hybrid framework composer to combine them into
169
+ a final solution for the original QUBO problem.
170
+
171
+ Returns:
172
+ tuple: A tuple containing:
173
+ - solution (np.ndarray): Binary solution vector for the QUBO problem.
174
+ - solution_energy (float): Energy/cost of the solution.
175
+
176
+ Raises:
177
+ RuntimeError: If programs haven't been run or if final probabilities
178
+ haven't been computed.
179
+ """
170
180
  super().aggregate_results()
171
181
 
172
182
  if any(len(program.probs) == 0 for program in self.programs.values()):
@@ -174,14 +184,21 @@ class QUBOPartitioningQAOA(ProgramBatch):
174
184
  "Not all final probabilities computed yet. Please call `run()` first."
175
185
  )
176
186
 
177
- for prog_id, subproblem in self.programs.items():
178
- bqm_subproblem_state = self.prog_id_to_bqm_subproblem_states[prog_id]
187
+ for (
188
+ prog_id,
189
+ bqm_subproblem_state,
190
+ ) in self.prog_id_to_bqm_subproblem_states.items():
191
+
192
+ if prog_id in self.trivial_program_ids:
193
+ # Case 1: Trivial problem. Solve classically.
194
+ # The solution is any bitstring (e.g., all zeros) with energy 0.
195
+ var_to_val = {v: 0 for v in bqm_subproblem_state.subproblem.variables}
196
+ else:
197
+ subproblem = self._programs[prog_id]
198
+ var_to_val = dict(
199
+ zip(bqm_subproblem_state.subproblem.variables, subproblem.solution)
200
+ )
179
201
 
180
- curr_final_solution = subproblem.solution
181
-
182
- var_to_val = dict(
183
- zip(bqm_subproblem_state.subproblem.variables, curr_final_solution)
184
- )
185
202
  sample_set = dimod.SampleSet.from_samples(
186
203
  dimod.as_samples(var_to_val), "BINARY", 0
187
204
  )
@@ -14,7 +14,7 @@ import matplotlib.pyplot as plt
14
14
  import numpy as np
15
15
  import pennylane as qml
16
16
 
17
- from divi.qprog import VQE, ProgramBatch, VQEAnsatz
17
+ from divi.qprog import VQE, Ansatz, ProgramBatch
18
18
  from divi.qprog.optimizers import MonteCarloOptimizer, Optimizer
19
19
 
20
20
 
@@ -392,7 +392,7 @@ class VQEHyperparameterSweep(ProgramBatch):
392
392
 
393
393
  def __init__(
394
394
  self,
395
- ansatze: Sequence[VQEAnsatz],
395
+ ansatze: Sequence[Ansatz],
396
396
  molecule_transformer: MoleculeTransformer,
397
397
  optimizer: Optimizer | None = None,
398
398
  max_iterations: int = 10,
@@ -403,7 +403,7 @@ class VQEHyperparameterSweep(ProgramBatch):
403
403
 
404
404
  Parameters
405
405
  ----------
406
- ansatze: Sequence[VQEAnsatz]
406
+ ansatze: Sequence[Ansatz]
407
407
  A sequence of ansatz circuits to test.
408
408
  molecule_transformer: MoleculeTransformer
409
409
  A `MoleculeTransformer` object defining the configuration for
@@ -430,6 +430,15 @@ class VQEHyperparameterSweep(ProgramBatch):
430
430
  )
431
431
 
432
432
  def create_programs(self):
433
+ """
434
+ Create VQE programs for all combinations of ansatze and molecule variants.
435
+
436
+ Generates molecule variants using the configured MoleculeTransformer, then
437
+ creates a VQE program for each (ansatz, molecule_variant) pair.
438
+
439
+ Note:
440
+ Program IDs are tuples of (ansatz_name, bond_modifier_value).
441
+ """
433
442
  super().create_programs()
434
443
 
435
444
  self.molecule_variants = self.molecule_transformer.generate()
@@ -437,17 +446,29 @@ class VQEHyperparameterSweep(ProgramBatch):
437
446
  for ansatz, (modifier, molecule) in product(
438
447
  self.ansatze, self.molecule_variants.items()
439
448
  ):
440
- _job_id = (ansatz, modifier)
441
- self.programs[_job_id] = self._constructor(
449
+ _job_id = (ansatz.name, modifier)
450
+ self._programs[_job_id] = self._constructor(
442
451
  job_id=_job_id,
443
452
  molecule=molecule,
444
453
  ansatz=ansatz,
445
- losses=self._manager.list(),
446
- final_params=self._manager.list(),
447
454
  progress_queue=self._queue,
448
455
  )
449
456
 
450
457
  def aggregate_results(self):
458
+ """
459
+ Find the best ansatz and bond configuration from all VQE runs.
460
+
461
+ Compares the final energies across all ansatz/molecule combinations
462
+ and returns the configuration that achieved the lowest ground state energy.
463
+
464
+ Returns:
465
+ tuple: A tuple containing:
466
+ - best_config (tuple): (ansatz_name, bond_modifier) of the best result.
467
+ - best_energy (float): The lowest energy achieved.
468
+
469
+ Raises:
470
+ RuntimeError: If programs haven't been run or have empty losses.
471
+ """
451
472
  super().aggregate_results()
452
473
 
453
474
  all_energies = {key: prog.losses[-1] for key, prog in self.programs.items()}
@@ -469,37 +490,49 @@ class VQEHyperparameterSweep(ProgramBatch):
469
490
  if self._executor is not None:
470
491
  self.join()
471
492
 
472
- data = []
473
- colors = ["blue", "g", "r", "c", "m", "y", "k"]
493
+ # Get the unique ansatz objects that were actually run
494
+ # Assumes `self.ansatze` is a list of the ansatz instances used.
495
+ unique_ansatze = self.ansatze
474
496
 
475
- ansatz_list = list(VQEAnsatz)
497
+ # Create a stable color mapping for each unique ansatz object
498
+ colors = ["blue", "g", "r", "c", "m", "y", "k"]
499
+ color_map = {
500
+ ansatz: colors[i % len(colors)] for i, ansatz in enumerate(unique_ansatze)
501
+ }
476
502
 
477
503
  if graph_type == "scatter":
478
- for ansatz, modifier in self.programs.keys():
504
+ # Plot each ansatz's results as a separate series for clarity
505
+ for ansatz in unique_ansatze:
506
+ modifiers = []
479
507
  min_energies = []
480
-
481
- curr_energies = self.programs[(ansatz, modifier)].losses[-1]
482
- min_energies.append(
483
- (
484
- modifier,
485
- min(curr_energies.values()),
486
- colors[ansatz_list.index(ansatz)],
487
- )
508
+ for modifier in self.molecule_transformer.bond_modifiers:
509
+ program_key = (ansatz.name, modifier)
510
+ if program_key in self._programs:
511
+ modifiers.append(modifier)
512
+ curr_energies = self._programs[program_key].losses[-1]
513
+ min_energies.append(min(curr_energies.values()))
514
+
515
+ # Use the new .name property for the label and the color_map
516
+ plt.scatter(
517
+ modifiers,
518
+ min_energies,
519
+ color=color_map[ansatz],
520
+ label=ansatz.name,
488
521
  )
489
- data.extend(min_energies)
490
-
491
- x, y, z = zip(*data)
492
- plt.scatter(x, y, color=z, label=ansatz)
493
522
 
494
523
  elif graph_type == "line":
495
- for ansatz in self.ansatze:
524
+ for ansatz in unique_ansatze:
496
525
  energies = []
497
526
  for modifier in self.molecule_transformer.bond_modifiers:
498
527
  energies.append(
499
- min(self.programs[(ansatz, modifier)].losses[-1].values())
528
+ min(self._programs[(ansatz.name, modifier)].losses[-1].values())
500
529
  )
530
+
501
531
  plt.plot(
502
- self.molecule_transformer.bond_modifiers, energies, label=ansatz
532
+ self.molecule_transformer.bond_modifiers,
533
+ energies,
534
+ label=ansatz.name,
535
+ color=color_map[ansatz],
503
536
  )
504
537
 
505
538
  plt.xlabel(
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from ._pbar import make_progress_bar
6
+ from ._qlogger import disable_logging, enable_logging
7
+ from ._reporter import LoggingProgressReporter, ProgressReporter, QueueProgressReporter
@@ -0,0 +1,112 @@
1
+ # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from rich.progress import (
6
+ BarColumn,
7
+ MofNCompleteColumn,
8
+ Progress,
9
+ ProgressColumn,
10
+ SpinnerColumn,
11
+ TextColumn,
12
+ TimeElapsedColumn,
13
+ )
14
+ from rich.text import Text
15
+
16
+
17
+ class _UnfinishedTaskWrapper:
18
+ """Wrapper that forces a task to appear unfinished for spinner animation."""
19
+
20
+ def __init__(self, task):
21
+ self._task = task
22
+
23
+ def __getattr__(self, name):
24
+ if name == "finished":
25
+ return False
26
+ return getattr(self._task, name)
27
+
28
+
29
+ class ConditionalSpinnerColumn(ProgressColumn):
30
+ _FINAL_STATUSES = ("Success", "Failed", "Cancelled", "Aborted")
31
+
32
+ def __init__(self):
33
+ super().__init__()
34
+ self.spinner = SpinnerColumn("point")
35
+
36
+ def render(self, task):
37
+ status = task.fields.get("final_status")
38
+
39
+ if status in self._FINAL_STATUSES:
40
+ return Text("")
41
+
42
+ # Force the task to appear unfinished for spinner animation
43
+ return self.spinner.render(_UnfinishedTaskWrapper(task))
44
+
45
+
46
+ class PhaseStatusColumn(ProgressColumn):
47
+ def __init__(self, table_column=None):
48
+ super().__init__(table_column)
49
+
50
+ def render(self, task):
51
+ final_status = task.fields.get("final_status")
52
+
53
+ if final_status == "Success":
54
+ return Text("• Success! ✅", style="bold green")
55
+ elif final_status == "Failed":
56
+ return Text("• Failed! ❌", style="bold red")
57
+ elif final_status == "Cancelled":
58
+ return Text("• Cancelled ⏹️", style="bold yellow")
59
+ elif final_status == "Aborted":
60
+ return Text("• Aborted ⚠️", style="dim magenta")
61
+
62
+ message = task.fields.get("message")
63
+
64
+ poll_attempt = task.fields.get("poll_attempt", 0)
65
+ polling_str = ""
66
+ service_job_id = task.fields.get("service_job_id")
67
+
68
+ if service_job_id:
69
+ split_job_id = service_job_id.split("-")[0]
70
+ job_status = task.fields.get("job_status")
71
+
72
+ if job_status == "COMPLETED":
73
+ polling_str = f" [Job {split_job_id} is complete.]"
74
+ elif poll_attempt > 0:
75
+ max_retries = task.fields.get("max_retries")
76
+ polling_str = f" [Job {split_job_id} is {job_status}. Polling attempt {poll_attempt} / {max_retries}]"
77
+
78
+ final_text = Text(f"[{message}]{polling_str}")
79
+ if service_job_id:
80
+ final_text.highlight_words([split_job_id], "blue")
81
+
82
+ return final_text
83
+
84
+
85
+ def make_progress_bar(is_jupyter: bool = False) -> Progress:
86
+ """
87
+ Create a customized Rich progress bar for tracking quantum program execution.
88
+
89
+ Builds a progress bar with custom columns including job name, completion status,
90
+ elapsed time, spinner, and phase status indicators. Automatically adapts refresh
91
+ behavior for Jupyter notebook environments.
92
+
93
+ Args:
94
+ is_jupyter (bool, optional): Whether the progress bar is being displayed in
95
+ a Jupyter notebook environment. Affects refresh behavior. Defaults to False.
96
+
97
+ Returns:
98
+ Progress: A configured Rich Progress instance with custom columns for
99
+ quantum program tracking.
100
+ """
101
+ return Progress(
102
+ TextColumn("[bold blue]{task.fields[job_name]}"),
103
+ BarColumn(),
104
+ MofNCompleteColumn(),
105
+ TimeElapsedColumn(),
106
+ ConditionalSpinnerColumn(),
107
+ PhaseStatusColumn(),
108
+ # For jupyter notebooks, refresh manually instead
109
+ auto_refresh=not is_jupyter,
110
+ # Give a dummy positive value if is_jupyter
111
+ refresh_per_second=10 if not is_jupyter else 999,
112
+ )
@@ -30,6 +30,18 @@ def _is_jupyter():
30
30
  return False # IPython is not installed
31
31
 
32
32
 
33
+ class CustomFormatter(logging.Formatter):
34
+ """
35
+ A custom log formatter that removes '._reporter' from the logger name.
36
+ """
37
+
38
+ def format(self, record):
39
+ # Modify the record's name attribute in place
40
+ if record.name.endswith("._reporter"):
41
+ record.name = record.name.removesuffix("._reporter")
42
+ return super().format(record)
43
+
44
+
33
45
  class OverwriteStreamHandler(logging.StreamHandler):
34
46
  def __init__(self, stream=None):
35
47
  super().__init__(stream)
@@ -100,10 +112,26 @@ class OverwriteStreamHandler(logging.StreamHandler):
100
112
 
101
113
 
102
114
  def enable_logging(level=logging.INFO):
115
+ """
116
+ Enable logging for the divi package with custom formatting.
117
+
118
+ Sets up a custom logger with an OverwriteStreamHandler that supports
119
+ message overwriting (for progress updates) and removes the '._reporter'
120
+ suffix from logger names.
121
+
122
+ Args:
123
+ level (int, optional): Logging level to set (e.g., logging.INFO,
124
+ logging.DEBUG). Defaults to logging.INFO.
125
+
126
+ Note:
127
+ This function clears any existing handlers and sets up a new handler
128
+ with custom formatting.
129
+ """
103
130
  root_logger = logging.getLogger(__name__.split(".")[0])
104
131
 
105
- formatter = logging.Formatter(
106
- "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
132
+ formatter = CustomFormatter(
133
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s",
134
+ datefmt="%Y-%m-%d %H:%M:%S",
107
135
  )
108
136
 
109
137
  handler = OverwriteStreamHandler(sys.stdout)
@@ -115,6 +143,13 @@ def enable_logging(level=logging.INFO):
115
143
 
116
144
 
117
145
  def disable_logging():
146
+ """
147
+ Disable all logging for the divi package.
148
+
149
+ Removes all handlers and sets the logging level to above CRITICAL,
150
+ effectively suppressing all log messages. This is useful when using
151
+ progress bars that provide visual feedback.
152
+ """
118
153
  root_logger = logging.getLogger(__name__.split(".")[0])
119
154
  root_logger.handlers.clear()
120
155
  root_logger.setLevel(logging.CRITICAL + 1)
@@ -29,12 +29,9 @@ class ProgressReporter(ABC):
29
29
  class QueueProgressReporter(ProgressReporter):
30
30
  """Reports progress by putting structured dictionaries onto a Queue."""
31
31
 
32
- def __init__(
33
- self, job_id: str, progress_queue: Queue, has_final_computation: bool = False
34
- ):
32
+ def __init__(self, job_id: str, progress_queue: Queue):
35
33
  self._job_id = job_id
36
34
  self._queue = progress_queue
37
- self.has_final_computation = has_final_computation
38
35
 
39
36
  def update(self, **kwargs):
40
37
  payload = {"job_id": self._job_id, "progress": 1}
@@ -43,20 +40,19 @@ class QueueProgressReporter(ProgressReporter):
43
40
  def info(self, message: str, **kwargs):
44
41
  payload = {"job_id": self._job_id, "progress": 0, "message": message}
45
42
 
46
- # Determine if this message indicates the job is truly finished.
47
- is_final_step = "Computed Final Solution" in message or (
48
- "Finished Optimization" in message and not self.has_final_computation
49
- )
50
-
51
- if is_final_step:
43
+ if "Finished successfully!" in message:
52
44
  payload["final_status"] = "Success"
53
- elif "poll_attempt" in kwargs:
45
+
46
+ if "poll_attempt" in kwargs:
54
47
  # For polling, remove the message key so the last message persists.
55
48
  del payload["message"]
56
49
  payload["poll_attempt"] = kwargs["poll_attempt"]
57
50
  payload["max_retries"] = kwargs["max_retries"]
58
51
  payload["service_job_id"] = kwargs["service_job_id"]
59
52
  payload["job_status"] = kwargs["job_status"]
53
+ else:
54
+ # For any other message, explicitly reset the polling attempt counter.
55
+ payload["poll_attempt"] = 0
60
56
 
61
57
  self._queue.put(payload)
62
58
 
@@ -82,9 +78,7 @@ class LoggingProgressReporter(ProgressReporter):
82
78
  return
83
79
 
84
80
  if "iteration" in kwargs:
85
- logger.info(
86
- f"Running Iteration #{kwargs['iteration'] + 1} circuits: {message}\r"
87
- )
81
+ logger.info(f"Iteration #{kwargs['iteration'] + 1}: {message}\r")
88
82
  return
89
83
 
90
84
  logger.info(message)
divi/utils.py CHANGED
@@ -13,8 +13,20 @@ import scipy.sparse as sps
13
13
  def _is_sanitized(
14
14
  qubo_matrix: np.ndarray | sps.spmatrix,
15
15
  ) -> np.ndarray | sps.spmatrix:
16
- # Sanitize the QUBO matrix to ensure it is either symmetric or upper triangular.
16
+ """
17
+ Check if a QUBO matrix is either symmetric or upper triangular.
18
+
19
+ This function validates that the input QUBO matrix is in a proper format
20
+ for conversion to an Ising Hamiltonian. The matrix should be either
21
+ symmetric (equal to its transpose) or upper triangular.
17
22
 
23
+ Args:
24
+ qubo_matrix (np.ndarray | sps.spmatrix): The QUBO matrix to validate.
25
+ Can be a dense NumPy array or a sparse SciPy matrix.
26
+
27
+ Returns:
28
+ bool: True if the matrix is symmetric or upper triangular, False otherwise.
29
+ """
18
30
  is_sparse = sps.issparse(qubo_matrix)
19
31
 
20
32
  return (
@@ -33,17 +45,37 @@ def _is_sanitized(
33
45
  def convert_qubo_matrix_to_pennylane_ising(
34
46
  qubo_matrix: np.ndarray | sps.spmatrix,
35
47
  ) -> tuple[qml.operation.Operator, float]:
36
- """Convert QUBO matrix to Ising Hamiltonian in Pennylane.
48
+ """
49
+ Convert a QUBO matrix to an Ising Hamiltonian in PennyLane format.
50
+
51
+ The conversion follows the mapping from QUBO variables x_i ∈ {0,1} to
52
+ Ising variables σ_i ∈ {-1,1} via the transformation x_i = (1 - σ_i)/2. This
53
+ transforms a QUBO minimization problem into an equivalent Ising minimization
54
+ problem.
37
55
 
38
- The conversion follows the mapping:
39
- - QUBO variables x_i {0,1} map to Ising variables s_i ∈ {-1,1} via s_i = 2x_i - 1
40
- - This transforms a QUBO problem into an equivalent Ising problem
56
+ The function handles both dense NumPy arrays and sparse SciPy matrices efficiently.
57
+ If the input matrix is neither symmetric nor upper triangular, it will be
58
+ symmetrized automatically with a warning.
41
59
 
42
60
  Args:
43
- qubo_matrix: The QUBO matrix Q where the objective is to minimize x^T Q x
61
+ qubo_matrix (np.ndarray | sps.spmatrix): The QUBO matrix Q where the
62
+ objective is to minimize x^T Q x. Can be a dense NumPy array or a
63
+ sparse SciPy matrix (any format). Should be square and either
64
+ symmetric or upper triangular.
44
65
 
45
66
  Returns:
46
- A tuple of (Ising Hamiltonian as a PennyLane operator, constant term)
67
+ tuple[qml.operation.Operator, float]: A tuple containing:
68
+ - Ising Hamiltonian as a PennyLane operator (sum of Pauli Z terms)
69
+ - Constant offset term to be added to energy calculations
70
+
71
+ Raises:
72
+ UserWarning: If the QUBO matrix is neither symmetric nor upper triangular.
73
+
74
+ Example:
75
+ >>> import numpy as np
76
+ >>> qubo = np.array([[1, 2], [0, 3]])
77
+ >>> hamiltonian, offset = convert_qubo_matrix_to_pennylane_ising(qubo)
78
+ >>> print(f"Offset: {offset}")
47
79
  """
48
80
 
49
81
  if not _is_sanitized(qubo_matrix):
@@ -56,17 +88,24 @@ def convert_qubo_matrix_to_pennylane_ising(
56
88
  is_sparse = sps.issparse(qubo_matrix)
57
89
  backend = sps if is_sparse else np
58
90
 
91
+ symmetrized_qubo = (qubo_matrix + qubo_matrix.T) / 2
92
+
59
93
  # Gather non-zero indices in the upper triangle of the matrix
60
94
  triu_matrix = backend.triu(
61
- qubo_matrix,
95
+ symmetrized_qubo,
62
96
  **(
63
97
  {"format": qubo_matrix.format if qubo_matrix.format != "coo" else "csc"}
64
98
  if is_sparse
65
99
  else {}
66
100
  ),
67
101
  )
68
- rows, cols = triu_matrix.nonzero()
69
- values = triu_matrix[rows, cols].A1 if is_sparse else triu_matrix[rows, cols]
102
+
103
+ if is_sparse:
104
+ coo_mat = triu_matrix.tocoo()
105
+ rows, cols, values = coo_mat.row, coo_mat.col, coo_mat.data
106
+ else:
107
+ rows, cols = triu_matrix.nonzero()
108
+ values = triu_matrix[rows, cols]
70
109
 
71
110
  n = qubo_matrix.shape[0]
72
111
  linear_terms = np.zeros(n)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qoro-divi
3
- Version: 0.3.3
3
+ Version: 0.3.5
4
4
  Summary: A Python library to automate generating, parallelizing, and executing quantum programs.
5
5
  Author: Ahmed Darwish
6
6
  Author-email: ahmed@qoroquantum.de
@@ -17,6 +17,7 @@ Requires-Dist: networkx (>=3.5,<4.0)
17
17
  Requires-Dist: pennylane (>=0.42.3,<0.43.0)
18
18
  Requires-Dist: ply (>=3.11,<4.0)
19
19
  Requires-Dist: pymetis (>=2025.1.1,<2026.0.0)
20
+ Requires-Dist: pymoo (>=0.6.1.5,<0.7.0.0)
20
21
  Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
21
22
  Requires-Dist: qiskit (<2.0)
22
23
  Requires-Dist: qiskit-aer (>=0.17.1,<0.18.0)