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
divi/qprog/__init__.py
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
5
|
-
# isort: skip_file
|
|
6
5
|
from .quantum_program import QuantumProgram
|
|
6
|
+
from .variational_quantum_algorithm import VariationalQuantumAlgorithm, SolutionEntry
|
|
7
7
|
from .batch import ProgramBatch
|
|
8
|
-
from .
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
from .algorithms import (
|
|
9
|
+
QAOA,
|
|
10
|
+
GraphProblem,
|
|
11
|
+
VQE,
|
|
12
|
+
PCE,
|
|
13
|
+
CustomVQA,
|
|
14
|
+
Ansatz,
|
|
15
|
+
UCCSDAnsatz,
|
|
16
|
+
QAOAAnsatz,
|
|
17
|
+
HardwareEfficientAnsatz,
|
|
18
|
+
HartreeFockAnsatz,
|
|
19
|
+
GenericLayerAnsatz,
|
|
20
|
+
)
|
|
21
|
+
from .workflows import (
|
|
22
|
+
GraphPartitioningQAOA,
|
|
23
|
+
PartitioningConfig,
|
|
24
|
+
QUBOPartitioningQAOA,
|
|
25
|
+
VQEHyperparameterSweep,
|
|
26
|
+
MoleculeTransformer,
|
|
27
|
+
)
|
|
28
|
+
from .optimizers import ScipyOptimizer, ScipyMethod, MonteCarloOptimizer
|
|
29
|
+
from ._hamiltonians import (
|
|
30
|
+
convert_qubo_matrix_to_pennylane_ising,
|
|
31
|
+
convert_hamiltonian_to_pauli_string,
|
|
32
|
+
)
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import numpy.typing as npt
|
|
9
|
+
import pennylane as qml
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_structural_key(obs: qml.operation.Operation) -> tuple[str, ...]:
|
|
13
|
+
"""Generates a hashable, wire-independent key from an observable's structure.
|
|
14
|
+
|
|
15
|
+
This function is used to create a canonical representation of an observable
|
|
16
|
+
based on its constituent Pauli operators, ignoring the wires they act on.
|
|
17
|
+
This key is ideal for caching computed eigenvalues, as observables with the
|
|
18
|
+
same structure (e.g., PauliX(0) @ PauliZ(1) and PauliX(2) @ PauliZ(3))
|
|
19
|
+
share the same eigenvalues. It maps PauliX and PauliY to PauliZ because
|
|
20
|
+
they are all isospectral (have eigenvalues [1, -1]).
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
obs (qml.operation.Operation): A PennyLane observable (e.g., qml.PauliZ(0), qml.PauliX(0) @ qml.PauliY(1)).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
tuple[str, ...]: A tuple of strings representing the structure of the observable,
|
|
27
|
+
e.g., ('PauliZ',) or ('PauliZ', 'PauliZ').
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
# Pennylane returns the same eigenvalues for PauliX and PauliY
|
|
31
|
+
# since it handles diagonalizing gates internally anyway
|
|
32
|
+
name_map = {
|
|
33
|
+
"PauliY": "PauliZ",
|
|
34
|
+
"PauliX": "PauliZ",
|
|
35
|
+
"PauliZ": "PauliZ",
|
|
36
|
+
"Identity": "Identity",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if isinstance(obs, qml.ops.Prod):
|
|
40
|
+
# Recursively build a tuple of operator names
|
|
41
|
+
return tuple(name_map[o.name] for o in obs.operands)
|
|
42
|
+
|
|
43
|
+
# For single operators, return a single-element tuple
|
|
44
|
+
return (name_map[obs.name],)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@lru_cache(maxsize=512)
|
|
48
|
+
def _get_eigvals_from_key(key: tuple[str, ...]) -> npt.NDArray[np.int8]:
|
|
49
|
+
"""Computes and caches eigenvalues based on a structural key.
|
|
50
|
+
|
|
51
|
+
This function takes a key generated by `_get_structural_key` and computes
|
|
52
|
+
the eigenvalues of the corresponding tensor product of operators. The results
|
|
53
|
+
are memoized using @lru_cache to avoid redundant calculations.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
key (tuple[str, ...]): A tuple of strings representing the observable's structure.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
np.ndarray: A NumPy array containing the eigenvalues of the observable.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
# Define a mapping from name to the base eigenvalue array
|
|
63
|
+
eigvals_map = {
|
|
64
|
+
"PauliZ": np.array([1, -1], dtype=np.int8),
|
|
65
|
+
"Identity": np.array([1, 1], dtype=np.int8),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# Start with the eigenvalues of the first operator in the key
|
|
69
|
+
final_eigvals = eigvals_map[key[0]]
|
|
70
|
+
|
|
71
|
+
# Iteratively compute the kronecker product for the rest
|
|
72
|
+
for op_name in key[1:]:
|
|
73
|
+
final_eigvals = np.kron(final_eigvals, eigvals_map[op_name])
|
|
74
|
+
|
|
75
|
+
return final_eigvals
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _batched_expectation(
|
|
79
|
+
shots_dicts: list[dict[str, int]],
|
|
80
|
+
observables: list[qml.operation.Operation],
|
|
81
|
+
wire_order: tuple[int, ...],
|
|
82
|
+
) -> npt.NDArray[np.float64]:
|
|
83
|
+
"""Efficiently calculates expectation values for multiple observables across multiple shot histograms.
|
|
84
|
+
|
|
85
|
+
This function is optimized to compute expectation values in a fully vectorized
|
|
86
|
+
manner, minimizing Python loops. It operates in four main steps:
|
|
87
|
+
1. Aggregates all unique bitstrings measured across all histograms.
|
|
88
|
+
2. Builds a "reduced" eigenvalue matrix corresponding only to the unique states.
|
|
89
|
+
3. Builds a "reduced" probability matrix from the shot counts for each histogram.
|
|
90
|
+
4. Computes all expectation values with a single matrix multiplication.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
shots_dicts (list[dict[str, int]]): A list of shot dictionaries (histograms),
|
|
94
|
+
where each dictionary maps a measured bitstring to its count.
|
|
95
|
+
observables (list[qml.operation.Operation]): A list of PennyLane observables
|
|
96
|
+
for which to calculate expectation values.
|
|
97
|
+
wire_order (tuple[int, ...]): A tuple defining the order of wires, which maps
|
|
98
|
+
the bitstring to the qubits. Note: This is typically the reverse of the
|
|
99
|
+
qubit indices (e.g., (2, 1, 0) for a 3-qubit system).
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
npt.NDArray[np.float64]: A 2D NumPy array of shape (n_observables, n_shots) where
|
|
103
|
+
result[i, j] is the expectation value of observables[i] for the
|
|
104
|
+
histogram in shots_dicts[j].
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
n_histograms = len(shots_dicts)
|
|
108
|
+
n_total_wires = len(wire_order)
|
|
109
|
+
n_observables = len(observables)
|
|
110
|
+
|
|
111
|
+
# --- 1. Aggregate all unique measured states across all shots ---
|
|
112
|
+
all_measured_bitstrings = set()
|
|
113
|
+
for sd in shots_dicts:
|
|
114
|
+
all_measured_bitstrings.update(sd.keys())
|
|
115
|
+
|
|
116
|
+
unique_bitstrings = sorted(list(all_measured_bitstrings))
|
|
117
|
+
n_unique_states = len(unique_bitstrings)
|
|
118
|
+
|
|
119
|
+
bitstring_to_idx_map = {bs: i for i, bs in enumerate(unique_bitstrings)}
|
|
120
|
+
|
|
121
|
+
# --- 2. Build REDUCED Eigenvalue Matrix: (n_observables, n_unique_states) ---
|
|
122
|
+
# For systems with <=64 qubits, use fast integer conversion and bitwise operations.
|
|
123
|
+
# For larger systems, use character array to avoid integer overflow.
|
|
124
|
+
if n_total_wires <= 64:
|
|
125
|
+
# Fast path: convert to uint64 and use vectorized bitwise operations
|
|
126
|
+
unique_states_int = np.array(
|
|
127
|
+
[int(bs, 2) for bs in unique_bitstrings], dtype=np.uint64
|
|
128
|
+
)
|
|
129
|
+
use_integer_representation = True
|
|
130
|
+
else:
|
|
131
|
+
# Safe path: convert to character array for large qubit counts
|
|
132
|
+
bitstring_chars = np.array([list(bs) for bs in unique_bitstrings], dtype="U1")
|
|
133
|
+
use_integer_representation = False
|
|
134
|
+
|
|
135
|
+
reduced_eigvals_matrix = np.zeros((n_observables, n_unique_states))
|
|
136
|
+
wire_map = {w: i for i, w in enumerate(wire_order)}
|
|
137
|
+
|
|
138
|
+
powers_cache = {}
|
|
139
|
+
|
|
140
|
+
for obs_idx, observable in enumerate(observables):
|
|
141
|
+
obs_wires = observable.wires
|
|
142
|
+
n_obs_wires = len(obs_wires)
|
|
143
|
+
|
|
144
|
+
if n_obs_wires in powers_cache:
|
|
145
|
+
powers = powers_cache[n_obs_wires]
|
|
146
|
+
else:
|
|
147
|
+
powers = 2 ** np.arange(n_obs_wires - 1, -1, -1, dtype=np.intp)
|
|
148
|
+
powers_cache[n_obs_wires] = powers
|
|
149
|
+
|
|
150
|
+
obs_wire_indices = np.array([wire_map[w] for w in obs_wires], dtype=np.uint32)
|
|
151
|
+
eigvals = _get_eigvals_from_key(_get_structural_key(observable))
|
|
152
|
+
|
|
153
|
+
# Vectorized mapping, but on the *reduced* set of states
|
|
154
|
+
shifts = n_total_wires - 1 - obs_wire_indices
|
|
155
|
+
|
|
156
|
+
if use_integer_representation:
|
|
157
|
+
# Fast path: vectorized bitwise operations on integers
|
|
158
|
+
bits = ((unique_states_int[:, np.newaxis] >> shifts) & 1).astype(np.intp)
|
|
159
|
+
else:
|
|
160
|
+
# Safe path: extract bits from character array (vectorized)
|
|
161
|
+
bits = bitstring_chars[:, shifts].astype(np.intp)
|
|
162
|
+
|
|
163
|
+
obs_state_indices = np.dot(bits, powers)
|
|
164
|
+
|
|
165
|
+
reduced_eigvals_matrix[obs_idx, :] = eigvals[obs_state_indices]
|
|
166
|
+
|
|
167
|
+
# --- 3. Build REDUCED Probability Matrix: (n_shots, n_unique_states) ---
|
|
168
|
+
reduced_prob_matrix = np.zeros((n_histograms, n_unique_states), dtype=np.float32)
|
|
169
|
+
for i, shots_dict in enumerate(shots_dicts):
|
|
170
|
+
total = sum(shots_dict.values())
|
|
171
|
+
|
|
172
|
+
for bitstring, count in shots_dict.items():
|
|
173
|
+
col_idx = bitstring_to_idx_map[bitstring]
|
|
174
|
+
reduced_prob_matrix[i, col_idx] = count / total
|
|
175
|
+
|
|
176
|
+
# --- 4. Compute Final Expectation Values ---
|
|
177
|
+
# (n_shots, n_unique_states) @ (n_unique_states, n_observables)
|
|
178
|
+
result = reduced_prob_matrix @ reduced_eigvals_matrix.T
|
|
179
|
+
|
|
180
|
+
# Transpose to (n_observables, n_shots) as expected by the calling code
|
|
181
|
+
return result.T
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from functools import reduce
|
|
6
|
+
from warnings import warn
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import numpy.typing as npt
|
|
10
|
+
import pennylane as qml
|
|
11
|
+
import scipy.sparse as sps
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _clean_hamiltonian(
|
|
15
|
+
hamiltonian: qml.operation.Operator,
|
|
16
|
+
) -> tuple[qml.operation.Operator, float]:
|
|
17
|
+
"""Separate constant and non-constant terms in a Hamiltonian.
|
|
18
|
+
|
|
19
|
+
This function processes a PennyLane Hamiltonian to separate out any terms
|
|
20
|
+
that are constant (i.e., proportional to the identity operator). The sum
|
|
21
|
+
of these constant terms is returned, along with a new Hamiltonian containing
|
|
22
|
+
only the non-constant terms.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
hamiltonian: The Hamiltonian operator to process.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
tuple[qml.operation.Operator, float]: A tuple containing:
|
|
29
|
+
- The Hamiltonian without the constant (identity) component.
|
|
30
|
+
- The summed value of all constant terms.
|
|
31
|
+
"""
|
|
32
|
+
if hamiltonian is None:
|
|
33
|
+
return qml.Hamiltonian([], []), 0.0
|
|
34
|
+
|
|
35
|
+
terms = (
|
|
36
|
+
hamiltonian.operands if isinstance(hamiltonian, qml.ops.Sum) else [hamiltonian]
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
loss_constant = 0.0
|
|
40
|
+
non_constant_terms = []
|
|
41
|
+
|
|
42
|
+
for term in terms:
|
|
43
|
+
coeff = 1.0
|
|
44
|
+
base_op = term
|
|
45
|
+
if isinstance(term, qml.ops.SProd):
|
|
46
|
+
coeff = term.scalar
|
|
47
|
+
base_op = term.base
|
|
48
|
+
|
|
49
|
+
# Check for Identity term
|
|
50
|
+
is_constant = False
|
|
51
|
+
if isinstance(base_op, qml.Identity):
|
|
52
|
+
is_constant = True
|
|
53
|
+
elif isinstance(base_op, qml.ops.Prod) and all(
|
|
54
|
+
isinstance(op, qml.Identity) for op in base_op.operands
|
|
55
|
+
):
|
|
56
|
+
is_constant = True
|
|
57
|
+
|
|
58
|
+
if is_constant:
|
|
59
|
+
loss_constant += coeff
|
|
60
|
+
else:
|
|
61
|
+
non_constant_terms.append(term)
|
|
62
|
+
|
|
63
|
+
if not non_constant_terms:
|
|
64
|
+
return qml.Hamiltonian([], []), float(loss_constant)
|
|
65
|
+
|
|
66
|
+
# Reconstruct the Hamiltonian from non-constant terms
|
|
67
|
+
if len(non_constant_terms) > 1:
|
|
68
|
+
new_hamiltonian = qml.sum(*non_constant_terms)
|
|
69
|
+
else:
|
|
70
|
+
new_hamiltonian = non_constant_terms[0]
|
|
71
|
+
|
|
72
|
+
return new_hamiltonian.simplify(), float(loss_constant)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def convert_hamiltonian_to_pauli_string(
|
|
76
|
+
hamiltonian: qml.operation.Operator, n_qubits: int
|
|
77
|
+
) -> str:
|
|
78
|
+
"""
|
|
79
|
+
Convert a PennyLane Operator to a semicolon-separated string of Pauli operators.
|
|
80
|
+
|
|
81
|
+
Each term in the Hamiltonian is represented as a string of Pauli letters ('I', 'X', 'Y', 'Z'),
|
|
82
|
+
one per qubit. Multiple terms are separated by semicolons.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
hamiltonian (qml.operation.Operator): The PennyLane Operator (e.g., Hamiltonian, PauliZ) to convert.
|
|
86
|
+
n_qubits (int): Number of qubits to represent in the string.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
str: The Hamiltonian as a semicolon-separated string of Pauli operators.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: If an unknown Pauli operator is encountered or wire index is out of range.
|
|
93
|
+
"""
|
|
94
|
+
pauli_letters = {"PauliX": "X", "PauliY": "Y", "PauliZ": "Z"}
|
|
95
|
+
identity_row = np.full(n_qubits, "I", dtype="<U1")
|
|
96
|
+
|
|
97
|
+
# Handle both single operators and sums of operators (like Hamiltonians)
|
|
98
|
+
terms_to_process = (
|
|
99
|
+
hamiltonian.operands if isinstance(hamiltonian, qml.ops.Sum) else [hamiltonian]
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
terms = []
|
|
103
|
+
for term in terms_to_process:
|
|
104
|
+
op = term
|
|
105
|
+
while isinstance(op, qml.ops.SProd):
|
|
106
|
+
op = op.base
|
|
107
|
+
ops = op.operands if isinstance(op, qml.ops.Prod) else [op]
|
|
108
|
+
|
|
109
|
+
paulis = identity_row.copy()
|
|
110
|
+
for p in ops:
|
|
111
|
+
if isinstance(p, qml.Identity):
|
|
112
|
+
continue
|
|
113
|
+
# Better fallback logic with validation
|
|
114
|
+
if p.name in pauli_letters:
|
|
115
|
+
pauli = pauli_letters[p.name]
|
|
116
|
+
else:
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Unknown Pauli operator: {p.name}. "
|
|
119
|
+
"Expected 'PauliX', 'PauliY', or 'PauliZ'."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Bounds checking for wire indices
|
|
123
|
+
if not p.wires:
|
|
124
|
+
raise ValueError(f"Pauli operator {p.name} has no wires")
|
|
125
|
+
|
|
126
|
+
wire = int(p.wires[0])
|
|
127
|
+
if wire < 0 or wire >= n_qubits:
|
|
128
|
+
raise ValueError(
|
|
129
|
+
f"Wire index {wire} out of range for {n_qubits} qubits. "
|
|
130
|
+
f"Valid range: [0, {n_qubits - 1}]"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
paulis[wire] = pauli
|
|
134
|
+
terms.append("".join(paulis))
|
|
135
|
+
|
|
136
|
+
return ";".join(terms)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _is_sanitized(
|
|
140
|
+
qubo_matrix: npt.NDArray[np.float64] | sps.spmatrix,
|
|
141
|
+
) -> npt.NDArray[np.float64] | sps.spmatrix:
|
|
142
|
+
"""
|
|
143
|
+
Check if a QUBO matrix is either symmetric or upper triangular.
|
|
144
|
+
|
|
145
|
+
This function validates that the input QUBO matrix is in a proper format
|
|
146
|
+
for conversion to an Ising Hamiltonian. The matrix should be either
|
|
147
|
+
symmetric (equal to its transpose) or upper triangular.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
qubo_matrix (npt.NDArray[np.float64] | sps.spmatrix): The QUBO matrix to validate.
|
|
151
|
+
Can be a dense NumPy array or a sparse SciPy matrix.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
bool: True if the matrix is symmetric or upper triangular, False otherwise.
|
|
155
|
+
"""
|
|
156
|
+
is_sparse = sps.issparse(qubo_matrix)
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
(
|
|
160
|
+
((qubo_matrix != qubo_matrix.T).nnz == 0)
|
|
161
|
+
or ((qubo_matrix != sps.triu(qubo_matrix)).nnz == 0)
|
|
162
|
+
)
|
|
163
|
+
if is_sparse
|
|
164
|
+
else (
|
|
165
|
+
np.allclose(qubo_matrix, qubo_matrix.T)
|
|
166
|
+
or np.allclose(qubo_matrix, np.triu(qubo_matrix))
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def convert_qubo_matrix_to_pennylane_ising(
|
|
172
|
+
qubo_matrix: npt.NDArray[np.float64] | sps.spmatrix,
|
|
173
|
+
) -> tuple[qml.operation.Operator, float]:
|
|
174
|
+
"""
|
|
175
|
+
Convert a QUBO matrix to an Ising Hamiltonian in PennyLane format.
|
|
176
|
+
|
|
177
|
+
The conversion follows the mapping from QUBO variables x_i ∈ {0,1} to
|
|
178
|
+
Ising variables σ_i ∈ {-1,1} via the transformation x_i = (1 - σ_i)/2. This
|
|
179
|
+
transforms a QUBO minimization problem into an equivalent Ising minimization
|
|
180
|
+
problem.
|
|
181
|
+
|
|
182
|
+
The function handles both dense NumPy arrays and sparse SciPy matrices efficiently.
|
|
183
|
+
If the input matrix is neither symmetric nor upper triangular, it will be
|
|
184
|
+
symmetrized automatically with a warning.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
qubo_matrix (npt.NDArray[np.float64] | sps.spmatrix): The QUBO matrix Q where the
|
|
188
|
+
objective is to minimize x^T Q x. Can be a dense NumPy array or a
|
|
189
|
+
sparse SciPy matrix (any format). Should be square and either
|
|
190
|
+
symmetric or upper triangular.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
tuple[qml.operation.Operator, float]: A tuple containing:
|
|
194
|
+
- Ising Hamiltonian as a PennyLane operator (sum of Pauli Z terms)
|
|
195
|
+
- Constant offset term to be added to energy calculations
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
UserWarning: If the QUBO matrix is neither symmetric nor upper triangular.
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
>>> import numpy as np
|
|
202
|
+
>>> qubo = np.array([[1, 2], [0, 3]])
|
|
203
|
+
>>> hamiltonian, offset = convert_qubo_matrix_to_pennylane_ising(qubo)
|
|
204
|
+
>>> print(f"Offset: {offset}")
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
if not _is_sanitized(qubo_matrix):
|
|
208
|
+
warn(
|
|
209
|
+
"The QUBO matrix is neither symmetric nor upper triangular."
|
|
210
|
+
" Symmetrizing it for the Ising Hamiltonian creation."
|
|
211
|
+
)
|
|
212
|
+
qubo_matrix = (qubo_matrix + qubo_matrix.T) / 2
|
|
213
|
+
|
|
214
|
+
is_sparse = sps.issparse(qubo_matrix)
|
|
215
|
+
backend = sps if is_sparse else np
|
|
216
|
+
|
|
217
|
+
symmetrized_qubo = (qubo_matrix + qubo_matrix.T) / 2
|
|
218
|
+
|
|
219
|
+
# Gather non-zero indices in the upper triangle of the matrix
|
|
220
|
+
triu_matrix = backend.triu(
|
|
221
|
+
symmetrized_qubo,
|
|
222
|
+
**(
|
|
223
|
+
{"format": qubo_matrix.format if qubo_matrix.format != "coo" else "csc"}
|
|
224
|
+
if is_sparse
|
|
225
|
+
else {}
|
|
226
|
+
),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if is_sparse:
|
|
230
|
+
coo_mat = triu_matrix.tocoo()
|
|
231
|
+
rows, cols, values = coo_mat.row, coo_mat.col, coo_mat.data
|
|
232
|
+
else:
|
|
233
|
+
rows, cols = triu_matrix.nonzero()
|
|
234
|
+
values = triu_matrix[rows, cols]
|
|
235
|
+
|
|
236
|
+
n = qubo_matrix.shape[0]
|
|
237
|
+
linear_terms = np.zeros(n)
|
|
238
|
+
constant_term = 0.0
|
|
239
|
+
ising_terms = []
|
|
240
|
+
ising_weights = []
|
|
241
|
+
|
|
242
|
+
for i, j, weight in zip(rows, cols, values):
|
|
243
|
+
weight = float(weight)
|
|
244
|
+
i, j = int(i), int(j)
|
|
245
|
+
|
|
246
|
+
if i == j:
|
|
247
|
+
# Diagonal elements
|
|
248
|
+
linear_terms[i] -= weight / 2
|
|
249
|
+
constant_term += weight / 2
|
|
250
|
+
else:
|
|
251
|
+
# Off-diagonal elements (i < j since we're using triu)
|
|
252
|
+
ising_terms.append([i, j])
|
|
253
|
+
ising_weights.append(weight / 4)
|
|
254
|
+
|
|
255
|
+
# Update linear terms
|
|
256
|
+
linear_terms[i] -= weight / 4
|
|
257
|
+
linear_terms[j] -= weight / 4
|
|
258
|
+
|
|
259
|
+
# Update constant term
|
|
260
|
+
constant_term += weight / 4
|
|
261
|
+
|
|
262
|
+
# Add the linear terms (Z operators)
|
|
263
|
+
for i, curr_lin_term in filter(lambda x: x[1] != 0, enumerate(linear_terms)):
|
|
264
|
+
ising_terms.append([i])
|
|
265
|
+
ising_weights.append(float(curr_lin_term))
|
|
266
|
+
|
|
267
|
+
# Construct the Ising Hamiltonian as a PennyLane operator
|
|
268
|
+
pauli_string = qml.Identity(0) * 0
|
|
269
|
+
for term, weight in zip(ising_terms, ising_weights):
|
|
270
|
+
if len(term) == 1:
|
|
271
|
+
# Single-qubit term (Z operator)
|
|
272
|
+
curr_term = qml.Z(term[0]) * weight
|
|
273
|
+
else:
|
|
274
|
+
# Two-qubit term (ZZ interaction)
|
|
275
|
+
curr_term = (
|
|
276
|
+
reduce(lambda x, y: x @ y, map(lambda x: qml.Z(x), term)) * weight
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
pauli_string += curr_term
|
|
280
|
+
|
|
281
|
+
return pauli_string.simplify(), constant_term
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2025-2026 Qoro Quantum Ltd <divi@qoroquantum.de>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
from ._ansatze import (
|
|
6
|
+
Ansatz,
|
|
7
|
+
GenericLayerAnsatz,
|
|
8
|
+
HardwareEfficientAnsatz,
|
|
9
|
+
HartreeFockAnsatz,
|
|
10
|
+
QAOAAnsatz,
|
|
11
|
+
UCCSDAnsatz,
|
|
12
|
+
)
|
|
13
|
+
from ._custom_vqa import CustomVQA
|
|
14
|
+
from ._qaoa import QAOA, GraphProblem
|
|
15
|
+
from ._vqe import VQE
|
|
16
|
+
from ._pce import PCE
|