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
@@ -0,0 +1,262 @@
1
+ # SPDX-FileCopyrightText: 2026 Qoro Quantum Ltd <divi@qoroquantum.de>
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+
5
+ from warnings import warn
6
+
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+ import pennylane as qml
10
+ import sympy as sp
11
+
12
+ from divi.circuits import MetaCircuit
13
+ from divi.qprog.typing import QUBOProblemTypes, qubo_to_matrix
14
+
15
+ from ._vqe import VQE
16
+
17
+ # Pre-computed 8-bit popcount table for O(1) lookups
18
+ _POPCOUNT_TABLE_8BIT = np.array([bin(i).count("1") for i in range(256)], dtype=np.uint8)
19
+
20
+
21
+ def _fast_popcount_parity(arr_input: npt.NDArray[np.integer]) -> npt.NDArray[np.uint8]:
22
+ """
23
+ Vectorized calculation of (popcount % 2) for an array of integers.
24
+ Uses numpy view casting for extreme speed over large arrays.
25
+ """
26
+ # 1. Ensure array is uint64
27
+ arr_u64 = arr_input.astype(np.uint64)
28
+
29
+ # 2. View as bytes to use 8-bit lookup table
30
+ arr_bytes = arr_u64.view(np.uint8).reshape(arr_input.shape + (8,))
31
+
32
+ # 3. Lookup and sum bits
33
+ total_bits = _POPCOUNT_TABLE_8BIT[arr_bytes].sum(axis=-1)
34
+
35
+ # 4. Return Parity (0 or 1)
36
+ return total_bits % 2
37
+
38
+
39
+ def _aggregate_param_group(
40
+ param_group: list[tuple[str, dict[str, int]]],
41
+ merge_counts_fn,
42
+ ) -> tuple[list[str], npt.NDArray[np.float64], float]:
43
+ """Aggregate a parameter group into states, counts, and total shots."""
44
+ shots_dict = merge_counts_fn(param_group)
45
+ state_strings = list(shots_dict.keys())
46
+ counts = np.array(list(shots_dict.values()), dtype=float)
47
+ total_shots = counts.sum()
48
+ return state_strings, counts, float(total_shots)
49
+
50
+
51
+ def _decode_parities(
52
+ state_strings: list[str], variable_masks_u64: npt.NDArray[np.uint64]
53
+ ) -> npt.NDArray[np.uint8]:
54
+ """Decode bitstring parities using the precomputed variable masks."""
55
+ states = np.array([int(s, 2) for s in state_strings], dtype=np.uint64)
56
+ overlaps = variable_masks_u64[:, None] & states[None, :]
57
+ return _fast_popcount_parity(overlaps)
58
+
59
+
60
+ def _compute_soft_energy(
61
+ parities: npt.NDArray[np.uint8],
62
+ probs: npt.NDArray[np.float64],
63
+ alpha: float,
64
+ qubo_matrix: npt.NDArray[np.float64] | np.ndarray,
65
+ ) -> float:
66
+ """Compute the relaxed (soft) QUBO energy from parity expectations."""
67
+ mean_parities = parities.dot(probs)
68
+ z_expectations = 1.0 - (2.0 * mean_parities)
69
+ x_soft = 0.5 * (1.0 + np.tanh(alpha * z_expectations))
70
+ Qx = qubo_matrix @ x_soft
71
+ return float(np.dot(x_soft, Qx))
72
+
73
+
74
+ def _compute_hard_cvar_energy(
75
+ parities: npt.NDArray[np.uint8],
76
+ counts: npt.NDArray[np.float64],
77
+ total_shots: float,
78
+ qubo_matrix: npt.NDArray[np.float64] | np.ndarray,
79
+ alpha_cvar: float = 0.25,
80
+ ) -> float:
81
+ """Compute CVaR energy from sampled hard assignments."""
82
+ x_vals = 1.0 - parities.astype(float)
83
+ Qx = qubo_matrix @ x_vals
84
+ energies = np.einsum("ij,ij->j", x_vals, Qx)
85
+
86
+ sorted_indices = np.argsort(energies)
87
+ sorted_energies = energies[sorted_indices]
88
+ sorted_counts = counts[sorted_indices]
89
+
90
+ cutoff_count = int(np.ceil(alpha_cvar * total_shots))
91
+ accumulated_counts = np.cumsum(sorted_counts)
92
+ limit_idx = np.searchsorted(accumulated_counts, cutoff_count)
93
+
94
+ cvar_energy = 0.0
95
+ count_sum = 0
96
+ if limit_idx > 0:
97
+ cvar_energy += np.sum(sorted_energies[:limit_idx] * sorted_counts[:limit_idx])
98
+ count_sum += np.sum(sorted_counts[:limit_idx])
99
+
100
+ remaining = cutoff_count - count_sum
101
+ cvar_energy += sorted_energies[limit_idx] * remaining
102
+ return float(cvar_energy / cutoff_count)
103
+
104
+
105
+ class PCE(VQE):
106
+ """
107
+ Generalized Pauli Correlation Encoding (PCE) VQE.
108
+
109
+ Encodes an N-variable QUBO into O(log2(N)) qubits by mapping each variable
110
+ to a parity (Pauli-Z correlation) of the measured bitstring. The algorithm
111
+ uses the measurement distribution to estimate these parities, applies a
112
+ smooth relaxation when `alpha` is small, and evaluates the classical QUBO
113
+ objective: E = x.T @ Q @ x. For larger `alpha`, it switches to a discrete
114
+ objective (CVaR over sampled energies) for harder convergence.
115
+ """
116
+
117
+ def __init__(
118
+ self,
119
+ qubo_matrix: QUBOProblemTypes,
120
+ n_qubits: int | None = None,
121
+ alpha: float = 2.0,
122
+ **kwargs,
123
+ ):
124
+ """
125
+ Args:
126
+ qubo_matrix (QUBOProblemTypes): The N x N matrix to minimize. Accepts
127
+ a dense array, sparse matrix, list, or BinaryQuadraticModel.
128
+ n_qubits (int | None): Optional override. Must be >= ceil(log2(N)).
129
+ Larger values increase circuit size without adding representational power.
130
+ alpha (float): Scaling factor for the tanh() activation. Higher = harder
131
+ binary constraints, Lower = smoother gradient.
132
+ """
133
+
134
+ self.qubo_matrix = qubo_to_matrix(qubo_matrix)
135
+ self.n_vars = self.qubo_matrix.shape[0]
136
+ self.alpha = alpha
137
+ self._use_soft_objective = self.alpha < 5.0
138
+ self._final_vector: npt.NDArray[np.integer] | None = None
139
+
140
+ if kwargs.get("qem_protocol") is not None:
141
+ raise ValueError("PCE does not currently support qem_protocol.")
142
+
143
+ # Calculate required qubits (Logarithmic Scaling)
144
+ min_qubits = int(np.ceil(np.log2(self.n_vars + 1)))
145
+ if n_qubits is not None and n_qubits < min_qubits:
146
+ raise ValueError(
147
+ "n_qubits must be >= ceil(log2(N + 1)) to represent all variables. "
148
+ f"Got n_qubits={n_qubits}, minimum={min_qubits}."
149
+ )
150
+ if n_qubits is not None and n_qubits > min_qubits:
151
+ warn(
152
+ "n_qubits exceeds the minimum required; extra qubits increase circuit "
153
+ "size and can add noise without representing more variables.",
154
+ UserWarning,
155
+ )
156
+ self.n_qubits = n_qubits if n_qubits is not None else min_qubits
157
+
158
+ # Pre-compute U64 masks for the fast broadcasting step later
159
+ self._variable_masks_u64 = np.arange(1, self.n_vars + 1, dtype=np.uint64)
160
+
161
+ # Placeholder Hamiltonian required by VQE; we care about the measurement
162
+ # probability distribution, and Z-basis measurements provide it.
163
+ placeholder_hamiltonian = qml.Hamiltonian(
164
+ [1.0] * self.n_qubits, [qml.PauliZ(i) for i in range(self.n_qubits)]
165
+ )
166
+ super().__init__(hamiltonian=placeholder_hamiltonian, **kwargs)
167
+
168
+ def _create_meta_circuits_dict(self) -> dict[str, MetaCircuit]:
169
+ """Create meta circuits, handling the edge case of zero parameters."""
170
+ n_params = self.ansatz.n_params_per_layer(
171
+ self.n_qubits, n_electrons=self.n_electrons
172
+ )
173
+
174
+ weights_syms = sp.symarray("w", (self.n_layers, n_params))
175
+
176
+ ops = self.ansatz.build(
177
+ weights_syms,
178
+ n_qubits=self.n_qubits,
179
+ n_layers=self.n_layers,
180
+ n_electrons=self.n_electrons,
181
+ )
182
+
183
+ return {
184
+ "cost_circuit": self._meta_circuit_factory(
185
+ qml.tape.QuantumScript(
186
+ ops=ops, measurements=[qml.expval(self._cost_hamiltonian)]
187
+ ),
188
+ symbols=weights_syms.flatten(),
189
+ ),
190
+ "meas_circuit": self._meta_circuit_factory(
191
+ qml.tape.QuantumScript(ops=ops, measurements=[qml.probs()]),
192
+ symbols=weights_syms.flatten(),
193
+ grouping_strategy="wires",
194
+ ),
195
+ }
196
+
197
+ def _post_process_results(
198
+ self, results: dict[str, dict[str, int]]
199
+ ) -> dict[int, float]:
200
+ """
201
+ Calculates loss.
202
+ If self.alpha < 5.0, computes 'Soft Energy' (Relaxed VQE) for smooth gradients.
203
+ If self.alpha >= 5.0, computes 'Hard CVaR Energy' for final convergence.
204
+ """
205
+
206
+ # Return raw probabilities if requested (skip processing)
207
+ if getattr(self, "_is_compute_probabilities", False):
208
+ return super()._post_process_results(results)
209
+
210
+ losses = {}
211
+
212
+ for p_idx, qem_groups in self._group_results(results).items():
213
+ # PCE ignores QEM ids; aggregate all shots for this parameter set.
214
+ param_group = [
215
+ ("0", shots)
216
+ for shots_list in qem_groups.values()
217
+ for shots in shots_list
218
+ ]
219
+
220
+ state_strings, counts, total_shots = _aggregate_param_group(
221
+ param_group, self._merge_param_group_counts
222
+ )
223
+
224
+ parities = _decode_parities(state_strings, self._variable_masks_u64)
225
+ if self._use_soft_objective:
226
+ probs = counts / total_shots
227
+ losses[p_idx] = _compute_soft_energy(
228
+ parities, probs, self.alpha, self.qubo_matrix
229
+ )
230
+ else:
231
+ losses[p_idx] = _compute_hard_cvar_energy(
232
+ parities, counts, total_shots, self.qubo_matrix
233
+ )
234
+
235
+ return losses
236
+
237
+ def _perform_final_computation(self, **kwargs) -> None:
238
+ """Compute the final eigenstate and decode it into a PCE vector."""
239
+ super()._perform_final_computation(**kwargs)
240
+
241
+ if self._eigenstate is None:
242
+ self._final_vector = None
243
+ return
244
+
245
+ best_bitstring = "".join(str(x) for x in self._eigenstate)
246
+ state_int = int(best_bitstring, 2)
247
+ state_u64 = np.array([state_int], dtype=np.uint64)
248
+
249
+ overlaps = self._variable_masks_u64[:, None] & state_u64[None, :]
250
+ parities = _fast_popcount_parity(overlaps).flatten()
251
+ self._final_vector = 1 - parities
252
+
253
+ @property
254
+ def solution(self) -> npt.NDArray[np.integer]:
255
+ """
256
+ Returns the final optimized vector (hard binary 0/1) based on the best parameters found.
257
+ You must run .run() before calling this.
258
+ """
259
+ if self._final_vector is None:
260
+ raise RuntimeError("Run the VQE optimization first.")
261
+
262
+ return self._final_vector