diff --git a/qlasskit/tools/py2qasm.py b/qlasskit/tools/py2qasm.py new file mode 100644 index 00000000..8e6aa472 --- /dev/null +++ b/qlasskit/tools/py2qasm.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 + +# Copyright 2023-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. + +import argparse +import sys + +import qlasskit +from qlasskit.qcircuit import exporter_qasm +from qlasskit.qlassfun import QlassF +from qlasskit.tools.utils import parse_str + +from .tools import find_last_qlassf + + +def read_input(input_file): + if input_file == "-": + return sys.stdin.read() + with open(input_file, "r") as file: + return file.read() + + +def convert_to_quasm(qlassf: QlassF, compiler="internal", version=3): + qlassf.compile(compiler=compiler) + qcirc = qlassf.circuit() + exporter = exporter_qasm.QasmExporter(version=version) + return exporter.export(qcirc, mode="circuit") + + +def output_result(result, output_file): + if output_file == "-": + print(result) + else: + with open(output_file, "w") as file: + file.write(str(result)) + + +def main(): + parser = argparse.ArgumentParser( + description="Convert qlassf functions in a Python script to qasm code expressions." + ) + parser.add_argument( + "-i", "--input-file", default="-", help="Input file (default: stdin)" + ) + parser.add_argument("-e", "--entrypoint", help="Entrypoint function name") + parser.add_argument( + "-o", "--output", default="-", help="Output file (default: stdout)" + ) + parser.add_argument( + "-c", + "--compiler", + choices=["internal", "tweedledum", "recompiler"], + default="internal", + help="QASM compiler (default: internal)", + ) + parser.add_argument( + "-q", + "--qasm-version", + choices=["2.0", "3.0"], + default="3.0", + help="QASM version (default: 3.0)", + ) + parser.add_argument( + "-v", "--version", action="version", version=f"qlasskit {qlasskit.__version__}" + ) + + args = parser.parse_args() + + script = read_input(args.input_file) + qlassf_list = parse_str(script) + + if args.entrypoint: + qlassf = next((f[1] for f in qlassf_list if f[0] == args.entrypoint), None) + else: + qlassf = find_last_qlassf(qlassf_list) + + compiler = args.compiler + version = 3 if args.qasm_version == "3.0" else 2 + + if qlassf: + bool_expr = convert_to_quasm(qlassf, compiler=compiler, version=version) + output_result(bool_expr, args.output) + else: + print("No qlassf function found", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/setup.py b/setup.py index ab5712ec..5412891f 100644 --- a/setup.py +++ b/setup.py @@ -64,6 +64,7 @@ entry_points={ "console_scripts": [ "py2bexp=qlasskit.tools.py2bexp:main", + "py2qasm=qlasskit.tools.py2qasm:main", ] }, ) diff --git a/test/test_tools.py b/test/test_tools.py index d3e7e1c0..a59c75c5 100644 --- a/test/test_tools.py +++ b/test/test_tools.py @@ -20,11 +20,13 @@ import sympy from sympy.logic.boolalg import And, Not, Or, is_cnf, is_dnf, is_nnf - # from sympy.logic.boolalg import to_anf from sympy.logic.utilities.dimacs import load import qlasskit +from qlasskit.qcircuit import exporter_qasm +from qlasskit.qlassfun import qlassf + from qlasskit.tools import utils dummy_script = """ @@ -230,3 +232,149 @@ def test_stdin_input(self): expr = sympy.parse_expr(result.stdout) expected = sympy.parse_expr("(x | y | ~z) & (z | ~y)") self.assertTrue(expr.equals(expected)) + + +dummy_qlassf = """ +def c(x: bool, y: bool, z: bool) -> bool: + return (x or y or not z) and (not y or z) +""" + + +class TestPy2Qasm(unittest.TestCase): + + def setUp(self): + # Create a temporary file to hold the dummy script + self.temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".py") + self.temp_file.write(dummy_script.encode()) + self.temp_file.close() + + def tearDown(self): + # Remove the temporary file + os.unlink(self.temp_file.name) + + def run_command(self, args, stdin_input=None): + try: + result = subprocess.run( + args, input=stdin_input, capture_output=True, text=True, check=True + ) + return result + except subprocess.CalledProcessError as e: + print( + f"Command '{' '.join(e.cmd)}' returned non-zero exit status {e.returncode}" + ) + print(f"Standard output:\n{e.stdout}") + print(f"Standard error:\n{e.stderr}") + raise + + # def test_help(self): + # result = self.run_command(["python", "-m", "qlasskit.tools.py2qasm", "--help"]) + + def test_version(self): + result = self.run_command( + ["python", "-m", "qlasskit.tools.py2qasm", "--version"] + ) + self.assertTrue(result.stdout == f"qlasskit {qlasskit.__version__}\n") + + def test_output_to_stdout(self): + result = self.run_command( + ["python", "-m", "qlasskit.tools.py2qasm", "-i", self.temp_file.name] + ) + print(result.stdout) + qf = qlassf(dummy_qlassf, to_compile=True, compiler="internal") + exporter = exporter_qasm.QasmExporter(version=3) + expected = exporter.export(qf.circuit(), mode="circuit") + "\n" + print(expected) + self.assertTrue(result.stdout == expected) + + def test_specific_entrypoint(self): + result = self.run_command( + [ + "python", + "-m", + "qlasskit.tools.py2qasm", + "-i", + self.temp_file.name, + "-e", + "a", + ] + ) + print(result.stdout) + qf = qlassf( + "def a(b: bool) -> bool:\n\treturn not b", + to_compile=True, + compiler="internal", + ) + exporter = exporter_qasm.QasmExporter(version=3) + expected = exporter.export(qf.circuit(), mode="circuit") + "\n" + self.assertTrue(result.stdout == expected) + + def test_output_to_file(self): + with tempfile.NamedTemporaryFile(delete=False) as temp_output: + output_file = temp_output.name + try: + self.run_command( + [ + "python", + "-m", + "qlasskit.tools.py2qasm", + "-i", + self.temp_file.name, + "-o", + output_file, + ] + ) + with open(output_file, "r") as f: + content = f.read() + print(content) + qf = qlassf(dummy_qlassf, to_compile=True, compiler="internal") + exporter = exporter_qasm.QasmExporter(version=3) + expected = exporter.export(qf.circuit(), mode="circuit") + self.assertTrue(content == expected) + finally: + os.unlink(output_file) + + def test_stdin_input(self): + result = self.run_command( + ["python", "-m", "qlasskit.tools.py2qasm"], stdin_input=dummy_script + ) + print(result.stdout) + qf = qlassf(dummy_qlassf, to_compile=True, compiler="internal") + exporter = exporter_qasm.QasmExporter(version=3) + expected = exporter.export(qf.circuit(), mode="circuit") + "\n" + self.assertTrue(result.stdout == expected) + + def test_qasm_version_2(self): + result = self.run_command( + [ + "python", + "-m", + "qlasskit.tools.py2qasm", + "-i", + self.temp_file.name, + "-q", + "2.0", + ] + ) + print(result.stdout) + qf = qlassf(dummy_qlassf, to_compile=True, compiler="internal") + exporter = exporter_qasm.QasmExporter(version=2) + expected = exporter.export(qf.circuit(), mode="circuit") + "\n" + self.assertTrue(result.stdout == expected) + + def test_qasm_version_3(self): + result = self.run_command( + [ + "python", + "-m", + "qlasskit.tools.py2qasm", + "-i", + self.temp_file.name, + "-q", + "3.0", + ] + ) + print(result.stdout) + qf = qlassf(dummy_qlassf, to_compile=True, compiler="internal") + exporter = exporter_qasm.QasmExporter(version=3) + expected = exporter.export(qf.circuit(), mode="circuit") + "\n" + self.assertTrue(result.stdout == expected)