Skip to content
This repository was archived by the owner on Dec 7, 2021. It is now read-only.

SummedOp updates & optimization converters to use Opflow #1059

Merged
merged 38 commits into from
Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
f58bee5
simplify and reduce, add equals to SummedOp, update tests
Cryoris Jun 17, 2020
4b0232f
directly use new opflow, no need to go via WPO
Cryoris Jun 18, 2020
211aebd
update comments and docstrings
Cryoris Jun 18, 2020
ffa5020
directly use opflow
Cryoris Jun 18, 2020
4d431a8
Merge branch 'master' into summed-op-compare
Cryoris Jun 18, 2020
3e24231
don't do equality check in add
Cryoris Jun 18, 2020
4c6597d
directly use opflow
Cryoris Jun 18, 2020
951efb9
change order in reduce
Cryoris Jun 18, 2020
c4b3ca7
fix qaoa
Cryoris Jun 18, 2020
6678df1
add short test on summed op equality
Cryoris Jun 18, 2020
d2865d6
rm prints
Cryoris Jun 18, 2020
87f5448
use set comparison, rename simplify to collapse_summands
Cryoris Jun 18, 2020
f34f4ea
fix expected value, should be sqrt(2), not 2
Cryoris Jun 18, 2020
b5bd373
cast coeffs to complex
Cryoris Jun 18, 2020
a499d0b
add reno on equals
Cryoris Jun 18, 2020
d610890
Merge branch 'master' into summed-op-compare
manoelmarques Jun 18, 2020
eb8ef07
fix mypy
Cryoris Jun 18, 2020
898178c
Merge branch 'summed-op-compare' of github.com:Cryoris/qiskit-aqua in…
Cryoris Jun 18, 2020
5b77cb6
fix spell
Cryoris Jun 18, 2020
596d796
fix lint
Cryoris Jun 18, 2020
0fb497e
dont cast coefficient to complex
Cryoris Jun 18, 2020
9f80d9b
Merge branch 'master' into summed-op-compare
Cryoris Jun 22, 2020
308564b
use sum instead of reduce
Cryoris Jun 22, 2020
349dfb1
rm unused import
Cryoris Jun 22, 2020
892d36f
move __hash__ to primitive op and base on repr
Cryoris Jun 24, 2020
f82da04
Merge branch 'master' into summed-op-compare
manoelmarques Jun 24, 2020
42d23b9
use != over not ==
Cryoris Jun 26, 2020
823d6de
Merge branch 'summed-op-compare' of github.com:Cryoris/qiskit-aqua in…
Cryoris Jun 26, 2020
fc0daa4
Merge branch 'op-hash' into summed-op-compare
Cryoris Jun 26, 2020
bc03704
add summed op test for different primitives
Cryoris Jun 26, 2020
5a5bf03
check for opbase, not summedop
Cryoris Jun 26, 2020
7a61f23
Merge branch 'master' into summed-op-compare
Cryoris Jun 29, 2020
b3f389d
adress changes from review
Cryoris Jun 29, 2020
01008e1
fix spell
Cryoris Jun 29, 2020
6c625bf
Merge branch 'master' into summed-op-compare
Cryoris Jun 30, 2020
cbb4429
Merge branch 'master' into summed-op-compare
Cryoris Jul 2, 2020
dc8f68c
add note that equals is not mathematically sound
Cryoris Jul 3, 2020
8c81e9d
Merge branch 'summed-op-compare' of github.com:Cryoris/qiskit-aqua in…
Cryoris Jul 3, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 63 additions & 9 deletions qiskit/aqua/operators/list_ops/summed_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

""" SummedOp Class """

from functools import reduce
from typing import List, Union, cast

import numpy as np
Expand Down Expand Up @@ -72,9 +71,6 @@ def add(self, other: OperatorBase) -> OperatorBase:
Returns:
A ``SummedOp`` equivalent to the sum of self and other.
"""
if self == other:
return self.mul(2.0)

self_new_ops = self.oplist if self.coeff == 1 \
else [op.mul(self.coeff) for op in self.oplist]
if isinstance(other, SummedOp):
Expand All @@ -84,10 +80,10 @@ def add(self, other: OperatorBase) -> OperatorBase:
other_new_ops = [other]
return SummedOp(self_new_ops + other_new_ops)

def simplify(self) -> 'SummedOp':
def collapse_summands(self) -> 'SummedOp':
"""Return Operator by simplifying duplicate operators.

E.g., ``SummedOp([2 * X ^ Y, X ^ Y]).simplify() -> SummedOp([3 * X ^ Y])``.
E.g., ``SummedOp([2 * X ^ Y, X ^ Y]).collapse_summands() -> SummedOp([3 * X ^ Y])``.

Returns:
A simplified ``SummedOp`` equivalent to self.
Expand All @@ -113,11 +109,23 @@ def simplify(self) -> 'SummedOp':
coeffs.append(self.coeff)
return SummedOp([op * coeff for op, coeff in zip(oplist, coeffs)]) # type: ignore

# Try collapsing list or trees of Sums.
# TODO be smarter about the fact that any two ops in oplist could be evaluated for sum.
def reduce(self) -> OperatorBase:
reduced_ops = [op.reduce() for op in self.oplist]
reduced_ops = reduce(lambda x, y: x.add(y), reduced_ops) * self.coeff
"""Try collapsing list or trees of sums.

Tries to sum up duplicate operators and reduces the operators
in the sum.

Returns:
A collapsed version of self, if possible.
"""
# reduce constituents
reduced_ops = sum(op.reduce() for op in self.oplist) * self.coeff

# group duplicate operators
if isinstance(reduced_ops, SummedOp):
reduced_ops = reduced_ops.collapse_summands()

if isinstance(reduced_ops, SummedOp) and len(reduced_ops.oplist) == 1:
return reduced_ops.oplist[0]
else:
Expand All @@ -142,3 +150,49 @@ def to_legacy_op(self, massive: bool = False) -> LegacyBaseOperator:
coeff = cast(float, self.coeff)

return self.combo_fn(legacy_ops) * coeff

def equals(self, other: OperatorBase) -> bool:
"""Check if other is equal to self.

Note:
This is not a mathematical check for equality.
If ``self`` and ``other`` implement the same operation but differ
in the representation (e.g. different type of summands)
``equals`` will evaluate to ``False``.

Args:
other: The other operator to check for equality.

Returns:
True, if other and self are equal, otherwise False.

Examples:
>>> from qiskit.aqua.operators import X, Z
>>> 2 * X == X + X
True
>>> X + Z == Z + X
True
"""
self_reduced, other_reduced = self.reduce(), other.reduce()
if not isinstance(other_reduced, type(self_reduced)):
return False

# check if reduced op is still a SummedOp
if not isinstance(self_reduced, SummedOp):
return self_reduced == other_reduced

self_reduced = cast(SummedOp, self_reduced)
other_reduced = cast(SummedOp, other_reduced)
if len(self_reduced.oplist) != len(other_reduced.oplist):
return False

# absorb coeffs into the operators
if self_reduced.coeff != 1:
self_reduced = SummedOp(
[op * self_reduced.coeff for op in self_reduced.oplist]) # type: ignore
if other_reduced.coeff != 1:
other_reduced = SummedOp(
[op * other_reduced.coeff for op in other_reduced.oplist]) # type: ignore

# compare independent of order
return set(self_reduced) == set(other_reduced)
4 changes: 0 additions & 4 deletions qiskit/aqua/operators/primitive_ops/pauli_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,10 +231,6 @@ def exp_i(self) -> OperatorBase:
from ..evolutions.evolved_op import EvolvedOp
return EvolvedOp(self)

def __hash__(self) -> int:
# Need this to be able to easily construct AbelianGraphs
return hash(str(self))

def commutes(self, other_op: OperatorBase) -> bool:
""" Returns whether self commutes with other_op.

Expand Down
3 changes: 3 additions & 0 deletions qiskit/aqua/operators/primitive_ops/primitive_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ def log_i(self, massive: bool = False) -> OperatorBase:
def __str__(self) -> str:
raise NotImplementedError

def __hash__(self) -> int:
return hash(repr(self))

def __repr__(self) -> str:
return "{}({}, coeff={})".format(type(self).__name__, repr(self.primitive), self.coeff)

Expand Down
38 changes: 28 additions & 10 deletions qiskit/optimization/converters/ising_to_quadratic_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@

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

from typing import Optional
from typing import Optional, Union
import copy

import numpy as np

from qiskit.aqua.operators.legacy import WeightedPauliOperator
from qiskit.aqua.operators import OperatorBase, WeightedPauliOperator, SummedOp, ListOp
from ..problems.quadratic_program import QuadraticProgram
from ..exceptions import QiskitOptimizationError

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

def encode(self, qubit_op: WeightedPauliOperator, offset: float = 0.0) -> QuadraticProgram:
def encode(self, qubit_op: Union[OperatorBase, WeightedPauliOperator], offset: float = 0.0
) -> QuadraticProgram:
"""Convert a qubit operator and a shift value into a quadratic program

Args:
Expand All @@ -58,8 +59,18 @@ def encode(self, qubit_op: WeightedPauliOperator, offset: float = 0.0) -> Quadra
Raises:
QiskitOptimizationError: If there are Pauli Xs in any Pauli term
QiskitOptimizationError: If there are more than 2 Pauli Zs in any Pauli term
NotImplementedError: If the input operator is a ListOp
"""
# Set properties
if isinstance(qubit_op, WeightedPauliOperator):
qubit_op = qubit_op.to_opflow()

# No support for ListOp yet, this can be added in future
# pylint: disable=unidiomatic-typecheck
if type(qubit_op) == ListOp:
raise NotImplementedError('Conversion of a ListOp is not supported, convert each '
'operator in the ListOp separately.')

self._qubit_op = qubit_op
self._offset = copy.deepcopy(offset)
self._num_qubits = qubit_op.num_qubits
Expand Down Expand Up @@ -134,24 +145,31 @@ def _create_qubo_matrix(self):
# The other elements in the QUBO matrix is for quadratic terms of the qubit operator
self._qubo_matrix = np.zeros((self._num_qubits, self._num_qubits))

for pauli in self._qubit_op.paulis:
if not isinstance(self._qubit_op, SummedOp):
oplist = [self._qubit_op.to_pauli_op()]
else:
oplist = self._qubit_op.to_pauli_op().oplist

for pauli_op in oplist:
pauli = pauli_op.primitive
coeff = pauli_op.coeff
# Count the number of Pauli Zs in a Pauli term
lst_z = pauli[1].z.tolist()
lst_z = pauli.z.tolist()
z_index = [i for i, z in enumerate(lst_z) if z is True]
num_z = len(z_index)

# Add its weight of the Pauli term to the corresponding element of QUBO matrix
if num_z == 1:
self._qubo_matrix[z_index[0], z_index[0]] = pauli[0].real
self._qubo_matrix[z_index[0], z_index[0]] = coeff.real
elif num_z == 2:
self._qubo_matrix[z_index[0], z_index[1]] = pauli[0].real
self._qubo_matrix[z_index[0], z_index[1]] = coeff.real
else:
raise QiskitOptimizationError(
'There are more than 2 Pauli Zs in the Pauli term {}'.format(pauli[1].z)
'There are more than 2 Pauli Zs in the Pauli term {}'.format(pauli.z)
)

# If there are Pauli Xs in the Pauli term, raise an error
lst_x = pauli[1].x.tolist()
lst_x = pauli.x.tolist()
x_index = [i for i, x in enumerate(lst_x) if x is True]
if len(x_index) > 0:
raise QiskitOptimizationError('Pauli Xs exist in the Pauli {}'.format(pauli[1].x))
raise QiskitOptimizationError('Pauli Xs exist in the Pauli {}'.format(pauli.x))
13 changes: 10 additions & 3 deletions qiskit/optimization/converters/quadratic_program_to_ising.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import numpy as np
from qiskit.quantum_info import Pauli

from qiskit.aqua.operators import WeightedPauliOperator
from qiskit.aqua.operators import OperatorBase, PauliOp, I

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

def encode(self, op: QuadraticProgram) -> Tuple[WeightedPauliOperator, float]:
def encode(self, op: QuadraticProgram) -> Tuple[OperatorBase, float]:
"""Convert a problem into a qubit operator

Args:
Expand Down Expand Up @@ -114,6 +114,13 @@ def encode(self, op: QuadraticProgram) -> Tuple[WeightedPauliOperator, float]:
shift += weight

# Remove paulis whose coefficients are zeros.
qubit_op = WeightedPauliOperator(paulis=pauli_list)
qubit_op = sum(PauliOp(pauli, coeff=coeff) for coeff, pauli in pauli_list)

# qubit_op could be the integer 0, in this case return an identity operator of
# appropriate size
if isinstance(qubit_op, OperatorBase):
qubit_op = qubit_op.reduce()
else:
qubit_op = I ^ num_nodes

return qubit_op, shift
5 changes: 5 additions & 0 deletions releasenotes/notes/summed-op-compare-3df17046849af9a8.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
features:
- |
The ``SummedOp`` does a mathematically more correct check for equality, where
expressions such as ``X + X == 2*X`` and ``X + Z == Z + X`` evaluate to ``True``.
52 changes: 36 additions & 16 deletions test/aqua/operators/test_op_construction.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,26 @@

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


import unittest
from test.aqua import QiskitAquaTestCase
import itertools
import numpy as np
from ddt import ddt, data

from qiskit.circuit import QuantumCircuit, QuantumRegister, Instruction
from qiskit.extensions.exceptions import ExtensionError
from qiskit.quantum_info.operators import Operator, Pauli
from qiskit.circuit.library import CZGate
from qiskit.circuit.library import CZGate, ZGate

from qiskit.aqua.operators import (
X, Y, Z, I, CX, T, H, PrimitiveOp, SummedOp, PauliOp, Minus, CircuitOp
X, Y, Z, I, CX, T, H, PrimitiveOp, SummedOp, PauliOp, Minus, CircuitOp, MatrixOp
)


# pylint: disable=invalid-name

@ddt
class TestOpConstruction(QiskitAquaTestCase):
"""Operator Construction tests."""

Expand Down Expand Up @@ -235,7 +238,7 @@ def test_circuit_permute(self):
c_op_id = c_op_perm.permute(perm)
self.assertEqual(c_op, c_op_id)

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

sum_op = sum_op.simplify()
sum_op = sum_op.collapse_summands()
with self.subTest('SummedOp test 2-b'):
self.assertEqual(sum_op.coeff, 1)
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
Expand All @@ -263,7 +266,7 @@ def test_summed_op(self):
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY', 'YY', 'XX'])
self.assertListEqual([op.coeff for op in sum_op], [2, 1, 1, 2])

sum_op = sum_op.simplify()
sum_op = sum_op.reduce()
with self.subTest('SummedOp test 3-b'):
self.assertEqual(sum_op.coeff, 1)
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
Expand All @@ -275,7 +278,7 @@ def test_summed_op(self):
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
self.assertListEqual([op.coeff for op in sum_op], [2, 1])

sum_op = sum_op.simplify()
sum_op = sum_op.collapse_summands()
with self.subTest('SummedOp test 4-b'):
self.assertEqual(sum_op.coeff, 1)
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
Expand All @@ -288,7 +291,7 @@ def test_summed_op(self):
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY', 'YY'])
self.assertListEqual([op.coeff for op in sum_op], [4, 2, 1])

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

sum_op = sum_op.simplify()
sum_op = sum_op.collapse_summands()
with self.subTest('SummedOp test 6-b'):
self.assertEqual(sum_op.coeff, 1)
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
Expand All @@ -310,11 +313,11 @@ def test_summed_op(self):
sum_op = SummedOp([X ^ X * 2, Y ^ Y], 2)
sum_op += sum_op
with self.subTest('SummedOp test 7-a'):
self.assertEqual(sum_op.coeff, 4)
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY'])
self.assertListEqual([op.coeff for op in sum_op], [2, 1])
self.assertEqual(sum_op.coeff, 1)
self.assertListEqual([str(op.primitive) for op in sum_op], ['XX', 'YY', 'XX', 'YY'])
self.assertListEqual([op.coeff for op in sum_op], [4, 2, 4, 2])

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

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

def test_summed_op_equals(self):
"""Test corner cases of SummedOp's equals function."""
with self.subTest('multiplicative factor'):
self.assertEqual(2 * X, X + X)

with self.subTest('commutative'):
self.assertEqual(X + Z, Z + X)

with self.subTest('circuit and paulis'):
z = CircuitOp(ZGate())
self.assertEqual(Z + z, z + Z)

with self.subTest('matrix op and paulis'):
z = MatrixOp([[1, 0], [0, -1]])
self.assertEqual(Z + z, z + Z)

def test_circuit_compose_register_independent(self):
"""Test that CircuitOp uses combines circuits independent of the register.

Expand All @@ -344,13 +363,14 @@ def test_circuit_compose_register_independent(self):

self.assertEqual(composed.num_qubits, 2)

def test_pauli_op_hashing(self):
@data(Z, CircuitOp(ZGate()), MatrixOp([[1, 0], [0, -1]]))
def test_op_hashing(self, op):
"""Regression test against faulty set comparison.

Set comparisons rely on a hash table which requires identical objects to have identical
hashes. Thus, the PauliOp.__hash__ should support this requirement.
hashes. Thus, the PrimitiveOp.__hash__ should support this requirement.
"""
self.assertEqual(set([2*Z]), set([2*Z]))
self.assertEqual(set([2 * op]), set([2 * op]))


if __name__ == '__main__':
Expand Down
Loading