qilisdk 0.1.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.
- qilisdk/__init__.py +47 -0
- qilisdk/__init__.pyi +30 -0
- qilisdk/_optionals.py +105 -0
- qilisdk/analog/__init__.py +17 -0
- qilisdk/analog/algorithms.py +111 -0
- qilisdk/analog/analog_backend.py +43 -0
- qilisdk/analog/analog_result.py +114 -0
- qilisdk/analog/exceptions.py +19 -0
- qilisdk/analog/hamiltonian.py +706 -0
- qilisdk/analog/quantum_objects.py +486 -0
- qilisdk/analog/schedule.py +311 -0
- qilisdk/common/__init__.py +20 -0
- qilisdk/common/algorithm.py +17 -0
- qilisdk/common/backend.py +16 -0
- qilisdk/common/model.py +16 -0
- qilisdk/common/optimizer.py +136 -0
- qilisdk/common/optimizer_result.py +110 -0
- qilisdk/common/result.py +17 -0
- qilisdk/digital/__init__.py +66 -0
- qilisdk/digital/ansatz.py +143 -0
- qilisdk/digital/circuit.py +106 -0
- qilisdk/digital/digital_algorithm.py +20 -0
- qilisdk/digital/digital_backend.py +90 -0
- qilisdk/digital/digital_result.py +145 -0
- qilisdk/digital/exceptions.py +31 -0
- qilisdk/digital/gates.py +989 -0
- qilisdk/digital/vqe.py +165 -0
- qilisdk/extras/__init__.py +13 -0
- qilisdk/extras/cuda/__init__.py +18 -0
- qilisdk/extras/cuda/cuda_analog_result.py +19 -0
- qilisdk/extras/cuda/cuda_backend.py +398 -0
- qilisdk/extras/cuda/cuda_digital_result.py +19 -0
- qilisdk/extras/qaas/__init__.py +13 -0
- qilisdk/extras/qaas/keyring.py +54 -0
- qilisdk/extras/qaas/models.py +57 -0
- qilisdk/extras/qaas/qaas_backend.py +154 -0
- qilisdk/extras/qaas/qaas_digital_result.py +20 -0
- qilisdk/extras/qaas/qaas_settings.py +23 -0
- qilisdk/py.typed +0 -0
- qilisdk/utils/__init__.py +27 -0
- qilisdk/utils/openqasm2.py +215 -0
- qilisdk/utils/serialization.py +128 -0
- qilisdk/yaml.py +71 -0
- qilisdk-0.1.0.dist-info/METADATA +237 -0
- qilisdk-0.1.0.dist-info/RECORD +47 -0
- qilisdk-0.1.0.dist-info/WHEEL +4 -0
- qilisdk-0.1.0.dist-info/licenses/LICENCE +201 -0
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
# Copyright 2025 Qilimanjaro Quantum Tech
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import copy
|
|
17
|
+
import re
|
|
18
|
+
from abc import ABC
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
from functools import reduce
|
|
21
|
+
from typing import TYPE_CHECKING, Callable, ClassVar
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
from scipy.sparse import csc_array, identity, kron, spmatrix
|
|
25
|
+
|
|
26
|
+
from qilisdk.yaml import yaml
|
|
27
|
+
|
|
28
|
+
from .exceptions import InvalidHamiltonianOperation, NotSupportedOperation
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from collections.abc import Iterator
|
|
32
|
+
|
|
33
|
+
Complex = int | float | complex
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
###############################################################################
|
|
37
|
+
# Flyweight Cache
|
|
38
|
+
###############################################################################
|
|
39
|
+
_OPERATOR_CACHE: dict[tuple[str, int], PauliOperator] = {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_pauli(name: str, qubit: int) -> PauliOperator:
|
|
43
|
+
key = (name, qubit)
|
|
44
|
+
if key in _OPERATOR_CACHE:
|
|
45
|
+
return _OPERATOR_CACHE[key]
|
|
46
|
+
|
|
47
|
+
if name == "Z":
|
|
48
|
+
op = PauliZ(qubit)
|
|
49
|
+
elif name == "X":
|
|
50
|
+
op = PauliX(qubit) # type: ignore[assignment]
|
|
51
|
+
elif name == "Y":
|
|
52
|
+
op = PauliY(qubit) # type: ignore[assignment]
|
|
53
|
+
elif name == "I":
|
|
54
|
+
op = PauliI(qubit) # type: ignore[assignment]
|
|
55
|
+
else:
|
|
56
|
+
raise ValueError(f"Unknown Pauli operator name: {name}")
|
|
57
|
+
|
|
58
|
+
_OPERATOR_CACHE[key] = op
|
|
59
|
+
return op
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
###############################################################################
|
|
63
|
+
# Public Factory Functions
|
|
64
|
+
###############################################################################
|
|
65
|
+
def Z(qubit: int) -> PauliOperator:
|
|
66
|
+
return _get_pauli("Z", qubit)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def X(qubit: int) -> PauliOperator:
|
|
70
|
+
return _get_pauli("X", qubit)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def Y(qubit: int) -> PauliOperator:
|
|
74
|
+
return _get_pauli("Y", qubit)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def I(qubit: int = 0) -> PauliOperator: # noqa: E743
|
|
78
|
+
return _get_pauli("I", qubit)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
###############################################################################
|
|
82
|
+
# Abstract Base PauliOperator
|
|
83
|
+
###############################################################################
|
|
84
|
+
class PauliOperator(ABC):
|
|
85
|
+
"""
|
|
86
|
+
A generic Pauli operator that acts on one qubit.
|
|
87
|
+
Flyweight usage: do NOT instantiate directly—use X(q), Y(q), etc.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
_NAME: ClassVar[str]
|
|
91
|
+
_MATRIX: ClassVar[np.ndarray]
|
|
92
|
+
|
|
93
|
+
# __slots__ = ("_qubit",)
|
|
94
|
+
|
|
95
|
+
def __init__(self, qubit: int) -> None:
|
|
96
|
+
self._qubit = qubit
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def qubit(self) -> int:
|
|
100
|
+
return self._qubit
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def name(self) -> str:
|
|
104
|
+
return self._NAME
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def matrix(self) -> np.ndarray:
|
|
108
|
+
return self._MATRIX
|
|
109
|
+
|
|
110
|
+
def to_hamiltonian(self) -> Hamiltonian:
|
|
111
|
+
"""Convert this single operator to a Hamiltonian with one term.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Hamiltonian: The converted Hamiltonian.
|
|
115
|
+
"""
|
|
116
|
+
return Hamiltonian({(self,): 1})
|
|
117
|
+
|
|
118
|
+
def __hash__(self) -> int:
|
|
119
|
+
return hash((self._NAME, self._qubit))
|
|
120
|
+
|
|
121
|
+
def __eq__(self, other: object) -> bool:
|
|
122
|
+
if not isinstance(other, PauliOperator):
|
|
123
|
+
return False
|
|
124
|
+
return (self._NAME == other._NAME) and (self._qubit == other._qubit)
|
|
125
|
+
|
|
126
|
+
def __repr__(self) -> str:
|
|
127
|
+
return f"{self.name}({self.qubit})"
|
|
128
|
+
|
|
129
|
+
def __str__(self) -> str:
|
|
130
|
+
return f"{self.name}({self.qubit})"
|
|
131
|
+
|
|
132
|
+
# ----------- Arithmetic Operators ------------
|
|
133
|
+
|
|
134
|
+
def __add__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
135
|
+
return self.to_hamiltonian() + other
|
|
136
|
+
|
|
137
|
+
__radd__ = __add__
|
|
138
|
+
__iadd__ = __add__
|
|
139
|
+
|
|
140
|
+
def __sub__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
141
|
+
return self.to_hamiltonian() - other
|
|
142
|
+
|
|
143
|
+
def __rsub__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
144
|
+
return other - self.to_hamiltonian()
|
|
145
|
+
|
|
146
|
+
__isub__ = __sub__
|
|
147
|
+
|
|
148
|
+
def __mul__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
149
|
+
return self.to_hamiltonian() * other
|
|
150
|
+
|
|
151
|
+
def __rmul__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
152
|
+
return other * self.to_hamiltonian()
|
|
153
|
+
|
|
154
|
+
__imul__ = __mul__
|
|
155
|
+
|
|
156
|
+
def __truediv__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
157
|
+
return self.to_hamiltonian() / other
|
|
158
|
+
|
|
159
|
+
def __rtruediv__(self, _: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
160
|
+
raise NotSupportedOperation("Division by operators is not supported")
|
|
161
|
+
|
|
162
|
+
__itruediv__ = __truediv__
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
###############################################################################
|
|
166
|
+
# Concrete Flyweight Operator Classes
|
|
167
|
+
###############################################################################
|
|
168
|
+
@yaml.register_class
|
|
169
|
+
class PauliZ(PauliOperator):
|
|
170
|
+
# __slots__ = ()
|
|
171
|
+
_NAME: ClassVar[str] = "Z"
|
|
172
|
+
_MATRIX: ClassVar[np.ndarray] = np.array([[1, 0], [0, -1]], dtype=complex)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@yaml.register_class
|
|
176
|
+
class PauliX(PauliOperator):
|
|
177
|
+
# __slots__ = ()
|
|
178
|
+
_NAME: ClassVar[str] = "X"
|
|
179
|
+
_MATRIX: ClassVar[np.ndarray] = np.array([[0, 1], [1, 0]], dtype=complex)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@yaml.register_class
|
|
183
|
+
class PauliY(PauliOperator):
|
|
184
|
+
# __slots__ = ()
|
|
185
|
+
_NAME: ClassVar[str] = "Y"
|
|
186
|
+
_MATRIX: ClassVar[np.ndarray] = np.array([[0, -1j], [1j, 0]], dtype=complex)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@yaml.register_class
|
|
190
|
+
class PauliI(PauliOperator):
|
|
191
|
+
# __slots__ = ()
|
|
192
|
+
_NAME: ClassVar[str] = "I"
|
|
193
|
+
_MATRIX: ClassVar[np.ndarray] = np.array([[1, 0], [0, 1]], dtype=complex)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@yaml.register_class
|
|
197
|
+
class Hamiltonian:
|
|
198
|
+
"""
|
|
199
|
+
The `elements` dictionary now maps from a tuple of PauliOperator objects
|
|
200
|
+
to a complex coefficient. For example:
|
|
201
|
+
{
|
|
202
|
+
(Z(0), Y(1)): 1,
|
|
203
|
+
(X(1),): 1j,
|
|
204
|
+
}
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
_EPS: float = 1e-14
|
|
208
|
+
_PAULI_PRODUCT_TABLE: ClassVar[dict[tuple[str, str], tuple[complex, Callable[..., PauliOperator]]]] = {
|
|
209
|
+
("X", "X"): (1, I),
|
|
210
|
+
("X", "Y"): (1j, Z),
|
|
211
|
+
("X", "Z"): (-1j, Y),
|
|
212
|
+
("Y", "X"): (-1j, Z),
|
|
213
|
+
("Y", "Y"): (1, I),
|
|
214
|
+
("Y", "Z"): (1j, X),
|
|
215
|
+
("Z", "X"): (1j, Y),
|
|
216
|
+
("Z", "Y"): (-1j, X),
|
|
217
|
+
("Z", "Z"): (1, I),
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
ZERO: int = 0
|
|
221
|
+
|
|
222
|
+
def __init__(self, elements: dict[tuple[PauliOperator, ...], complex] | None = None) -> None:
|
|
223
|
+
self._elements: dict[tuple[PauliOperator, ...], complex] = defaultdict(complex)
|
|
224
|
+
if elements:
|
|
225
|
+
for key, val in elements.items():
|
|
226
|
+
self._elements[key] += val
|
|
227
|
+
self.simplify()
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def nqubits(self) -> int:
|
|
231
|
+
"""Number of qubits acting on the hamiltonian."""
|
|
232
|
+
qubits = {op.qubit for key in self._elements for op in key}
|
|
233
|
+
|
|
234
|
+
return max(qubits) + 1 if qubits else 0
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def elements(self) -> dict[tuple[PauliOperator, ...], complex]:
|
|
238
|
+
"""Returns the internal dictionary of elements (read-only)."""
|
|
239
|
+
return self._elements
|
|
240
|
+
|
|
241
|
+
def simplify(self) -> Hamiltonian:
|
|
242
|
+
# 1) Remove near-zero
|
|
243
|
+
keys_to_remove = [key for key, value in self._elements.items() if abs(value) < Hamiltonian._EPS]
|
|
244
|
+
for key in keys_to_remove:
|
|
245
|
+
del self._elements[key]
|
|
246
|
+
|
|
247
|
+
# 2) Accumulate identities that do NOT act on qubit=0 => I(0)
|
|
248
|
+
to_accumulate = [
|
|
249
|
+
(key, value)
|
|
250
|
+
for key, value in self._elements.items()
|
|
251
|
+
if len(key) == 1 and key[0].name == "I" and key[0].qubit != 0
|
|
252
|
+
]
|
|
253
|
+
for key, value in to_accumulate:
|
|
254
|
+
del self._elements[key]
|
|
255
|
+
self._elements[I(0),] += value
|
|
256
|
+
|
|
257
|
+
return self
|
|
258
|
+
|
|
259
|
+
def _apply_operator_on_qubit(self, terms: list[PauliOperator]) -> spmatrix:
|
|
260
|
+
"""Get the matrix representation of a single term by taking the tensor product
|
|
261
|
+
of operators acting on each qubit. For qubits with no operator in `terms`,
|
|
262
|
+
the identity is used.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
terms (list[PauliOperator]): A list of Pauli operators in the term.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
spmatrix: The full matrix representation of the term.
|
|
269
|
+
"""
|
|
270
|
+
# Build a list of factors for each qubit
|
|
271
|
+
factors = []
|
|
272
|
+
for q in range(self.nqubits):
|
|
273
|
+
# Look for an operator acting on qubit q
|
|
274
|
+
op = next((t for t in terms if t.qubit == q), None)
|
|
275
|
+
if op is not None:
|
|
276
|
+
# Wrap the operator's matrix as a sparse matrix.
|
|
277
|
+
factors.append(csc_array(np.array(op.matrix)))
|
|
278
|
+
else:
|
|
279
|
+
factors.append(identity(2, format="csc"))
|
|
280
|
+
# Compute the tensor (Kronecker) product over all qubits.
|
|
281
|
+
full_matrix = reduce(lambda A, B: kron(A, B, format="csc"), factors)
|
|
282
|
+
return full_matrix
|
|
283
|
+
|
|
284
|
+
def to_matrix(self) -> spmatrix:
|
|
285
|
+
"""Return the full matrix representation of the Hamiltonian by summing over all terms.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
spmatrix: The sparse matrix representation of the Hamiltonian.
|
|
289
|
+
"""
|
|
290
|
+
dim = 2**self.nqubits
|
|
291
|
+
# Initialize a zero matrix of the appropriate dimension.
|
|
292
|
+
result = csc_array(np.zeros((dim, dim), dtype=complex))
|
|
293
|
+
for coeff, term in self:
|
|
294
|
+
result += coeff * self._apply_operator_on_qubit(term)
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
def __iter__(self) -> Iterator[tuple[complex, list[PauliOperator]]]:
|
|
298
|
+
for key, value in self._elements.items():
|
|
299
|
+
yield value, list(key)
|
|
300
|
+
|
|
301
|
+
# ------- Equality & hashing --------
|
|
302
|
+
|
|
303
|
+
def __eq__(self, other: object) -> bool:
|
|
304
|
+
if other == Hamiltonian.ZERO:
|
|
305
|
+
return bool(
|
|
306
|
+
len(self._elements) == 0
|
|
307
|
+
or (len(self._elements) == 1 and (I(0),) in self._elements and self._elements[I(0),] == 0)
|
|
308
|
+
)
|
|
309
|
+
if isinstance(other, Complex):
|
|
310
|
+
return bool(len(self._elements) == 1 and (I(0),) in self._elements and self._elements[I(0),] == other)
|
|
311
|
+
if isinstance(other, PauliOperator):
|
|
312
|
+
other = other.to_hamiltonian()
|
|
313
|
+
if not isinstance(other, Hamiltonian):
|
|
314
|
+
return False
|
|
315
|
+
return dict(self._elements) == dict(other._elements)
|
|
316
|
+
|
|
317
|
+
def __ne__(self, other: object) -> bool:
|
|
318
|
+
return not self.__eq__(other)
|
|
319
|
+
|
|
320
|
+
def __hash__(self) -> int:
|
|
321
|
+
items_frozen = frozenset(self._elements.items())
|
|
322
|
+
return hash(items_frozen)
|
|
323
|
+
|
|
324
|
+
def __copy__(self) -> Hamiltonian:
|
|
325
|
+
return Hamiltonian(elements=self.elements.copy())
|
|
326
|
+
|
|
327
|
+
# ------- String representation --------
|
|
328
|
+
|
|
329
|
+
def __repr__(self) -> str:
|
|
330
|
+
return str(self)
|
|
331
|
+
|
|
332
|
+
def __str__(self) -> str:
|
|
333
|
+
# Return "0" if there are no terms
|
|
334
|
+
if not self._elements:
|
|
335
|
+
return "0"
|
|
336
|
+
|
|
337
|
+
def _format_coeff(c: complex) -> str:
|
|
338
|
+
re, im = c.real, c.imag
|
|
339
|
+
|
|
340
|
+
# 1) Purely real?
|
|
341
|
+
if abs(im) < Hamiltonian._EPS:
|
|
342
|
+
re_int = np.round(re)
|
|
343
|
+
if abs(re - re_int) < Hamiltonian._EPS:
|
|
344
|
+
return str(int(re_int)) # e.g. '2' instead of '2.0'
|
|
345
|
+
return str(re) # e.g. '2.5'
|
|
346
|
+
|
|
347
|
+
# 2) Purely imaginary?
|
|
348
|
+
if abs(re) < Hamiltonian._EPS:
|
|
349
|
+
im_int = np.round(im)
|
|
350
|
+
if abs(im - im_int) < Hamiltonian._EPS:
|
|
351
|
+
return f"{int(im_int)}j" # e.g. 2 => '2j', -3 => '-3j'
|
|
352
|
+
return f"{im}j" # e.g. '2.5j'
|
|
353
|
+
|
|
354
|
+
# 3) General complex with nonzero real & imag
|
|
355
|
+
s = str(c) # e.g. '(3+2j)'
|
|
356
|
+
return s
|
|
357
|
+
|
|
358
|
+
# We want to place the single identity term (I(0),) at the front if it exists
|
|
359
|
+
items = list(self._elements.items())
|
|
360
|
+
try:
|
|
361
|
+
i = next(idx for idx, (key, _) in enumerate(items) if len(key) == 1 and key[0] == (I(0)))
|
|
362
|
+
item = items.pop(i)
|
|
363
|
+
items.insert(0, item)
|
|
364
|
+
except StopIteration:
|
|
365
|
+
pass
|
|
366
|
+
|
|
367
|
+
parts = []
|
|
368
|
+
for idx, (operator, coeff) in enumerate(items):
|
|
369
|
+
base_str = _format_coeff(coeff)
|
|
370
|
+
|
|
371
|
+
if idx == 0:
|
|
372
|
+
# first term
|
|
373
|
+
if len(operator) == 1 and operator[0].name == "I":
|
|
374
|
+
coeff_str = base_str
|
|
375
|
+
elif base_str == "1":
|
|
376
|
+
coeff_str = ""
|
|
377
|
+
elif base_str == "-1":
|
|
378
|
+
coeff_str = "-"
|
|
379
|
+
else:
|
|
380
|
+
coeff_str = base_str
|
|
381
|
+
elif base_str == "1":
|
|
382
|
+
coeff_str = "+"
|
|
383
|
+
elif base_str == "-1":
|
|
384
|
+
coeff_str = "-"
|
|
385
|
+
elif base_str.startswith("-"):
|
|
386
|
+
coeff_str = f"- {base_str[1:]}"
|
|
387
|
+
else:
|
|
388
|
+
coeff_str = f"+ {base_str}"
|
|
389
|
+
|
|
390
|
+
# Operators string
|
|
391
|
+
ops_str = " ".join(str(op) for op in operator if op.name != "I")
|
|
392
|
+
if coeff_str and ops_str:
|
|
393
|
+
parts.append(f"{coeff_str} {ops_str}")
|
|
394
|
+
else:
|
|
395
|
+
parts.append(coeff_str + ops_str)
|
|
396
|
+
|
|
397
|
+
return " ".join(parts)
|
|
398
|
+
|
|
399
|
+
@classmethod
|
|
400
|
+
def parse(cls, ham_str: str) -> Hamiltonian:
|
|
401
|
+
ham_str = ham_str.strip()
|
|
402
|
+
# Special case: "0" => empty Hamiltonian
|
|
403
|
+
if ham_str == "0":
|
|
404
|
+
return cls({})
|
|
405
|
+
|
|
406
|
+
elements: dict[tuple[PauliOperator, ...], complex] = defaultdict(complex)
|
|
407
|
+
|
|
408
|
+
# If there's no initial +/- sign, prepend '+ ' for easier splitting
|
|
409
|
+
if not ham_str.startswith("+") and not ham_str.startswith("-"):
|
|
410
|
+
ham_str = "+ " + ham_str
|
|
411
|
+
|
|
412
|
+
# Replace " - " with " + - " so each term is split on " + "
|
|
413
|
+
ham_str = ham_str.replace(" - ", " + - ")
|
|
414
|
+
|
|
415
|
+
# Split on " + "
|
|
416
|
+
tokens = ham_str.split(" + ")
|
|
417
|
+
# Remove any empty tokens (can happen if the string started "+ ")
|
|
418
|
+
tokens = [t.strip() for t in tokens if t.strip()]
|
|
419
|
+
|
|
420
|
+
# Regex to match operator tokens like "Z(0)", "X(1)", "I(0)"
|
|
421
|
+
operator_pattern = re.compile(r"([XYZI])\((\d+)\)")
|
|
422
|
+
|
|
423
|
+
def parse_token(token: str) -> tuple[complex, list[PauliOperator]]:
|
|
424
|
+
def looks_like_number(text: str) -> bool:
|
|
425
|
+
# If it's empty, it's not a number
|
|
426
|
+
if not text:
|
|
427
|
+
return False
|
|
428
|
+
# If the first char is digit, '(', '.', '+', '-', or '0',
|
|
429
|
+
# or if 'j' is present, assume it's numeric
|
|
430
|
+
first = text[0]
|
|
431
|
+
if first.isdigit() or first in {"(", ".", "+", "-"}:
|
|
432
|
+
return True
|
|
433
|
+
return "j" in text
|
|
434
|
+
|
|
435
|
+
sign = 1
|
|
436
|
+
# Check leading sign
|
|
437
|
+
if token.startswith("-"):
|
|
438
|
+
sign = -1
|
|
439
|
+
token = token[1:].strip()
|
|
440
|
+
elif token.startswith("+"):
|
|
441
|
+
# optional leading '+'
|
|
442
|
+
token = token[1:].strip()
|
|
443
|
+
|
|
444
|
+
words = token.split()
|
|
445
|
+
if not words:
|
|
446
|
+
# e.g. just "-" or "+"
|
|
447
|
+
# means coefficient = ±1, no operators
|
|
448
|
+
return complex(sign), []
|
|
449
|
+
|
|
450
|
+
# Attempt to parse the first word as a numeric coefficient
|
|
451
|
+
maybe_coeff = words[0]
|
|
452
|
+
# Decide if 'maybe_coeff' is numeric or an operator
|
|
453
|
+
if looks_like_number(maybe_coeff):
|
|
454
|
+
# parse as a complex number
|
|
455
|
+
coeff_str = maybe_coeff
|
|
456
|
+
# If it's e.g. '(2.5+3j)', remove parentheses
|
|
457
|
+
if coeff_str.startswith("(") and coeff_str.endswith(")"):
|
|
458
|
+
coeff_str = coeff_str[1:-1]
|
|
459
|
+
coeff_val = complex(coeff_str) * sign
|
|
460
|
+
words = words[1:] # consume this word
|
|
461
|
+
else:
|
|
462
|
+
# No explicit coefficient => ±1
|
|
463
|
+
coeff_val = complex(sign)
|
|
464
|
+
|
|
465
|
+
# Now parse the remaining words as operators
|
|
466
|
+
ops = []
|
|
467
|
+
for w in words:
|
|
468
|
+
match = operator_pattern.fullmatch(w)
|
|
469
|
+
if not match:
|
|
470
|
+
raise ValueError(f"Unrecognized operator format: '{w}'")
|
|
471
|
+
name, qubit_str = match.groups()
|
|
472
|
+
qubit = int(qubit_str)
|
|
473
|
+
op = _get_pauli(name, qubit)
|
|
474
|
+
ops.append(op)
|
|
475
|
+
|
|
476
|
+
return coeff_val, ops
|
|
477
|
+
|
|
478
|
+
for tok in tokens:
|
|
479
|
+
coeff, op_list = parse_token(tok)
|
|
480
|
+
if not op_list:
|
|
481
|
+
# purely scalar => store as (I(0),)
|
|
482
|
+
elements[I(0),] += coeff
|
|
483
|
+
else:
|
|
484
|
+
# Sort operators by qubit for canonical ordering
|
|
485
|
+
op_list.sort(key=lambda op: op.qubit)
|
|
486
|
+
elements[tuple(op_list)] += coeff
|
|
487
|
+
|
|
488
|
+
ham = cls(elements)
|
|
489
|
+
ham.simplify()
|
|
490
|
+
return ham
|
|
491
|
+
|
|
492
|
+
# ------- Internal multiplication helpers --------
|
|
493
|
+
|
|
494
|
+
@staticmethod
|
|
495
|
+
def _multiply_sets(
|
|
496
|
+
set1: tuple[PauliOperator, ...], set2: tuple[PauliOperator, ...]
|
|
497
|
+
) -> tuple[complex, tuple[PauliOperator, ...]]:
|
|
498
|
+
# Combine all operators into a single list
|
|
499
|
+
combined = list(set1) + list(set2)
|
|
500
|
+
|
|
501
|
+
# Group by qubit
|
|
502
|
+
combined.sort(key=lambda op: op.qubit)
|
|
503
|
+
sum_dict: dict[int, list[PauliOperator]] = defaultdict(list)
|
|
504
|
+
for op in combined:
|
|
505
|
+
sum_dict[op.qubit].append(op)
|
|
506
|
+
|
|
507
|
+
accumulated_phase = complex(1)
|
|
508
|
+
final_ops: list[PauliOperator] = []
|
|
509
|
+
|
|
510
|
+
for qubit_ops in sum_dict.values():
|
|
511
|
+
op1 = qubit_ops[0]
|
|
512
|
+
phase = complex(1)
|
|
513
|
+
# Multiply together all operators on the same qubit
|
|
514
|
+
for op2 in qubit_ops[1:]:
|
|
515
|
+
aux_phase, op1 = Hamiltonian._multiply_pauli(op1, op2)
|
|
516
|
+
phase *= aux_phase
|
|
517
|
+
if op1.name != "I":
|
|
518
|
+
final_ops.append(op1)
|
|
519
|
+
accumulated_phase *= phase
|
|
520
|
+
|
|
521
|
+
# If everything simplified to identity, we store I(0)
|
|
522
|
+
if not final_ops:
|
|
523
|
+
final_ops = [I(0)]
|
|
524
|
+
|
|
525
|
+
# Sort again by qubit (to keep canonical form)
|
|
526
|
+
final_ops.sort(key=lambda op: op.qubit)
|
|
527
|
+
return accumulated_phase, tuple(final_ops)
|
|
528
|
+
|
|
529
|
+
@staticmethod
|
|
530
|
+
def _multiply_pauli(op1: PauliOperator, op2: PauliOperator) -> tuple[complex, PauliOperator]:
|
|
531
|
+
if op1.qubit != op2.qubit:
|
|
532
|
+
raise ValueError("Operators must act on the same qubit for multiplication.")
|
|
533
|
+
|
|
534
|
+
# If either is identity, no phase
|
|
535
|
+
if op1.name == "I":
|
|
536
|
+
return (1, op2)
|
|
537
|
+
if op2.name == "I":
|
|
538
|
+
return (1, op1)
|
|
539
|
+
|
|
540
|
+
# Look up the product in the table
|
|
541
|
+
key = (op1.name, op2.name)
|
|
542
|
+
result = Hamiltonian._PAULI_PRODUCT_TABLE.get(key)
|
|
543
|
+
if result is None:
|
|
544
|
+
raise NotSupportedOperation(f"Multiplying {op1} and {op2} not supported.")
|
|
545
|
+
phase, op_cls = result
|
|
546
|
+
|
|
547
|
+
# By convention, an I operator is always I(0) in this code
|
|
548
|
+
if op_cls is I:
|
|
549
|
+
return phase, I(0)
|
|
550
|
+
# Otherwise, keep the same qubit
|
|
551
|
+
return phase, op_cls(op1.qubit)
|
|
552
|
+
|
|
553
|
+
@staticmethod
|
|
554
|
+
def _multiply_hamiltonians(h1: Hamiltonian, h2: Hamiltonian) -> Hamiltonian:
|
|
555
|
+
out = Hamiltonian()
|
|
556
|
+
for key1, c1 in h1.elements.items():
|
|
557
|
+
for key2, c2 in h2.elements.items():
|
|
558
|
+
phase, new_key = Hamiltonian._multiply_sets(key1, key2)
|
|
559
|
+
out.elements[new_key] += phase * c1 * c2
|
|
560
|
+
return out.simplify()
|
|
561
|
+
|
|
562
|
+
# ------- Public arithmetic operators --------
|
|
563
|
+
|
|
564
|
+
def __add__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
565
|
+
out = copy.copy(self)
|
|
566
|
+
out._add_inplace(other)
|
|
567
|
+
return out.simplify()
|
|
568
|
+
|
|
569
|
+
def __radd__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
570
|
+
return self.__add__(other)
|
|
571
|
+
|
|
572
|
+
def __sub__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
573
|
+
out = copy.copy(self)
|
|
574
|
+
out._sub_inplace(other)
|
|
575
|
+
return out.simplify()
|
|
576
|
+
|
|
577
|
+
def __rsub__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
578
|
+
# (other - self)
|
|
579
|
+
out = copy.copy(other if isinstance(other, Hamiltonian) else Hamiltonian() + other)
|
|
580
|
+
out._sub_inplace(self)
|
|
581
|
+
return out.simplify()
|
|
582
|
+
|
|
583
|
+
def __mul__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
584
|
+
out = copy.copy(self)
|
|
585
|
+
out._mul_inplace(other)
|
|
586
|
+
return out.simplify()
|
|
587
|
+
|
|
588
|
+
def __rmul__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
589
|
+
if isinstance(other, Hamiltonian):
|
|
590
|
+
out = copy.copy(other)
|
|
591
|
+
out._mul_inplace(self)
|
|
592
|
+
return out.simplify()
|
|
593
|
+
return self.__mul__(other)
|
|
594
|
+
|
|
595
|
+
def __truediv__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
596
|
+
out = copy.copy(self)
|
|
597
|
+
out._div_inplace(other)
|
|
598
|
+
return out.simplify()
|
|
599
|
+
|
|
600
|
+
def __rtruediv__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
601
|
+
# (other / self)
|
|
602
|
+
if not isinstance(other, (int, float, complex)):
|
|
603
|
+
raise InvalidHamiltonianOperation("Division by operators is not supported")
|
|
604
|
+
out = copy.copy(self)
|
|
605
|
+
return (1 / other) * out
|
|
606
|
+
|
|
607
|
+
def __rfloordiv__(self, other: Complex | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
608
|
+
if not isinstance(other, (int, float, complex)):
|
|
609
|
+
raise NotSupportedOperation("Division of operators is not supported")
|
|
610
|
+
return (1 / other) * self
|
|
611
|
+
|
|
612
|
+
__iadd__ = __add__
|
|
613
|
+
__isub__ = __sub__
|
|
614
|
+
__imul__ = __mul__
|
|
615
|
+
__itruediv__ = __truediv__
|
|
616
|
+
|
|
617
|
+
def _add_inplace(self, other: Complex | PauliOperator | Hamiltonian) -> None:
|
|
618
|
+
if isinstance(other, Hamiltonian):
|
|
619
|
+
# If it's empty, do nothing
|
|
620
|
+
if not other.elements:
|
|
621
|
+
return
|
|
622
|
+
# Otherwise, add each term
|
|
623
|
+
for key, val in other.elements.items():
|
|
624
|
+
self._elements[key] += val
|
|
625
|
+
elif isinstance(other, PauliOperator):
|
|
626
|
+
# Just add 1 to that single operator key
|
|
627
|
+
self._elements[other,] += 1
|
|
628
|
+
elif isinstance(other, (int, float, complex)):
|
|
629
|
+
if other == 0:
|
|
630
|
+
return
|
|
631
|
+
# Add the scalar to (I(0),)
|
|
632
|
+
self._elements[I(0),] += other
|
|
633
|
+
else:
|
|
634
|
+
raise InvalidHamiltonianOperation(f"Invalid addition between Hamiltonian and {other.__class__.__name__}.")
|
|
635
|
+
|
|
636
|
+
def _sub_inplace(self, other: Complex | PauliOperator | Hamiltonian) -> None:
|
|
637
|
+
if isinstance(other, Hamiltonian):
|
|
638
|
+
for key, val in other.elements.items():
|
|
639
|
+
self._elements[key] -= val
|
|
640
|
+
elif isinstance(other, PauliOperator):
|
|
641
|
+
self._elements[other,] -= 1
|
|
642
|
+
elif isinstance(other, (int, float, complex)):
|
|
643
|
+
if other == 0:
|
|
644
|
+
return
|
|
645
|
+
self._elements[I(0),] -= other
|
|
646
|
+
else:
|
|
647
|
+
raise InvalidHamiltonianOperation(
|
|
648
|
+
f"Invalid subtraction between Hamiltonian and {other.__class__.__name__}."
|
|
649
|
+
)
|
|
650
|
+
|
|
651
|
+
def _mul_inplace(self, other: Complex | PauliOperator | Hamiltonian) -> None:
|
|
652
|
+
if isinstance(other, (int, float, complex)):
|
|
653
|
+
# 0 short-circuit
|
|
654
|
+
if other == 0:
|
|
655
|
+
# everything becomes 0
|
|
656
|
+
self._elements.clear()
|
|
657
|
+
return None
|
|
658
|
+
# 1 short-circuit
|
|
659
|
+
if other == 1:
|
|
660
|
+
return None
|
|
661
|
+
# scale all coefficients
|
|
662
|
+
for k in self._elements:
|
|
663
|
+
self._elements[k] *= other
|
|
664
|
+
return None
|
|
665
|
+
|
|
666
|
+
if isinstance(other, PauliOperator):
|
|
667
|
+
# Convert single PauliOperator -> Hamiltonian with 1 key
|
|
668
|
+
# Then do the single-key Hamiltonian path below
|
|
669
|
+
other = other.to_hamiltonian()
|
|
670
|
+
|
|
671
|
+
if isinstance(other, Hamiltonian):
|
|
672
|
+
if not other.elements:
|
|
673
|
+
# Multiply by "0" Hamiltonian => 0
|
|
674
|
+
self._elements.clear()
|
|
675
|
+
return None
|
|
676
|
+
|
|
677
|
+
# Check if 'other' is purely scalar identity => short-circuit
|
|
678
|
+
if len(other.elements) == 1:
|
|
679
|
+
((ops2, c2),) = other.elements.items() # single item
|
|
680
|
+
if len(ops2) == 1:
|
|
681
|
+
op2 = ops2[0]
|
|
682
|
+
if op2.name == "I" and op2.qubit == 0:
|
|
683
|
+
# effectively scalar c2
|
|
684
|
+
return self._mul_inplace(c2)
|
|
685
|
+
|
|
686
|
+
# Otherwise, we do the general multiply
|
|
687
|
+
new_dict: dict[tuple[PauliOperator, ...], complex] = defaultdict(complex)
|
|
688
|
+
for ops1, c1 in self._elements.items():
|
|
689
|
+
for ops2, c2 in other.elements.items():
|
|
690
|
+
phase, new_ops = self._multiply_sets(ops1, ops2)
|
|
691
|
+
new_dict[new_ops] += phase * c1 * c2
|
|
692
|
+
self._elements = new_dict
|
|
693
|
+
|
|
694
|
+
else:
|
|
695
|
+
raise InvalidHamiltonianOperation(
|
|
696
|
+
f"Invalid multiplication between Hamiltonian and {other.__class__.__name__}."
|
|
697
|
+
)
|
|
698
|
+
return None
|
|
699
|
+
|
|
700
|
+
def _div_inplace(self, other: Complex | PauliOperator | Hamiltonian) -> None:
|
|
701
|
+
# Only valid for scalars
|
|
702
|
+
if not isinstance(other, (int, float, complex)):
|
|
703
|
+
raise InvalidHamiltonianOperation("Division by operators is not supported")
|
|
704
|
+
if other == 0:
|
|
705
|
+
raise ZeroDivisionError("Cannot divide by zero.")
|
|
706
|
+
self._mul_inplace(1 / other)
|