diff --git a/TODO.md b/TODO.md index d95be3a4..29aa2deb 100644 --- a/TODO.md +++ b/TODO.md @@ -92,6 +92,7 @@ - [x] QuTip Support - [x] Parameter bind (https://github.com/dakk/qlasskit/issues/10) - [x] QUBO, Ising and BQM exporter +- [x] Datatype: Char ## Future features @@ -103,7 +104,6 @@ - [ ] Int arithmetic: div - [ ] Lambda - [ ] Builtin function: map -- [ ] Datatype: Char - [ ] Datatype: Dict - [ ] Datatype: Float - [ ] Datatype: Enum diff --git a/docs/source/supported.rst b/docs/source/supported.rst index b1e371f6..c935731a 100644 --- a/docs/source/supported.rst +++ b/docs/source/supported.rst @@ -33,6 +33,11 @@ Unsigned integers; this type has subtypes for different Qint sizes (Qint2, Qint4 Single bit of the Qint are accessible by the subscript operator `[]`. +Qchar +^^^^^ + +A character. + Tuple ^^^^^ @@ -184,7 +189,8 @@ Bultin functions: - `sum(Tuple)`, `sum(Qlist)`: returns the sum of the elemnts of a tuple / list - `all(Tuple)`, `all(Qlist)`: returns True if all of the elemnts are True - `any(Tuple)`, `any(Qlist)`: returns True if any of the elemnts are True - +- `ord(Qchar)`: returns the integer value of the given Qchar +- `chr(Qint)`: returns the char given its ascii code Statements diff --git a/qlasskit/__init__.py b/qlasskit/__init__.py index 89928d92..641d75bf 100644 --- a/qlasskit/__init__.py +++ b/qlasskit/__init__.py @@ -23,12 +23,14 @@ const_to_qtype, interpret_as_qtype, Qtype, + Qchar, Qint, Qint2, Qint3, Qint4, Qint5, Qint6, + Qint7, Qint8, Qint12, Qint16, diff --git a/qlasskit/ast2ast.py b/qlasskit/ast2ast.py index c250ecfe..1eca9b1e 100644 --- a/qlasskit/ast2ast.py +++ b/qlasskit/ast2ast.py @@ -460,6 +460,18 @@ def __call_anyall(self, node): op = ast.Or() if node.func.id == "any" else ast.And() return ast.BoolOp(op=op, values=args) + def __call_chr(self, node): + if len(node.args) != 1: + raise Exception(f"chr() takes exactly 1 argument ({len(node.args)} given)") + args = self.__unroll_arg(node.args[0]) + return args[0] + + def __call_ord(self, node): + if len(node.args) != 1: + raise Exception(f"ord() takes exactly 1 argument ({len(node.args)} given)") + args = self.__unroll_arg(node.args[0]) + return args[0] + def visit_Call(self, node): node.args = [self.visit(ar) for ar in node.args] if not hasattr(node.func, "id"): @@ -477,6 +489,12 @@ def visit_Call(self, node): elif node.func.id == "sum": return self.__call_sum(node) + elif node.func.id == "ord": + return self.__call_ord(node) + + elif node.func.id == "chr": + return self.__call_chr(node) + elif node.func.id in ["any", "all"]: return self.__call_anyall(node) diff --git a/qlasskit/types/__init__.py b/qlasskit/types/__init__.py index 2e910746..8bda5ad8 100644 --- a/qlasskit/types/__init__.py +++ b/qlasskit/types/__init__.py @@ -47,6 +47,7 @@ def _full_adder(c, a, b): # Carry x Sum from .qbool import Qbool # noqa: F401, E402 from .qlist import Qlist # noqa: F401, E402 from .qmatrix import Qmatrix # noqa: F401, E402 +from .qchar import Qchar # noqa: F401, E402 from .qint import ( # noqa: F401, E402 Qint, Qint2, @@ -54,6 +55,7 @@ def _full_adder(c, a, b): # Carry x Sum Qint4, Qint5, Qint6, + Qint7, Qint8, Qint12, Qint16, @@ -66,9 +68,11 @@ def _full_adder(c, a, b): # Carry x Sum Qint4, Qint5, Qint6, + Qint7, Qint8, Qint12, Qint16, + Qchar, Qlist, Qmatrix, ] @@ -82,6 +86,9 @@ def const_to_qtype(value: Any) -> TExp: raise Exception(f"Constant value is too big: {value}") + elif isinstance(value, str): + return Qchar.const(value) + raise Exception(f"Unable to infer type of constant: {value}") diff --git a/qlasskit/types/qchar.py b/qlasskit/types/qchar.py new file mode 100644 index 00000000..ed4052f1 --- /dev/null +++ b/qlasskit/types/qchar.py @@ -0,0 +1,83 @@ +# Copyright 2024 Davide Gessa + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Any, List + +from sympy.logic import And, Or, false, true + +from . import _eq, _neq +from .qint import Qint +from .qtype import Qtype, TExp + + +class Qchar(str, Qtype): + BIT_SIZE = 8 + + def __init__(self, value: str): + super().__init__() + assert len(value) == 1 + self.value = value[0] + + def to_bin(self) -> str: + s = bin(ord(self.value))[2:][0 : self.BIT_SIZE] + return ("0" * (self.BIT_SIZE - len(s)) + s)[::-1] + + def to_amplitudes(self) -> List[float]: + ampl = [0.0] * 2**self.BIT_SIZE + ampl[ord(self.value)] = 1 + return ampl + + @classmethod + def from_bool(cls, v: List[bool]): + bin_str = "".join(map(lambda x: "1" if x else "0", v)) + return cls(chr(int(bin_str[::-1], 2))) + + @classmethod + def comparable(cls, other_type=None) -> bool: + return other_type == cls or issubclass(other_type, Qint) + + @classmethod + def fill(cls, v: TExp) -> TExp: + if len(v[1]) < cls.BIT_SIZE: # type: ignore + v = ( + cls, + v[1] + (cls.BIT_SIZE - len(v[1])) * [False], # type: ignore + ) + return v + + @classmethod + def const(cls, value: Any) -> TExp: + assert len(value) == 1 + cval = list( + map(lambda c: True if c == "1" else False, bin(ord(value))[2:][::-1]) + ) # [::-1] + return cls.fill((cls, cval)) + + # Comparators + + @staticmethod + def eq(tleft: TExp, tcomp: TExp) -> TExp: + ex = true + for x in zip(tleft[1], tcomp[1]): + ex = And(ex, _eq(x[0], x[1])) + + return (bool, ex) + + @staticmethod + def neq(tleft: TExp, tcomp: TExp) -> TExp: + ex = false + for x in zip(tleft[1], tcomp[1]): + ex = Or(ex, _neq(x[0], x[1])) + + return (bool, ex) diff --git a/qlasskit/types/qint.py b/qlasskit/types/qint.py index 994cefc9..d429678f 100644 --- a/qlasskit/types/qint.py +++ b/qlasskit/types/qint.py @@ -275,6 +275,10 @@ class Qint6(Qint): BIT_SIZE = 6 +class Qint7(Qint): + BIT_SIZE = 7 + + class Qint8(Qint): BIT_SIZE = 8 diff --git a/test/qlassf/test_char.py b/test/qlassf/test_char.py new file mode 100644 index 00000000..a5e7ab17 --- /dev/null +++ b/test/qlassf/test_char.py @@ -0,0 +1,103 @@ +# Copyright 2023 Davide Gessa + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +from parameterized import parameterized_class +from sympy import Symbol +from sympy.logic import And, Not + +from qlasskit import Qchar, qlassf + +from ..utils import COMPILATION_ENABLED, ENABLED_COMPILERS, compute_and_compare_results + + +@parameterized_class(("compiler"), ENABLED_COMPILERS) +class TestQchar(unittest.TestCase): + def test_qchar_to_bin_and_from_bool(self): + c = Qchar("a").to_bin() + self.assertEqual(c, "01100001"[::-1]) + self.assertEqual(c, Qchar("a").export("binary")) + self.assertEqual( + Qchar.from_bool( + [False, True, True, False, False, False, False, True][::-1] + ), + "a", + ) + + def test_char_arg_eq(self): + f = "def test(a: Qchar) -> bool:\n\treturn a == 'a'" + qf = qlassf(f, to_compile=COMPILATION_ENABLED, compiler=self.compiler) + self.assertEqual(len(qf.expressions), 1) + self.assertEqual(qf.expressions[0][0], Symbol("_ret")) + a = [Symbol(f"a.{i}") for i in range(8)] + self.assertEqual( + qf.expressions[0][1], + And( + a[0], a[5], a[6], Not(a[1]), Not(a[2]), Not(a[3]), Not(a[4]), Not(a[7]) + ), + ) + compute_and_compare_results(self, qf) + + def test_char_arg_neq(self): + f = "def test(a: Qchar) -> bool:\n\treturn a != 'a'" + qf = qlassf(f, to_compile=COMPILATION_ENABLED, compiler=self.compiler) + self.assertEqual(len(qf.expressions), 1) + self.assertEqual(qf.expressions[0][0], Symbol("_ret")) + a = [Symbol(f"a.{i}") for i in range(8)] + self.assertEqual( + qf.expressions[0][1], + Not( + And( + a[0], + a[5], + a[6], + Not(a[1]), + Not(a[2]), + Not(a[3]), + Not(a[4]), + Not(a[7]), + ) + ), + ) + compute_and_compare_results(self, qf) + + def test_char_return(self): + f = "def test(a: Qchar) -> Qchar:\n\treturn 'z' if a == 'a' else 'a'" + qf = qlassf(f, to_compile=COMPILATION_ENABLED, compiler=self.compiler) + compute_and_compare_results(self, qf) + + def test_char_return2(self): + f = "def test(a: Qchar) -> Qchar:\n\treturn a" + qf = qlassf(f, to_compile=COMPILATION_ENABLED, compiler=self.compiler) + compute_and_compare_results(self, qf) + + def test_char_return_tuple(self): + f = ( + "def test(a: Qchar) -> Tuple[Qchar, bool]:\n" + "\tb = a == 'z'\n\treturn (a, b)" + ) + qf = qlassf(f, to_compile=COMPILATION_ENABLED, compiler=self.compiler) + self.assertEqual(len(qf.expressions), 9) + compute_and_compare_results(self, qf) + + def test_char_ord(self): + f = "def test(a: Qchar) -> bool:\n\treturn ord(a) == 97" + qf = qlassf(f, to_compile=COMPILATION_ENABLED, compiler=self.compiler) + compute_and_compare_results(self, qf) + + def test_char_chr(self): + f = "def test(a: Qchar) -> bool:\n\treturn a == chr(97)" + qf = qlassf(f, to_compile=COMPILATION_ENABLED, compiler=self.compiler) + compute_and_compare_results(self, qf) diff --git a/test/utils.py b/test/utils.py index 0d461aaa..a2c9938b 100644 --- a/test/utils.py +++ b/test/utils.py @@ -168,7 +168,7 @@ def res_to_str(res): return "1" if res else "0" elif type(res) is tuple or type(res) is list: return "".join([res_to_str(x) for x in res]) - elif type(res) is int: + elif type(res) is int or type(res) is str: qc = const_to_qtype(res) try: qi = qf.returns.ttype.from_bool(qc[1])