From 009daf3c803a3571c763d728fd4c8474fda2fcb5 Mon Sep 17 00:00:00 2001 From: Aidan Sims Date: Sun, 28 Dec 2025 21:38:25 -0800 Subject: [PATCH] Add the floor division method to the QInt The method is broken into a few functions to satisfy the complexity linter. It checks for edge cases like the dividend being 0 or the divisor being 1 or 0. It also tries to use the shift trick for even divisors. Tests have been added under test_int.py --- qlasskit/ast2logic/t_expression.py | 2 + qlasskit/types/qint.py | 99 +++++++++++++++++++++++++++++- qlasskit/types/qtype.py | 4 ++ test/qlassf/test_int.py | 40 +++++++++++- test/utils.py | 71 +++++++++++---------- 5 files changed, 179 insertions(+), 37 deletions(-) diff --git a/qlasskit/ast2logic/t_expression.py b/qlasskit/ast2logic/t_expression.py index 53c1d99..5b28db3 100644 --- a/qlasskit/ast2logic/t_expression.py +++ b/qlasskit/ast2logic/t_expression.py @@ -285,6 +285,8 @@ def unfold(v_exps, op): return tleft[0].mul(tleft, tright) elif isinstance(expr.op, ast.Mod): return tleft[0].mod(tleft, tright) # type: ignore + elif isinstance(expr.op, ast.FloorDiv) and hasattr(tleft[0], "floor_div"): + return tleft[0].floor_div(tleft, tright) elif isinstance(expr.op, ast.BitXor) and hasattr(tleft[0], "bitwise_xor"): return tleft[0].bitwise_xor(tleft, tright) elif isinstance(expr.op, ast.BitAnd) and hasattr(tleft[0], "bitwise_and"): diff --git a/qlasskit/types/qint.py b/qlasskit/types/qint.py index 8446a19..23b43ba 100644 --- a/qlasskit/types/qint.py +++ b/qlasskit/types/qint.py @@ -12,10 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import List, cast +from typing import List, Optional, cast from sympy import Symbol -from sympy.logic import And, Not, Or, Xor, false, true +from sympy.logic import ITE, And, Not, Or, Xor, false, true from . import TypeErrorException, _eq, _full_adder, _neq from .qtype import Qtype, TExp, TType, bin_to_bool_list, bool_list_to_bin @@ -295,6 +295,101 @@ def mod(cls, tleft: TExp, tright: TExp) -> TExp: # noqa: C901 tval = tright[0].sub(tright, tright[0].const(1)) return tleft[0].bitwise_and(tleft, tval) + @staticmethod + def _floor_div_const_optimize( + cls, tleft: TExp, tright_value: int + ) -> Optional[TExp]: + """Optimize division by constant divisors""" + if tright_value == 1: + return tleft + if tright_value > 0 and (tright_value & (tright_value - 1)) == 0: + power = 0 + temp = tright_value + while temp > 1: + temp >>= 1 + power += 1 + return cls.shift_right(tleft, power) + if tright_value > 1 and tright_value % 2 == 0: + return cls.floor_div( + cls.shift_right(tleft, 1), cls.const(tright_value // 2) + ) + return None + + @staticmethod + def _floor_div_handle_const_divisor( + cls, tleft: TExp, tright: TExp + ) -> Optional[TExp]: + """Handle constant divisor optimizations""" + if not cls.is_const(tright): + return None + tright_qtype = cast(Qtype, tright[0]).from_bool(tright[1]) + tright_value = cast(QintImp, tright_qtype) + if tright_value == 0: + raise ZeroDivisionError("division by zero") + return cls._floor_div_const_optimize(cls, tleft, tright_value) + + @staticmethod + def _floor_div_normalize_sizes(cls, tleft: TExp, tright: TExp) -> tuple[TExp, TExp]: + """Normalize operand sizes""" + if len(tleft[1]) > len(tright[1]): + tright = cast(Qtype, tleft[0]).fill(tright) + elif len(tleft[1]) < len(tright[1]): + tleft = cast(Qtype, tright[0]).fill(tleft) + return tleft, tright + + @staticmethod + def _floor_div_long_division(cls, tleft: TExp, tright: TExp, n: int) -> TExp: + """Perform long division algorithm""" + tleft_type = cast(Qtype, tleft[0]) + Q = tleft_type.fill(cls.const(0)) + R = tleft_type.fill(cls.const(0)) + + for i in range(n - 1, -1, -1): + R = cls.shift_left(R, 1) + R_bits = list(R[1]) + R_bits[0] = tleft[1][i] + R = (R[0], R_bits) + + r_gte_d = cls.gte(R, tright) + new_R = cls.sub(R, tright) + R = ( + R[0], + [ITE(r_gte_d[1], new_R[1][j], R[1][j]) for j in range(len(R[1]))], + ) + + Q_bits = list(Q[1]) + Q_bits[i] = Or(Q_bits[i], r_gte_d[1]) + Q = (Q[0], Q_bits) + + return Q + + @classmethod + def floor_div(cls, tleft: TExp, tright: TExp) -> TExp: + """Floor divide two Qint""" + if not issubclass(tleft[0], Qtype) or not issubclass(tright[0], Qtype): + raise TypeErrorException( + tleft[0] if not issubclass(tleft[0], Qtype) else tright[0], Qtype + ) + + opt_result = cls._floor_div_handle_const_divisor(cls, tleft, tright) + if opt_result is not None: + return opt_result + + if cls.is_const(tleft): + tleft_value = cast(Qtype, tleft[0]).from_bool(tleft[1]) + if tleft_value == 0: + return cls.const(0) + + tleft, tright = cls._floor_div_normalize_sizes(cls, tleft, tright) + + if cls.is_const(tleft) and cls.is_const(tright): + tleft_val = cast(QintImp, cast(Qtype, tleft[0]).from_bool(tleft[1])) + tright_val = cast(QintImp, cast(Qtype, tright[0]).from_bool(tright[1])) + if tleft_val < tright_val: + return cls.const(0) + + return cls._floor_div_long_division(cls, tleft, tright, len(tleft[1])) + @classmethod def bitwise_generic(cls, op, tleft: TExp, tright: TExp) -> TExp: """Bitwise generic""" diff --git a/qlasskit/types/qtype.py b/qlasskit/types/qtype.py index 14f5c40..ee24f37 100644 --- a/qlasskit/types/qtype.py +++ b/qlasskit/types/qtype.py @@ -216,6 +216,10 @@ def mul(tleft: TExp, tright: TExp) -> TExp: def mod(tleft: TExp, tright: TExp) -> TExp: raise Exception("abstract mod") + @staticmethod + def floor_div(tleft: TExp, tright: TExp) -> TExp: + raise Exception("abstract floor_div") + @staticmethod def bitwise_xor(tleft: TExp, tright: TExp) -> TExp: raise Exception("abstract bitwise_xor") diff --git a/test/qlassf/test_int.py b/test/qlassf/test_int.py index f46527c..f0d1d84 100644 --- a/test/qlassf/test_int.py +++ b/test/qlassf/test_int.py @@ -130,7 +130,7 @@ def test_int_identity(self): compute_and_compare_results(self, qf) def test_int_const_compare_eq(self): - f = f"def test(a: {self.ttype_str}) -> bool:\n\treturn a == {int(self.ttype_size/2-1)}" + f = f"def test(a: {self.ttype_str}) -> bool:\n\treturn a == {int(self.ttype_size / 2 - 1)}" qf = qlassf(f, to_compile=COMPILATION_ENABLED, compiler=self.compiler) self.assertEqual(len(qf.expressions), 1) self.assertEqual(qf.expressions[0][0], _ret) @@ -386,6 +386,44 @@ def test_mod_const_in_var(self): compute_and_compare_results(self, qf) +@parameterized_class( + ("val", "compiler"), + inject_parameterized_compilers( + [ + (2,), + (3,), + (4,), + (5,), + (6,), + (8,), + ] + ), +) +class TestQlassfIntFloorDiv(unittest.TestCase): + def test_floor_div_const(self): + f = f"def test(a: Qint[4]) -> Qint[4]: return a // {self.val}" + qf = qlassf(f, to_compile=COMPILATION_ENABLED, compiler=self.compiler) + compute_and_compare_results(self, qf) + + def test_floor_div_const_in_var(self): + f = f"def test(a: Qint[4]) -> Qint[4]:\n\tb = {self.val}\n\treturn a // b" + qf = qlassf(f, to_compile=COMPILATION_ENABLED, compiler=self.compiler) + compute_and_compare_results(self, qf) + + +@parameterized_class(("compiler"), ENABLED_COMPILERS) +class TestQlassfIntFloorDivGeneral(unittest.TestCase): + def test_floor_div(self): + f = "def test(a: Qint[4], b: Qint[4]) -> Qint[4]: return a // b" + qf = qlassf(f, to_compile=COMPILATION_ENABLED, compiler=self.compiler) + compute_and_compare_results(self, qf) + + def test_floor_div_qint2(self): + f = "def test(a: Qint[2], b: Qint[2]) -> Qint[2]: return a // b" + qf = qlassf(f, to_compile=COMPILATION_ENABLED, compiler=self.compiler) + compute_and_compare_results(self, qf) + + @parameterized_class(("compiler"), ENABLED_COMPILERS) class TestQlassfIntSub(unittest.TestCase): def test_sub_const(self): diff --git a/test/utils.py b/test/utils.py index 826cd74..cfe3785 100644 --- a/test/utils.py +++ b/test/utils.py @@ -209,63 +209,66 @@ def res_to_str(res): return res_original_str -def compute_and_compare_results(cls, qf, test_original_f=True, test_qcircuit=True): - """Create and simulate the qcircuit, and compare the result with the - truthtable and with the original_f""" +def _prepare_truth_table(qf, test_qcircuit): + """Prepare truth table and quantum circuit truth subset""" MAX_Q_SIM = 64 MAX_C_SIM = 2**9 - qc_truth = None test_qcircuit = test_qcircuit and COMPILATION_ENABLED truth_table = qf.truth_table(MAX_C_SIM) - if len(truth_table) > MAX_C_SIM: truth_table = [random.choice(truth_table) for x in range(MAX_C_SIM)] + qc_truth = None if len(truth_table) > MAX_Q_SIM and test_qcircuit: qc_truth = [random.choice(truth_table) for x in range(MAX_Q_SIM)] elif test_qcircuit: qc_truth = truth_table - # circ_qi = qf.circuit().export("circuit", "qiskit") + return truth_table, qc_truth + + +def _test_qcircuit_result(cls, qf, truth_line, truth_str): + """Test quantum circuit result for a truth line""" + max_qubits = ( + qf.input_size + + len(qf.expressions) + + sum([gateinputcount(e[1]) for e in qf.expressions]) + ) + cls.assertLessEqual(qf.num_qubits, max_qubits) + + if os.getenv("GITHUB_ACTIONS"): + res_qc = compute_result_of_qcircuit(cls, qf, truth_line) + cls.assertEqual(truth_str, res_qc) + else: + try: + res_qc2 = compute_result_of_qcircuit_using_cnotsim(cls, qf, truth_line) + cls.assertEqual(truth_str, res_qc2) + except GateNotSimulableException: + res_qc = compute_result_of_qcircuit(cls, qf, truth_line) + cls.assertEqual(truth_str, res_qc) + + +def compute_and_compare_results(cls, qf, test_original_f=True, test_qcircuit=True): + """Create and simulate the qcircuit, and compare the result with the + truthtable and with the original_f""" + truth_table, qc_truth = _prepare_truth_table(qf, test_qcircuit) update_statistics( qf.circuit().num_qubits, qf.circuit().num_gates, qf.circuit().gate_stats ) - # print(qf.expressions) - # print(circ_qi.draw("text")) - # print(circ_qi.qasm()) - for truth_line in truth_table: - # Extract str of truthtable and result truth_str = "".join( map(lambda x: "1" if x else "0", truth_line[-qf.output_size :]) ) - # Calculate and compare the originalf result if test_original_f: - res_original = compute_result_of_originalf(cls, qf, truth_line) - cls.assertEqual(truth_str, res_original) + try: + res_original = compute_result_of_originalf(cls, qf, truth_line) + cls.assertEqual(truth_str, res_original) + except ZeroDivisionError: + continue - # Calculate and compare the gate result if qc_truth and truth_line in qc_truth and test_qcircuit: - max_qubits = ( - qf.input_size - + len(qf.expressions) - + sum([gateinputcount(e[1]) for e in qf.expressions]) - ) - cls.assertLessEqual(qf.num_qubits, max_qubits) - - if os.getenv("GITHUB_ACTIONS"): - res_qc = compute_result_of_qcircuit(cls, qf, truth_line) - cls.assertEqual(truth_str, res_qc) - else: - try: - res_qc2 = compute_result_of_qcircuit_using_cnotsim( - cls, qf, truth_line - ) - cls.assertEqual(truth_str, res_qc2) - except GateNotSimulableException: - res_qc = compute_result_of_qcircuit(cls, qf, truth_line) - cls.assertEqual(truth_str, res_qc) + _test_qcircuit_result(cls, qf, truth_line, truth_str)