diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..177c408 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-merge-conflict + - id: debug-statements + - repo: https://github.com/psf/black + rev: 23.1.0 + hooks: + - id: black + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + args: ["--profile", "black"] + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade diff --git a/src/qibotn/qasm_quimb.py b/src/qibotn/qasm_quimb.py deleted file mode 100644 index a552ad0..0000000 --- a/src/qibotn/qasm_quimb.py +++ /dev/null @@ -1,211 +0,0 @@ -import re - -import quimb as qu -import quimb.tensor as qtn -import numpy as np - - -def get_gate_params(operation): - if "h " in operation: - qbit_no = [int(re.findall(r"\d+", operation)[0])] - qbit_no.insert(0, "H") - elif "x " in operation: - qbit_no = [int(re.findall(r"\d+", operation)[0])] - qbit_no.insert(0, "X") - elif "y " in operation: - qbit_no = [int(re.findall(r"\d+", operation)[0])] - qbit_no.insert(0, "Y") - elif "z " in operation: - qbit_no = [int(re.findall(r"\d+", operation)[0])] - qbit_no.insert(0, "Z") - elif "s " in operation: - qbit_no = [int(re.findall(r"\d+", operation)[0])] - qbit_no.insert(0, "S") - elif "t " in operation: - qbit_no = [int(re.findall(r"\d+", operation)[0])] - qbit_no.insert(0, "T") - elif "cu1" in operation: - lambda_ = float( - ".".join(re.findall( - r"\b\d+(?:[Ee][+-]?\d+)?", operation.split(" ")[0])) - ) - qbit_no = re.findall(r"\d+", operation.split(" ")[1]) - qbit_no = [int(x) for x in qbit_no] - qbit_no[0:0] = ["CU1", lambda_] - elif "cu2" in operation: - angles = re.findall(r"\b\d+(?:[Ee][+-]?\d+)?", operation.split(" ")[0]) - phi = float(".".join(angles[0:2])) - lambda_ = float(".".join(angles[2:])) - qbit_no = re.findall(r"\d+", operation.split(" ")[1]) - qbit_no = [int(x) for x in qbit_no] - qbit_no[0:0] = ["CU2", phi, lambda_] - elif "cu3" in operation: - angles = re.findall(r"\b\d+(?:[Ee][+-]?\d+)?", operation.split(" ")[0]) - theta = float(".".join(angles[0:2])) - phi = float(".".join(angles[2:4])) - lambda_ = float(".".join(angles[4:])) - qbit_no = re.findall(r"\d+", operation.split(" ")[1]) - qbit_no = [int(x) for x in qbit_no] - qbit_no[0:0] = ["CU3", theta, phi, lambda_] - elif " cx " in operation: - qbit_no = re.findall(r"\d+", operation.split(" ")[1]) - qbit_no = [int(x) for x in qbit_no] - qbit_no.insert(0, "CX") - elif " cy " in operation: - qbit_no = re.findall(r"\d+", operation.split(" ")[1]) - qbit_no = [int(x) for x in qbit_no] - qbit_no.insert(0, "CY") - elif " cz " in operation: - qbit_no = re.findall(r"\d+", operation.split(" ")[1]) - qbit_no = [int(x) for x in qbit_no] - qbit_no.insert(0, "CZ") - elif " ccx " in operation: - qbit_no = re.findall(r"\d+", operation.split(" ")[1]) - qbit_no = [int(x) for x in qbit_no] - qbit_no.insert(0, "CCX") - elif " ccy " in operation: - qbit_no = re.findall(r"\d+", operation.split(" ")[1]) - qbit_no = [int(x) for x in qbit_no] - qbit_no.insert(0, "CCY") - elif " ccz " in operation: - qbit_no = re.findall(r"\d+", operation.split(" ")[1]) - qbit_no = [int(x) for x in qbit_no] - qbit_no.insert(0, "CCZ") - elif " rx " in operation: - theta = float( - ".".join(re.findall( - r"\b\d+(?:[Ee][+-]?\d+)?", operation.split(" ")[0])) - ) - qbit_no = [int(re.findall(r"\d+", operation)[0])] - qbit_no[0:0] = ["RX", theta] - elif "^ry " in operation: - theta = float( - ".".join(re.findall( - r"\b\d+(?:[Ee][+-]?\d+)?", operation.split(" ")[0])) - ) - qbit_no = [int(re.findall(r"\d+", operation)[0])] - qbit_no[0:0] = ["RY", theta] - elif "^rz " in operation: - theta = float( - ".".join(re.findall( - r"\b\d+(?:[Ee][+-]?\d+)?", operation.split(" ")[0])) - ) - qbit_no = [int(re.findall(r"\d+", operation)[0])] - qbit_no[0:0] = ["RZ", theta] - elif "^rzz " in operation: - theta = float( - ".".join(re.findall( - r"\b\d+(?:[Ee][+-]?\d+)?", operation.split(" ")[0])) - ) - qbit_no = re.findall(r"\d+", operation.split(" ")[1]) - qbit_no = [int(x) for x in qbit_no] - qbit_no[0:0] = ["RZZ", theta] - elif "^u1 " in operation: - lambda_ = float( - ".".join(re.findall( - r"\b\d+(?:[Ee][+-]?\d+)?", operation.split(" ")[0])) - ) - qbit_no = [int(re.findall(r"\d+", operation)[0])] - qbit_no[0:0] = ["U1", lambda_] - elif "^u2 " in operation: - angles = re.findall(r"\b\d+(?:[Ee][+-]?\d+)?", operation.split(" ")[0]) - phi = float(".".join(angles[0:2])) - lambda_ = float(".".join(angles[2:])) - qbit_no = int(re.findall(r"\d+", operation)[0]) - qbit_no[0:0] = ["U2", phi, lambda_] # pylint: disable=E1137 - elif "^u3 " in operation: - angles = re.findall(r"\b\d+(?:[Ee][+-]?\d+)?", operation.split(" ")[0]) - theta = float(".".join(angles[0:2])) - phi = float(".".join(angles[2:4])) - lambda_ = float(".".join(angles[4:])) - qbit_no = int(re.findall(r"\d+", operation)[0]) - qbit_no[0:0] = ["U3", theta, phi, lambda_] # pylint: disable=E1137 - else: - assert "Unsupported gate" - - return qbit_no - - -def get_gate_functions(qasm_str, start_idx): - func_list = [] - result = [] - idx_inc = 0 - for line in qasm_str[start_idx:]: - if "gate " in line: - result = re.findall("[^,\s()]+", line) - elif result and "{" not in line and "}" not in line: - params = get_gate_params(line) - func_list.append(*params) - elif "}" in line: - print("Returning the list") - print(func_list) - return func_list, idx_inc - idx_inc += 1 - - -def qasm_QFT(nqubits: int, qasm_str: str, with_swaps: bool = True, psi0=None): - circ = qtn.Circuit(nqubits, psi0=psi0) - - qasm_str = qasm_str.split("\n") - for idx, line in enumerate(qasm_str): - command = line.split(" ")[0] - if re.search("include|//|OPENQASM", command): - continue - elif "qreg" in command: - nbits = int(re.findall(r"\d+", line)[0]) - assert nbits == nqubits - elif "swap" in command: - break - elif "gate" in command: # TODO: Complete gate handling - gate_func, increment = get_gate_functions(qasm_str, idx) - pass - elif "barrier" in command: # TODO: Complete barrier handling - pass - elif "measure" in command: # TODO: Complete measure handling - pass - else: - params = get_gate_params(line) - circ.apply_gate(*params) - - if with_swaps: - for i in range(nqubits // 2): # TODO: Ignore the barrier indices? - circ.apply_gate("SWAP", i, nqubits - i - 1) - - return circ - - -def init_state_tn(nqubits, init_state_sv, tn_lib="quimb"): - dims = tuple(2 * np.ones(nqubits, dtype=int)) - - if tn_lib == "quimb": - init_state_MPS = qtn.tensor_1d.MatrixProductState.from_dense( - init_state_sv, dims) - else: - # TODO: Add cuquantum later - assert False, "Unsupported tensor network backend in initilization" - - return init_state_MPS - - -def tn_circ_eval(nqubits, qasm_circ, init_state, swaps=True, tn_lib="quimb", - backend='numpy'): - if tn_lib == "quimb": - - circ_quimb = qasm_QFT(nqubits, qasm_circ, swaps, psi0=init_state) - interim = circ_quimb.psi.full_simplify(seq="DRC") - result = interim.to_dense(backend=backend).flatten() - return result - else: - # TODO: Change assert or value. Add cuquantum later - assert False, "Unsupported tensor network library" - - -def eval_QI_qft(nqubits, qasm_circ, init_state, backend="numpy", swaps=True): - # backend (quimb): numpy, cupy, jax. Passed to ``opt_einsum``. - - # Quimb circuit - init_state_mps = init_state_tn(nqubits=nqubits, init_state_sv=init_state) - amplitudes = tn_circ_eval(nqubits=nqubits, qasm_circ=qasm_circ, - init_state=init_state_mps, swaps=swaps, - tn_lib="quimb") - return amplitudes diff --git a/src/qibotn/quimb.py b/src/qibotn/quimb.py new file mode 100644 index 0000000..8414540 --- /dev/null +++ b/src/qibotn/quimb.py @@ -0,0 +1,39 @@ +import numpy as np +import quimb.tensor as qtn +from qibo.models import Circuit as QiboCircuit + + +def from_qibo(circuit: QiboCircuit, psi0=None): + nqubits = circuit.nqubits + tncirc = qtn.Circuit(nqubits, psi0=psi0) + + for gate in circuit.queue: + tncirc.apply_gate( + gate.name, + *gate.parameters, + *gate.qubits, + parametrize=len(gate.parameters) > 0 + ) + + return tncirc + + +def init_state_tn(nqubits, init_state_sv): + dims = tuple(2 * np.ones(nqubits, dtype=int)) + + return qtn.tensor_1d.MatrixProductState.from_dense(init_state_sv, dims) + + +def eval(qasm: str, init_state, backend="numpy"): + """Evaluate QASM with Quimb + + backend (quimb): numpy, cupy, jax. Passed to ``opt_einsum``. + + """ + circuit = QiboCircuit.from_qasm(qasm) + init_state_mps = init_state_tn(circuit.nqubits, init_state) + circ_quimb = from_qibo(circuit, psi0=init_state_mps) + interim = circ_quimb.psi.full_simplify(seq="DRC") + amplitudes = interim.to_dense(backend=backend).flatten() + + return amplitudes diff --git a/tests/config.py b/tests/config.py index 96621a2..7403673 100644 --- a/tests/config.py +++ b/tests/config.py @@ -1,10 +1,12 @@ -qibo = dict( - backend = 'qibojit', - platform = 'numpy', - swaps = True -) +from dataclasses import dataclass +from typing import Optional -quimb = dict( - backend = 'numpy', - swaps = True -) + +@dataclass +class Executor: + backend: str + platform: Optional[str] = None + + +qibo = Executor(backend="qibojit", platform="numpy") +quimb = Executor(backend="numpy") diff --git a/tests/test_qasm_quimb_backend.py b/tests/test_qasm_quimb_backend.py index 450104b..e962018 100644 --- a/tests/test_qasm_quimb_backend.py +++ b/tests/test_qasm_quimb_backend.py @@ -1,22 +1,18 @@ -import os - -import pytest -import qibo -from qibo.models import QFT -import numpy as np import copy +import os from timeit import default_timer as timer import config +import numpy as np +import pytest +import qibo +from qibo.models import QFT -def init_state_sv(nqubits): - init_state = np.random.random(2**nqubits) + \ - 1j * np.random.random(2**nqubits) +def create_init_state(nqubits): + init_state = np.random.random(2**nqubits) + 1j * np.random.random(2**nqubits) init_state = init_state / np.sqrt((np.abs(init_state) ** 2).sum()) - # An unmodified init_state has to be converted to tn format - init_state_for_tn = copy.deepcopy(init_state) - return init_state, init_state_for_tn + return init_state def qibo_qft(nqubits, init_state, swaps): @@ -25,33 +21,40 @@ def qibo_qft(nqubits, init_state, swaps): return circ_qibo, state_vec +def time(func): + start = timer() + res = func() + end = timer() + time = end - start + return time, res + + @pytest.mark.parametrize("nqubits", [1, 2, 5, 10]) def test_eval(nqubits: int): + # hack quimb to use the correct number of processes + # TODO: remove completely, or at least delegate to the backend + # implementation os.environ["QUIMB_NUM_PROCS"] = str(os.cpu_count()) - from qibotn import qasm_quimb + import qibotn.quimb - init_state_qibo, init_state_for_tn = init_state_sv(nqubits=nqubits) + init_state = create_init_state(nqubits=nqubits) + init_state_tn = copy.deepcopy(init_state) # Test qibo - qibo.set_backend(backend=config.qibo['backend'], - platform=config.qibo['platform']) - start_time = timer() - qibo_circ, result_sv = qibo_qft(nqubits, init_state=init_state_qibo, - swaps=config.qibo['swaps']) - end_time = timer() - qibo_time = end_time - start_time + qibo.set_backend(backend=config.qibo.backend, platform=config.qibo.platform) + qibo_time, (qibo_circ, result_sv) = time( + lambda: qibo_qft(nqubits, init_state, swaps=True) + ) # Convert to qasm for other backends qasm_circ = qibo_circ.to_qasm() # Test quimb - start_time = timer() - result_tn = qasm_quimb.eval_QI_qft(nqubits=nqubits, qasm_circ=qasm_circ, - init_state=init_state_for_tn, - backend=config.quimb['backend'], - swaps=config.quimb['swaps']) - end_time = timer() - quimb_time = end_time - start_time + quimb_time, result_tn = time( + lambda: qibotn.quimb.eval( + qasm_circ, init_state_tn, backend=config.quimb.backend + ) + ) - assert np.allclose(result_sv, result_tn), \ - "Resulting dense vectors do not match" + assert 1e-2 * qibo_time < quimb_time < 1e2 * qibo_time + assert np.allclose(result_sv, result_tn), "Resulting dense vectors do not match"