diff --git a/MQT_Qudits_Tutorial/MQT slide.pdf b/MQT_Qudits_Tutorial/MQT slide.pdf deleted file mode 100644 index 234844c..0000000 Binary files a/MQT_Qudits_Tutorial/MQT slide.pdf and /dev/null differ diff --git a/src/mqt/qudits/quantum_circuit/__init__.py b/src/mqt/qudits/quantum_circuit/__init__.py index 7e81772..b9695a9 100644 --- a/src/mqt/qudits/quantum_circuit/__init__.py +++ b/src/mqt/qudits/quantum_circuit/__init__.py @@ -2,7 +2,7 @@ from __future__ import annotations -from .circuit import QuantumCircuit +from .circuit import QuantumCircuit, QuantumRegister from .qasm import QASM -__all__ = ["QASM", "QuantumCircuit"] +__all__ = ["QASM", "QuantumCircuit", "QuantumRegister"] diff --git a/src/mqt/qudits/quantum_circuit/circuit.py b/src/mqt/qudits/quantum_circuit/circuit.py index 75b928f..5cf47fb 100644 --- a/src/mqt/qudits/quantum_circuit/circuit.py +++ b/src/mqt/qudits/quantum_circuit/circuit.py @@ -1,10 +1,12 @@ from __future__ import annotations import copy -import locale +from pathlib import Path from typing import TYPE_CHECKING -from .components.quantum_register import QuantumRegister +import numpy as np + +from .components import ClassicRegister, QuantumRegister from .gates import ( LS, MS, @@ -28,12 +30,16 @@ from .qasm import QASM if TYPE_CHECKING: - import numpy as np - from .components.extensions.controls import ControlData from .gate import Gate +def is_not_none_or_empty(variable): + return (variable is not None and hasattr(variable, "__iter__") and len(variable) > 0) or ( + isinstance(variable, np.ndarray) and variable.size > 0 + ) + + def add_gate_decorator(func): def gate_constructor(circ, *args): gate = func(circ, *args) @@ -67,14 +73,18 @@ class QuantumCircuit: } def __init__(self, *args) -> None: + self.cl_inverse_sitemap = {} self.inverse_sitemap = {} self.number_gates = 0 self.instructions = [] self.quantum_registers = [] + self.classic_registers = [] self._sitemap = {} + self._classic_site_map = {} self._num_cl = 0 self._num_qudits = 0 self._dimensions = [] + self.path_save = None if len(args) == 0: return @@ -104,14 +114,18 @@ def dimensions(self): return self._dimensions def reset(self) -> None: + self.cl_inverse_sitemap = {} + self.inverse_sitemap = {} self.number_gates = 0 self.instructions = [] self.quantum_registers = [] - self.inverse_sitemap = {} + self.classic_registers = [] self._sitemap = {} + self._classic_site_map = {} self._num_cl = 0 self._num_qudits = 0 self._dimensions = [] + self.path_save = None def copy(self): return copy.deepcopy(self) @@ -127,6 +141,16 @@ def append(self, qreg: QuantumRegister) -> None: self._sitemap[(str(qreg.label), i)] = (num_lines_stored + i, qreg.dimensions[i]) self.inverse_sitemap[num_lines_stored + i] = (str(qreg.label), i) + def append_classic(self, creg: ClassicRegister) -> None: + self.classic_registers.append(creg) + self._num_cl += creg.size + + num_lines_stored = len(self._classic_site_map) + for i in range(creg.size): + creg.local_sitemap[i] = num_lines_stored + i + self._classic_site_map[(str(creg.label), i)] = (num_lines_stored + i,) + self.cl_inverse_sitemap[num_lines_stored + i] = (str(creg.label), i) + @add_gate_decorator def csum(self, qudits: list[int]): return CSum( @@ -140,7 +164,7 @@ def cu_one(self, qudits: int, parameters: np.ndarray, controls: ControlData | No ) @add_gate_decorator - def cu_two(self, qudits: int, parameters: np.ndarray, controls: ControlData | None = None): + def cu_two(self, qudits: list[int], parameters: np.ndarray, controls: ControlData | None = None): return CustomTwo( self, "CUt" + str([self.dimensions[i] for i in qudits]), @@ -151,7 +175,7 @@ def cu_two(self, qudits: int, parameters: np.ndarray, controls: ControlData | No ) @add_gate_decorator - def cu_multi(self, qudits: int, parameters: np.ndarray, controls: ControlData | None = None): + def cu_multi(self, qudits: list[int], parameters: np.ndarray, controls: ControlData | None = None): return CustomMulti( self, "CUm" + str([self.dimensions[i] for i in qudits]), @@ -259,14 +283,19 @@ def set_instructions(self, sequence: list[Gate]): return self def from_qasm(self, qasm_prog) -> None: + """Create a circuit from qasm text""" self.reset() qasm_parser = QASM().parse_ditqasm2_str(qasm_prog) instructions = qasm_parser["instructions"] temp_sitemap = qasm_parser["sitemap"] + cl_sitemap = qasm_parser["sitemap_classic"] for qreg in QuantumRegister.from_map(temp_sitemap): self.append(qreg) + for creg in ClassicRegister.from_map(cl_sitemap): + self.append_classic(creg) + qasm_set = self.get_qasm_set() for op in instructions: @@ -285,7 +314,7 @@ def from_qasm(self, qasm_prog) -> None: # Extract the first element from each tuple and return as a list else: qudits_call = [t[0] for t in list(tuples_qudits)] - if op["params"]: + if is_not_none_or_empty(op["params"]): if op["controls"]: function(qudits_call, op["params"], op["controls"]) else: @@ -316,7 +345,7 @@ def to_qasm(self): return text - def save_to_file(self, file_name, file_path="/"): + def save_to_file(self, file_name: str, file_path: str = ".") -> str: """ Save qasm into a file with the specified name and path. @@ -329,15 +358,17 @@ def save_to_file(self, file_name, file_path="/"): str: The full path of the saved file. """ # Combine the file path and name to get the full file path - full_file_path = f"{file_path}/{file_name}.qasm" + self.path_save = file_path + full_file_path = Path(file_path) / (file_name + ".qasm") # Write the text to the file - with open(full_file_path, "w+", encoding=locale.getpreferredencoding(False)) as file: + with full_file_path.open("w+") as file: file.write(self.to_qasm()) - return full_file_path + self.path_save = None + return str(full_file_path) - def load_from_file(self, file_path): + def load_from_file(self, file_path: str) -> None: """ Load text from a file. @@ -347,12 +378,9 @@ def load_from_file(self, file_path): Returns: str: The text loaded from the file. """ - try: - with open(file_path, encoding=locale.getpreferredencoding(False)) as file: - text = file.read() - return self.from_qasm(text) - except FileNotFoundError: - return None + with Path(file_path).open("r") as file: + text = file.read() + self.from_qasm(text) def draw(self) -> None: # TODO diff --git a/src/mqt/qudits/quantum_circuit/components/__init__.py b/src/mqt/qudits/quantum_circuit/components/__init__.py index e69de29..cb6f4c3 100644 --- a/src/mqt/qudits/quantum_circuit/components/__init__.py +++ b/src/mqt/qudits/quantum_circuit/components/__init__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from .classic_register import ClassicRegister +from .quantum_register import QuantumRegister + +__all__ = ["ClassicRegister", "QuantumRegister"] diff --git a/src/mqt/qudits/quantum_circuit/components/classic_register.py b/src/mqt/qudits/quantum_circuit/components/classic_register.py new file mode 100644 index 0000000..c81c7ea --- /dev/null +++ b/src/mqt/qudits/quantum_circuit/components/classic_register.py @@ -0,0 +1,40 @@ +from __future__ import annotations + + +class ClassicRegister: + @classmethod + def from_map(cls, sitemap: dict) -> list[ClassicRegister]: + registers_map = {} + + for creg_with_index, line_info in sitemap.items(): + reg_name, inreg_line_index = creg_with_index + if reg_name not in registers_map: + registers_map[reg_name] = [{inreg_line_index: creg_with_index[0]}, line_info] + else: + registers_map[reg_name][0][inreg_line_index] = line_info + + registers_from_qasm = [] + for label, data in registers_map.items(): + temp = ClassicRegister(label, len(data[0])) + temp.local_sitemap = data[0] + registers_from_qasm.append(temp) + + return registers_from_qasm + + def __init__(self, name, size) -> None: + """ + Class for classical register memories + """ + self.label = name + self.size = size + self.local_sitemap = {} + + def __qasm__(self): + return "creg " + self.label + " [" + str(self.size) + "]" + ";" + + def __getitem__(self, key): + if isinstance(key, slice): + start, stop = key.start, key.stop + return [self.local_sitemap[i] for i in range(start, stop)] + + return self.local_sitemap[key] diff --git a/src/mqt/qudits/quantum_circuit/gate.py b/src/mqt/qudits/quantum_circuit/gate.py index bba2057..b63c66b 100644 --- a/src/mqt/qudits/quantum_circuit/gate.py +++ b/src/mqt/qudits/quantum_circuit/gate.py @@ -1,8 +1,13 @@ from __future__ import annotations +import random +import string from abc import ABC, abstractmethod +from pathlib import Path from typing import TYPE_CHECKING +import numpy as np + from mqt.qudits.quantum_circuit.components.extensions.matrix_factory import MatrixFactory from ..exceptions import CircuitError @@ -12,8 +17,6 @@ if TYPE_CHECKING: import enum - import numpy as np - from .circuit import QuantumCircuit @@ -33,7 +36,7 @@ def __init__( gate_type: enum, target_qudits: list[int] | int, dimensions: list[int] | int, - params: list | None = None, + params: list | np.ndarray | None = None, control_set=None, label: str | None = None, duration=None, @@ -127,8 +130,11 @@ def validate_parameter(self, parameter): pass def __qasm__(self) -> str: + """Generate QASM for Gate export""" string = f"{self.qasm_tag} " - if self._params: + if isinstance(self._params, np.ndarray): + string += self.return_custom_data() + elif self._params: string += "(" for parameter in self._params: string += f"{parameter}, " @@ -187,3 +193,12 @@ def control_info(self): "params": self._params, "controls": self._controls_data, } + + def return_custom_data(self) -> str: + if not self.parent_circuit.path_save: + return "(custom_data) " + + key = "".join(random.choice(string.ascii_letters) for _ in range(4)) + file_path = Path(self.parent_circuit.path_save) / f"{self._name}_{key}.npy" + np.save(file_path, self._params) + return f"({file_path}) " diff --git a/src/mqt/qudits/quantum_circuit/gates/custom_multi.py b/src/mqt/qudits/quantum_circuit/gates/custom_multi.py index 43bf582..f13cf63 100644 --- a/src/mqt/qudits/quantum_circuit/gates/custom_multi.py +++ b/src/mqt/qudits/quantum_circuit/gates/custom_multi.py @@ -13,6 +13,8 @@ class CustomMulti(Gate): + """Multi body custom gate""" + def __init__( self, circuit: QuantumCircuit, @@ -29,6 +31,7 @@ def __init__( target_qudits=target_qudits, dimensions=dimensions, control_set=controls, + params=parameters, ) if self.validate_parameter(parameters): self.__array_storage = parameters @@ -38,7 +41,7 @@ def __init__( def __array__(self) -> np.ndarray: return self.__array_storage - def validate_parameter(self, parameter=None): + def validate_parameter(self, parameter=None) -> bool: return isinstance(parameter, np.ndarray) def __str__(self) -> str: diff --git a/src/mqt/qudits/quantum_circuit/gates/custom_one.py b/src/mqt/qudits/quantum_circuit/gates/custom_one.py index 058abfc..edac01c 100644 --- a/src/mqt/qudits/quantum_circuit/gates/custom_one.py +++ b/src/mqt/qudits/quantum_circuit/gates/custom_one.py @@ -13,6 +13,8 @@ class CustomOne(Gate): + """One body custom gate""" + def __init__( self, circuit: QuantumCircuit, @@ -29,16 +31,16 @@ def __init__( target_qudits=target_qudits, dimensions=dimensions, control_set=controls, + params=parameters, ) if self.validate_parameter(parameters): self.__array_storage = parameters - self._params = "--" self.qasm_tag = "cuone" def __array__(self) -> np.ndarray: return self.__array_storage - def validate_parameter(self, parameter=None): + def validate_parameter(self, parameter=None) -> bool: return isinstance(parameter, np.ndarray) def __str__(self) -> str: diff --git a/src/mqt/qudits/quantum_circuit/gates/custom_two.py b/src/mqt/qudits/quantum_circuit/gates/custom_two.py index ed6dc07..1dafd32 100644 --- a/src/mqt/qudits/quantum_circuit/gates/custom_two.py +++ b/src/mqt/qudits/quantum_circuit/gates/custom_two.py @@ -13,6 +13,8 @@ class CustomTwo(Gate): + """Two body custom gate""" + def __init__( self, circuit: QuantumCircuit, @@ -29,16 +31,16 @@ def __init__( target_qudits=target_qudits, dimensions=dimensions, control_set=controls, + params=parameters, ) if self.validate_parameter(parameters): self.__array_storage = parameters - self.qasm_tag = "cutwo" def __array__(self) -> np.ndarray: return self.__array_storage - def validate_parameter(self, parameter=None): + def validate_parameter(self, parameter=None) -> bool: return isinstance(parameter, np.ndarray) def __str__(self) -> str: diff --git a/src/mqt/qudits/quantum_circuit/gates/perm.py b/src/mqt/qudits/quantum_circuit/gates/perm.py index c315f81..c57eda6 100644 --- a/src/mqt/qudits/quantum_circuit/gates/perm.py +++ b/src/mqt/qudits/quantum_circuit/gates/perm.py @@ -1,6 +1,7 @@ from __future__ import annotations import operator +from collections.abc import Collection from functools import reduce from typing import TYPE_CHECKING @@ -41,7 +42,10 @@ def __array__(self) -> np.ndarray: return np.eye(reduce(operator.mul, self._dimensions))[:, self.perm_data] def validate_parameter(self, parameter) -> bool: - assert isinstance(parameter, list), "Input is not a list" + """Verify that the input is a list of indices""" + if not isinstance(parameter, Collection): + return False + num_nums = reduce(operator.mul, self._dimensions) assert all( (0 <= num < len(parameter) and num < num_nums) for num in parameter diff --git a/src/mqt/qudits/quantum_circuit/qasm.py b/src/mqt/qudits/quantum_circuit/qasm.py index d7be4e7..119caff 100644 --- a/src/mqt/qudits/quantum_circuit/qasm.py +++ b/src/mqt/qudits/quantum_circuit/qasm.py @@ -5,10 +5,14 @@ import numpy as np -from mqt.qudits.quantum_circuit.components.extensions.controls import ControlData +from .components.extensions.controls import ControlData class QASM: + """ + Class that manages the parsing of QASM programs + """ + def __init__(self) -> None: self._program = None @@ -51,6 +55,15 @@ def parse_qreg(self, line, rgxs, sitemap) -> bool: return True return False + def parse_creg(self, line, rgxs, sitemap_classic) -> bool: + match = rgxs["creg"].match(line) + if match: + name, nclassics = match.groups() + for i in range(int(nclassics)): + sitemap_classic[(str(name), i)] = len(sitemap_classic) + return True + return False + def safe_eval_math_expression(self, expression): try: expression = expression.replace("pi", str(np.pi)) @@ -71,17 +84,15 @@ def parse_gate(self, line, rgxs, sitemap, gates) -> bool: ctl_qudits = match.group(6) ctl_levels = match.group(8) - # params = ( - # tuple(sp.sympify(param.replace("pi", str(sp.pi))) for param in params.strip("()").split(",")) - # if params - # else () - # ) # Evaluate params using NumPy and NumExpr - params = ( - tuple(self.safe_eval_math_expression(param) for param in params.strip("()").split(",")) - if params - else () - ) + if params: + if ".npy" in params: + params = np.load(params) + else: + # TODO: This does not handle "custom_data" correctly + params = tuple(self.safe_eval_math_expression(param) for param in params.strip("()[]").split(",")) + else: + params = () qudits_list = [] for dit in qudits.split(","): @@ -142,6 +153,7 @@ def parse_ditqasm2_str(self, contents): "comment_start": re.compile(r"/\*"), "comment_end": re.compile(r"\*/"), "qreg": re.compile(r"qreg\s+(\w+)\s+(\[\s*\d+\s*\])(?:\s*\[(\d+(?:,\s*\d+)*)\])?;"), + "creg": re.compile(r"creg\s+(\w+)\s*\[\s*(\d+)\s*\]\s*;"), # "ctrl_id": re.compile(r"\s+(\w+)\s*(\[\s*\other_size+\s*\])\s*(\s*\w+\s*\[\other_size+\])*\s*"), "qreg_indexing": re.compile(r"\s*(\w+)\s*(\[\s*\d+\s*\])"), # "gate_matrix": @@ -152,11 +164,13 @@ def parse_ditqasm2_str(self, contents): r"\s*;" ), "error": re.compile(r"^(gate_matrix|if)"), - "ignore": re.compile(r"^(creg|measure|barrier)"), + "ignore": re.compile(r"^(measure|barrier)"), } # initialise number of qubits to zero and an empty list for instructions sitemap = {} + sitemap_classic = {} + gates = [] # only want to warn once about each ignored instruction warned = {} @@ -173,6 +187,9 @@ def parse_ditqasm2_str(self, contents): if self.parse_qreg(line, rgxs, sitemap): continue + if self.parse_creg(line, rgxs, sitemap_classic): + continue + if self.parse_ignore(line, rgxs, warned): continue @@ -190,6 +207,7 @@ def parse_ditqasm2_str(self, contents): self._program = { "circuits_size": len(sitemap), "sitemap": sitemap, + "sitemap_classic": sitemap_classic, "instructions": gates, "n_gates": len(gates), } diff --git a/test/python/qudits_circuits/test_circuit.py b/test/python/qudits_circuits/test_circuit.py index 96e84da..56e5a11 100644 --- a/test/python/qudits_circuits/test_circuit.py +++ b/test/python/qudits_circuits/test_circuit.py @@ -2,7 +2,42 @@ from unittest import TestCase +import numpy as np + +from mqt.qudits.quantum_circuit import QuantumCircuit, QuantumRegister + class TestQuantumCircuit(TestCase): - def test_circuit(self): - pass + def test_to_qasm(self): + """Export circuit as QASM program""" + qreg_field = QuantumRegister("field", 7, [7, 7, 7, 7, 7, 7, 7]) + qreg_matter = QuantumRegister("matter", 2, [2, 2]) + + # Initialize the circuit + circ = QuantumCircuit(qreg_field) + circ.append(qreg_matter) + + # Apply operations + circ.x(qreg_field[0]) + circ.h(qreg_matter[0]) + circ.cx([qreg_field[0], qreg_field[1]]) + circ.cx([qreg_field[1], qreg_field[2]]) + circ.r(qreg_matter[1], [0, 1, np.pi, np.pi / 2]) + circ.csum([qreg_field[2], qreg_matter[1]]) + circ.pm([qreg_matter[0], qreg_matter[1]], [1, 0, 2, 3]) + circ.rh(qreg_field[2], [0, 1]) + circ.ls([qreg_field[2], qreg_matter[0]], [np.pi / 3]) + circ.ms([qreg_field[2], qreg_matter[0]], [np.pi / 3]) + circ.rz(qreg_matter[1], [0, 1, np.pi / 5]) + circ.s(qreg_field[6]) + circ.virtrz(qreg_field[6], [1, np.pi / 5]) + circ.z(qreg_field[4]) + circ.randu([qreg_field[0], qreg_matter[0], qreg_field[1]]) + circ.cu_one(qreg_field[0], np.identity(7)) + circ.cu_two([qreg_field[0], qreg_matter[1]], np.identity(7 * 2)) + circ.cu_multi([qreg_field[0], qreg_matter[1], qreg_matter[0]], np.identity(7 * 2 * 2)) + + file = circ.save_to_file(file_name="test") + circ.to_qasm() + circ_new = QuantumCircuit() + circ_new.load_from_file(file) diff --git a/test/python/qudits_circuits/test_qasm.py b/test/python/qudits_circuits/test_qasm.py index 019f08e..b33519b 100644 --- a/test/python/qudits_circuits/test_qasm.py +++ b/test/python/qudits_circuits/test_qasm.py @@ -6,24 +6,56 @@ class TestQASM(TestCase): - qasm = """ - DITQASM 2.0; - - qreg field [7][5,5,5,5,5,5,5]; - qreg matter [2]; - - creg meas_matter[7]; - creg meas_fields[3]; - - h matter[0] ctl field[0] field[1] [0,0]; - cx field[2], matter[0]; - cx field[2], matter[1]; - rxy (0, 1, pi, pi/2) field[3]; - - measure q[0] -> meas[0]; - measure q[1] -> meas[1]; - measure q[2] -> meas[2]; - """ - - circuit = QuantumCircuit() - circuit.from_qasm(qasm) + def test_from_qasm(self): + """Create circuit from QASM program""" + qasm = """ + DITQASM 2.0; + qreg field [7][5,5,5,5,5,5,5]; + qreg matter [2]; + creg meas [2]; + creg fieldc [7]; + x field[0]; + h matter[0] ctl field[0] field[1] [0,0]; + cx (0, 1, 1, pi/2) field[2], matter[0]; + cx (1, 2, 0, pi ) field[2], matter[1]; + rxy (0, 1, pi, pi/2) field[3]; + csum field[2], matter[1]; + pm ([1, 0, 2, 3]) matter[0], matter[1]; + rh (0, 1) field[3]; + ls (pi/3) field[2], matter[0]; + ms (pi/3) field[5], matter[1]; + rz (0, 1, pi) field[3]; + s field[6]; + virtrz (1, pi/5) field[6]; + z field[4]; + rdu matter[0], field[1], field[2]; + measure q[0] -> meas[0]; + measure q[1] -> meas[1]; + measure q[2] -> meas[2]; + """ + + circuit = QuantumCircuit() + circuit.from_qasm(qasm) + assert circuit._num_cl == 9 + assert circuit._num_qudits == 9 + assert circuit._dimensions == [5, 5, 5, 5, 5, 5, 5, 2, 2] + assert circuit.number_gates == 15 + assert len(circuit.quantum_registers) == 2 + assert len(circuit.classic_registers) == 2 + assert [s.qasm_tag for s in circuit.instructions] == [ + "x", + "h", + "cx", + "cx", + "rxy", + "csum", + "pm", + "rh", + "ls", + "ms", + "rz", + "s", + "virtrz", + "z", + "rdu", + ]