Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Created SumOfProducts subclass of ControlValues #5755

Merged
merged 10 commits into from
Jul 15, 2022
1 change: 1 addition & 0 deletions cirq-core/cirq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@
SQRT_ISWAP_INV,
SWAP,
SwapPowGate,
SumOfProducts,
T,
TaggedOperation,
ThreeQubitDiagonalGate,
Expand Down
1 change: 1 addition & 0 deletions cirq-core/cirq/json_resolver_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ def _symmetricalqidpair(qids):
'SqrtIswapTargetGateset': cirq.SqrtIswapTargetGateset,
'StabilizerStateChForm': cirq.StabilizerStateChForm,
'StatePreparationChannel': cirq.StatePreparationChannel,
'SumOfProducts': cirq.SumOfProducts,
'SwapPowGate': cirq.SwapPowGate,
'SympyCondition': cirq.SympyCondition,
'TaggedOperation': cirq.TaggedOperation,
Expand Down
2 changes: 1 addition & 1 deletion cirq-core/cirq/ops/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,4 @@

from cirq.ops.state_preparation_channel import StatePreparationChannel

from cirq.ops.control_values import AbstractControlValues, ProductOfSums
from cirq.ops.control_values import AbstractControlValues, ProductOfSums, SumOfProducts
77 changes: 77 additions & 0 deletions cirq-core/cirq/ops/control_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,80 @@ def __and__(self, other: AbstractControlValues) -> 'ProductOfSums':
f'And operation not supported between types ProductOfSums and {type(other)}'
)
return type(self)(self._internal_representation + other._internal_representation)


@dataclass(frozen=True, eq=False)
class SumOfProducts(AbstractControlValues):
"""SumOfProducts represents control values in a form of a list or products.

SumOfProducts allows the creation of control values combinations that
can't be factored as the product of independent expressions (e.g. XOR)


Examples:
xor = SumOfProducts(((0, 1), (1, 0))) # xor of two bits.
nand = SumOfProducts(((0, 0), (0, 1), (1, 0))) # nand of two bits
"""
NoureldinYosri marked this conversation as resolved.
Show resolved Hide resolved

_internal_representation: Tuple[Tuple[int, ...], ...]
NoureldinYosri marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a post init method which verifies that length of all nested tuples in self._internal_representation is the same? This is assumed to be true in the implementation of methods like _number_variables below

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added, I also test for the uniqueness of products in it


def _identifier(self) -> Tuple[Tuple[int, ...], ...]:
return self._internal_representation

def _expand(self) -> Iterator[Tuple[int, ...]]:
"""Returns the combinations tracked by the object."""
self = cast('SumOfProducts', self)
NoureldinYosri marked this conversation as resolved.
Show resolved Hide resolved
for c in self._internal_representation:
yield c

def __repr__(self) -> str:
return f'cirq.SumOfProducts({str(self._identifier())})'

def _number_variables(self) -> int:
return len(self._internal_representation[0])

def __len__(self) -> int:
return self._number_variables()

def __hash__(self) -> int:
return hash(self._internal_representation)

def validate(self, qid_shapes: Union[Tuple[int, ...], List[int]]) -> None:
for vals in self._internal_representation:
if len(qid_shapes) != len(vals):
raise ValueError(
f'number of values in product {vals} doesn\'t equal number'
f' of qubits(={len(qid_shapes)})'
)

for i, v in enumerate(vals):
if not (0 <= v and v < qid_shapes[i]):
raise ValueError(
f'Control values <{v}> in combination {vals} is outside'
f' of range for control qubit number <{i}>.'
)

def _are_ones(self) -> bool:
return frozenset(self._internal_representation) == {(1,) * self._number_variables()}

def diagram_repr(self) -> str:
return NotImplemented
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the diagram_repr not implemented?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the most basic implementation is problematic since the strings will be quite large since they have size num_products x num_qubits => O(num_qubits x 2^num_qubits), this is why I'm hestiant to implement it, however I added it anyway :)


def __getitem__(
self, key: Union[int, slice]
) -> Union['AbstractControlValues', Tuple[int, ...]]:
return NotImplemented
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this NotImplemented? Also, I don't like the pattern of
a) Declaring abstract methods in the API, telling the users that all implementations will implement these abstract methods.
b) Returning NotImplemented in the implementation of the abstract methods.

If we want to allow implementations to return NotImplemented; then these shouldn't be abstract methods in the first place.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the reason that I'm hesitant to implement it is that it will have a different meaning from that of ProductOfSums, where in ProductOfSums we get valide values of qubit, here we get a product


def _json_dict_(self) -> Dict[str, Any]:
return {'_internal_representation': self._internal_representation}

def __and__(self, other: AbstractControlValues) -> 'SumOfProducts':
if not isinstance(other, SumOfProducts):
raise TypeError(
f'And operation not supported between types SumOfProducts and {type(other)}'
)
NoureldinYosri marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +278 to +281
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not doing the the linked-list style concatenation discussed earlier? Will that come in a follow-up PR?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would wait until next PR, I want to introduct the SumOfProducts class before Cirq 1.0, because it has full expressive power (i.e. fixes the original issue), the linked structure can be introducted later without affecting users

the difference between the current state and what will happen when the linked structure is introduced is expressions like this ((x xor y) and (z and w)) and can now be represented by a SumOfProducts objects that has 8 products, while when we introduce the linked structure then we will only need to store 5 products (2 for the first and 3 for the second) and similarly for larger expressions where we would could in some cases store an exponenetial number of products in pseudopolynomial space.

combined = map(
lambda p: tuple(itertools.chain(*p)),
itertools.product(self._internal_representation, other._internal_representation),
)
return SumOfProducts(tuple(combined))
83 changes: 79 additions & 4 deletions cirq-core/cirq/ops/control_values_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,53 @@ def test_init_productOfSum():
((((0, 1), (1, 0))), {(0, 0), (0, 1), (1, 0), (1, 1)}),
]
for control_values, want in tests:
print(control_values)
got = {c for c in cv.ProductOfSums(control_values)}
eq.add_equality_group(got, want)


def test_init_SumOfProducts():
eq = cirq.testing.EqualsTester()
tests = [
(((1,),), {(1,)}),
(((0, 1), (1, 0)), {(0, 1), (1, 0)}), # XOR
(((0, 0), (0, 1), (1, 0)), {(0, 0), (0, 1), (1, 0)}), # NAND
]
for control_values, want in tests:
got = {c for c in cv.SumOfProducts(control_values)}
eq.add_equality_group(got, want)


def test_and_operation():
eq = cirq.testing.EqualsTester()
originals = [((1,),), ((0, 1), (1,)), (((0, 1), (1, 0)))]
for control_values1 in originals:
for control_values2 in originals:
product_of_sums_data = [((1,),), ((0, 1), (1,)), (((0, 1), (1, 0)))]
for control_values1 in product_of_sums_data:
for control_values2 in product_of_sums_data:
control_vals1 = cv.ProductOfSums(control_values1)
control_vals2 = cv.ProductOfSums(control_values2)
want = [v1 + v2 for v1 in control_vals1 for v2 in control_vals2]
got = [c for c in control_vals1 & control_vals2]
eq.add_equality_group(got, want)

sum_of_products_data = [((1,),), ((0, 1),), ((0, 0), (0, 1), (1, 0))]
eq = cirq.testing.EqualsTester()
for control_values1 in sum_of_products_data:
for control_values2 in sum_of_products_data:
control_vals1 = cv.SumOfProducts(control_values1)
control_vals2 = cv.SumOfProducts(control_values2)
want = [v1 + v2 for v1 in control_vals1 for v2 in control_vals2]
got = [c for c in control_vals1 & control_vals2]
eq.add_equality_group(got, want)

with pytest.raises(TypeError):
pos = cv.ProductOfSums(product_of_sums_data[0])
sop = cv.SumOfProducts(sum_of_products_data[0])
_ = pos & sop

with pytest.raises(TypeError):
pos = cv.ProductOfSums(product_of_sums_data[0])
sop = cv.SumOfProducts(sum_of_products_data[0])
_ = sop & pos


def test_and_supported_types():
CV = cv.ProductOfSums((1,))
Expand All @@ -52,3 +83,47 @@ def test_repr():
product_of_sums_data = [((1,),), ((0, 1), (1,)), (((0, 1), (1, 0)))]
for t in map(cv.ProductOfSums, product_of_sums_data):
cirq.testing.assert_equivalent_repr(t)

sum_of_products_data = [((1,),), ((0, 1)), ((0, 0), (0, 1), (1, 0))]

for t in map(cv.SumOfProducts, sum_of_products_data):
cirq.testing.assert_equivalent_repr(t)


def test_validate():
control_val = cv.SumOfProducts(((1, 2), (0, 1)))

_ = control_val.validate([2, 3])

with pytest.raises(ValueError):
_ = control_val.validate([2, 2])

# number of qubits != number of control values.
with pytest.raises(ValueError):
_ = control_val.validate([2])


def test_len():
data = [((1,),), ((0, 1),), ((0, 0), (0, 1), (1, 0))]
for vals in data:
c = cv.SumOfProducts(vals)
assert len(c) == len(vals[0])


def test_hash():
data = [((1,),), ((0, 1),), ((0, 0), (0, 1), (1, 0))]
assert len(set(map(hash, map(cv.SumOfProducts, data)))) == 3


def test_are_ones():
data = [((1,),), ((0, 1),), ((0, 0), (0, 1), (1, 0)), ((1, 1, 1, 1),)]
are_ones = [True, False, False]
for vals, want in zip(data, are_ones):
c = cv.SumOfProducts(vals)
assert c._are_ones() == want


def test_sum_of_products_unsupported():
c = cv.SumOfProducts(((1,),))
assert c.diagram_repr() == NotImplemented
assert c[0] == NotImplemented
4 changes: 4 additions & 0 deletions cirq-core/cirq/protocols/json_test_data/SumOfProducts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"_internal_representation": [[1, 0], [1, 2]],
"cirq_type": "SumOfProducts"
}
1 change: 1 addition & 0 deletions cirq-core/cirq/protocols/json_test_data/SumOfProducts.repr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
cirq.SumOfProducts([[1, 0], [1, 2]])