diff --git a/poetry.lock b/poetry.lock index 582effbfb..7ebb566f6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4546,45 +4546,30 @@ files = [ {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, - {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a52d48f4e7bf9005e8f0a89209bf9a73f7190ddf0489eee5eb51377385f59f2a"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, - {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1492a6051dab8d912fc2adeef0e8c72216b24d57bd896ea607cb90bb0c4981d3"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, - {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b82a7c94a498853aa0b272fd5bc67f29008da798d4f93a2f9f289feb8426a58d"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, - {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f6f3eac23941b32afccc23081e1f50612bdbe4e982012ef4f5797986828cd01"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, - {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2c59aa6170b990d8d2719323e628aaf36f3bfbc1c26279c0eeeb24d05d2d11c7"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, @@ -4845,7 +4830,6 @@ python-versions = "*" groups = ["docs"] files = [ {file = "sphinx-multiversion-0.2.4.tar.gz", hash = "sha256:5cd1ca9ecb5eed63cb8d6ce5e9c438ca13af4fa98e7eb6f376be541dd4990bcb"}, - {file = "sphinx_multiversion-0.2.4-py2.py3-none-any.whl", hash = "sha256:5c38d5ce785a335d8c8d768b46509bd66bfb9c6252b93b700ca8c05317f207d6"}, {file = "sphinx_multiversion-0.2.4-py3-none-any.whl", hash = "sha256:dec29f2a5890ad68157a790112edc0eb63140e70f9df0a363743c6258fbeb478"}, ] diff --git a/src/qat/compiler/transform_passes.py b/src/qat/compiler/transform_passes.py index f38c4f7f7..15bb4048c 100644 --- a/src/qat/compiler/transform_passes.py +++ b/src/qat/compiler/transform_passes.py @@ -40,7 +40,7 @@ Pulse, ) from qat.purr.integrations.qasm import CloudQasmParser, Qasm3Parser -from qat.purr.integrations.tket import run_tket_optimizations +from qat.purr.integrations.tket import run_tket_optimizations_qasm class PhaseOptimisation(TransformPass): @@ -194,7 +194,7 @@ def run_qasm_optimisation(self, qasm_string, optimizations, met_mgr, *args, **kw isinstance(optimizations, Tket) and optimizations.tket_optimizations != TketOptimizations.Empty ): - qasm_string = run_tket_optimizations( + qasm_string = run_tket_optimizations_qasm( qasm_string, optimizations.tket_optimizations, self.hardware ) diff --git a/src/qat/purr/compiler/frontends.py b/src/qat/purr/compiler/frontends.py index 9c10cff2a..80c779619 100644 --- a/src/qat/purr/compiler/frontends.py +++ b/src/qat/purr/compiler/frontends.py @@ -6,7 +6,13 @@ from typing import Tuple import regex -from compiler_config.config import CompilerConfig, Languages, get_optimizer_config +from compiler_config.config import ( + CompilerConfig, + Languages, + Tket, + TketOptimizations, + get_optimizer_config, +) from qat.purr.backends.calibrations.remote import find_calibration from qat.purr.backends.realtime_chip_simulator import get_default_RTCS_hardware @@ -94,15 +100,33 @@ def _parse_from_file( metrics = CompilationMetrics() metrics.enable(compiler_config.metrics) - parser = QIRParser(hardware) if compiler_config.optimizations is None: compiler_config.optimizations = get_optimizer_config(Languages.QIR) - if compiler_config.results_format.format is not None: - parser.results_format = compiler_config.results_format.format + with log_duration("Compilation completed, took {} seconds."): + log.info(f"Processing QIR as parser and {str(hardware)} as hardware.") + + optimizations = compiler_config.optimizations + if ( + isinstance(optimizations, Tket) + and optimizations.tket_optimizations != TketOptimizations.Empty + ): + quantum_builder = DefaultOptimizers(metrics).optimize_qir( + path_or_str, + get_model(hardware), + compiler_config.optimizations, + compiler_config.results_format.format, + ) + else: + parser = QIRParser( + hardware, results_format=compiler_config.results_format.format + ) + quantum_builder = parser.parse(path_or_str) - quantum_builder = parser.parse(path_or_str) - return self._build_instructions(quantum_builder, hardware, compiler_config), metrics + return ( + self._build_instructions(quantum_builder, hardware, compiler_config), + metrics, + ) def parse( self, path_or_str: str, hardware=None, compiler_config: CompilerConfig = None diff --git a/src/qat/purr/compiler/optimisers.py b/src/qat/purr/compiler/optimisers.py index 2874cf575..7c71b58c4 100644 --- a/src/qat/purr/compiler/optimisers.py +++ b/src/qat/purr/compiler/optimisers.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023-2024 Oxford Quantum Circuits Ltd from compiler_config.config import ( + InlineResultsProcessing, MetricsType, OptimizationConfig, Qiskit, @@ -13,7 +14,10 @@ from qat.purr.compiler.hardware_models import QuantumHardwareModel from qat.purr.compiler.metrics import MetricsMixin -from qat.purr.integrations.tket import run_tket_optimizations +from qat.purr.integrations.tket import ( + run_tket_optimizations_qasm, + run_tket_optimizations_qir, +) from qat.purr.utils.logger import get_default_logger from qat.purr.utils.logging_utils import log_duration @@ -37,7 +41,7 @@ def optimize_qasm( isinstance(optimizations, Tket) and optimizations.tket_optimizations != TketOptimizations.Empty ): - qasm_string = run_tket_optimizations( + qasm_string = run_tket_optimizations_qasm( qasm_string, optimizations.tket_optimizations, hardware ) @@ -54,6 +58,20 @@ def optimize_qasm( self.record_metric(MetricsType.OptimizedCircuit, qasm_string) return qasm_string + def optimize_qir( + self, + qir_string: str, + hardware: QuantumHardwareModel, + optimizations: OptimizationConfig, + results_format: InlineResultsProcessing = None, + ): + """Run TKET optimizers on QIR input, returning the circuit as a QASM file.""" + with log_duration("QIR optimization took {} seconds."): + builder = run_tket_optimizations_qir( + qir_string, optimizations.tket_optimizations, hardware, results_format + ) + return builder + def run_qiskit_optimization(self, qasm_string, level): """ TODO: [QK] Current setup is unlikely to provide much benefit, refine settings diff --git a/src/qat/purr/integrations/qir.py b/src/qat/purr/integrations/qir.py index 65f16a418..f8ccd195a 100644 --- a/src/qat/purr/integrations/qir.py +++ b/src/qat/purr/integrations/qir.py @@ -34,13 +34,14 @@ def __init__( self, hardware: Union[QuantumHardwareModel, InstructionExecutionEngine], builder=None, + results_format=None, ): if isinstance(hardware, InstructionExecutionEngine): hardware = hardware.model self.hardware: QuantumHardwareModel = hardware self.builder: InstructionBuilder = builder or get_builder(hardware) - self.results_format = InlineResultsProcessing.Program + self.results_format = results_format or InlineResultsProcessing.Program self.result_variables = [] def _get_qubit(self, id_: int): @@ -101,6 +102,12 @@ def y(self, qubit: int): def z(self, qubit: int): self.builder.Z(self._get_qubit(qubit)) + def returns(self, result_name=None): + self.builder.returns(result_name) + + def assign(self, name, value): + self.builder.assign(name, value) + def process_instructions(self, instructions): for inst in instructions: if isinstance(inst, Call): @@ -219,10 +226,10 @@ def parse(self, qir_file: str): else: result_name = "_".join(potential_names) - self.builder.assign(result_name, [val[0] for val in self.result_variables]) - self.builder.returns(result_name) + self.assign(result_name, [val[0] for val in self.result_variables]) + self.returns(result_name) else: - self.builder.returns() + self.returns() complete_builder = self.builder self.builder = get_builder(self.hardware) diff --git a/src/qat/purr/integrations/tket.py b/src/qat/purr/integrations/tket.py index 4a0a3235c..f931bdf21 100644 --- a/src/qat/purr/integrations/tket.py +++ b/src/qat/purr/integrations/tket.py @@ -2,10 +2,11 @@ # Copyright (c) 2023-2024 Oxford Quantum Circuits Ltd from copy import deepcopy from numbers import Number -from typing import List +from typing import List, Union -from compiler_config.config import TketOptimizations -from pytket import Bit, Circuit, Qubit +import numpy as np +from compiler_config.config import InlineResultsProcessing, TketOptimizations +from pytket import Bit, Circuit, OpType, Qubit from pytket._tket.architecture import Architecture, RingArch from pytket._tket.circuit import CustomGateDef from pytket._tket.predicates import ( @@ -16,6 +17,7 @@ ) from pytket._tket.transform import Transform from pytket.passes import ( + AutoRebase, CliffordSimp, ContextSimp, DecomposeArbitrarilyControlledGates, @@ -25,7 +27,6 @@ GlobalisePhasedX, KAKDecomposition, PeepholeOptimise2Q, - RebaseTket, RemoveBarriers, RemoveDiscarded, RemoveRedundancies, @@ -39,8 +40,12 @@ from qiskit.circuit.library import CXGate, UGate from sympy import pi, sympify -from qat.purr.compiler.execution import QuantumHardwareModel +from qat.purr.compiler.builders import InstructionBuilder +from qat.purr.compiler.execution import InstructionExecutionEngine, QuantumHardwareModel +from qat.purr.compiler.hardware_models import QuantumHardwareModel +from qat.purr.compiler.instructions import Variable from qat.purr.integrations.qasm import BitRegister, Qasm2Parser, QasmContext, QubitRegister +from qat.purr.integrations.qir import QIRParser from qat.purr.utils.logger import get_default_logger log = get_default_logger() @@ -219,6 +224,220 @@ def process_program(self, builder, qasm): return builder +class TketQIRParser(QIRParser): + """QIR parser than turns circuits into Tket structures.""" + + def __init__(self, hardware: Union[QuantumHardwareModel, InstructionExecutionEngine]): + if isinstance(hardware, InstructionExecutionEngine): + hardware = hardware.model + + self.hardware: QuantumHardwareModel = hardware + self.circuit: Circuit = Circuit(len(hardware.qubits), len(hardware.qubits)) + self.results_format = InlineResultsProcessing.Program + self.result_variables = [] + + def _get_qubit(self, id_: int): + return self.hardware.qubits[id_] + + @staticmethod + def normalize_parameter(param): + return param / np.pi + + def ccx(self, control1, control2, target): + self.circuit.CCX(control1, control2, target) + + def cx(self, control: int, target: int): + self.circuit.CX(control, target) + + def cz(self, control: int, target: int): + self.circuit.CZ(control, target) + + def h(self, target: int): + self.circuit.H(target) + + def mz(self, qubit: int, target: int): + self.circuit.Measure(qubit, target) + + def reset(self, target: int): + self.circuit.Reset(target) + + def rx(self, theta: float, qubit: int): + self.circuit.Rx(self.normalize_parameter(theta), qubit) + + def ry(self, theta: float, qubit: int): + self.circuit.Ry(self.normalize_parameter(theta), qubit) + + def rz(self, theta: float, qubit: int): + self.circuit.Rz(self.normalize_parameter(theta), qubit) + + def s(self, qubit: int): + self.circuit.S(qubit) + + def s_adj(self, qubit: int): + self.circuit.Sdg(qubit) + + def t(self, qubit: int): + self.circuit.T(qubit) + + def t_adj(self, qubit: int): + self.circuit.Tdg(qubit) + + def x(self, qubit: int): + self.circuit.X(qubit) + + def y(self, qubit: int): + self.circuit.Y(qubit) + + def z(self, qubit: int): + self.circuit.Z(qubit) + + def returns(self, result_name=None): + """Returns are dealt with upon converting back to QatIR.""" + pass + + def assign(self, name, value): + """Assigns are dealt with upon converting back to QatIR.""" + pass + + @property + def builder(self): + """Returns the circuit and variables for where results are stored in Qat IR. + + This property overwrites the "builder" in the :class:`QIRParser` so that the circuit + and output variables are returned in its place.""" + return self.circuit, self.result_variables + + @builder.setter + def builder(self, _): + """The QIR parser resets the builder after being parsed (possibly for reuse). This + setter is just to reset.""" + + self.circuit = Circuit(len(self.hardware.qubits), len(self.hardware.qubits)) + + +class TketToQatIRConverter: + """Converts a Tket circuit into Qat IR.""" + + def __init__(self, model: QuantumHardwareModel): + self.model = model + self.builder = model.create_builder() + self.output_variables = [] + + def get_qubit(self, index: int): + return self.model.get_qubit(index) + + @staticmethod + def convert_parameter(arg: str): + r"""A parameter stored in a Tket operation is in units of :math:`\pi`. Parameters + are returned as a string, sometimes in fractional form: we need to convert it to an + absolute value.""" + + if "/" in arg: + a, b = arg.split("/") + arg = float(a) / float(b) + else: + arg = float(arg) + return np.pi * arg + + def convert(self, circuit: Circuit): + """Converts a Tket circuit into Qat IR, adding any necesarry assigns and returns. + + :param circuit: Program as a Tket circuit. + :param result_format: Specifies how measurement results are formatted. + """ + + for command in circuit.to_dict()["commands"]: + # Retrieves the qubit / clbit indices for each operation + args = [arg[1][0] for arg in command["args"]] + + match command["op"]["type"]: + # One-qubit gates + case "X": + self.builder.X(self.get_qubit(args[0])) + case "Y": + self.builder.Y(self.get_qubit(args[0])) + case "Z": + self.builder.Z(self.get_qubit(args[0])) + case "H": + self.builder.had(self.get_qubit(args[0])) + case "SX": + self.builder.SX(self.get_qubit(args[0])) + case "SXdg": + self.builder.SXdg(self.get_qubit(args[0])) + case "S": + self.builder.S(self.get_qubit(args[0])) + case "Sdg": + self.builder.Sdg(self.get_qubit(args[0])) + case "T": + self.builder.T(self.get_qubit(args[0])) + case "Tdg": + self.builder.Tdg(self.get_qubit(args[0])) + case "Rx": + self.builder.X( + self.get_qubit(args[0]), + self.convert_parameter(command["op"]["params"][0]), + ) + case "Ry": + self.builder.Y( + self.get_qubit(args[0]), + self.convert_parameter(command["op"]["params"][0]), + ) + case "Rz": + self.builder.Z( + self.get_qubit(args[0]), + self.convert_parameter(command["op"]["params"][0]), + ) + case "U1": + self.builder.Z( + self.get_qubit(args[0]), + self.convert_parameter(command["op"]["params"][0]), + ) + case "U2": + self.builder.U( + self.get_qubit(args[0]), + np.pi / 2, + self.convert_parameter(command["op"]["params"][0]), + self.convert_parameter(command["op"]["params"][1]), + ) + case "U3": + self.builder.U( + self.get_qubit(args[0]), + self.convert_parameter(command["op"]["params"][0]), + self.convert_parameter(command["op"]["params"][1]), + self.convert_parameter(command["op"]["params"][2]), + ) + + # Two-qubit gates + case "CX": + self.builder.cnot(self.get_qubit(args[0]), self.get_qubit(args[1])) + case "ECR": + self.builder.ECR(self.get_qubit(args[0]), self.get_qubit(args[1])) + case "SWAP": + self.builder.swap(self.get_qubit(args[0]), self.get_qubit(args[1])) + + # Operations + case "Measure": + output_var = str(args[1]) + self.builder.measure_single_shot_z( + self.get_qubit(args[0]), output_variable=output_var + ) + self.output_variables.append(output_var) + case "Barrier": + self.builder.synchronize([self.get_qubit(arg) for arg in args]) + case "Reset": + # Reset operation not implemented: do nothing instead of throwing an + # error to maintain support with non-optimised QIR. + continue + case _: + raise NotImplementedError( + f"Command {command['op']['type']} not implemented." + ) + + builder = self.builder + self.builder = self.model.create_builder() + return builder + + def fetch_default_passes(architecture, opts, pass_list: List = None, add_delay=True): pass_list = pass_list or [] if TketOptimizations.DefaultMappingPass in opts: @@ -326,7 +545,7 @@ def optimize_circuit(circ, architecture, opts): # routing. passes = fetch_default_passes(architecture, opts, passes) - passes += [RebaseTket()] + passes += [AutoRebase({OpType.CX, OpType.U3})] # Not everything in the list is a pass, we've also got transforms. # Everything in the list should have an apply function though. @@ -371,25 +590,19 @@ def get_coupling_subgraphs(couplings): return subgraphs -def run_tket_optimizations(qasm_string, opts, hardware: QuantumHardwareModel) -> str: +def run_tket_optimizations(circ: Circuit, opts, hardware: QuantumHardwareModel) -> Circuit: """ Runs tket-based optimizations and modifications. Routing will always happen no matter the level. Will run optimizations in sections if a full suite fails until a minimal subset of passing optimizations is found. - """ - try: - tket_builder: TketBuilder = TketQasmParser().parse(TketBuilder(), qasm_string) - circ = tket_builder.circuit - log.info(f"Number of gates before tket optimization: {circ.n_gates}") - except Exception as e: # Parsing is too fragile, can cause almost any exception. - log.warning( - f"Tket failed during QASM parsing with error: {_full_stopalize(e)}. " - "Skipping this optimization pass." - ) - return qasm_string + :param circ: Tket circuit to optimize. The source file must be already parsed to a + TKET circuit. + :param opts: Specifies which TKET optimizations to run. + :param hardware: The hardware model is used for routing and placement purposes. + """ couplings = deepcopy(hardware.qubit_direction_couplings) optimizations_failed = False @@ -468,6 +681,33 @@ def run_tket_optimizations(qasm_string, opts, hardware: QuantumHardwareModel) -> apply_default_transforms(circ, architecture, opts) check_validity(circ, architecture) + return circ + + +def run_tket_optimizations_qasm(qasm_string, opts, hardware: QuantumHardwareModel) -> str: + """ + Runs tket-based optimizations and modifications. Routing will always happen no + matter the level. + + Will run optimizations in sections if a full suite fails until a minimal subset of + passing optimizations is found. + + :param qasm_string: The circuit as a QASM string. + :param opts: Specifies which TKET optimizations to run. + :param hardware: The hardware model is used for routing and placement purposes. + """ + try: + tket_builder: TketBuilder = TketQasmParser().parse(TketBuilder(), qasm_string) + circ = tket_builder.circuit + log.info(f"Number of gates before tket optimization: {circ.n_gates}") + except Exception as e: # Parsing is too fragile, can cause almost any exception. + log.warning( + f"Tket failed during QASM parsing with error: {_full_stopalize(e)}. " + "Skipping this optimization pass." + ) + return qasm_string + + circ = run_tket_optimizations(circ, opts, hardware) try: qasm_string = circuit_to_qasm_str(circ) @@ -480,3 +720,43 @@ def run_tket_optimizations(qasm_string, opts, hardware: QuantumHardwareModel) -> # TODO: Return result object with more information about compilation/errors return qasm_string + + +def run_tket_optimizations_qir( + file_or_str, + opts, + hardware: QuantumHardwareModel, + results_format: InlineResultsProcessing = None, +) -> InstructionBuilder: + """ + Runs tket-based optimizations and modifications. Routing will always happen no + matter the level. + + Will run optimizations in sections if a full suite fails until a minimal subset of + passing optimizations is found. + + :param file_or_str: The QIR program as a string, or its file path. + :param opts: Specifies which TKET optimizations to run. + :param hardware: The hardware model is used for routing and placement purposes. + """ + results_format = results_format or InlineResultsProcessing.Program + circ, output_variables = TketQIRParser(hardware).parse(file_or_str) + log.info(f"Number of gates before tket optimization: {circ.n_gates}") + + circ = run_tket_optimizations(circ, opts, hardware) + + builder = TketToQatIRConverter(hardware).convert(circ) + for output_variable in output_variables: + builder.results_processing(output_variable[0].name, results_format) + if any(output_variables): + potential_names = [val[1] for val in output_variables if len(val[1] or "") != 0] + if not any(potential_names): + result_name = Variable.generate_name() + else: + result_name = "_".join(potential_names) + builder.assign(result_name, [val[0] for val in output_variables]) + builder.returns(result_name) + else: + builder.returns() + log.info(f"Number of gates after tket optimization: {circ.n_gates}") + return builder diff --git a/tests/qat/files/qir/cudaq-ghz.ll b/tests/qat/files/qir/cudaq-ghz.ll new file mode 100644 index 000000000..26f57d526 --- /dev/null +++ b/tests/qat/files/qir/cudaq-ghz.ll @@ -0,0 +1,34 @@ +; ModuleID = 'LLVMDialectModule' +source_filename = "LLVMDialectModule" +target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" +target triple = "x86_64-unknown-linux-gnu" +%Qubit = type opaque +%Result = type opaque +@cstr.72303030303000 = private constant [7 x i8] c"r00000\00" +@cstr.72303030303100 = private constant [7 x i8] c"r00001\00" +@cstr.72303030303200 = private constant [7 x i8] c"r00002\00" +declare void @__quantum__qis__h__body(%Qubit*) local_unnamed_addr +declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) local_unnamed_addr +declare void @__quantum__rt__result_record_output(%Result*, i8*) local_unnamed_addr +declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) local_unnamed_addr #0 +define void @__nvqpp__mlirgen__ghz() local_unnamed_addr #1 { +"0": + tail call void @__quantum__qis__h__body(%Qubit* null) + tail call void @__quantum__qis__cnot__body(%Qubit* null, %Qubit* nonnull inttoptr (i64 7 to %Qubit*)) + tail call void @__quantum__qis__cnot__body(%Qubit* nonnull inttoptr (i64 7 to %Qubit*), %Qubit* nonnull inttoptr (i64 3 to %Qubit*)) + tail call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly null) + tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 7 to %Qubit*), %Result* nonnull writeonly inttoptr (i64 1 to %Result*)) + tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 3 to %Qubit*), %Result* nonnull writeonly inttoptr (i64 2 to %Result*)) + tail call void @__quantum__rt__result_record_output(%Result* null, i8* nonnull getelementptr inbounds ([7 x i8], [7 x i8]* @cstr.72303030303000, i64 0, i64 0)) + tail call void @__quantum__rt__result_record_output(%Result* nonnull inttoptr (i64 1 to %Result*), i8* nonnull getelementptr inbounds ([7 x i8], [7 x i8]* @cstr.72303030303100, i64 0, i64 0)) + tail call void @__quantum__rt__result_record_output(%Result* nonnull inttoptr (i64 2 to %Result*), i8* nonnull getelementptr inbounds ([7 x i8], [7 x i8]* @cstr.72303030303200, i64 0, i64 0)) + ret void +} +attributes #0 = { "irreversible" } +attributes #1 = { "entry_point" "output_labeling_schema"="schema_id" "output_names"="[[[0,[0,\22r00000\22]],[1,[1,\22r00001\22]],[2,[2,\22r00002\22]]]]" "qir_profiles"="base_profile" "requiredQubits"="10" "requiredResults"="3" } + +!0 = !{i32 2, !"Debug Info Version", i32 3} +!1 = !{i32 1, !"qir_major_version", i32 1} +!2 = !{i32 7, !"qir_minor_version", i32 0} +!3 = !{i32 1, !"dynamic_qubit_management", i1 false} +!4 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/tests/qat/test_core.py b/tests/qat/test_core.py index 51f436049..1237d3e27 100644 --- a/tests/qat/test_core.py +++ b/tests/qat/test_core.py @@ -69,6 +69,7 @@ skip_qir = [ "teleportchain.ll", "bell_qir_measure.bc", + "cudaq-ghz.ll", # test is designed to fail for no TKET optims ] @@ -313,6 +314,10 @@ def test_qat_compile_qir( ): """Test that the same instructions are generated with both stacks.""" + # skip if using Tket placement + if request.node.callspec._idlist[2] != TketOptimizations.Empty: + pytest.skip() + monkeypatch.setattr(Instruction, "__eq__", equivalent_vars) monkeypatch.setattr(Variable, "__eq__", equivalent_vars) monkeypatch.setattr(Acquire, "__eq__", equivalent_generated) diff --git a/tests/qat/test_qir.py b/tests/qat/test_qir.py index a93d7ca84..0b199220b 100644 --- a/tests/qat/test_qir.py +++ b/tests/qat/test_qir.py @@ -6,7 +6,7 @@ import numpy as np import pytest -from compiler_config.config import CompilerConfig +from compiler_config.config import CompilerConfig, Tket from qat.purr.backends.echo import get_default_echo_hardware from qat.purr.backends.realtime_chip_simulator import qutip_available @@ -45,8 +45,11 @@ def test_valid_ll_path(self): get_default_echo_hardware(2), ) - def test_qir_bell(self): - config = CompilerConfig() + @pytest.mark.parametrize( + "optim_config", [Tket().disable(), Tket().minimum(), Tket().default()] + ) + def test_qir_bell(self, optim_config): + config = CompilerConfig(optimizations=optim_config) config.results_format.squash_binary_result_arrays() results = execute_qir( _get_qir_path("generator-bell.ll"), get_default_echo_hardware(4), config @@ -63,7 +66,6 @@ def test_cudaq_input(self): get_test_file_path(ProgramFileType.QIR, "basic_cudaq.ll"), get_default_echo_hardware(6), ) - assert results.get("r00000") == [0] @pytest.mark.skip("Needs full runtime.") @@ -106,32 +108,44 @@ def test_qir_instruction_builder(self): builder = parser.parse(_get_qir_path("generator-bell.ll")) assert len(builder.instructions) == 96 - def test_common_entrypoint_file(self): - config = CompilerConfig() + @pytest.mark.parametrize( + "optim_config", [Tket().disable(), Tket().minimum(), Tket().default()] + ) + def test_common_entrypoint_file(self, optim_config): + config = CompilerConfig(optimizations=optim_config) config.results_format.binary_count() results = execute( _get_qir_path("generator-bell.ll"), get_default_echo_hardware(4), config ) assert results == {"00": 1000} - def test_common_entrypoint_string(self): - config = CompilerConfig() + @pytest.mark.parametrize( + "optim_config", [Tket().disable(), Tket().minimum(), Tket().default()] + ) + def test_common_entrypoint_string(self, optim_config): + config = CompilerConfig(optimizations=optim_config) config.results_format.binary_count() results = execute( _get_contents("generator-bell.ll"), get_default_echo_hardware(4), config ) assert results == {"00": 1000} - def test_common_entrypoint_bitcode(self): - config = CompilerConfig() + @pytest.mark.parametrize( + "optim_config", [Tket().disable(), Tket().minimum(), Tket().default()] + ) + def test_common_entrypoint_bitcode(self, optim_config): + config = CompilerConfig(optimizations=optim_config) config.results_format.binary_count() program = _get_contents("base64_bitcode_ghz") program = base64.b64decode(program) results = execute(program, get_default_echo_hardware(4), config) assert results == {"0": 1000} - def test_invalid_QIR(self): - config = CompilerConfig() + @pytest.mark.parametrize( + "optim_config", [Tket().disable(), Tket().minimum(), Tket().default()] + ) + def test_invalid_QIR(self, optim_config): + config = CompilerConfig(optimizations=optim_config) config.results_format.binary_count() with pytest.raises(ValueError): execute("", get_default_echo_hardware(4), config) @@ -219,8 +233,11 @@ def test_parser_bell_theta_minus(self): @pytest.mark.skipif( not qutip_available, reason="Qutip is not available on this platform" ) - def test_qir_bell_binary_count(self): - config = CompilerConfig() + @pytest.mark.parametrize( + "optim_config", [Tket().disable(), Tket().minimum(), Tket().default()] + ) + def test_qir_bell_binary_count(self, optim_config): + config = CompilerConfig(optimizations=optim_config) config.results_format.binary_count() results = execute_qir(_get_qir_path("generator-bell.ll"), compiler_config=config) assert len(results) == 4 @@ -232,8 +249,11 @@ def test_qir_bell_binary_count(self): @pytest.mark.skipif( not qutip_available, reason="Qutip is not available on this platform" ) - def test_qir_out_of_order_measure_declaration(self): - config = CompilerConfig() + @pytest.mark.parametrize( + "optim_config", [Tket().disable(), Tket().minimum(), Tket().default()] + ) + def test_qir_out_of_order_measure_declaration(self, optim_config): + config = CompilerConfig(optimizations=optim_config) config.results_format.binary_count() results = execute_qir( _get_qir_path("out_of_order_measure.ll"), compiler_config=config @@ -278,3 +298,21 @@ def test_execute_different_qat_input_types(self): with pytest.raises(TypeError): execute_qir(qat_input=builder.instructions) + + def test_cudaq_ghz(self): + """Tests routing via Tket qubit placement gives a program that executes.""" + config = CompilerConfig(optimizations=Tket().disable()) + config.results_format.binary_count() + with pytest.raises(ValueError): + results = execute_qir( + _get_qir_path("cudaq-ghz.ll"), get_default_echo_hardware(10), config + ) + + config = CompilerConfig(optimizations=Tket().minimum()) + config.results_format.binary_count() + results = execute_qir( + _get_qir_path("cudaq-ghz.ll"), get_default_echo_hardware(10), config + ) + res = next(iter(results.values())) + assert len(res) == 1 + assert "000" in res diff --git a/tests/qat/test_tket.py b/tests/qat/test_tket.py index 8d15b9903..81334e18c 100644 --- a/tests/qat/test_tket.py +++ b/tests/qat/test_tket.py @@ -1,12 +1,18 @@ # SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2023-2024 Oxford Quantum Circuits Ltd +import numpy as np import pytest from compiler_config.config import Qasm2Optimizations, TketOptimizations +from pytket import Circuit from pytket.architecture import Architecture, RingArch +from qat.purr.backends.echo import get_default_echo_hardware +from qat.purr.compiler.devices import Qubit +from qat.purr.compiler.instructions import PhaseShift from qat.purr.integrations.tket import ( TketBuilder, TketQasmParser, + TketToQatIRConverter, get_coupling_subgraphs, optimize_circuit, ) @@ -94,3 +100,47 @@ def test_context_simp(self): def test_full_peephole(self): assert self._run_random(TketOptimizations.FullPeepholeOptimise) + + +class TestTketToQatIRConverter: + + def test_get_qubit(self): + model = get_default_echo_hardware(10) + converter = TketToQatIRConverter(model) + for i in range(10): + qubit = converter.get_qubit(i) + assert isinstance(qubit, Qubit) + assert qubit.index == i + + @pytest.mark.parametrize( + "params", + [ + ("0.5", 0.5), + ("1/2", 0.5), + ("0", 0.0), + ("0.254", 0.254), + ("1", 1.0), + ("-4/3", -4 / 3), + ], + ) + def test_convert_parameter(self, params): + np.isclose(TketToQatIRConverter.convert_parameter(params[0]), params[1] * np.pi) + + def test_basic_commands(self): + model = get_default_echo_hardware(10) + converter = TketToQatIRConverter(model) + circ = Circuit(2).Ry(4 / 3, 0).Rx(0.254 / np.pi, 1).Rz(1 / 2, 1).CX(1, 0).ECR(0, 1) + builder = converter.convert(circ) + direct_builder = model.create_builder() + q0 = model.get_qubit(0) + q1 = model.get_qubit(1) + direct_builder.Y(q0, 4 * np.pi / 3).X(q1, 0.254).Z(q1, np.pi / 2).cnot(q1, q0).ECR( + q0, q1 + ) + for i, inst in enumerate(builder.instructions): + assert type(inst) == type(direct_builder.instructions[i]) + if isinstance(inst, PhaseShift): + assert np.isclose( + inst.phase % (2 * np.pi), + direct_builder.instructions[i].phase % (2 * np.pi), + )