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 21 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
62 changes: 56 additions & 6 deletions qiskit/aqua/operators/list_ops/summed_op.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,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 +81,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 +110,24 @@ 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:
"""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 = [op.reduce() for op in self.oplist]
reduced_ops = reduce(lambda x, y: x.add(y), reduced_ops) * 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 +152,43 @@ 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.

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 not 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)
30 changes: 20 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
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 @@ -60,6 +61,8 @@ def encode(self, qubit_op: WeightedPauliOperator, offset: float = 0.0) -> Quadra
QiskitOptimizationError: If there are more than 2 Pauli Zs in any Pauli term
"""
# Set properties
if isinstance(qubit_op, WeightedPauliOperator):
qubit_op = qubit_op.to_opflow()
self._qubit_op = qubit_op
self._offset = copy.deepcopy(offset)
self._num_qubits = qubit_op.num_qubits
Expand Down Expand Up @@ -134,24 +137,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))
8 changes: 5 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, SummedOp

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,8 @@ 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)
if isinstance(qubit_op, SummedOp):
qubit_op = qubit_op.reduce()

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``.
30 changes: 19 additions & 11 deletions test/aqua/operators/test_op_construction.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,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 +250,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 +263,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 +275,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 +288,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 +301,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 +310,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 +326,20 @@ 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)

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

Expand Down
3 changes: 2 additions & 1 deletion test/aqua/operators/test_pauli_expectation.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ def test_pauli_expect_op_vector(self):
# !!NOTE!!: Depolarizing channel (Sampling) means interference
# does not happen between circuits in sum, so expectation does
# not equal expectation for Zero!!
np.testing.assert_array_almost_equal(sampled_zero_mean.eval(), [0, 0, 0, 2], decimal=1)
np.testing.assert_array_almost_equal(sampled_zero_mean.eval(), [0, 0, 0, 2 ** 0.5],
decimal=1)

for i, op in enumerate(paulis_op.oplist):
mat_op = op.to_matrix()
Expand Down
35 changes: 12 additions & 23 deletions test/optimization/test_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import numpy as np
from docplex.mp.model import Model

from qiskit.aqua.operators import WeightedPauliOperator
from qiskit.aqua.operators import Z, I
from qiskit.aqua.algorithms import NumPyMinimumEigensolver
from qiskit.optimization import QuadraticProgram, QiskitOptimizationError
from qiskit.optimization.problems import Constraint, Variable
Expand All @@ -34,25 +34,19 @@
)
from qiskit.optimization.algorithms import MinimumEigenOptimizer, CplexOptimizer, ADMMOptimizer
from qiskit.optimization.algorithms.admm_optimizer import ADMMParameters
from qiskit.quantum_info import Pauli

logger = logging.getLogger(__name__)


QUBIT_OP_MAXIMIZE_SAMPLE = WeightedPauliOperator(
paulis=[
[(-199999.5 + 0j), Pauli(z=[True, False, False, False], x=[False, False, False, False])],
[(-399999.5 + 0j), Pauli(z=[False, True, False, False], x=[False, False, False, False])],
[(-599999.5 + 0j), Pauli(z=[False, False, True, False], x=[False, False, False, False])],
[(-799999.5 + 0j), Pauli(z=[False, False, False, True], x=[False, False, False, False])],
[(100000 + 0j), Pauli(z=[True, True, False, False], x=[False, False, False, False])],
[(150000 + 0j), Pauli(z=[True, False, True, False], x=[False, False, False, False])],
[(300000 + 0j), Pauli(z=[False, True, True, False], x=[False, False, False, False])],
[(200000 + 0j), Pauli(z=[True, False, False, True], x=[False, False, False, False])],
[(400000 + 0j), Pauli(z=[False, True, False, True], x=[False, False, False, False])],
[(600000 + 0j), Pauli(z=[False, False, True, True], x=[False, False, False, False])],
]
)
QUBIT_OP_MAXIMIZE_SAMPLE = -199999.5 * (I ^ I ^ I ^ Z) + \
-399999.5 * (I ^ I ^ Z ^ I) + \
-599999.5 * (I ^ Z ^ I ^ I) + \
-799999.5 * (Z ^ I ^ I ^ I) + \
100000 * (I ^ I ^ Z ^ Z) + \
150000 * (I ^ Z ^ I ^ Z) + \
300000 * (I ^ Z ^ Z ^ I) + \
200000 * (Z ^ I ^ I ^ Z) + \
400000 * (Z ^ I ^ Z ^ I) + \
600000 * (Z ^ Z ^ I ^ I)
OFFSET_MAXIMIZE_SAMPLE = 1149998


Expand Down Expand Up @@ -432,12 +426,7 @@ def test_optimizationproblem_to_ising(self):
op2 = penalize.encode(op)
qubitop, offset = op2ope.encode(op2)

# the encoder uses a dictionary, in which the order of items in Python 3.5 is not
# maintained, therefore don't do a list compare but dictionary compare
qubit_op_as_dict = dict(qubitop.paulis)
for coeff, paulis in QUBIT_OP_MAXIMIZE_SAMPLE.paulis:
self.assertEqual(paulis, qubit_op_as_dict[coeff])

self.assertEqual(qubitop, QUBIT_OP_MAXIMIZE_SAMPLE)
self.assertEqual(offset, OFFSET_MAXIMIZE_SAMPLE)

def test_ising_to_quadraticprogram_linear(self):
Expand Down
Loading