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/__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 ._qaoa import QAOA, GraphProblem
9
- from ._vqe import VQE, VQEAnsatz
10
- from ._mlae import MLAE
11
- from ._graph_partitioning import GraphPartitioningQAOA, PartitioningConfig
12
- from ._vqe_sweep import VQEHyperparameterSweep
13
- from .optimizers import Optimizer
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