From c85b9a48d7b8329845567ceb3c89352b380eec53 Mon Sep 17 00:00:00 2001 From: "Davide Gessa (dakk)" Date: Wed, 8 Nov 2023 16:12:28 +0100 Subject: [PATCH] separate boolean optmization in boolopt module --- qlasskit/ast2ast.py | 1 + qlasskit/boolopt/__init__.py | 16 +++ qlasskit/{ => boolopt}/bool_optimizer.py | 119 ++++++++++++++++++++++- qlasskit/boolopt/exp_transformers.py | 63 ++++++++++++ qlasskit/boolopt/sympytransformer.py | 37 +++++++ qlasskit/compiler/__init__.py | 2 +- qlasskit/compiler/compiler.py | 60 ------------ qlasskit/qlassf.py | 15 +-- setup.py | 1 + test/utils.py | 2 +- 10 files changed, 240 insertions(+), 76 deletions(-) create mode 100644 qlasskit/boolopt/__init__.py rename qlasskit/{ => boolopt}/bool_optimizer.py (55%) create mode 100644 qlasskit/boolopt/exp_transformers.py create mode 100644 qlasskit/boolopt/sympytransformer.py diff --git a/qlasskit/ast2ast.py b/qlasskit/ast2ast.py index e8fc220b..1151cfe3 100644 --- a/qlasskit/ast2ast.py +++ b/qlasskit/ast2ast.py @@ -403,4 +403,5 @@ def ast2ast(a_tree): a_tree = IndexReplacer().visit(a_tree) a_tree = ASTRewriter().visit(a_tree) + # print(ast.dump(a_tree)) return a_tree diff --git a/qlasskit/boolopt/__init__.py b/qlasskit/boolopt/__init__.py new file mode 100644 index 00000000..9edd87e6 --- /dev/null +++ b/qlasskit/boolopt/__init__.py @@ -0,0 +1,16 @@ +# 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. +# isort:skip_file + +from .sympytransformer import SympyTransformer # noqa: F401 diff --git a/qlasskit/bool_optimizer.py b/qlasskit/boolopt/bool_optimizer.py similarity index 55% rename from qlasskit/bool_optimizer.py rename to qlasskit/boolopt/bool_optimizer.py index 1fb0f160..597f89aa 100644 --- a/qlasskit/bool_optimizer.py +++ b/qlasskit/boolopt/bool_optimizer.py @@ -15,9 +15,16 @@ from typing import Dict from sympy import Symbol -from sympy.logic.boolalg import Boolean +from sympy.logic.boolalg import Boolean, simplify_logic -from .ast2logic import BoolExpList +from ..ast2logic import BoolExpList +from . import SympyTransformer +from .exp_transformers import ( + remove_Implies, + remove_ITE, + transform_or2and, + transform_or2xor, +) def remove_const_exps(exps: BoolExpList) -> BoolExpList: @@ -110,19 +117,123 @@ def should_add(s, e, n_exps2): def merge_unnecessary_assigns(exps: BoolExpList) -> BoolExpList: """Translate exp like: __a.0 = !a, a = __a.0 ===> a = !a""" n_exps: BoolExpList = [] + rep_d = {} for s, e in exps: - if len(n_exps) >= 1 and n_exps[-1][0] == e: + if len(n_exps) >= 1 and n_exps[-1][0] == e: # and n_exps[-1][0].name[2:] == s: old = n_exps.pop() - n_exps.append((s, old[1])) + rep_d[old[0]] = old[1] + n_exps.append((s, e.subs(rep_d))) else: + n_exps.append((s, e.subs(rep_d))) + + return n_exps + + +def remove_unnecessary_aliases(exps: BoolExpList) -> BoolExpList: + """Translate exps like: (__d.0, a), (d.0, __d.0 & a) to => (d.0, a & a)""" + n_exps: BoolExpList = [] + rep_d = {} + + for s, e in exps: + if len(n_exps) >= 1 and n_exps[-1][0] in e.free_symbols: + old = n_exps.pop() + rep_d[old[0]] = old[1] + n_exps.append((s, e.subs(rep_d))) + else: + n_exps.append((s, e.subs(rep_d))) + + return n_exps + + +def remove_aliases(exps: BoolExpList) -> BoolExpList: + aliases = {} + n_exps = [] + for s, e in exps: + if isinstance(e, Symbol): + aliases[s] = e + elif s in aliases: + del aliases[s] + n_exps.append((s, e.subs(aliases))) + else: + n_exps.append((s, e.subs(aliases))) + + return n_exps + + +def s2_mega(exps: BoolExpList) -> BoolExpList: + n_exps: BoolExpList = [] + exp_d = {} + + for s, e in exps: + exp_d[s] = e + n_exps.append((s, e.subs(exp_d))) + + s_count = {} + exps = n_exps + + for s, e in exps: + if s.name not in s_count: + s_count[s.name] = 0 + + for x in e.free_symbols: + if x.name in s_count: + s_count[x.name] += 1 + + n_exps = [] + for s, e in exps: + if s_count[s.name] > 0 or s.name[0:4] == "_ret": n_exps.append((s, e)) return n_exps +def exps_simplify(exps: BoolExpList) -> BoolExpList: + return list(map(lambda e: (e[0], simplify_logic(e[1])), exps)) + + # [(h, a_list.0.0 & a_list.0.1), (h, a_list.1.0 & a_list.1.1 & h), # (h, a_list.2.0 & a_list.2.1 & h), (_ret, a_list.3.0 & a_list.3.1 & h)] # TO # (_ret, a_list_3_0 & a_list_3_1 & a_list_2_0 & a_list_2_1 & a_list_1_0 & a_list_1_1 & # a_list_0_0 & a_list_0_1) + + +class BoolOptimizerProfile: + def __init__(self, steps): + self.steps = steps + + def apply(self, exps): + for opt in self.steps: + if isinstance(opt, SympyTransformer): + exps = list(map(lambda e: (e[0], opt.visit(e[1])), exps)) + else: + exps = opt(exps) + return exps + + +bestWorkingOptimizer = BoolOptimizerProfile( + [ + remove_const_exps, + remove_unnecessary_assigns, + merge_unnecessary_assigns, + remove_ITE(), + remove_Implies(), + transform_or2xor(), + transform_or2and(), + ] +) + + +experimentalOptimizer = BoolOptimizerProfile( + [ + remove_const_exps, + remove_unnecessary_assigns, + merge_unnecessary_assigns, + # remove_unnecessary_aliases, + # s2_mega, + # subsitute_exps, + # exps_simplify, + # remove_aliases, + ] +) diff --git a/qlasskit/boolopt/exp_transformers.py b/qlasskit/boolopt/exp_transformers.py new file mode 100644 index 00000000..5313467e --- /dev/null +++ b/qlasskit/boolopt/exp_transformers.py @@ -0,0 +1,63 @@ +# 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. + +from sympy.logic import And, Not, Or, Xor + +from . import SympyTransformer + + +class remove_ITE(SympyTransformer): + def visit_ITE(self, expr): + c = self.visit(expr.args[0]) + return self.visit( + Or( + And(c, self.visit(expr.args[1])), + And(Not(c), self.visit(expr.args[2])), + ) + ) + + +class remove_Implies(SympyTransformer): + def visit_Implies(self, expr): + return self.visit(Or(Not(self.visit(expr.args[0])), self.visit(expr.args[1]))) + + +class transform_or2xor(SympyTransformer): + # Or(And(a,b), And(!a,!b)) = !Xor(a,b) + def visit_Or(self, expr): + if ( + len(expr.args) == 2 + and isinstance(expr.args[0], And) + and isinstance(expr.args[1], And) + and ( + ( + expr.args[1].args[0] == Not(expr.args[0].args[0]) + and expr.args[1].args[1] == Not(expr.args[0].args[1]) + ) + or ( + Not(expr.args[1].args[0]) == expr.args[0].args[0] + and Not(expr.args[1].args[1]) == expr.args[0].args[1] + ) + ) + ): + a = self.visit(expr.args[0].args[0]) + b = self.visit(expr.args[0].args[1]) + return Not(Xor(a, b)) + else: + return super().visit_Or(expr) + + +class transform_or2and(SympyTransformer): + def visit_Or(self, expr): + return Not(And(*[Not(self.visit(e)) for e in expr.args])) diff --git a/qlasskit/boolopt/sympytransformer.py b/qlasskit/boolopt/sympytransformer.py new file mode 100644 index 00000000..e83f7838 --- /dev/null +++ b/qlasskit/boolopt/sympytransformer.py @@ -0,0 +1,37 @@ +from sympy.logic import ITE, And, Implies, Not, Or, Xor + + +class SympyTransformer: + def visit(self, e): + if isinstance(e, And): + return self.visit_And(e) + elif isinstance(e, Or): + return self.visit_Or(e) + elif isinstance(e, Not): + return self.visit_Not(e) + elif isinstance(e, Implies): + return self.visit_Implies(e) + elif isinstance(e, ITE): + return self.visit_ITE(e) + elif isinstance(e, Xor): + return self.visit_Xor(e) + else: + return e + + def visit_And(self, e): + return And(*[self.visit(a) for a in e.args]) + + def visit_Or(self, e): + return Or(*[self.visit(a) for a in e.args]) + + def visit_Not(self, e): + return Not(self.visit(e.args[0])) + + def visit_ITE(self, e): + return ITE(*[self.visit(a) for a in e.args]) + + def visit_Implies(self, e): + return Implies(*[self.visit(a) for a in e.args]) + + def visit_Xor(self, e): + return Xor(*[self.visit(a) for a in e.args]) diff --git a/qlasskit/compiler/__init__.py b/qlasskit/compiler/__init__.py index a4cee12a..6d44d03e 100644 --- a/qlasskit/compiler/__init__.py +++ b/qlasskit/compiler/__init__.py @@ -16,7 +16,7 @@ from typing import Literal, get_args from .expqmap import ExpQMap # noqa: F401 -from .compiler import Compiler, CompilerException, optimizer # noqa: F401 +from .compiler import Compiler, CompilerException # noqa: F401 try: import tweedledum # noqa: F401 diff --git a/qlasskit/compiler/compiler.py b/qlasskit/compiler/compiler.py index f87ede38..4d1152d0 100644 --- a/qlasskit/compiler/compiler.py +++ b/qlasskit/compiler/compiler.py @@ -12,11 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. - -from sympy import Symbol -from sympy.logic import ITE, And, Implies, Not, Or, Xor -from sympy.logic.boolalg import Boolean, BooleanFalse, BooleanTrue # , to_anf - from .. import QCircuit from ..ast2logic.typing import Arg, Args, BoolExpList @@ -25,66 +20,11 @@ class CompilerException(Exception): pass -def optimizer(expr: Boolean) -> Boolean: - if isinstance(expr, Symbol): - return expr - - elif isinstance(expr, ITE): - c = optimizer(expr.args[0]) - return optimizer( - Or(And(c, optimizer(expr.args[1])), And(Not(c), optimizer(expr.args[2]))) - ) - - elif isinstance(expr, Implies): - return optimizer(Or(Not(optimizer(expr.args[0])), optimizer(expr.args[1]))) - - elif isinstance(expr, Not): - return Not(optimizer(expr.args[0])) - - elif isinstance(expr, And): - return And(*[optimizer(e) for e in expr.args]) - - # Or(And(a,b), And(!a,!b)) = !Xor(a,b) - elif ( - isinstance(expr, Or) - and len(expr.args) == 2 - and isinstance(expr.args[0], And) - and isinstance(expr.args[1], And) - and ( - ( - expr.args[1].args[0] == Not(expr.args[0].args[0]) - and expr.args[1].args[1] == Not(expr.args[0].args[1]) - ) - or ( - Not(expr.args[1].args[0]) == expr.args[0].args[0] - and Not(expr.args[1].args[1]) == expr.args[0].args[1] - ) - ) - ): - a = optimizer(expr.args[0].args[0]) - b = optimizer(expr.args[0].args[1]) - return Not(Xor(a, b)) - - # Translate or to and - elif isinstance(expr, Or): - return Not(And(*[Not(optimizer(e)) for e in expr.args])) - - elif isinstance(expr, Xor): - return Xor(*[optimizer(e) for e in expr.args]) - - elif isinstance(expr, BooleanTrue) or isinstance(expr, BooleanFalse): - return expr - - else: - raise Exception(expr) - - class Compiler: def __init__(self): pass def _symplify_exp(self, exp): - exp = optimizer(exp) # exp = to_anf(exp, deep=False) return exp diff --git a/qlasskit/qlassf.py b/qlasskit/qlassf.py index 0ba7db0d..ad132eed 100644 --- a/qlasskit/qlassf.py +++ b/qlasskit/qlassf.py @@ -22,11 +22,8 @@ from .ast2ast import ast2ast from .ast2logic import Arg, Args, BoolExpList, LogicFun, flatten, translate_ast -from .bool_optimizer import ( - merge_unnecessary_assigns, - remove_const_exps, - remove_unnecessary_assigns, -) +from .boolopt.bool_optimizer import bestWorkingOptimizer # noqa: F401 +from .boolopt.bool_optimizer import experimentalOptimizer # noqa: F401 from .compiler import SupportedCompiler, to_quantum from .types import * # noqa: F403, F401 from .types import Qtype, type_repr @@ -203,11 +200,9 @@ def from_function( fun_name, args, fun_ret, exps = translate_ast(fun, types, defs) original_f = eval(fun_name) if isinstance(f, str) else f - # Optimize the expression list - exps = remove_const_exps(exps) - exps = remove_unnecessary_assigns(exps) - exps = merge_unnecessary_assigns(exps) - # exps = subsitute_exps(exps) + exps = bestWorkingOptimizer.apply(exps) + + # print(exps) # Return the qlassf object qf = QlassF(fun_name, original_f, args, fun_ret, exps) diff --git a/setup.py b/setup.py index 17247839..b8c05963 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ author_email="gessadavide@gmail.com", packages=[ "qlasskit", + "qlasskit.boolopt", "qlasskit.types", "qlasskit.ast2logic", "qlasskit.qcircuit", diff --git a/test/utils.py b/test/utils.py index 125c1b72..3f0444c6 100644 --- a/test/utils.py +++ b/test/utils.py @@ -101,7 +101,7 @@ def compute_result_of_qcircuit(cls, qf, truth_line): max_qubits = ( qf.input_size + len(qf.expressions) - + sum([gateinputcount(compiler.optimizer(e[1])) for e in qf.expressions]) + + sum([gateinputcount(e[1]) for e in qf.expressions]) ) cls.assertLessEqual(qf.gate().num_qubits, max_qubits)