Skip to content

Commit 5d3f65a

Browse files
Cryorismanoelmarques
authored andcommitted
SummedOp updates & optimization converters to use Opflow (qiskit-community#1059)
* simplify and reduce, add equals to SummedOp, update tests * directly use new opflow, no need to go via WPO * update comments and docstrings * directly use opflow * don't do equality check in add * directly use opflow * change order in reduce * fix qaoa * add short test on summed op equality * rm prints * use set comparison, rename simplify to collapse_summands * fix expected value, should be sqrt(2), not 2 * cast coeffs to complex * add reno on equals * fix mypy * fix spell * fix lint * dont cast coefficient to complex leads to problems if the coeff is exponentitated and not supposed to be complex * use sum instead of reduce * rm unused import * move __hash__ to primitive op and base on repr * use != over not == * add summed op test for different primitives * check for opbase, not summedop * adress changes from review * explicitly raise an error upon ListOp input * return identity op instead of the int 0 * fix spell * add note that equals is not mathematically sound Co-authored-by: Manoel Marques <[email protected]>
1 parent 7fd9db7 commit 5d3f65a

File tree

10 files changed

+209
-170
lines changed

10 files changed

+209
-170
lines changed

qiskit/aqua/operators/list_ops/summed_op.py

Lines changed: 63 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
""" SummedOp Class """
1616

17-
from functools import reduce
1817
from typing import List, Union, cast
1918

2019
import numpy as np
@@ -72,9 +71,6 @@ def add(self, other: OperatorBase) -> OperatorBase:
7271
Returns:
7372
A ``SummedOp`` equivalent to the sum of self and other.
7473
"""
75-
if self == other:
76-
return self.mul(2.0)
77-
7874
self_new_ops = self.oplist if self.coeff == 1 \
7975
else [op.mul(self.coeff) for op in self.oplist]
8076
if isinstance(other, SummedOp):
@@ -84,10 +80,10 @@ def add(self, other: OperatorBase) -> OperatorBase:
8480
other_new_ops = [other]
8581
return SummedOp(self_new_ops + other_new_ops)
8682

87-
def simplify(self) -> 'SummedOp':
83+
def collapse_summands(self) -> 'SummedOp':
8884
"""Return Operator by simplifying duplicate operators.
8985
90-
E.g., ``SummedOp([2 * X ^ Y, X ^ Y]).simplify() -> SummedOp([3 * X ^ Y])``.
86+
E.g., ``SummedOp([2 * X ^ Y, X ^ Y]).collapse_summands() -> SummedOp([3 * X ^ Y])``.
9187
9288
Returns:
9389
A simplified ``SummedOp`` equivalent to self.
@@ -113,11 +109,23 @@ def simplify(self) -> 'SummedOp':
113109
coeffs.append(self.coeff)
114110
return SummedOp([op * coeff for op, coeff in zip(oplist, coeffs)]) # type: ignore
115111

116-
# Try collapsing list or trees of Sums.
117112
# TODO be smarter about the fact that any two ops in oplist could be evaluated for sum.
118113
def reduce(self) -> OperatorBase:
119-
reduced_ops = [op.reduce() for op in self.oplist]
120-
reduced_ops = reduce(lambda x, y: x.add(y), reduced_ops) * self.coeff
114+
"""Try collapsing list or trees of sums.
115+
116+
Tries to sum up duplicate operators and reduces the operators
117+
in the sum.
118+
119+
Returns:
120+
A collapsed version of self, if possible.
121+
"""
122+
# reduce constituents
123+
reduced_ops = sum(op.reduce() for op in self.oplist) * self.coeff
124+
125+
# group duplicate operators
126+
if isinstance(reduced_ops, SummedOp):
127+
reduced_ops = reduced_ops.collapse_summands()
128+
121129
if isinstance(reduced_ops, SummedOp) and len(reduced_ops.oplist) == 1:
122130
return reduced_ops.oplist[0]
123131
else:
@@ -142,3 +150,49 @@ def to_legacy_op(self, massive: bool = False) -> LegacyBaseOperator:
142150
coeff = cast(float, self.coeff)
143151

144152
return self.combo_fn(legacy_ops) * coeff
153+
154+
def equals(self, other: OperatorBase) -> bool:
155+
"""Check if other is equal to self.
156+
157+
Note:
158+
This is not a mathematical check for equality.
159+
If ``self`` and ``other`` implement the same operation but differ
160+
in the representation (e.g. different type of summands)
161+
``equals`` will evaluate to ``False``.
162+
163+
Args:
164+
other: The other operator to check for equality.
165+
166+
Returns:
167+
True, if other and self are equal, otherwise False.
168+
169+
Examples:
170+
>>> from qiskit.aqua.operators import X, Z
171+
>>> 2 * X == X + X
172+
True
173+
>>> X + Z == Z + X
174+
True
175+
"""
176+
self_reduced, other_reduced = self.reduce(), other.reduce()
177+
if not isinstance(other_reduced, type(self_reduced)):
178+
return False
179+
180+
# check if reduced op is still a SummedOp
181+
if not isinstance(self_reduced, SummedOp):
182+
return self_reduced == other_reduced
183+
184+
self_reduced = cast(SummedOp, self_reduced)
185+
other_reduced = cast(SummedOp, other_reduced)
186+
if len(self_reduced.oplist) != len(other_reduced.oplist):
187+
return False
188+
189+
# absorb coeffs into the operators
190+
if self_reduced.coeff != 1:
191+
self_reduced = SummedOp(
192+
[op * self_reduced.coeff for op in self_reduced.oplist]) # type: ignore
193+
if other_reduced.coeff != 1:
194+
other_reduced = SummedOp(
195+
[op * other_reduced.coeff for op in other_reduced.oplist]) # type: ignore
196+
197+
# compare independent of order
198+
return set(self_reduced) == set(other_reduced)

qiskit/aqua/operators/primitive_ops/pauli_op.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,6 @@ def exp_i(self) -> OperatorBase:
231231
from ..evolutions.evolved_op import EvolvedOp
232232
return EvolvedOp(self)
233233

234-
def __hash__(self) -> int:
235-
# Need this to be able to easily construct AbelianGraphs
236-
return hash(str(self))
237-
238234
def commutes(self, other_op: OperatorBase) -> bool:
239235
""" Returns whether self commutes with other_op.
240236

qiskit/aqua/operators/primitive_ops/primitive_op.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,9 @@ def log_i(self, massive: bool = False) -> OperatorBase:
198198
def __str__(self) -> str:
199199
raise NotImplementedError
200200

201+
def __hash__(self) -> int:
202+
return hash(repr(self))
203+
201204
def __repr__(self) -> str:
202205
return "{}({}, coeff={})".format(type(self).__name__, repr(self.primitive), self.coeff)
203206

qiskit/optimization/converters/ising_to_quadratic_program.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515

1616
"""The converter from a ```Operator``` to ``QuadraticProgram``."""
1717

18-
from typing import Optional
18+
from typing import Optional, Union
1919
import copy
2020

2121
import numpy as np
2222

23-
from qiskit.aqua.operators.legacy import WeightedPauliOperator
23+
from qiskit.aqua.operators import OperatorBase, WeightedPauliOperator, SummedOp, ListOp
2424
from ..problems.quadratic_program import QuadraticProgram
2525
from ..exceptions import QiskitOptimizationError
2626

@@ -44,7 +44,8 @@ def __init__(self, linear: bool = False) -> None:
4444
self._qp = None # type: Optional[QuadraticProgram]
4545
self._linear = linear
4646

47-
def encode(self, qubit_op: WeightedPauliOperator, offset: float = 0.0) -> QuadraticProgram:
47+
def encode(self, qubit_op: Union[OperatorBase, WeightedPauliOperator], offset: float = 0.0
48+
) -> QuadraticProgram:
4849
"""Convert a qubit operator and a shift value into a quadratic program
4950
5051
Args:
@@ -58,8 +59,18 @@ def encode(self, qubit_op: WeightedPauliOperator, offset: float = 0.0) -> Quadra
5859
Raises:
5960
QiskitOptimizationError: If there are Pauli Xs in any Pauli term
6061
QiskitOptimizationError: If there are more than 2 Pauli Zs in any Pauli term
62+
NotImplementedError: If the input operator is a ListOp
6163
"""
6264
# Set properties
65+
if isinstance(qubit_op, WeightedPauliOperator):
66+
qubit_op = qubit_op.to_opflow()
67+
68+
# No support for ListOp yet, this can be added in future
69+
# pylint: disable=unidiomatic-typecheck
70+
if type(qubit_op) == ListOp:
71+
raise NotImplementedError('Conversion of a ListOp is not supported, convert each '
72+
'operator in the ListOp separately.')
73+
6374
self._qubit_op = qubit_op
6475
self._offset = copy.deepcopy(offset)
6576
self._num_qubits = qubit_op.num_qubits
@@ -134,24 +145,31 @@ def _create_qubo_matrix(self):
134145
# The other elements in the QUBO matrix is for quadratic terms of the qubit operator
135146
self._qubo_matrix = np.zeros((self._num_qubits, self._num_qubits))
136147

137-
for pauli in self._qubit_op.paulis:
148+
if not isinstance(self._qubit_op, SummedOp):
149+
oplist = [self._qubit_op.to_pauli_op()]
150+
else:
151+
oplist = self._qubit_op.to_pauli_op().oplist
152+
153+
for pauli_op in oplist:
154+
pauli = pauli_op.primitive
155+
coeff = pauli_op.coeff
138156
# Count the number of Pauli Zs in a Pauli term
139-
lst_z = pauli[1].z.tolist()
157+
lst_z = pauli.z.tolist()
140158
z_index = [i for i, z in enumerate(lst_z) if z is True]
141159
num_z = len(z_index)
142160

143161
# Add its weight of the Pauli term to the corresponding element of QUBO matrix
144162
if num_z == 1:
145-
self._qubo_matrix[z_index[0], z_index[0]] = pauli[0].real
163+
self._qubo_matrix[z_index[0], z_index[0]] = coeff.real
146164
elif num_z == 2:
147-
self._qubo_matrix[z_index[0], z_index[1]] = pauli[0].real
165+
self._qubo_matrix[z_index[0], z_index[1]] = coeff.real
148166
else:
149167
raise QiskitOptimizationError(
150-
'There are more than 2 Pauli Zs in the Pauli term {}'.format(pauli[1].z)
168+
'There are more than 2 Pauli Zs in the Pauli term {}'.format(pauli.z)
151169
)
152170

153171
# If there are Pauli Xs in the Pauli term, raise an error
154-
lst_x = pauli[1].x.tolist()
172+
lst_x = pauli.x.tolist()
155173
x_index = [i for i, x in enumerate(lst_x) if x is True]
156174
if len(x_index) > 0:
157-
raise QiskitOptimizationError('Pauli Xs exist in the Pauli {}'.format(pauli[1].x))
175+
raise QiskitOptimizationError('Pauli Xs exist in the Pauli {}'.format(pauli.x))

qiskit/optimization/converters/quadratic_program_to_ising.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import numpy as np
2121
from qiskit.quantum_info import Pauli
2222

23-
from qiskit.aqua.operators import WeightedPauliOperator
23+
from qiskit.aqua.operators import OperatorBase, PauliOp, I
2424

2525
from ..problems.quadratic_program import QuadraticProgram
2626
from ..exceptions import QiskitOptimizationError
@@ -33,7 +33,7 @@ def __init__(self) -> None:
3333
"""Initialize the internal data structure."""
3434
self._src = None # type: Optional[QuadraticProgram]
3535

36-
def encode(self, op: QuadraticProgram) -> Tuple[WeightedPauliOperator, float]:
36+
def encode(self, op: QuadraticProgram) -> Tuple[OperatorBase, float]:
3737
"""Convert a problem into a qubit operator
3838
3939
Args:
@@ -114,6 +114,13 @@ def encode(self, op: QuadraticProgram) -> Tuple[WeightedPauliOperator, float]:
114114
shift += weight
115115

116116
# Remove paulis whose coefficients are zeros.
117-
qubit_op = WeightedPauliOperator(paulis=pauli_list)
117+
qubit_op = sum(PauliOp(pauli, coeff=coeff) for coeff, pauli in pauli_list)
118+
119+
# qubit_op could be the integer 0, in this case return an identity operator of
120+
# appropriate size
121+
if isinstance(qubit_op, OperatorBase):
122+
qubit_op = qubit_op.reduce()
123+
else:
124+
qubit_op = I ^ num_nodes
118125

119126
return qubit_op, shift
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
features:
3+
- |
4+
The ``SummedOp`` does a mathematically more correct check for equality, where
5+
expressions such as ``X + X == 2*X`` and ``X + Z == Z + X`` evaluate to ``True``.

test/aqua/operators/test_op_construction.py

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,26 @@
1414

1515
""" Test Operator construction, including OpPrimitives and singletons. """
1616

17+
1718
import unittest
1819
from test.aqua import QiskitAquaTestCase
1920
import itertools
2021
import numpy as np
22+
from ddt import ddt, data
2123

2224
from qiskit.circuit import QuantumCircuit, QuantumRegister, Instruction
2325
from qiskit.extensions.exceptions import ExtensionError
2426
from qiskit.quantum_info.operators import Operator, Pauli
25-
from qiskit.circuit.library import CZGate
27+
from qiskit.circuit.library import CZGate, ZGate
2628

2729
from qiskit.aqua.operators import (
28-
X, Y, Z, I, CX, T, H, PrimitiveOp, SummedOp, PauliOp, Minus, CircuitOp
30+
X, Y, Z, I, CX, T, H, PrimitiveOp, SummedOp, PauliOp, Minus, CircuitOp, MatrixOp
2931
)
3032

3133

3234
# pylint: disable=invalid-name
3335

36+
@ddt
3437
class TestOpConstruction(QiskitAquaTestCase):
3538
"""Operator Construction tests."""
3639

@@ -235,7 +238,7 @@ def test_circuit_permute(self):
235238
c_op_id = c_op_perm.permute(perm)
236239
self.assertEqual(c_op, c_op_id)
237240

238-
def test_summed_op(self):
241+
def test_summed_op_reduce(self):
239242
"""Test SummedOp"""
240243
sum_op = (X ^ X * 2) + (Y ^ Y) # type: SummedOp
241244
with self.subTest('SummedOp test 1'):
@@ -250,7 +253,7 @@ def test_summed_op(self):
250253
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY', 'YY'])
251254
self.assertListEqual([op.coeff for op in sum_op], [2, 1, 1])
252255

253-
sum_op = sum_op.simplify()
256+
sum_op = sum_op.collapse_summands()
254257
with self.subTest('SummedOp test 2-b'):
255258
self.assertEqual(sum_op.coeff, 1)
256259
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
@@ -263,7 +266,7 @@ def test_summed_op(self):
263266
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY', 'YY', 'XX'])
264267
self.assertListEqual([op.coeff for op in sum_op], [2, 1, 1, 2])
265268

266-
sum_op = sum_op.simplify()
269+
sum_op = sum_op.reduce()
267270
with self.subTest('SummedOp test 3-b'):
268271
self.assertEqual(sum_op.coeff, 1)
269272
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
@@ -275,7 +278,7 @@ def test_summed_op(self):
275278
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
276279
self.assertListEqual([op.coeff for op in sum_op], [2, 1])
277280

278-
sum_op = sum_op.simplify()
281+
sum_op = sum_op.collapse_summands()
279282
with self.subTest('SummedOp test 4-b'):
280283
self.assertEqual(sum_op.coeff, 1)
281284
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
@@ -288,7 +291,7 @@ def test_summed_op(self):
288291
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY', 'YY'])
289292
self.assertListEqual([op.coeff for op in sum_op], [4, 2, 1])
290293

291-
sum_op = sum_op.simplify()
294+
sum_op = sum_op.collapse_summands()
292295
with self.subTest('SummedOp test 5-b'):
293296
self.assertEqual(sum_op.coeff, 1)
294297
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
@@ -301,7 +304,7 @@ def test_summed_op(self):
301304
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY', 'XX', 'YY'])
302305
self.assertListEqual([op.coeff for op in sum_op], [4, 2, 2, 1])
303306

304-
sum_op = sum_op.simplify()
307+
sum_op = sum_op.collapse_summands()
305308
with self.subTest('SummedOp test 6-b'):
306309
self.assertEqual(sum_op.coeff, 1)
307310
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
@@ -310,11 +313,11 @@ def test_summed_op(self):
310313
sum_op = SummedOp([X ^ X * 2, Y ^ Y], 2)
311314
sum_op += sum_op
312315
with self.subTest('SummedOp test 7-a'):
313-
self.assertEqual(sum_op.coeff, 4)
314-
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
315-
self.assertListEqual([op.coeff for op in sum_op], [2, 1])
316+
self.assertEqual(sum_op.coeff, 1)
317+
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY', 'XX', 'YY'])
318+
self.assertListEqual([op.coeff for op in sum_op], [4, 2, 4, 2])
316319

317-
sum_op = sum_op.simplify()
320+
sum_op = sum_op.collapse_summands()
318321
with self.subTest('SummedOp test 7-b'):
319322
self.assertEqual(sum_op.coeff, 1)
320323
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
@@ -326,12 +329,28 @@ def test_summed_op(self):
326329
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY', 'XX', 'ZZ'])
327330
self.assertListEqual([op.coeff for op in sum_op], [4, 2, 6, 3])
328331

329-
sum_op = sum_op.simplify()
332+
sum_op = sum_op.collapse_summands()
330333
with self.subTest('SummedOp test 8-b'):
331334
self.assertEqual(sum_op.coeff, 1)
332335
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY', 'ZZ'])
333336
self.assertListEqual([op.coeff for op in sum_op], [10, 2, 3])
334337

338+
def test_summed_op_equals(self):
339+
"""Test corner cases of SummedOp's equals function."""
340+
with self.subTest('multiplicative factor'):
341+
self.assertEqual(2 * X, X + X)
342+
343+
with self.subTest('commutative'):
344+
self.assertEqual(X + Z, Z + X)
345+
346+
with self.subTest('circuit and paulis'):
347+
z = CircuitOp(ZGate())
348+
self.assertEqual(Z + z, z + Z)
349+
350+
with self.subTest('matrix op and paulis'):
351+
z = MatrixOp([[1, 0], [0, -1]])
352+
self.assertEqual(Z + z, z + Z)
353+
335354
def test_circuit_compose_register_independent(self):
336355
"""Test that CircuitOp uses combines circuits independent of the register.
337356
@@ -344,13 +363,14 @@ def test_circuit_compose_register_independent(self):
344363

345364
self.assertEqual(composed.num_qubits, 2)
346365

347-
def test_pauli_op_hashing(self):
366+
@data(Z, CircuitOp(ZGate()), MatrixOp([[1, 0], [0, -1]]))
367+
def test_op_hashing(self, op):
348368
"""Regression test against faulty set comparison.
349369
350370
Set comparisons rely on a hash table which requires identical objects to have identical
351-
hashes. Thus, the PauliOp.__hash__ should support this requirement.
371+
hashes. Thus, the PrimitiveOp.__hash__ should support this requirement.
352372
"""
353-
self.assertEqual(set([2*Z]), set([2*Z]))
373+
self.assertEqual(set([2 * op]), set([2 * op]))
354374

355375

356376
if __name__ == '__main__':

0 commit comments

Comments
 (0)