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.
- divi/__init__.py +1 -2
- divi/backends/__init__.py +7 -0
- divi/backends/_circuit_runner.py +46 -0
- divi/{parallel_simulator.py → backends/_parallel_simulator.py} +136 -53
- divi/backends/_qoro_service.py +531 -0
- divi/circuits/__init__.py +5 -0
- divi/circuits/_core.py +226 -0
- divi/{qasm.py → circuits/qasm.py} +21 -2
- divi/{exp → extern}/cirq/_validator.py +9 -7
- divi/qprog/__init__.py +18 -5
- divi/qprog/algorithms/__init__.py +14 -0
- divi/qprog/algorithms/_ansatze.py +311 -0
- divi/qprog/{_qaoa.py → algorithms/_qaoa.py} +69 -41
- divi/qprog/{_vqe.py → algorithms/_vqe.py} +79 -135
- divi/qprog/batch.py +239 -55
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +219 -18
- divi/qprog/quantum_program.py +389 -57
- divi/qprog/workflows/__init__.py +10 -0
- divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +3 -34
- divi/qprog/{_qubo_partitioning.py → workflows/_qubo_partitioning.py} +42 -25
- divi/qprog/{_vqe_sweep.py → workflows/_vqe_sweep.py} +59 -26
- divi/reporting/__init__.py +7 -0
- divi/reporting/_pbar.py +112 -0
- divi/{qlogger.py → reporting/_qlogger.py} +37 -2
- divi/{reporter.py → reporting/_reporter.py} +8 -14
- divi/utils.py +49 -10
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/METADATA +2 -1
- qoro_divi-0.3.5.dist-info/RECORD +69 -0
- divi/_pbar.py +0 -70
- divi/circuits.py +0 -139
- divi/interfaces.py +0 -25
- divi/qoro_service.py +0 -425
- qoro_divi-0.3.3.dist-info/RECORD +0 -62
- /divi/{qpu_system.py → backends/_qpu_system.py} +0 -0
- /divi/{qem.py → circuits/qem.py} +0 -0
- /divi/{exp → extern}/cirq/__init__.py +0 -0
- /divi/{exp → extern}/cirq/_lexer.py +0 -0
- /divi/{exp → extern}/cirq/_parser.py +0 -0
- /divi/{exp → extern}/cirq/_qasm_export.py +0 -0
- /divi/{exp → extern}/cirq/_qasm_import.py +0 -0
- /divi/{exp → extern}/cirq/exception.py +0 -0
- /divi/{exp → extern}/scipy/_cobyla.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/LICENCE.txt +0 -0
- /divi/{exp → extern}/scipy/pyprima/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/cobyla.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/cobylb.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/geometry.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/initialize.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/trustregion.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/cobyla/update.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/__init__.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_bounds.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_linear_constraints.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_nonlinear_constraints.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/_project.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/checkbreak.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/consts.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/evaluate.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/history.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/infos.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/linalg.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/message.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/powalg.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/preproc.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/present.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/ratio.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/redrho.py +0 -0
- /divi/{exp → extern}/scipy/pyprima/common/selectx.py +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSE +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/.license-header +0 -0
- {qoro_divi-0.3.3.dist-info → qoro_divi-0.3.5.dist-info}/LICENSES/Apache-2.0.txt +0 -0
- {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.
|
|
16
|
-
from divi.qprog.
|
|
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
|
-
|
|
159
|
-
self.
|
|
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
|
|
178
|
-
|
|
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,
|
|
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[
|
|
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[
|
|
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.
|
|
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
|
-
|
|
473
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
min(curr_energies.values())
|
|
486
|
-
|
|
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
|
|
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.
|
|
528
|
+
min(self._programs[(ansatz.name, modifier)].losses[-1].values())
|
|
500
529
|
)
|
|
530
|
+
|
|
501
531
|
plt.plot(
|
|
502
|
-
self.molecule_transformer.bond_modifiers,
|
|
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
|
divi/reporting/_pbar.py
ADDED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
+
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)
|