1from typing import overload
2
3from qiskit.quantum_info import SparsePauliOp
4
5
[docs]
6class Equation:
7 """## Hampy Equation
8 Represents a binary or general-purpose Hamiltonian equation built on
9 :class:`qiskit.quantum_info.SparsePauliOp`.
10 The class provides logical-style operators (AND, OR, XOR, NOT) interpreted
11 as operations on Hamiltonians with binary energies {0, 1}, along with basic
12 arithmetic operations and utilities for analyzing the resulting operator.
13
14 Examples
15 --------
16 Create an empty equation of size 3:
17 >>> eq = Equation(3)
18 >>> eq.hamiltonian
19 SparsePauliOp(['III'], coeffs=[0.])
20
21 Create from a SparsePauliOp:
22 >>> from qiskit.quantum_info import SparsePauliOp
23 >>> H = SparsePauliOp.from_sparse_list([('Z', [0], 1.0)], 1)
24 >>> eq = Equation(H)
25 >>> eq.get_order()
26 1
27
28 Use variables and logical operators:
29 >>> eq = Equation(2)
30 >>> x0 = eq[0]
31 >>> x1 = eq[1]
32
33 XOR:
34 >>> h_xor = x0 ^ x1
35 >>> h_xor.hamiltonian
36 SparsePauliOp([...])
37
38 OR:
39 >>> h_or = x0 | x1
40
41 AND:
42 >>> h_and = x0 & x1
43
44 Negation:
45 >>> h_not = ~x0
46
47 Combine equations arithmetically:
48 >>> h_sum = (x0 ^ x1) + (x0 & x1)
49 >>> h_scaled = 2.0 * h_sum
50 >>> h_divided = h_sum / 3
51
52 Export back to SparsePauliOp:
53 >>> H = h_sum.hamiltonian
54 """
55
56 @overload
57 def __init__(self, size: int, /): ...
58 @overload
59 def __init__(self, hamiltonian: SparsePauliOp, /): ...
60 @overload
61 def __init__(self, sparse_list: list[tuple], size: int, /): ...
62
63 def __init__(self, argument, *args):
64 self.size: int
65 if isinstance(argument, int):
66 self.size = argument
67 self._hamiltonian = SparsePauliOp.from_sparse_list([('I', [], 0)], argument)
68 elif isinstance(argument, SparsePauliOp):
69 if not isinstance(argument.num_qubits, int):
70 raise TypeError('Cannot read number of qubits from provided SparsePauliOp')
71 self.size = argument.num_qubits
72 self._hamiltonian = argument
73 elif isinstance(argument, list) and len(args) > 0 and isinstance(args[0], int):
74 self.size = args[0]
75 self._hamiltonian = SparsePauliOp.from_sparse_list(argument, args[0])
76 else:
77 raise TypeError('Wrong arguments!')
78
[docs]
79 def get_variable(self, index: int) -> 'Variable':
80 return Variable(index, self.size)
81
82 @property
83 def hamiltonian(self) -> SparsePauliOp:
84 return self._hamiltonian.simplify()
85
86 @hamiltonian.setter
87 def hamiltonian(self, new_hamiltonian: SparsePauliOp) -> None:
88 self._hamiltonian = new_hamiltonian
89
[docs]
90 def to_sparse_pauli_op(self) -> SparsePauliOp:
91 return self.hamiltonian
92
[docs]
93 def get_order(self) -> int:
94 equation_order = 0
95 for Z_term in self.hamiltonian.paulis:
96 equation_order = max(equation_order, str(Z_term).count('Z'))
97 return equation_order
98
[docs]
99 def is_quadratic(self) -> bool:
100 return all(term.z.sum() <= 2 for term in self.hamiltonian.paulis)
101
102 def __or__(self, other: 'Variable | Equation', /) -> 'Equation':
103 if isinstance(other, Variable):
104 other = other.to_equation()
105
106 return Equation(self.hamiltonian + other.hamiltonian - self.hamiltonian.compose(other.hamiltonian))
107
108 def __and__(self, other: 'Variable | Equation', /) -> 'Equation':
109 if isinstance(other, Variable):
110 other = other.to_equation()
111
112 return self * other
113
114 def __xor__(self, other: 'Variable | Equation', /) -> 'Equation':
115 if isinstance(other, Variable):
116 other = other.to_equation()
117
118 return Equation(self.hamiltonian + other.hamiltonian - (2 * self.hamiltonian.compose(other.hamiltonian)))
119
120 def __invert__(self) -> 'Equation':
121 I_term = ('I', [], 1)
122 identity = SparsePauliOp.from_sparse_list([I_term], self.size)
123 return Equation(identity - self.hamiltonian)
124
125 def __getitem__(self, variable_number: int):
126 return self.get_variable(variable_number)
127
128 def __eq__(self, other: object) -> bool:
129 if not isinstance(other, (Variable, Equation)):
130 raise TypeError(f'Cannot compare hampy.Equation to {type(other)}')
131 if isinstance(other, Variable):
132 other = other.to_equation()
133
134 return self.hamiltonian == other.hamiltonian
135
136 def __add__(self, other: 'Variable | Equation | SparsePauliOp') -> 'Equation':
137 if isinstance(other, SparsePauliOp):
138 other = Equation(other)
139 elif isinstance(other, Variable):
140 other = other.to_equation()
141
142 return Equation(self.hamiltonian + other.hamiltonian)
143
144 def __radd__(self, other: 'Equation') -> 'Equation':
145 if isinstance(other, Variable):
146 other = other.to_equation()
147
148 return Equation(self.hamiltonian + other.hamiltonian)
149
150 def __mul__(self, other: 'Equation | float') -> 'Equation':
151 if isinstance(other, Variable):
152 other = other.to_equation()
153 if isinstance(other, (float, int)):
154 return Equation(float(other) * self.hamiltonian)
155 return Equation(self.hamiltonian.compose(other.hamiltonian))
156
157 def __rmul__(self, other: 'Equation | float') -> 'Equation':
158 if isinstance(other, Variable):
159 other = other.to_equation()
160 if isinstance(other, (float, int)):
161 return Equation(float(other) * self.hamiltonian)
162 return Equation(self.hamiltonian.compose(other.hamiltonian))
163
164 def __truediv__(self, other: float) -> 'Equation':
165 return Equation(self.hamiltonian / other)
166
167
[docs]
168class Variable:
169 def __init__(self, index: int, size: int):
170 self.index = index
171 self.size = size
172
173 def __xor__(self, other: 'Equation | Variable', /) -> Equation:
174 if isinstance(other, Equation):
175 return self.to_equation() ^ other
176
177 I_term = ('I', [], 0.5)
178 Z_term = ('ZZ', [self.index, other.index], -0.5)
179 return Equation(SparsePauliOp.from_sparse_list([I_term, Z_term], self.size))
180
181 def __or__(self, other: 'Variable | Equation', /) -> Equation:
182 if isinstance(other, Equation):
183 return self.to_equation() | other
184
185 I_term = ('I', [], 0.75)
186 Z1_term = ('Z', [self.index], -0.25)
187 Z2_term = ('Z', [other.index], -0.25)
188 ZZ_term = ('ZZ', [self.index, other.index], -0.25)
189 return Equation([I_term, Z1_term, Z2_term, ZZ_term], self.size)
190
191 def __and__(self, other: 'Variable | Equation', /) -> Equation:
192 if isinstance(other, Equation):
193 return self.to_equation() & other
194
195 I_term = ('I', [], 0.25)
196 Z1_term = ('Z', [self.index], -0.25)
197 Z2_term = ('Z', [other.index], -0.25)
198 ZZ_term = ('ZZ', [self.index, other.index], 0.25)
199 return Equation([I_term, Z1_term, Z2_term, ZZ_term], self.size)
200
201 def __invert__(self) -> Equation:
202 I_term = ('I', [], 0.5)
203 Z_term = ('Z', [self.index], 0.5)
204 return Equation([I_term, Z_term], self.size)
205
[docs]
206 def to_equation(self) -> Equation:
207 I_term = ('I', [], 0.5)
208 Z_term = ('Z', [self.index], -0.5)
209 return Equation([I_term, Z_term], self.size)