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.
- divi/__init__.py +1 -2
- divi/backends/__init__.py +10 -0
- divi/backends/_backend_properties_conversion.py +227 -0
- divi/backends/_circuit_runner.py +70 -0
- divi/backends/_execution_result.py +70 -0
- divi/backends/_parallel_simulator.py +486 -0
- divi/backends/_qoro_service.py +663 -0
- divi/backends/_qpu_system.py +101 -0
- divi/backends/_results_processing.py +133 -0
- divi/circuits/__init__.py +13 -0
- divi/{exp/cirq → circuits/_cirq}/__init__.py +1 -2
- divi/circuits/_cirq/_parser.py +110 -0
- divi/circuits/_cirq/_qasm_export.py +78 -0
- divi/circuits/_core.py +391 -0
- divi/{qasm.py → circuits/_qasm_conversion.py} +73 -14
- divi/circuits/_qasm_validation.py +694 -0
- divi/qprog/__init__.py +27 -8
- divi/qprog/_expectation.py +181 -0
- divi/qprog/_hamiltonians.py +281 -0
- divi/qprog/algorithms/__init__.py +16 -0
- divi/qprog/algorithms/_ansatze.py +368 -0
- divi/qprog/algorithms/_custom_vqa.py +263 -0
- divi/qprog/algorithms/_pce.py +262 -0
- divi/qprog/algorithms/_qaoa.py +579 -0
- divi/qprog/algorithms/_vqe.py +262 -0
- divi/qprog/batch.py +387 -74
- divi/qprog/checkpointing.py +556 -0
- divi/qprog/exceptions.py +9 -0
- divi/qprog/optimizers.py +1014 -43
- divi/qprog/quantum_program.py +243 -412
- divi/qprog/typing.py +62 -0
- divi/qprog/variational_quantum_algorithm.py +1208 -0
- divi/qprog/workflows/__init__.py +10 -0
- divi/qprog/{_graph_partitioning.py → workflows/_graph_partitioning.py} +139 -95
- divi/qprog/workflows/_qubo_partitioning.py +221 -0
- divi/qprog/workflows/_vqe_sweep.py +560 -0
- divi/reporting/__init__.py +7 -0
- divi/reporting/_pbar.py +127 -0
- divi/reporting/_qlogger.py +68 -0
- divi/reporting/_reporter.py +155 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/METADATA +43 -15
- qoro_divi-0.6.0.dist-info/RECORD +47 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info}/WHEEL +1 -1
- qoro_divi-0.6.0.dist-info/licenses/LICENSES/.license-header +3 -0
- divi/_pbar.py +0 -73
- divi/circuits.py +0 -139
- divi/exp/cirq/_lexer.py +0 -126
- divi/exp/cirq/_parser.py +0 -889
- divi/exp/cirq/_qasm_export.py +0 -37
- divi/exp/cirq/_qasm_import.py +0 -35
- divi/exp/cirq/exception.py +0 -21
- divi/exp/scipy/_cobyla.py +0 -342
- divi/exp/scipy/pyprima/LICENCE.txt +0 -28
- divi/exp/scipy/pyprima/__init__.py +0 -263
- divi/exp/scipy/pyprima/cobyla/__init__.py +0 -0
- divi/exp/scipy/pyprima/cobyla/cobyla.py +0 -599
- divi/exp/scipy/pyprima/cobyla/cobylb.py +0 -849
- divi/exp/scipy/pyprima/cobyla/geometry.py +0 -240
- divi/exp/scipy/pyprima/cobyla/initialize.py +0 -269
- divi/exp/scipy/pyprima/cobyla/trustregion.py +0 -540
- divi/exp/scipy/pyprima/cobyla/update.py +0 -331
- divi/exp/scipy/pyprima/common/__init__.py +0 -0
- divi/exp/scipy/pyprima/common/_bounds.py +0 -41
- divi/exp/scipy/pyprima/common/_linear_constraints.py +0 -46
- divi/exp/scipy/pyprima/common/_nonlinear_constraints.py +0 -64
- divi/exp/scipy/pyprima/common/_project.py +0 -224
- divi/exp/scipy/pyprima/common/checkbreak.py +0 -107
- divi/exp/scipy/pyprima/common/consts.py +0 -48
- divi/exp/scipy/pyprima/common/evaluate.py +0 -101
- divi/exp/scipy/pyprima/common/history.py +0 -39
- divi/exp/scipy/pyprima/common/infos.py +0 -30
- divi/exp/scipy/pyprima/common/linalg.py +0 -452
- divi/exp/scipy/pyprima/common/message.py +0 -336
- divi/exp/scipy/pyprima/common/powalg.py +0 -131
- divi/exp/scipy/pyprima/common/preproc.py +0 -393
- divi/exp/scipy/pyprima/common/present.py +0 -5
- divi/exp/scipy/pyprima/common/ratio.py +0 -56
- divi/exp/scipy/pyprima/common/redrho.py +0 -49
- divi/exp/scipy/pyprima/common/selectx.py +0 -346
- divi/interfaces.py +0 -25
- divi/parallel_simulator.py +0 -258
- divi/qlogger.py +0 -119
- divi/qoro_service.py +0 -343
- divi/qprog/_mlae.py +0 -182
- divi/qprog/_qaoa.py +0 -440
- divi/qprog/_vqe.py +0 -275
- divi/qprog/_vqe_sweep.py +0 -144
- divi/utils.py +0 -116
- qoro_divi-0.2.0b1.dist-info/RECORD +0 -58
- /divi/{qem.py → circuits/qem.py} +0 -0
- {qoro_divi-0.2.0b1.dist-info → qoro_divi-0.6.0.dist-info/licenses}/LICENSE +0 -0
- {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
|