qilisdk 0.1.4__py3-none-any.whl → 0.1.5__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 +11 -2
- qilisdk/__init__.pyi +2 -3
- qilisdk/_logging.py +135 -0
- qilisdk/_optionals.py +5 -7
- qilisdk/analog/__init__.py +3 -18
- qilisdk/analog/exceptions.py +2 -4
- qilisdk/analog/hamiltonian.py +455 -110
- qilisdk/analog/linear_schedule.py +118 -0
- qilisdk/analog/schedule.py +272 -79
- qilisdk/backends/__init__.py +45 -0
- qilisdk/{digital/digital_algorithm.py → backends/__init__.pyi} +3 -5
- qilisdk/backends/backend.py +117 -0
- qilisdk/{extras/cuda → backends}/cuda_backend.py +152 -159
- qilisdk/backends/qutip_backend.py +492 -0
- qilisdk/common/__init__.py +48 -2
- qilisdk/common/algorithm.py +2 -1
- qilisdk/{extras/qaas/qaas_settings.py → common/exceptions.py} +12 -6
- qilisdk/common/model.py +1019 -1
- qilisdk/common/parameterizable.py +75 -0
- qilisdk/common/qtensor.py +666 -0
- qilisdk/common/result.py +2 -1
- qilisdk/common/variables.py +1931 -0
- qilisdk/{extras/cuda/cuda_analog_result.py → cost_functions/__init__.py} +3 -4
- qilisdk/cost_functions/cost_function.py +77 -0
- qilisdk/cost_functions/model_cost_function.py +145 -0
- qilisdk/cost_functions/observable_cost_function.py +109 -0
- qilisdk/digital/__init__.py +3 -22
- qilisdk/digital/ansatz.py +203 -160
- qilisdk/digital/circuit.py +81 -9
- qilisdk/digital/exceptions.py +12 -6
- qilisdk/digital/gates.py +228 -85
- qilisdk/{extras/qaas/qaas_analog_result.py → functionals/__init__.py} +14 -5
- qilisdk/functionals/functional.py +39 -0
- qilisdk/{extras/cuda/cuda_digital_result.py → functionals/functional_result.py} +3 -4
- qilisdk/functionals/sampling.py +81 -0
- qilisdk/functionals/sampling_result.py +92 -0
- qilisdk/functionals/time_evolution.py +98 -0
- qilisdk/functionals/time_evolution_result.py +84 -0
- qilisdk/functionals/variational_program.py +80 -0
- qilisdk/functionals/variational_program_result.py +69 -0
- qilisdk/logging_config.yaml +16 -0
- qilisdk/{common/backend.py → optimizers/__init__.py} +2 -1
- qilisdk/optimizers/optimizer.py +39 -0
- qilisdk/{common → optimizers}/optimizer_result.py +3 -12
- qilisdk/{common/optimizer.py → optimizers/scipy_optimizer.py} +10 -28
- qilisdk/settings.py +78 -0
- qilisdk/{extras → speqtrum}/__init__.py +7 -8
- qilisdk/{extras → speqtrum}/__init__.pyi +3 -3
- qilisdk/speqtrum/experiments/__init__.py +25 -0
- qilisdk/speqtrum/experiments/experiment_functional.py +124 -0
- qilisdk/speqtrum/experiments/experiment_result.py +231 -0
- qilisdk/{extras/qaas → speqtrum}/keyring.py +8 -4
- qilisdk/speqtrum/speqtrum.py +432 -0
- qilisdk/speqtrum/speqtrum_models.py +300 -0
- qilisdk/utils/__init__.py +0 -14
- qilisdk/utils/openqasm2.py +1 -1
- qilisdk/utils/serialization.py +1 -1
- qilisdk/utils/visualization/PlusJakartaSans-SemiBold.ttf +0 -0
- qilisdk/utils/visualization/__init__.py +24 -0
- qilisdk/utils/visualization/circuit_renderers.py +781 -0
- qilisdk/utils/visualization/schedule_renderers.py +161 -0
- qilisdk/utils/visualization/style.py +154 -0
- qilisdk/utils/visualization/themes.py +76 -0
- qilisdk/yaml.py +126 -0
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/METADATA +180 -134
- qilisdk-0.1.5.dist-info/RECORD +69 -0
- qilisdk/analog/algorithms.py +0 -111
- qilisdk/analog/analog_backend.py +0 -43
- qilisdk/analog/analog_result.py +0 -114
- qilisdk/analog/quantum_objects.py +0 -596
- qilisdk/digital/digital_backend.py +0 -90
- qilisdk/digital/digital_result.py +0 -145
- qilisdk/digital/vqe.py +0 -166
- qilisdk/extras/cuda/__init__.py +0 -13
- qilisdk/extras/qaas/__init__.py +0 -13
- qilisdk/extras/qaas/models.py +0 -132
- qilisdk/extras/qaas/qaas_backend.py +0 -255
- qilisdk/extras/qaas/qaas_digital_result.py +0 -20
- qilisdk/extras/qaas/qaas_time_evolution_result.py +0 -20
- qilisdk/extras/qaas/qaas_vqe_result.py +0 -20
- qilisdk-0.1.4.dist-info/RECORD +0 -51
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/WHEEL +0 -0
- {qilisdk-0.1.4.dist-info → qilisdk-0.1.5.dist-info}/licenses/LICENCE +0 -0
qilisdk/analog/hamiltonian.py
CHANGED
|
@@ -18,19 +18,24 @@ import re
|
|
|
18
18
|
from abc import ABC
|
|
19
19
|
from collections import defaultdict
|
|
20
20
|
from functools import reduce
|
|
21
|
+
from itertools import product
|
|
21
22
|
from typing import TYPE_CHECKING, Callable, ClassVar
|
|
22
23
|
|
|
23
24
|
import numpy as np
|
|
24
|
-
from scipy.sparse import
|
|
25
|
+
from scipy.sparse import csr_matrix, identity, kron, spmatrix
|
|
25
26
|
|
|
27
|
+
from qilisdk.common.parameterizable import Parameterizable
|
|
28
|
+
from qilisdk.common.qtensor import QTensor
|
|
29
|
+
from qilisdk.common.variables import BaseVariable, Parameter, Term
|
|
26
30
|
from qilisdk.yaml import yaml
|
|
27
31
|
|
|
28
|
-
from .exceptions import InvalidHamiltonianOperation
|
|
32
|
+
from .exceptions import InvalidHamiltonianOperation
|
|
29
33
|
|
|
30
34
|
if TYPE_CHECKING:
|
|
31
35
|
from collections.abc import Iterator
|
|
32
36
|
|
|
33
|
-
|
|
37
|
+
|
|
38
|
+
Number = int | float | complex
|
|
34
39
|
|
|
35
40
|
|
|
36
41
|
###############################################################################
|
|
@@ -62,20 +67,20 @@ def _get_pauli(name: str, qubit: int) -> PauliOperator:
|
|
|
62
67
|
###############################################################################
|
|
63
68
|
# Public Factory Functions
|
|
64
69
|
###############################################################################
|
|
65
|
-
def Z(qubit: int) ->
|
|
66
|
-
return _get_pauli("Z", qubit)
|
|
70
|
+
def Z(qubit: int) -> Hamiltonian:
|
|
71
|
+
return _get_pauli("Z", qubit).to_hamiltonian()
|
|
67
72
|
|
|
68
73
|
|
|
69
|
-
def X(qubit: int) ->
|
|
70
|
-
return _get_pauli("X", qubit)
|
|
74
|
+
def X(qubit: int) -> Hamiltonian:
|
|
75
|
+
return _get_pauli("X", qubit).to_hamiltonian()
|
|
71
76
|
|
|
72
77
|
|
|
73
|
-
def Y(qubit: int) ->
|
|
74
|
-
return _get_pauli("Y", qubit)
|
|
78
|
+
def Y(qubit: int) -> Hamiltonian:
|
|
79
|
+
return _get_pauli("Y", qubit).to_hamiltonian()
|
|
75
80
|
|
|
76
81
|
|
|
77
|
-
def I(qubit: int = 0) ->
|
|
78
|
-
return _get_pauli("I", qubit)
|
|
82
|
+
def I(qubit: int = 0) -> Hamiltonian: # noqa: E743
|
|
83
|
+
return _get_pauli("I", qubit).to_hamiltonian()
|
|
79
84
|
|
|
80
85
|
|
|
81
86
|
###############################################################################
|
|
@@ -83,8 +88,18 @@ def I(qubit: int = 0) -> PauliOperator: # noqa: E743
|
|
|
83
88
|
###############################################################################
|
|
84
89
|
class PauliOperator(ABC):
|
|
85
90
|
"""
|
|
86
|
-
A generic Pauli operator that acts on one qubit.
|
|
87
|
-
|
|
91
|
+
A generic abstract Pauli operator that acts on one qubit.
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
.. code-block:: python
|
|
95
|
+
|
|
96
|
+
from qilisdk.analog import PauliX
|
|
97
|
+
|
|
98
|
+
op = PauliX(0)
|
|
99
|
+
|
|
100
|
+
Flyweight usage: do NOT instantiate directly—use PauliX(q), PauliY(q), etc.
|
|
101
|
+
|
|
102
|
+
Note: You can also use the factory functions X(q), Y(q), Z(q), I(q) to get a Hamiltonian object.
|
|
88
103
|
"""
|
|
89
104
|
|
|
90
105
|
_NAME: ClassVar[str]
|
|
@@ -131,33 +146,33 @@ class PauliOperator(ABC):
|
|
|
131
146
|
|
|
132
147
|
# ----------- Arithmetic Operators ------------
|
|
133
148
|
|
|
134
|
-
def __add__(self, other:
|
|
149
|
+
def __add__(self, other: Number | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
135
150
|
return self.to_hamiltonian() + other
|
|
136
151
|
|
|
137
152
|
__radd__ = __add__
|
|
138
153
|
__iadd__ = __add__
|
|
139
154
|
|
|
140
|
-
def __sub__(self, other:
|
|
155
|
+
def __sub__(self, other: Number | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
141
156
|
return self.to_hamiltonian() - other
|
|
142
157
|
|
|
143
|
-
def __rsub__(self, other:
|
|
158
|
+
def __rsub__(self, other: Number | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
144
159
|
return other - self.to_hamiltonian()
|
|
145
160
|
|
|
146
161
|
__isub__ = __sub__
|
|
147
162
|
|
|
148
|
-
def __mul__(self, other:
|
|
163
|
+
def __mul__(self, other: Number | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
149
164
|
return self.to_hamiltonian() * other
|
|
150
165
|
|
|
151
|
-
def __rmul__(self, other:
|
|
166
|
+
def __rmul__(self, other: Number | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
152
167
|
return other * self.to_hamiltonian()
|
|
153
168
|
|
|
154
169
|
__imul__ = __mul__
|
|
155
170
|
|
|
156
|
-
def __truediv__(self, other:
|
|
171
|
+
def __truediv__(self, other: Number | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
157
172
|
return self.to_hamiltonian() / other
|
|
158
173
|
|
|
159
|
-
def __rtruediv__(self, _:
|
|
160
|
-
raise
|
|
174
|
+
def __rtruediv__(self, _: Number | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
175
|
+
raise InvalidHamiltonianOperation("Division by operators is not supported")
|
|
161
176
|
|
|
162
177
|
__itruediv__ = __truediv__
|
|
163
178
|
|
|
@@ -194,53 +209,174 @@ class PauliI(PauliOperator):
|
|
|
194
209
|
|
|
195
210
|
|
|
196
211
|
@yaml.register_class
|
|
197
|
-
class Hamiltonian:
|
|
212
|
+
class Hamiltonian(Parameterizable):
|
|
198
213
|
"""
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
214
|
+
Represent a Hamiltonian expressed as a linear combination of Pauli operators.
|
|
215
|
+
|
|
216
|
+
Example:
|
|
217
|
+
.. code-block:: python
|
|
218
|
+
|
|
219
|
+
from qilisdk.analog.hamiltonian import Hamiltonian, X, Z
|
|
220
|
+
|
|
221
|
+
H = X(0) * X(1) + Z(1)
|
|
205
222
|
"""
|
|
206
223
|
|
|
207
224
|
_EPS: float = 1e-14
|
|
208
225
|
_PAULI_PRODUCT_TABLE: ClassVar[dict[tuple[str, str], tuple[complex, Callable[..., PauliOperator]]]] = {
|
|
209
|
-
("X", "X"): (1,
|
|
210
|
-
("X", "Y"): (1j,
|
|
211
|
-
("X", "Z"): (-1j,
|
|
212
|
-
("Y", "X"): (-1j,
|
|
213
|
-
("Y", "Y"): (1,
|
|
214
|
-
("Y", "Z"): (1j,
|
|
215
|
-
("Z", "X"): (1j,
|
|
216
|
-
("Z", "Y"): (-1j,
|
|
217
|
-
("Z", "Z"): (1,
|
|
226
|
+
("X", "X"): (1, PauliI),
|
|
227
|
+
("X", "Y"): (1j, PauliZ),
|
|
228
|
+
("X", "Z"): (-1j, PauliY),
|
|
229
|
+
("Y", "X"): (-1j, PauliZ),
|
|
230
|
+
("Y", "Y"): (1, PauliI),
|
|
231
|
+
("Y", "Z"): (1j, PauliX),
|
|
232
|
+
("Z", "X"): (1j, PauliY),
|
|
233
|
+
("Z", "Y"): (-1j, PauliX),
|
|
234
|
+
("Z", "Z"): (1, PauliI),
|
|
218
235
|
}
|
|
219
236
|
|
|
220
237
|
ZERO: int = 0
|
|
221
238
|
|
|
222
|
-
def __init__(self, elements: dict[tuple[PauliOperator, ...], complex] | None = None) -> None:
|
|
223
|
-
|
|
239
|
+
def __init__(self, elements: dict[tuple[PauliOperator, ...], complex | Term | Parameter] | None = None) -> None:
|
|
240
|
+
"""
|
|
241
|
+
Build a Hamiltonian from a mapping of Pauli operator products to coefficients.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
elements (dict[tuple[PauliOperator, ...], complex | Term | Parameter], optional):
|
|
245
|
+
Mapping from operator tuples to numerical coefficients or symbolic parameters. For example:
|
|
246
|
+
|
|
247
|
+
.. code-block:: python
|
|
248
|
+
|
|
249
|
+
{
|
|
250
|
+
(Z(0), Y(1)): 1.0,
|
|
251
|
+
(X(1),): 1j,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
Defaults to None, which creates an empty Hamiltonian.
|
|
255
|
+
|
|
256
|
+
Raises:
|
|
257
|
+
ValueError: If the provided coefficients include generic variables instead of parameters.
|
|
258
|
+
"""
|
|
259
|
+
self._elements: dict[tuple[PauliOperator, ...], complex | Term | Parameter] = defaultdict(complex)
|
|
260
|
+
self._parameters: dict[str, Parameter] = {}
|
|
224
261
|
if elements:
|
|
225
262
|
for key, val in elements.items():
|
|
263
|
+
if isinstance(val, Term):
|
|
264
|
+
for v in val.variables():
|
|
265
|
+
if isinstance(v, Parameter):
|
|
266
|
+
self._parameters[v.label] = v
|
|
267
|
+
else:
|
|
268
|
+
raise ValueError(
|
|
269
|
+
"Only Parameters are allowed to be used in hamiltonians. Generic Variables are not supported"
|
|
270
|
+
)
|
|
271
|
+
elif isinstance(val, BaseVariable):
|
|
272
|
+
if isinstance(val, Parameter):
|
|
273
|
+
self._parameters[val.label] = val
|
|
274
|
+
|
|
275
|
+
else:
|
|
276
|
+
raise ValueError(
|
|
277
|
+
"Only Parameters are allowed to be used in hamiltonians. Generic Variables are not supported"
|
|
278
|
+
)
|
|
226
279
|
self._elements[key] += val
|
|
227
280
|
self.simplify()
|
|
228
281
|
|
|
229
282
|
@property
|
|
230
283
|
def nqubits(self) -> int:
|
|
231
|
-
"""Number of qubits
|
|
284
|
+
"""Number of qubits on which the Hamiltonian acts."""
|
|
232
285
|
qubits = {op.qubit for key in self._elements for op in key}
|
|
233
286
|
|
|
234
287
|
return max(qubits) + 1 if qubits else 0
|
|
235
288
|
|
|
236
289
|
@property
|
|
237
290
|
def elements(self) -> dict[tuple[PauliOperator, ...], complex]:
|
|
238
|
-
"""
|
|
239
|
-
return
|
|
291
|
+
"""Return the stored operator-coefficient mapping with symbolic terms evaluated."""
|
|
292
|
+
return {
|
|
293
|
+
k: (v if isinstance(v, complex) else (v.evaluate({}) if isinstance(v, Term) else v.evaluate()))
|
|
294
|
+
for k, v in self._elements.items()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def nparameters(self) -> int:
|
|
299
|
+
"""Return the number of unique symbolic parameters contained in the Hamiltonian."""
|
|
300
|
+
return len(self._parameters)
|
|
301
|
+
|
|
302
|
+
@property
|
|
303
|
+
def parameters(self) -> dict[str, Parameter]:
|
|
304
|
+
"""Return a mapping from parameter labels to their corresponding parameter objects."""
|
|
305
|
+
return self._parameters
|
|
306
|
+
|
|
307
|
+
def get_parameter_values(self) -> list[float]:
|
|
308
|
+
"""Return the current numeric values of the Hamiltonian parameters."""
|
|
309
|
+
return [param.value for param in self._parameters.values()]
|
|
310
|
+
|
|
311
|
+
def get_parameter_names(self) -> list[str]:
|
|
312
|
+
"""Return the ordered list of parameter labels defined in the Hamiltonian."""
|
|
313
|
+
return list(self._parameters.keys())
|
|
314
|
+
|
|
315
|
+
def get_parameters(self) -> dict[str, float]:
|
|
316
|
+
"""Return a mapping from parameter labels to their current numerical values."""
|
|
317
|
+
return {label: param.value for label, param in self._parameters.items()}
|
|
318
|
+
|
|
319
|
+
def set_parameter_values(self, values: list[float]) -> None:
|
|
320
|
+
"""
|
|
321
|
+
Update the numerical values of the Hamiltonian parameters.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
values (list[float]): New values ordered according to ``get_parameter_names()``.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
ValueError: If the number of provided values does not match ``nparameters``.
|
|
328
|
+
"""
|
|
329
|
+
if len(values) != self.nparameters:
|
|
330
|
+
raise ValueError(f"Provided {len(values)} but Hamiltonian has {self.nparameters} parameters.")
|
|
331
|
+
for i, parameter in enumerate(self._parameters.values()):
|
|
332
|
+
parameter.set_value(values[i])
|
|
333
|
+
|
|
334
|
+
def set_parameters(self, parameter_dict: dict[str, float]) -> None:
|
|
335
|
+
"""
|
|
336
|
+
Update a subset of parameters by label.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
parameter_dict (dict[str, float]): Mapping from parameter labels to new numerical values.
|
|
340
|
+
|
|
341
|
+
Raises:
|
|
342
|
+
ValueError: If an unknown parameter label is provided.
|
|
343
|
+
"""
|
|
344
|
+
for label, param in parameter_dict.items():
|
|
345
|
+
if label not in self._parameters:
|
|
346
|
+
raise ValueError(f"Parameter {label} is not defined in this hamiltonian.")
|
|
347
|
+
self._parameters[label].set_value(param)
|
|
348
|
+
|
|
349
|
+
def get_parameter_bounds(self) -> dict[str, tuple[float, float]]:
|
|
350
|
+
"""Return the lower and upper bounds currently associated with each parameter."""
|
|
351
|
+
return {k: v.bounds for k, v in self._parameters.items()}
|
|
352
|
+
|
|
353
|
+
def set_parameter_bounds(self, ranges: dict[str, tuple[float, float]]) -> None:
|
|
354
|
+
"""
|
|
355
|
+
Update parameter bounds.
|
|
356
|
+
|
|
357
|
+
Args:
|
|
358
|
+
ranges (dict[str, tuple[float, float]]): Mapping from parameter labels to ``(lower, upper)`` bounds.
|
|
359
|
+
|
|
360
|
+
Raises:
|
|
361
|
+
ValueError: If an unknown parameter label is provided.
|
|
362
|
+
"""
|
|
363
|
+
for label, bound in ranges.items():
|
|
364
|
+
if label not in self._parameters:
|
|
365
|
+
raise ValueError(
|
|
366
|
+
f"The provided parameter label {label} is not defined in the list of parameters in this object."
|
|
367
|
+
)
|
|
368
|
+
self._parameters[label].set_bounds(bound[0], bound[1])
|
|
240
369
|
|
|
241
370
|
def simplify(self) -> Hamiltonian:
|
|
371
|
+
"""Simplify the Hamiltonian expression by removing near-zero terms and accumulating constant terms.
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Hamiltonian: Simplified Hamiltonian
|
|
375
|
+
"""
|
|
242
376
|
# 1) Remove near-zero
|
|
243
|
-
keys_to_remove = [
|
|
377
|
+
keys_to_remove = [
|
|
378
|
+
key for key, value in self._elements.items() if isinstance(value, complex) and abs(value) < Hamiltonian._EPS
|
|
379
|
+
]
|
|
244
380
|
for key in keys_to_remove:
|
|
245
381
|
del self._elements[key]
|
|
246
382
|
|
|
@@ -252,11 +388,11 @@ class Hamiltonian:
|
|
|
252
388
|
]
|
|
253
389
|
for key, value in to_accumulate:
|
|
254
390
|
del self._elements[key]
|
|
255
|
-
self._elements[
|
|
391
|
+
self._elements[PauliI(0),] += value
|
|
256
392
|
|
|
257
393
|
return self
|
|
258
394
|
|
|
259
|
-
def _apply_operator_on_qubit(self, terms: list[PauliOperator]) -> spmatrix:
|
|
395
|
+
def _apply_operator_on_qubit(self, terms: list[PauliOperator], padding: int = 0) -> spmatrix:
|
|
260
396
|
"""Get the matrix representation of a single term by taking the tensor product
|
|
261
397
|
of operators acting on each qubit. For qubits with no operator in `terms`,
|
|
262
398
|
the identity is used.
|
|
@@ -269,12 +405,12 @@ class Hamiltonian:
|
|
|
269
405
|
"""
|
|
270
406
|
# Build a list of factors for each qubit
|
|
271
407
|
factors = []
|
|
272
|
-
for q in range(self.nqubits):
|
|
408
|
+
for q in range(self.nqubits + padding):
|
|
273
409
|
# Look for an operator acting on qubit q
|
|
274
410
|
op = next((t for t in terms if t.qubit == q), None)
|
|
275
411
|
if op is not None:
|
|
276
412
|
# Wrap the operator's matrix as a sparse matrix.
|
|
277
|
-
factors.append(
|
|
413
|
+
factors.append(csr_matrix(np.array(op.matrix)))
|
|
278
414
|
else:
|
|
279
415
|
factors.append(identity(2, format="csc"))
|
|
280
416
|
# Compute the tensor (Kronecker) product over all qubits.
|
|
@@ -289,13 +425,51 @@ class Hamiltonian:
|
|
|
289
425
|
"""
|
|
290
426
|
dim = 2**self.nqubits
|
|
291
427
|
# Initialize a zero matrix of the appropriate dimension.
|
|
292
|
-
result =
|
|
428
|
+
result = csr_matrix(np.zeros((dim, dim), dtype=complex))
|
|
293
429
|
for coeff, term in self:
|
|
294
430
|
result += coeff * self._apply_operator_on_qubit(term)
|
|
295
431
|
return result
|
|
296
432
|
|
|
433
|
+
def to_qtensor(self, total_nqubits: int | None = None) -> QTensor:
|
|
434
|
+
"""Return the Hamiltonian as a ``QTensor`` built from the sparse matrix representation.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
total_nqubits (int, optional): Specify the total number of qubits that this hamiltonian acts on. Defaults to None.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
QTensor: The QTensor object representation of the Hamiltonian.
|
|
441
|
+
|
|
442
|
+
Raises:
|
|
443
|
+
ValueError: If the total_nqubits provided is lower than the number of qubits effected by the hamiltonian.
|
|
444
|
+
"""
|
|
445
|
+
|
|
446
|
+
nqubits = total_nqubits or self.nqubits
|
|
447
|
+
padding = nqubits - self.nqubits
|
|
448
|
+
if nqubits < self.nqubits:
|
|
449
|
+
raise ValueError(
|
|
450
|
+
f"The total number of qubits can't be less than the number of the qubits effected by this hamiltonian ({self.nqubits})"
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
dim = 2 ** (nqubits)
|
|
454
|
+
|
|
455
|
+
# Initialize a zero matrix of the appropriate dimension.
|
|
456
|
+
result = csr_matrix(np.zeros((dim, dim), dtype=complex))
|
|
457
|
+
for coeff, term in self:
|
|
458
|
+
result += coeff * self._apply_operator_on_qubit(term, padding=padding)
|
|
459
|
+
return QTensor(result)
|
|
460
|
+
|
|
461
|
+
def get_static_hamiltonian(self) -> Hamiltonian:
|
|
462
|
+
"""Return a Hamiltonian containing only constant coefficients."""
|
|
463
|
+
out = Hamiltonian()
|
|
464
|
+
for pauli, value in self.elements.items():
|
|
465
|
+
aux: Hamiltonian | PauliOperator = pauli[0]
|
|
466
|
+
for p in list(pauli)[1:]:
|
|
467
|
+
aux *= p
|
|
468
|
+
out += aux * value
|
|
469
|
+
return out
|
|
470
|
+
|
|
297
471
|
def __iter__(self) -> Iterator[tuple[complex, list[PauliOperator]]]:
|
|
298
|
-
for key, value in self.
|
|
472
|
+
for key, value in self.elements.items():
|
|
299
473
|
yield value, list(key)
|
|
300
474
|
|
|
301
475
|
# ------- Equality & hashing --------
|
|
@@ -304,10 +478,12 @@ class Hamiltonian:
|
|
|
304
478
|
if other == Hamiltonian.ZERO:
|
|
305
479
|
return bool(
|
|
306
480
|
len(self._elements) == 0
|
|
307
|
-
or (len(self._elements) == 1 and (
|
|
481
|
+
or (len(self._elements) == 1 and (PauliI(0),) in self._elements and self._elements[PauliI(0),] == 0)
|
|
482
|
+
)
|
|
483
|
+
if isinstance(other, Number):
|
|
484
|
+
return bool(
|
|
485
|
+
len(self._elements) == 1 and (PauliI(0),) in self._elements and self._elements[PauliI(0),] == other
|
|
308
486
|
)
|
|
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
487
|
if isinstance(other, PauliOperator):
|
|
312
488
|
other = other.to_hamiltonian()
|
|
313
489
|
if not isinstance(other, Hamiltonian):
|
|
@@ -322,7 +498,7 @@ class Hamiltonian:
|
|
|
322
498
|
return hash(items_frozen)
|
|
323
499
|
|
|
324
500
|
def __copy__(self) -> Hamiltonian:
|
|
325
|
-
return Hamiltonian(elements=self.
|
|
501
|
+
return Hamiltonian(elements=self._elements.copy())
|
|
326
502
|
|
|
327
503
|
# ------- String representation --------
|
|
328
504
|
|
|
@@ -356,9 +532,9 @@ class Hamiltonian:
|
|
|
356
532
|
return s
|
|
357
533
|
|
|
358
534
|
# We want to place the single identity term (I(0),) at the front if it exists
|
|
359
|
-
items = list(self.
|
|
535
|
+
items = list(self.elements.items())
|
|
360
536
|
try:
|
|
361
|
-
i = next(idx for idx, (key, _) in enumerate(items) if len(key) == 1 and key[0] == (
|
|
537
|
+
i = next(idx for idx, (key, _) in enumerate(items) if len(key) == 1 and key[0] == (PauliI(0)))
|
|
362
538
|
item = items.pop(i)
|
|
363
539
|
items.insert(0, item)
|
|
364
540
|
except StopIteration:
|
|
@@ -397,23 +573,102 @@ class Hamiltonian:
|
|
|
397
573
|
return " ".join(parts)
|
|
398
574
|
|
|
399
575
|
@classmethod
|
|
400
|
-
def
|
|
401
|
-
|
|
576
|
+
def from_qtensor(cls, tensor: QTensor, tol: float = 1e-10, prune: float = 1e-12) -> Hamiltonian:
|
|
577
|
+
"""
|
|
578
|
+
Expand a qtensor (dense operator) on n qubits into a sum of Pauli strings,
|
|
579
|
+
returning a qilisdk.analog.Hamiltonian.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
tol (float): Hermiticity check tolerance.
|
|
583
|
+
prune (float): Drop coefficients whose absolute value satisfies ``abs(c) < prune`` to reduce numerical noise.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Hamiltonian: Sum_{P in {I,X,Y,Z}^{⊗ n}} c_P * P with c_P = Tr(qt * P) / 2^n
|
|
587
|
+
|
|
588
|
+
Raises
|
|
589
|
+
ValueError: If the input is not square, not a power-of-two dimension, or not Hermitian w.r.t. `tol`.
|
|
590
|
+
"""
|
|
591
|
+
A = np.asarray(tensor.dense)
|
|
592
|
+
|
|
593
|
+
dim = tensor.shape[0]
|
|
594
|
+
n = round(np.log2(dim))
|
|
595
|
+
if 2**n != dim:
|
|
596
|
+
raise ValueError(f"Matrix dimension {dim} is not a power of two.")
|
|
597
|
+
if not tensor.is_hermitian():
|
|
598
|
+
raise ValueError("Matrix is not Hermitian within tolerance; cannot form a Hamiltonian.")
|
|
599
|
+
|
|
600
|
+
# QiliSDK Pauli constructors indexed by qubit id
|
|
601
|
+
pauli_for: dict[int, Callable[[int], Hamiltonian]] = {0: I, 1: X, 2: Y, 3: Z}
|
|
602
|
+
|
|
603
|
+
# Normalization from orthonormality: Tr(P_a P_b) = 2^n δ_ab
|
|
604
|
+
norm = 1.0 / (2**n)
|
|
605
|
+
|
|
606
|
+
# Prebuild per-qubit operator “letters” so we can construct full strings quickly
|
|
607
|
+
|
|
608
|
+
H = Hamiltonian() # start additive Hamiltonian expression
|
|
609
|
+
# Full Pauli basis (includes identity on any subset automatically)
|
|
610
|
+
for word in product((0, 1, 2, 3), repeat=n):
|
|
611
|
+
# Compose the word into a QiliSDK operator acting on the proper qubits
|
|
612
|
+
# Example word=(3,1,0) for n=3 -> Z(0)*X(1)*I(2)
|
|
613
|
+
op = Hamiltonian()
|
|
614
|
+
for q, letter in enumerate(word):
|
|
615
|
+
# multiply by the operator on qubit q
|
|
616
|
+
op = op * pauli_for[letter](q) if op != 0 else pauli_for[letter](q)
|
|
617
|
+
|
|
618
|
+
# Convert to dense once; no padding needed because it spans all n qubits
|
|
619
|
+
P_dense = op.to_qtensor(n).dense
|
|
620
|
+
|
|
621
|
+
# Coefficient c_P = Tr(A P) / 2^n (P is Hermitian)
|
|
622
|
+
c = norm * np.trace(A @ P_dense)
|
|
623
|
+
|
|
624
|
+
# Numerical safety: coefficients should be real for Hermitian A and P
|
|
625
|
+
if abs(c.imag) < tol:
|
|
626
|
+
c = c.real
|
|
627
|
+
|
|
628
|
+
if abs(c) > prune:
|
|
629
|
+
H += c * op
|
|
630
|
+
|
|
631
|
+
# Optional: verify round-trip (use a slightly looser atol to tolerate pruning)
|
|
632
|
+
if not np.allclose(H.to_qtensor(n).dense, A, atol=max(10 * prune, 1e-9)):
|
|
633
|
+
# If this triggers, consider lowering `prune` or raising `tol`.
|
|
634
|
+
raise ValueError("Pauli expansion failed round-trip check; try adjusting tolerances.")
|
|
635
|
+
|
|
636
|
+
return H
|
|
637
|
+
|
|
638
|
+
@classmethod
|
|
639
|
+
def parse(cls, hamiltonian_str: str) -> Hamiltonian:
|
|
640
|
+
hamiltonian_str = hamiltonian_str.strip()
|
|
641
|
+
|
|
642
|
+
# 1) remove *all* spaces inside any ( … ) group (coefficients or indices)
|
|
643
|
+
hamiltonian_str = re.sub(
|
|
644
|
+
r"\(\s*([0-9A-Za-z.+\-j\s]+?)\s*\)",
|
|
645
|
+
lambda m: "(" + re.sub(r"\s+", "", str(m.group(1))) + ")",
|
|
646
|
+
hamiltonian_str,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
# 2) collapse multiple spaces down to one (outside the parens now)
|
|
650
|
+
hamiltonian_str = re.sub(r"\s+", " ", hamiltonian_str)
|
|
651
|
+
|
|
652
|
+
# 3) ensure a single space between a closing “)” and the next operator token like X(0)/Y(1)/etc.
|
|
653
|
+
hamiltonian_str = re.sub(r"\)\s*(?=[XYZI]\()", ") ", hamiltonian_str)
|
|
654
|
+
|
|
402
655
|
# Special case: "0" => empty Hamiltonian
|
|
403
|
-
if
|
|
656
|
+
if hamiltonian_str == "0":
|
|
404
657
|
return cls({})
|
|
405
658
|
|
|
406
|
-
elements: dict[tuple[PauliOperator, ...], complex] = defaultdict(
|
|
659
|
+
elements: dict[tuple[PauliOperator, ...], complex | Term | Parameter] = defaultdict(
|
|
660
|
+
complex
|
|
661
|
+
) # TODO (ameer): the parsing doesn't support Term and Parameters
|
|
407
662
|
|
|
408
663
|
# If there's no initial +/- sign, prepend '+ ' for easier splitting
|
|
409
|
-
if not
|
|
410
|
-
|
|
664
|
+
if not hamiltonian_str.startswith("+") and not hamiltonian_str.startswith("-"):
|
|
665
|
+
hamiltonian_str = "+ " + hamiltonian_str
|
|
411
666
|
|
|
412
667
|
# Replace " - " with " + - " so each term is split on " + "
|
|
413
|
-
|
|
668
|
+
hamiltonian_str = hamiltonian_str.replace(" - ", " + - ")
|
|
414
669
|
|
|
415
670
|
# Split on " + "
|
|
416
|
-
tokens =
|
|
671
|
+
tokens = hamiltonian_str.split(" + ")
|
|
417
672
|
# Remove any empty tokens (can happen if the string started "+ ")
|
|
418
673
|
tokens = [t.strip() for t in tokens if t.strip()]
|
|
419
674
|
|
|
@@ -475,19 +730,74 @@ class Hamiltonian:
|
|
|
475
730
|
|
|
476
731
|
return coeff_val, ops
|
|
477
732
|
|
|
478
|
-
for
|
|
479
|
-
coeff, op_list = parse_token(
|
|
733
|
+
for token in tokens:
|
|
734
|
+
coeff, op_list = parse_token(token)
|
|
480
735
|
if not op_list:
|
|
481
736
|
# purely scalar => store as (I(0),)
|
|
482
|
-
elements[
|
|
737
|
+
elements[PauliI(0),] += coeff
|
|
483
738
|
else:
|
|
484
739
|
# Sort operators by qubit for canonical ordering
|
|
485
740
|
op_list.sort(key=lambda op: op.qubit)
|
|
486
741
|
elements[tuple(op_list)] += coeff
|
|
487
742
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
return
|
|
743
|
+
hamiltonian = cls(elements)
|
|
744
|
+
hamiltonian.simplify()
|
|
745
|
+
return hamiltonian
|
|
746
|
+
|
|
747
|
+
def commutator(self, h: Hamiltonian) -> Hamiltonian:
|
|
748
|
+
"""compute the commutator of the current hamiltonian with another hamiltonian (h)
|
|
749
|
+
|
|
750
|
+
Args:
|
|
751
|
+
h (Hamiltonian): the second hamiltonian.
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
Hamiltonian: the commutator.
|
|
755
|
+
"""
|
|
756
|
+
return self * h - h * self
|
|
757
|
+
|
|
758
|
+
def anticommutator(self, h: Hamiltonian) -> Hamiltonian:
|
|
759
|
+
"""compute the anticommutator of the current hamiltonian with another hamiltonian (h)
|
|
760
|
+
|
|
761
|
+
Args:
|
|
762
|
+
h (Hamiltonian): the second hamiltonian.
|
|
763
|
+
|
|
764
|
+
Returns:
|
|
765
|
+
Hamiltonian: the anticommutator.
|
|
766
|
+
"""
|
|
767
|
+
return self * h + h * self
|
|
768
|
+
|
|
769
|
+
def vector_norm(self) -> float:
|
|
770
|
+
"""
|
|
771
|
+
Returns:
|
|
772
|
+
float: the vector norm of the hamiltonian.
|
|
773
|
+
"""
|
|
774
|
+
s = 0
|
|
775
|
+
for coeff, _ in self:
|
|
776
|
+
s += np.conj(coeff) * coeff
|
|
777
|
+
return np.real(np.sqrt(s))
|
|
778
|
+
|
|
779
|
+
def frobenius_norm(self) -> float:
|
|
780
|
+
"""
|
|
781
|
+
Returns:
|
|
782
|
+
float: the forbenius norm of the hamiltonian.
|
|
783
|
+
"""
|
|
784
|
+
n = self.nqubits
|
|
785
|
+
s = 0
|
|
786
|
+
for coeff, _ in self:
|
|
787
|
+
s += np.conj(coeff) * coeff
|
|
788
|
+
return np.real(np.sqrt(s) * np.sqrt(2**n))
|
|
789
|
+
|
|
790
|
+
def trace(self) -> Number:
|
|
791
|
+
"""
|
|
792
|
+
Returns:
|
|
793
|
+
float: the trace of the hamiltonian.
|
|
794
|
+
"""
|
|
795
|
+
t = self._elements.get((PauliI(0),), 0)
|
|
796
|
+
if isinstance(t, Parameter):
|
|
797
|
+
return t.evaluate()
|
|
798
|
+
if isinstance(t, Term):
|
|
799
|
+
return t.evaluate({})
|
|
800
|
+
return t
|
|
491
801
|
|
|
492
802
|
# ------- Internal multiplication helpers --------
|
|
493
803
|
|
|
@@ -520,7 +830,7 @@ class Hamiltonian:
|
|
|
520
830
|
|
|
521
831
|
# If everything simplified to identity, we store I(0)
|
|
522
832
|
if not final_ops:
|
|
523
|
-
final_ops = [
|
|
833
|
+
final_ops = [PauliI(0)]
|
|
524
834
|
|
|
525
835
|
# Sort again by qubit (to keep canonical form)
|
|
526
836
|
final_ops.sort(key=lambda op: op.qubit)
|
|
@@ -541,87 +851,87 @@ class Hamiltonian:
|
|
|
541
851
|
key = (op1.name, op2.name)
|
|
542
852
|
result = Hamiltonian._PAULI_PRODUCT_TABLE.get(key)
|
|
543
853
|
if result is None:
|
|
544
|
-
raise
|
|
854
|
+
raise InvalidHamiltonianOperation(f"Multiplying {op1} and {op2} not supported.")
|
|
545
855
|
phase, op_cls = result
|
|
546
856
|
|
|
547
857
|
# By convention, an I operator is always I(0) in this code
|
|
548
|
-
if op_cls is
|
|
549
|
-
return phase,
|
|
858
|
+
if op_cls is PauliI:
|
|
859
|
+
return phase, PauliI(0)
|
|
550
860
|
# Otherwise, keep the same qubit
|
|
551
861
|
return phase, op_cls(op1.qubit)
|
|
552
862
|
|
|
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
863
|
# ------- Public arithmetic operators --------
|
|
563
864
|
|
|
564
|
-
def __add__(self, other:
|
|
865
|
+
def __add__(self, other: Number | PauliOperator | Hamiltonian | Term | Parameter) -> Hamiltonian:
|
|
565
866
|
out = copy.copy(self)
|
|
867
|
+
if isinstance(other, Term) and not other.is_parameterized_term():
|
|
868
|
+
raise ValueError("Term provided contains generic variables that are not Parameter.")
|
|
566
869
|
out._add_inplace(other)
|
|
567
870
|
return out.simplify()
|
|
568
871
|
|
|
569
|
-
def __radd__(self, other:
|
|
872
|
+
def __radd__(self, other: Number | PauliOperator | Hamiltonian | Term | Parameter) -> Hamiltonian:
|
|
873
|
+
if isinstance(other, Term) and not other.is_parameterized_term():
|
|
874
|
+
raise ValueError("Term provided contains generic variables that are not Parameter.")
|
|
570
875
|
return self.__add__(other)
|
|
571
876
|
|
|
572
|
-
def __sub__(self, other:
|
|
877
|
+
def __sub__(self, other: Number | PauliOperator | Hamiltonian | Term | Parameter) -> Hamiltonian:
|
|
878
|
+
if isinstance(other, Term) and not other.is_parameterized_term():
|
|
879
|
+
raise ValueError("Term provided contains generic variables that are not Parameter.")
|
|
573
880
|
out = copy.copy(self)
|
|
574
881
|
out._sub_inplace(other)
|
|
575
882
|
return out.simplify()
|
|
576
883
|
|
|
577
|
-
def __rsub__(self, other:
|
|
884
|
+
def __rsub__(self, other: Number | PauliOperator | Hamiltonian | Term | Parameter) -> Hamiltonian:
|
|
578
885
|
# (other - self)
|
|
886
|
+
if isinstance(other, Term) and not other.is_parameterized_term():
|
|
887
|
+
raise ValueError("Term provided contains generic variables that are not Parameter.")
|
|
579
888
|
out = copy.copy(other if isinstance(other, Hamiltonian) else Hamiltonian() + other)
|
|
580
889
|
out._sub_inplace(self)
|
|
581
890
|
return out.simplify()
|
|
582
891
|
|
|
583
|
-
def
|
|
892
|
+
def __neg__(self) -> Hamiltonian:
|
|
893
|
+
return -1 * self
|
|
894
|
+
|
|
895
|
+
def __mul__(self, other: Number | PauliOperator | Hamiltonian | Term | Parameter) -> Hamiltonian:
|
|
896
|
+
if isinstance(other, Term) and not other.is_parameterized_term():
|
|
897
|
+
raise ValueError("Term provided contains generic variables that are not Parameter.")
|
|
584
898
|
out = copy.copy(self)
|
|
585
899
|
out._mul_inplace(other)
|
|
586
900
|
return out.simplify()
|
|
587
901
|
|
|
588
|
-
def __rmul__(self, other:
|
|
902
|
+
def __rmul__(self, other: Number | PauliOperator | Hamiltonian | Term | Parameter) -> Hamiltonian:
|
|
903
|
+
if isinstance(other, Term) and not other.is_parameterized_term():
|
|
904
|
+
raise ValueError("Term provided contains generic variables that are not Parameter.")
|
|
589
905
|
if isinstance(other, Hamiltonian):
|
|
590
906
|
out = copy.copy(other)
|
|
591
907
|
out._mul_inplace(self)
|
|
592
908
|
return out.simplify()
|
|
593
909
|
return self.__mul__(other)
|
|
594
910
|
|
|
595
|
-
def __truediv__(self, other:
|
|
911
|
+
def __truediv__(self, other: Number | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
596
912
|
out = copy.copy(self)
|
|
597
913
|
out._div_inplace(other)
|
|
598
914
|
return out.simplify()
|
|
599
915
|
|
|
600
|
-
def __rtruediv__(self, other:
|
|
916
|
+
def __rtruediv__(self, other: Number | PauliOperator | Hamiltonian) -> Hamiltonian:
|
|
601
917
|
# (other / self)
|
|
602
|
-
|
|
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
|
|
918
|
+
raise InvalidHamiltonianOperation("Division by operators is not supported")
|
|
611
919
|
|
|
612
920
|
__iadd__ = __add__
|
|
613
921
|
__isub__ = __sub__
|
|
614
922
|
__imul__ = __mul__
|
|
615
923
|
__itruediv__ = __truediv__
|
|
616
924
|
|
|
617
|
-
def _add_inplace(self, other:
|
|
925
|
+
def _add_inplace(self, other: Number | PauliOperator | Hamiltonian | Term | Parameter) -> None:
|
|
618
926
|
if isinstance(other, Hamiltonian):
|
|
619
927
|
# If it's empty, do nothing
|
|
620
928
|
if not other.elements:
|
|
621
929
|
return
|
|
622
930
|
# Otherwise, add each term
|
|
623
|
-
for key, val in other.
|
|
931
|
+
for key, val in other._elements.items(): # noqa: SLF001
|
|
624
932
|
self._elements[key] += val
|
|
933
|
+
|
|
934
|
+
self._parameters.update(other.parameters)
|
|
625
935
|
elif isinstance(other, PauliOperator):
|
|
626
936
|
# Just add 1 to that single operator key
|
|
627
937
|
self._elements[other,] += 1
|
|
@@ -629,26 +939,47 @@ class Hamiltonian:
|
|
|
629
939
|
if other == 0:
|
|
630
940
|
return
|
|
631
941
|
# Add the scalar to (I(0),)
|
|
632
|
-
self._elements[
|
|
942
|
+
self._elements[PauliI(0),] += other
|
|
943
|
+
elif isinstance(other, (Term, Parameter)):
|
|
944
|
+
if isinstance(other, Term):
|
|
945
|
+
if not other.is_parameterized_term():
|
|
946
|
+
raise ValueError(
|
|
947
|
+
"Only Parameters are allowed to be used in hamiltonians. Generic Variables are not supported"
|
|
948
|
+
)
|
|
949
|
+
self._parameters.update({v.label: v for v in other if isinstance(v, Parameter)})
|
|
950
|
+
else:
|
|
951
|
+
self._parameters[other.label] = other
|
|
952
|
+
self._elements[PauliI(0),] += other
|
|
633
953
|
else:
|
|
634
954
|
raise InvalidHamiltonianOperation(f"Invalid addition between Hamiltonian and {other.__class__.__name__}.")
|
|
635
955
|
|
|
636
|
-
def _sub_inplace(self, other:
|
|
956
|
+
def _sub_inplace(self, other: Number | PauliOperator | Hamiltonian | Term | Parameter) -> None:
|
|
637
957
|
if isinstance(other, Hamiltonian):
|
|
638
|
-
for key, val in other.
|
|
958
|
+
for key, val in other._elements.items(): # noqa: SLF001
|
|
639
959
|
self._elements[key] -= val
|
|
960
|
+
self._parameters.update(other._parameters) # noqa: SLF001
|
|
640
961
|
elif isinstance(other, PauliOperator):
|
|
641
962
|
self._elements[other,] -= 1
|
|
642
963
|
elif isinstance(other, (int, float, complex)):
|
|
643
964
|
if other == 0:
|
|
644
965
|
return
|
|
645
|
-
self._elements[
|
|
966
|
+
self._elements[PauliI(0),] -= other
|
|
967
|
+
elif isinstance(other, (Term, Parameter)):
|
|
968
|
+
if isinstance(other, Term):
|
|
969
|
+
if not other.is_parameterized_term():
|
|
970
|
+
raise ValueError(
|
|
971
|
+
"Only Parameters are allowed to be used in hamiltonians. Generic Variables are not supported"
|
|
972
|
+
)
|
|
973
|
+
self._parameters.update({v.label: v for v in other if isinstance(v, Parameter)})
|
|
974
|
+
else:
|
|
975
|
+
self._parameters[other.label] = other
|
|
976
|
+
self._elements[PauliI(0),] -= other
|
|
646
977
|
else:
|
|
647
978
|
raise InvalidHamiltonianOperation(
|
|
648
979
|
f"Invalid subtraction between Hamiltonian and {other.__class__.__name__}."
|
|
649
980
|
)
|
|
650
981
|
|
|
651
|
-
def _mul_inplace(self, other:
|
|
982
|
+
def _mul_inplace(self, other: Number | PauliOperator | Hamiltonian | Term | Parameter) -> None:
|
|
652
983
|
if isinstance(other, (int, float, complex)):
|
|
653
984
|
# 0 short-circuit
|
|
654
985
|
if other == 0:
|
|
@@ -663,6 +994,19 @@ class Hamiltonian:
|
|
|
663
994
|
self._elements[k] *= other
|
|
664
995
|
return None
|
|
665
996
|
|
|
997
|
+
if isinstance(other, (Term, Parameter)):
|
|
998
|
+
if isinstance(other, Term):
|
|
999
|
+
if not other.is_parameterized_term():
|
|
1000
|
+
raise ValueError(
|
|
1001
|
+
"Only Parameters are allowed to be used in hamiltonians. Generic Variables are not supported"
|
|
1002
|
+
)
|
|
1003
|
+
self._parameters.update({v.label: v for v in other if isinstance(v, Parameter)})
|
|
1004
|
+
else:
|
|
1005
|
+
self._parameters[other.label] = other
|
|
1006
|
+
for k in self._elements:
|
|
1007
|
+
self._elements[k] *= other
|
|
1008
|
+
return None
|
|
1009
|
+
|
|
666
1010
|
if isinstance(other, PauliOperator):
|
|
667
1011
|
# Convert single PauliOperator -> Hamiltonian with 1 key
|
|
668
1012
|
# Then do the single-key Hamiltonian path below
|
|
@@ -676,7 +1020,7 @@ class Hamiltonian:
|
|
|
676
1020
|
|
|
677
1021
|
# Check if 'other' is purely scalar identity => short-circuit
|
|
678
1022
|
if len(other.elements) == 1:
|
|
679
|
-
((ops2, c2),) = other.
|
|
1023
|
+
((ops2, c2),) = other._elements.items() # single item # noqa: SLF001
|
|
680
1024
|
if len(ops2) == 1:
|
|
681
1025
|
op2 = ops2[0]
|
|
682
1026
|
if op2.name == "I" and op2.qubit == 0:
|
|
@@ -684,12 +1028,13 @@ class Hamiltonian:
|
|
|
684
1028
|
return self._mul_inplace(c2)
|
|
685
1029
|
|
|
686
1030
|
# Otherwise, we do the general multiply
|
|
687
|
-
new_dict: dict[tuple[PauliOperator, ...], complex] = defaultdict(complex)
|
|
1031
|
+
new_dict: dict[tuple[PauliOperator, ...], complex | Term | Parameter] = defaultdict(complex)
|
|
688
1032
|
for ops1, c1 in self._elements.items():
|
|
689
|
-
for ops2, c2 in other.
|
|
1033
|
+
for ops2, c2 in other._elements.items(): # noqa: SLF001
|
|
690
1034
|
phase, new_ops = self._multiply_sets(ops1, ops2)
|
|
691
1035
|
new_dict[new_ops] += phase * c1 * c2
|
|
692
1036
|
self._elements = new_dict
|
|
1037
|
+
self._parameters.update(other._parameters) # noqa: SLF001
|
|
693
1038
|
|
|
694
1039
|
else:
|
|
695
1040
|
raise InvalidHamiltonianOperation(
|
|
@@ -697,7 +1042,7 @@ class Hamiltonian:
|
|
|
697
1042
|
)
|
|
698
1043
|
return None
|
|
699
1044
|
|
|
700
|
-
def _div_inplace(self, other:
|
|
1045
|
+
def _div_inplace(self, other: Number | PauliOperator | Hamiltonian) -> None:
|
|
701
1046
|
# Only valid for scalars
|
|
702
1047
|
if not isinstance(other, (int, float, complex)):
|
|
703
1048
|
raise InvalidHamiltonianOperation("Division by operators is not supported")
|