qoro-divi 0.2.0b1__py3-none-any.whl → 0.6.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.
Files changed (92) hide show
  1. divi/__init__.py +1 -2
  2. divi/backends/__init__.py +10 -0
  3. divi/backends/_backend_properties_conversion.py +227 -0
  4. divi/backends/_circuit_runner.py +70 -0
  5. divi/backends/_execution_result.py +70 -0
  6. divi/backends/_parallel_simulator.py +486 -0
  7. divi/backends/_qoro_service.py +663 -0
  8. divi/backends/_qpu_system.py +101 -0
  9. divi/backends/_results_processing.py +133 -0
  10. divi/circuits/__init__.py +13 -0
  11. divi/{exp/cirq → circuits/_cirq}/__init__.py +1 -2
  12. divi/circuits/_cirq/_parser.py +110 -0
  13. divi/circuits/_cirq/_qasm_export.py +78 -0
  14. divi/circuits/_core.py +391 -0
  15. divi/{qasm.py → circuits/_qasm_conversion.py} +73 -14
  16. divi/circuits/_qasm_validation.py +694 -0
  17. divi/qprog/__init__.py +27 -8
  18. divi/qprog/_expectation.py +181 -0
  19. divi/qprog/_hamiltonians.py +281 -0
  20. divi/qprog/algorithms/__init__.py +16 -0
  21. divi/qprog/algorithms/_ansatze.py +368 -0
  22. divi/qprog/algorithms/_custom_vqa.py +263 -0
  23. divi/qprog/algorithms/_pce.py +262 -0
  24. divi/qprog/algorithms/_qaoa.py +579 -0
  25. divi/qprog/algorithms/_vqe.py +262 -0
  26. divi/qprog/batch.py +387 -74
  27. divi/qprog/checkpointing.py +556 -0
  28. divi/qprog/exceptions.py +9 -0
  29. divi/qprog/optimizers.py +1014 -43
  30. divi/qprog/quantum_program.py +243 -412
  31. divi/qprog/typing.py +62 -0
  32. divi/qprog/variational_quantum_algorithm.py +1208 -0
  33. divi/qprog/workflows/__init__.py +10 -0
  34. divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +139 -95
  35. divi/qprog/workflows/_qubo_partitioning.py +221 -0
  36. divi/qprog/workflows/_vqe_sweep.py +560 -0
  37. divi/reporting/__init__.py +7 -0
  38. divi/reporting/_pbar.py +127 -0
  39. divi/reporting/_qlogger.py +68 -0
  40. divi/reporting/_reporter.py +155 -0
  41. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/METADATA +43 -15
  42. qoro_divi-0.6.0.dist-info/RECORD +47 -0
  43. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/WHEEL +1 -1
  44. qoro_divi-0.6.0.dist-info/licenses/LICENSES/.license-header +3 -0
  45. divi/_pbar.py +0 -73
  46. divi/circuits.py +0 -139
  47. divi/exp/cirq/_lexer.py +0 -126
  48. divi/exp/cirq/_parser.py +0 -889
  49. divi/exp/cirq/_qasm_export.py +0 -37
  50. divi/exp/cirq/_qasm_import.py +0 -35
  51. divi/exp/cirq/exception.py +0 -21
  52. divi/exp/scipy/_cobyla.py +0 -342
  53. divi/exp/scipy/pyprima/LICENCE.txt +0 -28
  54. divi/exp/scipy/pyprima/__init__.py +0 -263
  55. divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
  56. divi/exp/scipy/pyprima/cobyla/cobyla.py +0 -599
  57. divi/exp/scipy/pyprima/cobyla/cobylb.py +0 -849
  58. divi/exp/scipy/pyprima/cobyla/geometry.py +0 -240
  59. divi/exp/scipy/pyprima/cobyla/initialize.py +0 -269
  60. divi/exp/scipy/pyprima/cobyla/trustregion.py +0 -540
  61. divi/exp/scipy/pyprima/cobyla/update.py +0 -331
  62. divi/exp/scipy/pyprima/common/__init__.py +0 -0
  63. divi/exp/scipy/pyprima/common/_bounds.py +0 -41
  64. divi/exp/scipy/pyprima/common/_linear_constraints.py +0 -46
  65. divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +0 -64
  66. divi/exp/scipy/pyprima/common/_project.py +0 -224
  67. divi/exp/scipy/pyprima/common/checkbreak.py +0 -107
  68. divi/exp/scipy/pyprima/common/consts.py +0 -48
  69. divi/exp/scipy/pyprima/common/evaluate.py +0 -101
  70. divi/exp/scipy/pyprima/common/history.py +0 -39
  71. divi/exp/scipy/pyprima/common/infos.py +0 -30
  72. divi/exp/scipy/pyprima/common/linalg.py +0 -452
  73. divi/exp/scipy/pyprima/common/message.py +0 -336
  74. divi/exp/scipy/pyprima/common/powalg.py +0 -131
  75. divi/exp/scipy/pyprima/common/preproc.py +0 -393
  76. divi/exp/scipy/pyprima/common/present.py +0 -5
  77. divi/exp/scipy/pyprima/common/ratio.py +0 -56
  78. divi/exp/scipy/pyprima/common/redrho.py +0 -49
  79. divi/exp/scipy/pyprima/common/selectx.py +0 -346
  80. divi/interfaces.py +0 -25
  81. divi/parallel_simulator.py +0 -258
  82. divi/qlogger.py +0 -119
  83. divi/qoro_service.py +0 -343
  84. divi/qprog/_mlae.py +0 -182
  85. divi/qprog/_qaoa.py +0 -440
  86. divi/qprog/_vqe.py +0 -275
  87. divi/qprog/_vqe_sweep.py +0 -144
  88. divi/utils.py +0 -116
  89. qoro_divi-0.2.0b1.dist-info/RECORD +0 -58
  90. /divi/{qem.py → circuits/qem.py} +0 -0
  91. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSE +0 -0
  92. {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSES/Apache-2.0.txt +0 -0
divi/qprog/_qaoa.py DELETED
@@ -1,440 +0,0 @@
1
- # SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
2
- #
3
- # SPDX-License-Identifier: Apache-2.0
4
-
5
- import re
6
- from enum import Enum
7
- from functools import reduce
8
- from typing import Literal, Optional, get_args
9
- from warnings import warn
10
-
11
- import matplotlib.pyplot as plt
12
- import networkx as nx
13
- import numpy as np
14
- import pennylane as qml
15
- import pennylane.qaoa as pqaoa
16
- import rustworkx as rx
17
- import scipy.sparse as sps
18
- import sympy as sp
19
- from qiskit_optimization import QuadraticProgram
20
- from qiskit_optimization.converters import QuadraticProgramToQubo
21
- from qiskit_optimization.problems import VarType
22
-
23
- from divi.circuits import MetaCircuit
24
- from divi.qprog import QuantumProgram
25
- from divi.qprog.optimizers import Optimizer
26
- from divi.utils import convert_qubo_matrix_to_pennylane_ising
27
-
28
- GraphProblemTypes = nx.Graph | rx.PyGraph
29
- QUBOProblemTypes = list | np.ndarray | sps.spmatrix | QuadraticProgram
30
-
31
-
32
- def draw_graph_solution_nodes(main_graph, partition_nodes):
33
- # Create a dictionary for node colors
34
- node_colors = [
35
- "red" if node in partition_nodes else "lightblue" for node in main_graph.nodes()
36
- ]
37
-
38
- plt.figure(figsize=(10, 8))
39
-
40
- pos = nx.spring_layout(main_graph)
41
-
42
- nx.draw_networkx_nodes(main_graph, pos, node_color=node_colors, node_size=500)
43
- nx.draw_networkx_edges(main_graph, pos)
44
- nx.draw_networkx_labels(main_graph, pos, font_size=10, font_weight="bold")
45
-
46
- # Remove axes
47
- plt.axis("off")
48
-
49
- # Show the plot
50
- plt.tight_layout()
51
- plt.show()
52
-
53
-
54
- class GraphProblem(Enum):
55
- MAX_CLIQUE = ("max_clique", "Zeros", "Superposition")
56
- MAX_INDEPENDENT_SET = ("max_independent_set", "Zeros", "Superposition")
57
- MAX_WEIGHT_CYCLE = ("max_weight_cycle", "Superposition", "Superposition")
58
- MAXCUT = ("maxcut", "Superposition", "Superposition")
59
- MIN_VERTEX_COVER = ("min_vertex_cover", "Ones", "Superposition")
60
-
61
- # This is an internal problem with no pennylane equivalent
62
- EDGE_PARTITIONING = ("", "", "")
63
-
64
- def __init__(
65
- self,
66
- pl_string: str,
67
- constrained_initial_state: str,
68
- unconstrained_initial_state: str,
69
- ):
70
- self.pl_string = pl_string
71
-
72
- # Recommended initial state as per Pennylane's documentation.
73
- # Value is duplicated if not applicable to the problem
74
- self.constrained_initial_state = constrained_initial_state
75
- self.unconstrained_initial_state = unconstrained_initial_state
76
-
77
-
78
- _SUPPORTED_INITIAL_STATES_LITERAL = Literal[
79
- "Zeros", "Ones", "Superposition", "Recommended"
80
- ]
81
-
82
-
83
- def _convert_quadratic_program_to_pennylane_ising(qp: QuadraticProgram):
84
- qiskit_sparse_op, constant = qp.to_ising()
85
-
86
- pauli_list = qiskit_sparse_op.paulis
87
-
88
- pennylane_ising = 0.0
89
- for pauli_string, coeff in zip(pauli_list.z, qiskit_sparse_op.coeffs):
90
- sanitized_coeff = coeff.real if np.isreal(coeff) else coeff
91
-
92
- curr_term = (
93
- reduce(
94
- lambda x, y: x @ y,
95
- map(lambda x: qml.Z(x), np.flatnonzero(pauli_string)),
96
- )
97
- * sanitized_coeff.item()
98
- )
99
-
100
- pennylane_ising += curr_term
101
-
102
- return pennylane_ising, constant.item(), pauli_list.num_qubits
103
-
104
-
105
- def _resolve_circuit_layers(
106
- initial_state, problem, graph_problem, **kwargs
107
- ) -> tuple[qml.operation.Operator, qml.operation.Operator, Optional[dict], str]:
108
- """
109
- Generates the cost and mixer hamiltonians for a given problem, in addition to
110
- optional metadata returned by Pennylane if applicable
111
- """
112
-
113
- if isinstance(problem, GraphProblemTypes):
114
- is_constrained = kwargs.pop("is_constrained", True)
115
-
116
- if graph_problem == GraphProblem.MAXCUT:
117
- params = (problem,)
118
- else:
119
- params = (problem, is_constrained)
120
-
121
- if initial_state == "Recommended":
122
- resolved_initial_state = (
123
- graph_problem.constrained_initial_state
124
- if is_constrained
125
- else graph_problem.constrained_initial_state
126
- )
127
- else:
128
- resolved_initial_state = initial_state
129
-
130
- return *getattr(pqaoa, graph_problem.pl_string)(*params), resolved_initial_state
131
- else:
132
- if isinstance(problem, QuadraticProgram):
133
- cost_hamiltonian, constant, n_qubits = (
134
- _convert_quadratic_program_to_pennylane_ising(problem)
135
- )
136
- else:
137
- cost_hamiltonian, constant = convert_qubo_matrix_to_pennylane_ising(problem)
138
-
139
- n_qubits = problem.shape[0]
140
-
141
- return (
142
- cost_hamiltonian,
143
- pqaoa.x_mixer(range(n_qubits)),
144
- {"constant": constant},
145
- "Superposition",
146
- )
147
-
148
-
149
- class QAOA(QuantumProgram):
150
- def __init__(
151
- self,
152
- problem: GraphProblemTypes | QUBOProblemTypes,
153
- graph_problem: Optional[GraphProblem] = None,
154
- n_layers: int = 1,
155
- initial_state: _SUPPORTED_INITIAL_STATES_LITERAL = "Recommended",
156
- optimizer: Optimizer = Optimizer.MONTE_CARLO,
157
- max_iterations: int = 10,
158
- **kwargs,
159
- ):
160
- """
161
- Initialize the QAOA problem.
162
-
163
- Args:
164
- problem: The problem to solve, can either be a graph or a QUBO.
165
- For graph inputs, the graph problem to solve must be provided
166
- through the `graph_problem` variable.
167
- graph_problem (str): The graph problem to solve.
168
- n_layers (int): number of QAOA layers
169
- initial_state (str): The initial state of the circuit
170
- """
171
-
172
- if isinstance(problem, QUBOProblemTypes):
173
- if graph_problem is not None:
174
- warn("Ignoring the 'problem' argument as it is not applicable to QUBO.")
175
-
176
- self.graph_problem = None
177
-
178
- if isinstance(problem, QuadraticProgram):
179
- if (
180
- any(var.vartype != VarType.BINARY for var in problem.variables)
181
- or problem.linear_constraints
182
- or problem.quadratic_constraints
183
- ):
184
- warn(
185
- "Quadratic Program contains non-binary variables. Converting to QUBO."
186
- )
187
- self._qp_converter = QuadraticProgramToQubo()
188
- problem = self._qp_converter.convert(problem)
189
-
190
- self.n_qubits = problem.get_num_vars()
191
- else:
192
- if isinstance(problem, list):
193
- problem = np.array(problem)
194
-
195
- if problem.ndim != 2 or problem.shape[0] != problem.shape[1]:
196
- raise ValueError(
197
- "Invalid QUBO matrix."
198
- f" Got array of shape {problem.shape}."
199
- " Must be a square matrix."
200
- )
201
-
202
- self.n_qubits = problem.shape[1]
203
- else:
204
- if not isinstance(graph_problem, GraphProblem):
205
- raise ValueError(
206
- f"Unsupported Problem. Got '{graph_problem}'. Must be one of type divi.qprog.GraphProblem."
207
- )
208
-
209
- self.graph_problem = graph_problem
210
- self.n_qubits = problem.number_of_nodes()
211
-
212
- self.problem = problem
213
-
214
- if initial_state not in get_args(_SUPPORTED_INITIAL_STATES_LITERAL):
215
- raise ValueError(
216
- f"Unsupported Initial State. Got {initial_state}. Must be one of: {get_args(_SUPPORTED_INITIAL_STATES_LITERAL)}"
217
- )
218
-
219
- # Local Variables
220
- self.n_layers = n_layers
221
- self.optimizer = optimizer
222
- self.max_iterations = max_iterations
223
- self.current_iteration = 0
224
- self._solution_nodes = None
225
- self.n_params = 2
226
- self._is_compute_probabilites = False
227
-
228
- # Shared Variables
229
- self.probs = kwargs.pop("probs", {})
230
-
231
- (
232
- self.cost_hamiltonian,
233
- self.mixer_hamiltonian,
234
- *problem_metadata,
235
- self.initial_state,
236
- ) = _resolve_circuit_layers(
237
- initial_state=initial_state,
238
- problem=problem,
239
- graph_problem=graph_problem,
240
- **kwargs,
241
- )
242
- self.problem_metadata = problem_metadata[0] if problem_metadata else {}
243
-
244
- self.loss_constant = self.problem_metadata.get("constant", 0.0)
245
-
246
- kwargs.pop("is_constrained", None)
247
- super().__init__(**kwargs)
248
-
249
- self._meta_circuits = self._create_meta_circuits_dict()
250
-
251
- @property
252
- def solution(self):
253
- return (
254
- self._solution_nodes
255
- if self.graph_problem is not None
256
- else self._solution_bitstring
257
- )
258
-
259
- def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
260
- """
261
- Generate the meta circuits for the QAOA problem.
262
-
263
- In this method, we generate the scaffolding for the circuits that will be
264
- executed during optimization.
265
- """
266
-
267
- betas = sp.symarray("β", self.n_layers)
268
- gammas = sp.symarray("γ", self.n_layers)
269
-
270
- sym_params = np.vstack((betas, gammas)).transpose()
271
-
272
- def _qaoa_layer(params):
273
- gamma, beta = params
274
- pqaoa.cost_layer(gamma, self.cost_hamiltonian)
275
- pqaoa.mixer_layer(beta, self.mixer_hamiltonian)
276
-
277
- def _prepare_circuit(hamiltonian, params, final_measurement):
278
- """
279
- Prepare the circuit for the QAOA problem.
280
- Args:
281
- hamiltonian (qml.Hamiltonian): The Hamiltonian term to measure
282
- """
283
-
284
- # Note: could've been done as qml.[Insert Gate](wires=range(self.n_qubits))
285
- # but there seems to be a bug with program capture in Pennylane.
286
- # Maybe check when a new version comes out?
287
- if self.initial_state == "Ones":
288
- for i in range(self.n_qubits):
289
- qml.PauliX(wires=i)
290
- elif self.initial_state == "Superposition":
291
- for i in range(self.n_qubits):
292
- qml.Hadamard(wires=i)
293
-
294
- qml.layer(_qaoa_layer, self.n_layers, params)
295
-
296
- if final_measurement:
297
- return qml.probs()
298
- else:
299
- return qml.expval(hamiltonian)
300
-
301
- return {
302
- "cost_circuit": self._meta_circuit_factory(
303
- qml.tape.make_qscript(_prepare_circuit)(
304
- self.cost_hamiltonian, sym_params, final_measurement=False
305
- ),
306
- symbols=sym_params.flatten(),
307
- ),
308
- "meas_circuit": self._meta_circuit_factory(
309
- qml.tape.make_qscript(_prepare_circuit)(
310
- self.cost_hamiltonian, sym_params, final_measurement=True
311
- ),
312
- symbols=sym_params.flatten(),
313
- ),
314
- }
315
-
316
- def _generate_circuits(self):
317
- """
318
- Generate the circuits for the QAOA problem.
319
-
320
- In this method, we generate bulk circuits based on the selected parameters.
321
- """
322
-
323
- circuit_type = (
324
- "cost_circuit" if not self._is_compute_probabilites else "meas_circuit"
325
- )
326
-
327
- for p, params_group in enumerate(self._curr_params):
328
- circuit = self._meta_circuits[circuit_type].initialize_circuit_from_params(
329
- params_group, tag_prefix=f"{p}"
330
- )
331
-
332
- self.circuits.append(circuit)
333
-
334
- def _post_process_results(self, results):
335
- """
336
- Post-process the results of the QAOA problem.
337
-
338
- Returns:
339
- (dict) The losses for each parameter set grouping.
340
- """
341
-
342
- if self._is_compute_probabilites:
343
- return {
344
- outer_k: {
345
- inner_k: inner_v / self.backend.shots
346
- for inner_k, inner_v in outer_v.items()
347
- }
348
- for outer_k, outer_v in results.items()
349
- }
350
-
351
- losses = super()._post_process_results(results)
352
-
353
- return losses
354
-
355
- def _run_final_measurement(self):
356
- self._is_compute_probabilites = True
357
-
358
- self._curr_params = np.array(self.final_params)
359
-
360
- self.circuits[:] = []
361
-
362
- self._generate_circuits()
363
-
364
- self.probs.update(self._dispatch_circuits_and_process_results())
365
-
366
- self._is_compute_probabilites = False
367
-
368
- def compute_final_solution(self):
369
- """
370
- Computes and extracts the final solution from the QAOA optimization process.
371
- This method performs the following steps:
372
- 1. Identifies the best solution index based on the lowest loss value from the last optimization step.
373
- 2. Executes the final measurement circuit to obtain the probability distributions of solutions.
374
- 3. Retrieves the bitstring representing the best solution, correcting for endianness.
375
- 4. Depending on the problem type:
376
- - For QUBO problems, stores the solution as a NumPy array of bits.
377
- - For graph problems, stores the solution as a list of node indices corresponding to '1's in the bitstring.
378
- 5. Returns the total circuit count and total runtime for the optimization process.
379
- Returns:
380
- tuple: A tuple containing:
381
- - int: The total number of circuits executed.
382
- - float: The total runtime of the optimization process.
383
- Raises:
384
- RuntimeError: If more than one/no matching key is found for the best solution index.
385
- """
386
-
387
- # Convert losses dict to list to apply ordinal operations
388
- final_losses_list = list(self.losses[-1].values())
389
-
390
- # Get the index of the smallest loss in the last operation
391
- best_solution_idx = min(
392
- range(len(final_losses_list)),
393
- key=lambda x: final_losses_list.__getitem__(x),
394
- )
395
-
396
- # Insert the measurement circuit here
397
- self._run_final_measurement()
398
-
399
- # Find the key matching the best_solution_idx with possible metadata in between
400
- pattern = re.compile(rf"^{best_solution_idx}(?:_[^_]*)*_0$")
401
- matching_keys = [k for k in self.probs.keys() if pattern.match(k)]
402
-
403
- # Some minor sanity checks
404
- if len(matching_keys) == 0:
405
- raise RuntimeError("No matching key found.")
406
- if len(matching_keys) > 1:
407
- raise RuntimeError(f"More than one matching key found.")
408
-
409
- best_solution_key = matching_keys[0]
410
- # Retrieve the probability distribution dictionary of the best solution
411
- best_solution_probs = self.probs[best_solution_key]
412
-
413
- # Retrieve the bitstring with the actual best solution
414
- # Reverse to account for the endianness difference
415
- best_solution_bitstring = max(best_solution_probs, key=best_solution_probs.get)[
416
- ::-1
417
- ]
418
-
419
- if isinstance(self.problem, QUBOProblemTypes):
420
- self._solution_bitstring = np.fromiter(
421
- best_solution_bitstring, dtype=np.int32
422
- )
423
-
424
- if isinstance(self.problem, GraphProblemTypes):
425
- self._solution_nodes = [
426
- m.start() for m in re.finditer("1", best_solution_bitstring)
427
- ]
428
-
429
- return self._total_circuit_count, self._total_run_time
430
-
431
- def draw_solution(self):
432
- if self.graph_problem is None:
433
- raise RuntimeError(
434
- "The problem is not a graph problem. Cannot draw solution."
435
- )
436
-
437
- if not self._solution_nodes:
438
- self.compute_final_solution()
439
-
440
- draw_graph_solution_nodes(self.problem, self._solution_nodes)