From 14dd85a041c22ca3785d791a204337a2d3495f45 Mon Sep 17 00:00:00 2001 From: KevinMTO <37836441+KevinMTO@users.noreply.github.com> Date: Fri, 20 Sep 2024 02:32:25 +0200 Subject: [PATCH] Quantum Week Tutorial Related Improvements (#53) ## Description This PR fixes bugs and code code usability issues along the compilation side of the stack. New compilation methods have been added for the default selection of compilation flags. ## Checklist: - [x] The pull request only contains commits that are related to it. - [x] I have added appropriate tests and documentation. - [x] I have made sure that all CI jobs on GitHub pass. - [x] The pull request introduces no new warnings and follows the project's style guidelines. --- .../naive_unitary_verifier.py | 14 +++ src/mqt/qudits/compiler/dit_compiler.py | 51 +++++++-- .../naive_local_resynth/local_resynth.py | 7 +- .../phy_local_adaptive_decomp.py | 1 - .../entanglement_qr/log_ent_qr_cex_decomp.py | 3 +- .../entanglement_qr/phy_ent_qr_cex_decomp.py | 22 +++- src/mqt/qudits/quantum_circuit/circuit.py | 12 +- .../qudits/simulation/backends/__init__.py | 2 + .../simulation/backends/innsbruck_01.py | 105 ++++++++++++++++++ src/mqt/qudits/simulation/qudit_provider.py | 3 +- .../twodit/entangled_qr/test_entangled_qr.py | 45 ++++++-- 11 files changed, 236 insertions(+), 29 deletions(-) create mode 100644 src/mqt/qudits/simulation/backends/innsbruck_01.py diff --git a/src/mqt/qudits/compiler/compilation_minitools/naive_unitary_verifier.py b/src/mqt/qudits/compiler/compilation_minitools/naive_unitary_verifier.py index 0195c0d..1c87664 100755 --- a/src/mqt/qudits/compiler/compilation_minitools/naive_unitary_verifier.py +++ b/src/mqt/qudits/compiler/compilation_minitools/naive_unitary_verifier.py @@ -34,6 +34,19 @@ def mini_sim(circuit: QuantumCircuit) -> NDArray[np.complex128]: return state +def phy_sdit_sim(circuit: QuantumCircuit) -> NDArray[np.complex128]: + assert circuit.mappings is not None + dim = circuit.dimensions[0] + permutation = np.eye(dim)[:, circuit.mappings[0]] + state = np.array(dim * [0.0 + 0.0j]) + state[0] = 1.0 + 0.0j + state = permutation @ state + + for gate in circuit.instructions: + state = gate.to_matrix(identities=2) @ state + return state + + class UnitaryVerifier: """Verifies unitary matrices. @@ -86,6 +99,7 @@ def verify(self) -> bool: for rotation in self.decomposition: target = rotation.to_matrix(identities=0) @ target + target.round(3) if self.permutation_matrix_final is not None: target = np.linalg.inv(self.permutation_matrix_final) @ target diff --git a/src/mqt/qudits/compiler/dit_compiler.py b/src/mqt/qudits/compiler/dit_compiler.py index f7896ea..b912752 100644 --- a/src/mqt/qudits/compiler/dit_compiler.py +++ b/src/mqt/qudits/compiler/dit_compiler.py @@ -1,6 +1,7 @@ from __future__ import annotations import typing +from typing import Optional from ..core.lanes import Lanes from ..quantum_circuit.components.extensions.gate_types import GateTypes @@ -47,14 +48,20 @@ def compile(self, backend: Backend, circuit: QuantumCircuit, passes_names: list[ elif "Multi" in str(compiler_pass): passes_dict[GateTypes.MULTI] = decomposition for gate in circuit.instructions: - decomposer = typing.cast(CompilerPass, passes_dict.get(gate.gate_type)) - new_instructions = decomposer.transpile_gate(gate) - new_instr.extend(new_instructions) - - circuit.set_instructions(new_instr) - circuit.set_mapping([graph.log_phy_map for graph in backend.energy_level_graphs]) + decomposer = typing.cast(Optional[CompilerPass], passes_dict.get(gate.gate_type)) + if decomposer is not None: + new_instructions = decomposer.transpile_gate(gate) + new_instr.extend(new_instructions) + else: + new_instr.append(gate) - return circuit + transpiled_circuit = circuit.copy() + mappings = [] + for i, graph in enumerate(backend.energy_level_graphs): + if i < circuit.num_qudits: + mappings.append([lev for lev in graph.log_phy_map if lev < circuit.dimensions[i]]) + transpiled_circuit.set_mapping(mappings) + return transpiled_circuit.set_instructions(new_instr) def compile_O0(self, backend: Backend, circuit: QuantumCircuit) -> QuantumCircuit: # noqa: N802 passes = ["PhyLocQRPass", "PhyEntQRCEXPass"] @@ -62,12 +69,37 @@ def compile_O0(self, backend: Backend, circuit: QuantumCircuit) -> QuantumCircui mappings = [] for i, graph in enumerate(backend.energy_level_graphs): - mappings.append([lev for lev in graph.log_phy_map if lev < circuit.dimensions[i]]) + if i < circuit.num_qudits: + mappings.append([lev for lev in graph.log_phy_map if lev < circuit.dimensions[i]]) compiled.set_mapping(mappings) return compiled @staticmethod def compile_O1(backend: Backend, circuit: QuantumCircuit) -> QuantumCircuit: # noqa: N802 + phyloc = PhyLocQRPass(backend) + phyent = PhyEntQRCEXPass(backend) + resynth = NaiveLocResynthOptPass(backend) + + circuit = resynth.transpile(circuit) + new_instructions = [] + for gate in circuit.instructions: + ins: list[Gate] = [] + if gate.gate_type is GateTypes.SINGLE: + ins = phyloc.transpile_gate(gate) + new_instructions.extend(ins) + else: + ins = phyent.transpile_gate(gate) + new_instructions.extend(ins) + transpiled_circuit = circuit.copy() + mappings = [] + for i, graph in enumerate(backend.energy_level_graphs): + if i < circuit.num_qudits: + mappings.append([lev for lev in graph.log_phy_map if lev < circuit.dimensions[i]]) + transpiled_circuit.set_mapping(mappings) + return transpiled_circuit.set_instructions(new_instructions) + + @staticmethod + def compile_O2(backend: Backend, circuit: QuantumCircuit) -> QuantumCircuit: # noqa: N802 phyent = PhyEntQRCEXPass(backend) lanes = Lanes(circuit) @@ -84,6 +116,7 @@ def compile_O1(backend: Backend, circuit: QuantumCircuit) -> QuantumCircuit: # transpiled_circuit = circuit.copy() mappings = [] for i, graph in enumerate(backend.energy_level_graphs): - mappings.append([lev for lev in graph.log_phy_map if lev < circuit.dimensions[i]]) + if i < circuit.num_qudits: + mappings.append([lev for lev in graph.log_phy_map if lev < circuit.dimensions[i]]) transpiled_circuit.set_mapping(mappings) return transpiled_circuit.set_instructions(new_instructions) diff --git a/src/mqt/qudits/compiler/naive_local_resynth/local_resynth.py b/src/mqt/qudits/compiler/naive_local_resynth/local_resynth.py index fa119ac..5dc5931 100644 --- a/src/mqt/qudits/compiler/naive_local_resynth/local_resynth.py +++ b/src/mqt/qudits/compiler/naive_local_resynth/local_resynth.py @@ -53,5 +53,10 @@ def transpile(self, circuit: QuantumCircuit) -> QuantumCircuit: new_instructions = self.lanes.extract_instructions() - transpiled_circuit = self.circuit.copy() + transpiled_circuit = circuit.copy() + mappings = [] + for i, graph in enumerate(self.backend.energy_level_graphs): + if i < circuit.num_qudits: + mappings.append([lev for lev in graph.log_phy_map if lev < circuit.dimensions[i]]) + transpiled_circuit.set_mapping(mappings) return transpiled_circuit.set_instructions(new_instructions) diff --git a/src/mqt/qudits/compiler/onedit/mapping_aware_transpilation/phy_local_adaptive_decomp.py b/src/mqt/qudits/compiler/onedit/mapping_aware_transpilation/phy_local_adaptive_decomp.py index 265ab20..6318bf3 100644 --- a/src/mqt/qudits/compiler/onedit/mapping_aware_transpilation/phy_local_adaptive_decomp.py +++ b/src/mqt/qudits/compiler/onedit/mapping_aware_transpilation/phy_local_adaptive_decomp.py @@ -30,7 +30,6 @@ from ....quantum_circuit.gate import Gate from ....simulation.backends.backendv2 import Backend - np.seterr(all="ignore") diff --git a/src/mqt/qudits/compiler/twodit/entanglement_qr/log_ent_qr_cex_decomp.py b/src/mqt/qudits/compiler/twodit/entanglement_qr/log_ent_qr_cex_decomp.py index 537d7bc..fe13562 100755 --- a/src/mqt/qudits/compiler/twodit/entanglement_qr/log_ent_qr_cex_decomp.py +++ b/src/mqt/qudits/compiler/twodit/entanglement_qr/log_ent_qr_cex_decomp.py @@ -33,7 +33,7 @@ def __init__(self, backend: Backend) -> None: def transpile_gate(gate: Gate) -> list[Gate]: eqr = EntangledQRCEX(gate) decomp, _countcr, _countpsw = eqr.execute() - return decomp + return [op.dag() for op in reversed(decomp)] def transpile(self, circuit: QuantumCircuit) -> QuantumCircuit: self.circuit = circuit @@ -109,7 +109,6 @@ def execute(self) -> tuple[list[Gate], int, int]: for rotation in sequence_rotation_involved: gate_matrix = self.get_gate_matrix(rotation, self.qudit_indices, self.dimensions) u_ = gate_matrix @ u_ - decomp += sequence_rotation_involved diag_u = np.diag(u_) diff --git a/src/mqt/qudits/compiler/twodit/entanglement_qr/phy_ent_qr_cex_decomp.py b/src/mqt/qudits/compiler/twodit/entanglement_qr/phy_ent_qr_cex_decomp.py index d37a364..1c24a28 100644 --- a/src/mqt/qudits/compiler/twodit/entanglement_qr/phy_ent_qr_cex_decomp.py +++ b/src/mqt/qudits/compiler/twodit/entanglement_qr/phy_ent_qr_cex_decomp.py @@ -4,10 +4,10 @@ from typing import TYPE_CHECKING, cast from mqt.qudits.compiler import CompilerPass -from mqt.qudits.compiler.onedit import PhyLocAdaPass +from mqt.qudits.compiler.onedit import PhyLocQRPass from mqt.qudits.compiler.twodit.entanglement_qr import EntangledQRCEX from mqt.qudits.quantum_circuit.components.extensions.gate_types import GateTypes -from mqt.qudits.quantum_circuit.gates import Perm +from mqt.qudits.quantum_circuit.gates import CEx, Perm if TYPE_CHECKING: from mqt.qudits.quantum_circuit import QuantumCircuit @@ -29,15 +29,27 @@ def transpile_gate(self, gate: Gate) -> list[Gate]: energy_graph_c = self.backend.energy_level_graphs[target_qudits[0]] energy_graph_t = self.backend.energy_level_graphs[target_qudits[1]] - lp_map_0 = [lev for lev in energy_graph_c.log_phy_map if lev < dimensions[target_qudits[0]]] - lp_map_1 = [lev for lev in energy_graph_t.log_phy_map if lev < dimensions[target_qudits[1]]] + lp_map_0 = [lev for lev in energy_graph_c.log_phy_map if lev < dimensions[0]] + lp_map_1 = [lev for lev in energy_graph_t.log_phy_map if lev < dimensions[1]] + + if isinstance(gate, CEx): + parent_circ = gate.parent_circuit + new_ctrl_lev = lp_map_0[gate.ctrl_lev] + new_la = lp_map_1[gate.lev_a] + new_lb = lp_map_1[gate.lev_b] + if new_la < new_lb: + new_parameters = [new_la, new_lb, new_ctrl_lev, gate.phi] + else: + new_parameters = [new_lb, new_la, new_ctrl_lev, gate.phi] + tcex = CEx(parent_circ, "CEx_t" + str(target_qudits), target_qudits, new_parameters, dimensions, None) + return [tcex] perm_0 = Perm(gate.parent_circuit, "Pm_ent_0", target_qudits[0], lp_map_0, dimensions[0]) perm_1 = Perm(gate.parent_circuit, "Pm_ent_1", target_qudits[1], lp_map_1, dimensions[1]) perm_0_dag = Perm(gate.parent_circuit, "Pm_ent_0", target_qudits[0], lp_map_0, dimensions[0]).dag() perm_1_dag = Perm(gate.parent_circuit, "Pm_ent_1", target_qudits[1], lp_map_1, dimensions[1]).dag() - phyloc = PhyLocAdaPass(self.backend) + phyloc = PhyLocQRPass(self.backend) perm_0_seq = phyloc.transpile_gate(perm_0) perm_1_seq = phyloc.transpile_gate(perm_1) perm_0_d_seq = phyloc.transpile_gate(perm_0_dag) diff --git a/src/mqt/qudits/quantum_circuit/circuit.py b/src/mqt/qudits/quantum_circuit/circuit.py index 356603e..eaf51dc 100644 --- a/src/mqt/qudits/quantum_circuit/circuit.py +++ b/src/mqt/qudits/quantum_circuit/circuit.py @@ -409,7 +409,17 @@ def simulate(self) -> NDArray: result = job.result() return result.get_state_vector() - def compile(self, backend_name: str) -> QuantumCircuit: + def compileO0(self, backend_name: str) -> QuantumCircuit: # noqa: N802 + from mqt.qudits.compiler import QuditCompiler + from mqt.qudits.simulation import MQTQuditProvider + + qudit_compiler = QuditCompiler() + provider = MQTQuditProvider() + backend_ion = provider.get_backend(backend_name) + + return qudit_compiler.compile_O0(backend_ion, self) + + def compileO1(self, backend_name: str) -> QuantumCircuit: # noqa: N802 from mqt.qudits.compiler import QuditCompiler from mqt.qudits.simulation import MQTQuditProvider diff --git a/src/mqt/qudits/simulation/backends/__init__.py b/src/mqt/qudits/simulation/backends/__init__.py index 7ce318e..5b51117 100644 --- a/src/mqt/qudits/simulation/backends/__init__.py +++ b/src/mqt/qudits/simulation/backends/__init__.py @@ -1,9 +1,11 @@ from __future__ import annotations +from .innsbruck_01 import Innsbruck01 from .misim import MISim from .tnsim import TNSim __all__ = [ + "Innsbruck01", "MISim", "TNSim", ] diff --git a/src/mqt/qudits/simulation/backends/innsbruck_01.py b/src/mqt/qudits/simulation/backends/innsbruck_01.py new file mode 100644 index 0000000..e291306 --- /dev/null +++ b/src/mqt/qudits/simulation/backends/innsbruck_01.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +import numpy as np +from typing_extensions import Unpack + +# from pyseq.mqt_qudits_runner.sequence_runner import quantum_circuit_runner +from ...core import LevelGraph +from ..jobs import Job, JobResult +from .backendv2 import Backend + +if TYPE_CHECKING: + from ...quantum_circuit import QuantumCircuit + from .. import MQTQuditProvider + from ..noise_tools import NoiseModel + + +class Innsbruck01(Backend): + @property + def version(self) -> int: + return 0 + + def __init__( + self, + provider: MQTQuditProvider, + **fields: Unpack[Backend.DefaultOptions], + ) -> None: + super().__init__( + provider=provider, + name="Innsbruck01", + description="Interface to the Innsbruck machine 01. Interface prototype with MVP.", + **fields, + ) + self.outcome: list[int] = [] + self.options["noise_model"] = self.__noise_model() + self.author = "" + self._energy_level_graphs: list[LevelGraph] = [] + + @property + def energy_level_graphs(self) -> list[LevelGraph]: + if len(self._energy_level_graphs) == 0: + e_graphs: list[LevelGraph] = [] + # declare the edges on the energy level graph between logic states . + edges = [ + (0, 1, {"delta_m": 0, "sensitivity": 3, "carrier": 0}), + (1, 2, {"delta_m": 0, "sensitivity": 4, "carrier": 1}), + ] + # name explicitly the logic states . + nodes = [0, 1, 2] + # declare physical levels in order of mapping of the logic states just declared . + # i.e. here we will have Logic 0 -> Phys. 0, have Logic 1 -> Phys. 1, have Logic 2 -> Phys. 2 . + nmap = [0, 1, 2] + graph_0 = LevelGraph(edges, nodes, nmap, [1]) + + edges_1 = [ + (0, 1, {"delta_m": 0, "sensitivity": 3, "carrier": 0}), + (1, 2, {"delta_m": 0, "sensitivity": 4, "carrier": 1}), + ] + # name explicitly the logic states . + nodes_1 = [0, 1, 2] + # declare physical levels in order of mapping of the logic states just declared . + # i.e. here we will have Logic 0 -> Phys. 0, have Logic 1 -> Phys. 1, have Logic 2 -> Phys. 2 . + nmap_1 = [0, 1, 2] + + graph_1 = LevelGraph(edges_1, nodes_1, nmap_1, [1]) + + e_graphs.extend((graph_0, graph_1)) + + self._energy_level_graphs = e_graphs + return self._energy_level_graphs + + def edge_to_carrier(self, leva: int, levb: int, graph_index: int) -> int: + e_graph = self.energy_level_graphs[graph_index] + edge_data: dict[str, int] = e_graph.get_edge_data(leva, levb) + return edge_data["carrier"] + + def __noise_model(self) -> NoiseModel | None: + return self.noise_model + + def run(self, circuit: QuantumCircuit, **options: Unpack[Backend.DefaultOptions]) -> Job: + job = Job(self) + + self._options.update(options) + self.noise_model = self._options.get("noise_model", None) + self.shots = self._options.get("shots", 50) + self.memory = self._options.get("memory", False) + self.full_state_memory = self._options.get("full_state_memory", False) + self.file_path = self._options.get("file_path", None) + self.file_name = self._options.get("file_name", None) + + assert self.shots >= 50, "Number of shots should be above 50" + self.execute(circuit) + job.set_result(JobResult(state_vector=np.array([]), counts=self.outcome)) + + return job + + def execute(self, circuit: QuantumCircuit, noise_model: NoiseModel | None = None) -> None: + _ = noise_model # Silences the unused argument warning + self.system_sizes = circuit.dimensions + self.circ_operations = circuit.instructions + + # quantum_circuit_runner(self.circ_operations) + + self.outcome = [] # quantum_circuit_runner(metadata, self.system_sizes) diff --git a/src/mqt/qudits/simulation/qudit_provider.py b/src/mqt/qudits/simulation/qudit_provider.py index c7fae58..bad06e3 100644 --- a/src/mqt/qudits/simulation/qudit_provider.py +++ b/src/mqt/qudits/simulation/qudit_provider.py @@ -3,7 +3,7 @@ import re from typing import TYPE_CHECKING, Any, ClassVar -from .backends import MISim, TNSim +from .backends import Innsbruck01, MISim, TNSim from .backends.fake_backends import FakeIonTraps2Six, FakeIonTraps2Trits, FakeIonTraps3Six if TYPE_CHECKING: @@ -18,6 +18,7 @@ def version(self) -> int: __backends: ClassVar[dict[str, type[Backend]]] = { "tnsim": TNSim, "misim": MISim, + "innsbruck01": Innsbruck01, "faketraps2trits": FakeIonTraps2Trits, "faketraps2six": FakeIonTraps2Six, "faketraps3six": FakeIonTraps3Six, diff --git a/test/python/compiler/twodit/entangled_qr/test_entangled_qr.py b/test/python/compiler/twodit/entangled_qr/test_entangled_qr.py index 4b3b08b..67db2cf 100644 --- a/test/python/compiler/twodit/entangled_qr/test_entangled_qr.py +++ b/test/python/compiler/twodit/entangled_qr/test_entangled_qr.py @@ -7,6 +7,7 @@ from scipy.stats import unitary_group # type: ignore[import-not-found] from mqt.qudits.compiler import QuditCompiler +from mqt.qudits.compiler.twodit.entanglement_qr import EntangledQRCEX from mqt.qudits.quantum_circuit import QuantumCircuit from mqt.qudits.simulation import MQTQuditProvider @@ -22,22 +23,48 @@ class TestEntangledQR(TestCase): def setUp(self) -> None: MQTQuditProvider() - self.circuit_33 = QuantumCircuit(2, [5, 3], 0) + self.circuit_53 = QuantumCircuit(2, [5, 3], 0) self.circuit_s = QuantumCircuit(2, [5, 3], 0) def test_entangling_qr(self): target = random_unitary_matrix(15) - self.circuit_33.cu_two([0, 1], target) + t = self.circuit_53.cu_two([0, 1], target) + MQTQuditProvider() + eqr = EntangledQRCEX(t) + decomp, _countcr, _countpsw = eqr.execute() + for rotation in decomp: + target = rotation.to_matrix(identities=2) @ target + target /= target[0][0] + res = (abs(target - np.identity(15, dtype="complex")) < 10e-5).all() + assert res + + @staticmethod + def test_entangling_qr_2(): + # Create the original circuit + circuit = QuantumCircuit(2, [3, 3], 0) + circuit.x(0) + circuit.csum([0, 1]) + + # Simulate the original circuit + original_state = circuit.simulate() + print("Original circuit simulation result:") + print(original_state.round(3)) + + # Set up the provider and backend provider = MQTQuditProvider() backend_ion = provider.get_backend("faketraps2trits") - qudit_compiler = QuditCompiler() + # Compile the circuit + qudit_compiler = QuditCompiler() passes = ["LogEntQRCEXPass"] - new_circuit = qudit_compiler.compile(backend_ion, self.circuit_33, passes) + new_circuit = qudit_compiler.compile(backend_ion, circuit, passes) - for rotation in new_circuit.instructions: - target = rotation.to_matrix(identities=2) @ target - target /= target[0][0] - res = (abs(target - np.identity(15, dtype="complex")) < 10e-5).all() - assert res + # Simulate the compiled circuit + compiled_state = new_circuit.simulate() + print("\nCompiled circuit simulation result:") + print(compiled_state.round(3)) + + # Compare the results + is_close = np.allclose(original_state, compiled_state) + print(f"\nAre the simulation results close? {is_close}")