diff --git a/.codespell.allow b/.codespell.allow index 1edf5ded..f5a46aab 100644 --- a/.codespell.allow +++ b/.codespell.allow @@ -3,3 +3,4 @@ braket te Ket ket +lamda diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebe2d38d..aab0bbb3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,12 +49,12 @@ jobs: - name: Generate requirement file (Unix) if: runner.os != 'Windows' run: | - python setup.py gen_reqfile --include-extras=test,azure-quantum,braket,revkit + python setup.py gen_reqfile --include-extras=test,azure-quantum,braket,revkit,qiskit,pyparsing - name: Generate requirement file (Windows) if: runner.os == 'Windows' run: | - python setup.py gen_reqfile --include-extras=test,azure-quantum,braket + python setup.py gen_reqfile --include-extras=test,azure-quantum,braket,qiskit,pyparsing - name: Prepare env run: | @@ -69,11 +69,11 @@ jobs: - name: Build and install package (Unix) if: runner.os != 'Windows' - run: python -m pip install -ve .[azure-quantum,braket,revkit,test] + run: python -m pip install -ve .[azure-quantum,braket,revkit,test,qiskit,pyparsing] - name: Build and install package (Windows) if: runner.os == 'Windows' - run: python -m pip install -ve .[azure-quantum,braket,test] + run: python -m pip install -ve .[azure-quantum,braket,test,qiskit,pyparsing] - name: Pytest run: | @@ -142,11 +142,17 @@ jobs: python3-pytest python3-pytest-cov python3-flaky python3-venv --no-install-recommends + - name: Fix environment variables for compilation with Clang + run: | + ld_flags=$(python3 -c "import sysconfig; print(' '.join(sysconfig.get_config_var('LDSHARED').split()[1:]))") + echo "LDSHARED=clang" >> $GITHUB_ENV + echo "LDFLAGS=$ld_flags" >> $GITHUB_ENV + - name: Prepare Python env run: | python3 -m venv venv ./venv/bin/python3 -m pip install -U pip setuptools wheel - ./venv/bin/python3 setup.py gen_reqfile --include-extras=test,azure-quantum,braket + ./venv/bin/python3 setup.py gen_reqfile --include-extras=test,azure-quantum,qiskit-terra,braket cat requirements.txt ./venv/bin/python3 -m pip install -r requirements.txt --prefer-binary @@ -154,7 +160,7 @@ jobs: run: ./venv/bin/python3 -m pip install --upgrade pybind11 flaky --prefer-binary - name: Build and install package - run: ./venv/bin/python3 -m pip install -ve .[azure-quantum,braket,test] + run: ./venv/bin/python3 -m pip install -ve .[azure-quantum,qiskit-terra,braket,test] - name: Pytest run: | @@ -198,7 +204,7 @@ jobs: run: | python3 -m venv venv ./venv/bin/python3 -m pip install -U pip setuptools wheel - ./venv/bin/python3 setup.py gen_reqfile --include-extras=test,azure-quantum,braket + ./venv/bin/python3 setup.py gen_reqfile --include-extras=test,azure-quantum,qiskit-terra,braket cat requirements.txt ./venv/bin/python3 -m pip install -r requirements.txt --prefer-binary @@ -206,7 +212,7 @@ jobs: run: ./venv/bin/python3 -m pip install --upgrade pybind11 flaky --prefer-binary - name: Build and install package - run: ./venv/bin/python3 -m pip install -ve .[azure-quantum,braket,test] + run: ./venv/bin/python3 -m pip install -ve .[azure-quantum,qiskit-terra,braket,test] - name: Pytest run: | @@ -283,9 +289,6 @@ jobs: git fetch --prune --unshallow git fetch --depth=1 origin +refs/tags/*:refs/tags/* - - name: Create pip cache dir - run: mkdir -p ~/.cache/pip - - name: Cache wheels uses: actions/cache@v3 with: @@ -296,7 +299,7 @@ jobs: - name: Install dependencies run: | python3 -m pip install -U pip setuptools wheel - python3 setup.py gen_reqfile --include-extras=test,azure-quantum,braket + python3 setup.py gen_reqfile --include-extras=test,azure-quantum,braket,qiskit-terra,pyparsing cat requirements.txt python3 -m pip install -r requirements.txt --prefer-binary @@ -333,6 +336,7 @@ jobs: - name: Install docs & setup requirements run: | + python3 -m pip install wheel python3 -m pip install .[docs] - name: Build docs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf59ca9c..3c460f80 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -117,7 +117,7 @@ repos: hooks: - id: pylint args: ['--score=n', '--disable=no-member'] - additional_dependencies: [pybind11>=2.6, numpy, requests, matplotlib, networkx] + additional_dependencies: [pybind11>=2.6, numpy, requests, matplotlib, networkx, pyparsing] - repo: https://github.com/mgedmin/check-manifest rev: '0.49' diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ac525fd..3bfbbc42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Python context manager `with flushing(MainEngine()) as eng:` +- Added OpenQASMBackend to output QASM from ProjectQ circuits +- Added projectq.libs.qasm to convert QASM to ProjectQ circuits ### Fixed diff --git a/projectq/backends/_qasm.py b/projectq/backends/_qasm.py new file mode 100644 index 00000000..c1d92078 --- /dev/null +++ b/projectq/backends/_qasm.py @@ -0,0 +1,306 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# 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. + +"""Backend to convert ProjectQ commands to OpenQASM.""" + +from copy import deepcopy + +from projectq.cengines import BasicEngine +from projectq.meta import get_control_count, has_negative_control +from projectq.ops import ( + NOT, + Allocate, + Barrier, + Deallocate, + FlushGate, + H, + Measure, + Ph, + R, + Rx, + Ry, + Rz, + S, + Sdag, + Swap, + T, + Tdag, + X, + Y, + Z, +) + +# ============================================================================== + + +class OpenQASMBackend(BasicEngine): # pylint: disable=too-many-instance-attributes + """Engine to convert ProjectQ commands to OpenQASM format (either string or file).""" + + def __init__( + self, + collate_callback=None, + qubit_callback='q{}'.format, + bit_callback='c{}'.format, + qubit_id_mapping_redux=True, + ): + """ + Initialize an OpenQASMBackend object. + + Contrary to OpenQASM, ProjectQ does not impose the restriction that a program must start with qubit/bit + allocations and end with some measurements. + + The user can configure what happens each time a FlushGate() is encountered by setting the `collate` and + `collate_func` arguments to an OpenQASMBackend constructor, + + Args: + output (list,file): + collate (function): Each time a FlushGate is received, this callback function will be called. If let + unspecified, the commands are appended to the existing file/string. + Function signature: Callable[[Sequence[str]], None] + qubit_callback (function): Callback function called upon create of each qubit to generate a name for the + qubit. + Function signature: Callable[[int], str] + bit_callback (function): Callback function called upon create of each qubit to generate a name for the + qubit. + Function signature: Callable[[int], str] + qubit_id_mapping_redux (bool): If True, try to allocate new Qubit IDs to the next available qreg/creg (if + any), otherwise create a new qreg/creg. If False, simply create a new qreg/creg for each new Qubit ID + """ + super().__init__() + self._collate_callback = collate_callback + self._gen_qubit_name = qubit_callback + self._gen_bit_name = bit_callback + self._qubit_id_mapping_redux = qubit_id_mapping_redux + + self._output = [] + self._qreg_dict = {} + self._creg_dict = {} + self._reg_index = 0 + self._available_indices = [] + + self._insert_openqasm_header() + + @property + def qasm(self): + """Access to the QASM representation of the circuit.""" + return self._output + + def is_available(self, cmd): + """ + Return true if the command can be executed. + + Args: + cmd (Command): Command for which to check availability + """ + if has_negative_control(cmd): + return False + + n_controls = get_control_count(cmd) + gate = cmd.gate + is_available = False + + if gate in (Measure, Allocate, Deallocate, Barrier): + is_available = True + + if n_controls == 0: + if gate in (H, S, Sdag, T, Tdag, X, NOT, Y, Z, Swap): + is_available = True + if isinstance(gate, (Ph, R, Rx, Ry, Rz)): + is_available = True + elif n_controls == 1: + if gate in (H, X, NOT, Y, Z): + is_available = True + if isinstance( + gate, + ( + R, + Rz, + ), + ): + is_available = True + elif n_controls == 2: + if gate in (X, NOT): + is_available = True + + if not is_available: + return False + if not self.is_last_engine: + return self.next_engine.is_available(cmd) + return True + + def receive(self, command_list): + """ + Receives a command list and, for each command, stores it until completion. + + Args: + command_list: List of commands to execute + """ + for cmd in command_list: + if not cmd.gate == FlushGate(): + self._store(cmd) + else: + self._reset_after_flush() + + if not self.is_last_engine: + self.send(command_list) + + def _store(self, cmd): # pylint: disable=too-many-branches,too-many-statements + """ + Temporarily store the command cmd. + + Translates the command and stores it the _openqasm_circuit attribute (self._openqasm_circuit) + + Args: + cmd: Command to store + """ + gate = cmd.gate + n_controls = get_control_count(cmd) + + def _format_angle(angle): + return f'({angle})' + + _ccontrolled_gates_func = { + X: 'ccx', + NOT: 'ccx', + } + _controlled_gates_func = { + H: 'ch', + Ph: 'cu1', + R: 'cu1', + Rz: 'crz', + X: 'cx', + NOT: 'cx', + Y: 'cy', + Z: 'cz', + Swap: 'cswap', + } + _gates_func = { + Barrier: 'barrier', + H: 'h', + Ph: 'u1', + S: 's', + Sdag: 'sdg', + T: 't', + Tdag: 'tdg', + R: 'u1', + Rx: 'rx', + Ry: 'ry', + Rz: 'rz', + X: 'x', + NOT: 'x', + Y: 'y', + Z: 'z', + Swap: 'swap', + } + + if gate == Allocate: + add = True + + # Perform qubit index reduction if possible. This typically means that existing qubit keep their indices + # between FlushGates but that qubit indices of deallocated qubit may be reused. + if self._qubit_id_mapping_redux and self._available_indices: + add = False + index = self._available_indices.pop() + else: + index = self._reg_index + self._reg_index += 1 + + qb_id = cmd.qubits[0][0].id + + # TODO: only create bit for qubits that are actually measured + self._qreg_dict[qb_id] = self._gen_qubit_name(index) + self._creg_dict[qb_id] = self._gen_bit_name(index) + + if add: + self._output.append(f'qubit {self._qreg_dict[qb_id]};') + self._output.append(f'bit {self._creg_dict[qb_id]};') + + elif gate == Deallocate: + qb_id = cmd.qubits[0][0].id + + if self._qubit_id_mapping_redux: + self._available_indices.append(qb_id) + del self._qreg_dict[qb_id] + del self._creg_dict[qb_id] + + elif gate == Measure: + assert len(cmd.qubits) == 1 and len(cmd.qubits[0]) == 1 + qb_id = cmd.qubits[0][0].id + + self._output.append(f'{self._creg_dict[qb_id]} = measure {self._qreg_dict[qb_id]};') + + elif n_controls == 2: + targets = [self._qreg_dict[qb.id] for qureg in cmd.qubits for qb in qureg] + controls = [self._qreg_dict[qb.id] for qb in cmd.control_qubits] + + try: + self._output.append(f'{_ccontrolled_gates_func[gate]} {",".join(controls + targets)};') + except KeyError as err: + raise RuntimeError(f'Unable to perform {gate} gate with n=2 control qubits') from err + + elif n_controls == 1: + target_qureg = [self._qreg_dict[qb.id] for qureg in cmd.qubits for qb in qureg] + + try: + if isinstance(gate, Ph): + self._output.append( + f'{_controlled_gates_func[type(gate)]}{_format_angle(-gate.angle / 2.0)} ' + f'{self._qreg_dict[cmd.control_qubits[0].id]},{target_qureg[0]};' + ) + elif isinstance( + gate, + ( + R, + Rz, + ), + ): + self._output.append( + f'{_controlled_gates_func[type(gate)]}{_format_angle(gate.angle)} ' + f'{self._qreg_dict[cmd.control_qubits[0].id]},{target_qureg[0]};' + ) + else: + self._output.append( + '{} {},{};'.format( # pylint: disable=consider-using-f-string + _controlled_gates_func[gate], self._qreg_dict[cmd.control_qubits[0].id], *target_qureg + ) + ) + except KeyError as err: + raise RuntimeError(f'Unable to perform {gate} gate with n=1 control qubits') from err + else: + target_qureg = [self._qreg_dict[qb.id] for qureg in cmd.qubits for qb in qureg] + if isinstance(gate, Ph): + self._output.append(f'{_gates_func[type(gate)]}{_format_angle(-gate.angle / 2.0)} {target_qureg[0]};') + elif isinstance(gate, (R, Rx, Ry, Rz)): + self._output.append(f'{_gates_func[type(gate)]}{_format_angle(gate.angle)} {target_qureg[0]};') + else: + self._output.append( + '{} {};'.format(_gates_func[gate], *target_qureg) # pylint: disable=consider-using-f-string + ) + + def _insert_openqasm_header(self): + self._output.append('OPENQASM 3;') + self._output.append('include "stdgates.inc";') + + def _reset_after_flush(self): + """Reset the internal quantum circuit after a FlushGate.""" + if not self._collate_callback: + self._output.append('# ' + '=' * 80) + else: + self._collate_callback(deepcopy(self._output)) + self._output.clear() + self._insert_openqasm_header() + for qubit_name in self._qreg_dict.values(): + self._output.append(f'qubit {qubit_name};') + for bit_name in self._creg_dict.values(): + self._output.append(f'bit {bit_name};') diff --git a/projectq/backends/_qasm_test.py b/projectq/backends/_qasm_test.py new file mode 100644 index 00000000..f2eb904c --- /dev/null +++ b/projectq/backends/_qasm_test.py @@ -0,0 +1,407 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# 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. +"""Tests for projectq.cengines._openqasm.py.""" + +import re + +import pytest + +from projectq.cengines import DummyEngine, MainEngine +from projectq.meta import Control +from projectq.ops import ( + NOT, + All, + Allocate, + Barrier, + Command, + Deallocate, + Entangle, + H, + Measure, + Ph, + R, + Rx, + Ry, + Rz, + S, + Sdagger, + T, + Tdagger, + X, + Y, + Z, +) +from projectq.types import WeakQubitRef + +from ._qasm import OpenQASMBackend + +# ============================================================================== + + +def test_qasm_init(): + eng = OpenQASMBackend() + assert isinstance(eng.qasm, list) + assert not eng._qreg_dict + assert not eng._creg_dict + assert eng._reg_index == 0 + assert not eng._available_indices + + +@pytest.mark.parametrize("qubit_id_redux", [False, True]) +def test_qasm_allocate_deallocate(qubit_id_redux): + backend = OpenQASMBackend(qubit_id_mapping_redux=qubit_id_redux) + assert backend._qubit_id_mapping_redux == qubit_id_redux + + eng = MainEngine(backend) + qubit = eng.allocate_qubit() + eng.flush() + + assert len(backend._qreg_dict) == 1 + assert len(backend._creg_dict) == 1 + assert backend._reg_index == 1 + assert not backend._available_indices + qasm = '\n'.join(eng.backend.qasm) + assert re.search(r'qubit\s+q0', qasm) + assert re.search(r'bit\s+c0', qasm) + + qureg = eng.allocate_qureg(5) # noqa: F841 + eng.flush() + + assert len(backend._qreg_dict) == 6 + assert len(backend._creg_dict) == 6 + assert backend._reg_index == 6 + assert not backend._available_indices + qasm = '\n'.join(eng.backend.qasm) + for i in range(1, 6): + assert re.search(fr'qubit\s+q{i}', qasm) + assert re.search(fr'bit\s+c{i}', qasm) + + del qubit + eng.flush() + if qubit_id_redux: + assert len(backend._qreg_dict) == 5 + assert len(backend._creg_dict) == 5 + assert backend._reg_index == 6 + assert backend._available_indices == [0] + else: + assert len(backend._qreg_dict) == 6 + assert len(backend._creg_dict) == 6 + assert backend._reg_index == 6 + assert not backend._available_indices + + qubit = eng.allocate_qubit() # noqa: F841 + eng.flush() + + if qubit_id_redux: + assert len(backend._qreg_dict) == 6 + assert len(backend._creg_dict) == 6 + assert backend._reg_index == 6 + assert not backend._available_indices + else: + assert len(backend._qreg_dict) == 7 + assert len(backend._creg_dict) == 7 + assert backend._reg_index == 7 + assert not backend._available_indices + + +@pytest.mark.parametrize( + "gate, is_available", + [ + (X, True), + (Y, True), + (Z, True), + (T, True), + (Tdagger, True), + (S, True), + (Sdagger, True), + (Allocate, True), + (Deallocate, True), + (Measure, True), + (NOT, True), + (Rx(0.5), True), + (Ry(0.5), True), + (Rz(0.5), True), + (R(0.5), True), + (Ph(0.5), True), + (Barrier, True), + (Entangle, False), + ], + ids=lambda lamda: str(lamda), +) +def test_qasm_is_available(gate, is_available): + eng = MainEngine(backend=DummyEngine(), engine_list=[OpenQASMBackend()]) + qubit1 = eng.allocate_qubit() + cmd = Command(eng, gate, (qubit1,)) + eng.is_available(cmd) == is_available + + eng = MainEngine(backend=OpenQASMBackend(), engine_list=[]) + qubit1 = eng.allocate_qubit() + cmd = Command(eng, gate, (qubit1,)) + eng.is_available(cmd) == is_available + + +@pytest.mark.parametrize( + "gate, is_available", + [ + (H, True), + (X, True), + (NOT, True), + (Y, True), + (Z, True), + (Rz(0.5), True), + (R(0.5), True), + (Rx(0.5), False), + (Ry(0.5), False), + ], + ids=lambda lamda: str(lamda), +) +def test_qasm_is_available_1control(gate, is_available): + eng = MainEngine(backend=DummyEngine(), engine_list=[OpenQASMBackend()]) + qubit1 = eng.allocate_qubit() + qureg = eng.allocate_qureg(1) + cmd = Command(eng, gate, (qubit1,), controls=qureg) + assert eng.is_available(cmd) == is_available + + eng = MainEngine(backend=OpenQASMBackend(), engine_list=[]) + qubit1 = eng.allocate_qubit() + qureg = eng.allocate_qureg(1) + cmd = Command(eng, gate, (qubit1,), controls=qureg) + assert eng.is_available(cmd) == is_available + + +@pytest.mark.parametrize( + "gate, is_available", + [ + (X, True), + (NOT, True), + (Y, False), + (Z, False), + (Rz(0.5), False), + (R(0.5), False), + (Rx(0.5), False), + (Ry(0.5), False), + ], + ids=lambda lamda: str(lamda), +) +def test_qasm_is_available_2control(gate, is_available): + eng = MainEngine(backend=DummyEngine(), engine_list=[OpenQASMBackend()]) + qubit1 = eng.allocate_qubit() + qureg = eng.allocate_qureg(2) + cmd = Command(eng, gate, (qubit1,), controls=qureg) + assert eng.is_available(cmd) == is_available + + eng = MainEngine(backend=OpenQASMBackend(), engine_list=[]) + qubit1 = eng.allocate_qubit() + qureg = eng.allocate_qureg(2) + cmd = Command(eng, gate, (qubit1,), controls=qureg) + assert eng.is_available(cmd) == is_available + + +def test_ibm_backend_is_available_negative_control(): + backend = OpenQASMBackend() + backend.is_last_engine = True + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1])) + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='1')) + assert not backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='0')) + + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1])) + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='1')) + assert not backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='0')) + + +def test_qasm_test_qasm_single_qubit_gates(): + eng = MainEngine(backend=OpenQASMBackend(), engine_list=[]) + qubit = eng.allocate_qubit() + + H | qubit + S | qubit + T | qubit + Sdagger | qubit + Tdagger | qubit + X | qubit + Y | qubit + Z | qubit + R(0.5) | qubit + Rx(0.5) | qubit + Ry(0.5) | qubit + Rz(0.5) | qubit + Ph(0.5) | qubit + NOT | qubit + Measure | qubit + eng.flush() + + qasm = eng.backend.qasm + # Note: ignoring header and footer for comparison + assert qasm[2:-1] == [ + 'qubit q0;', + 'bit c0;', + 'h q0;', + 's q0;', + 't q0;', + 'sdg q0;', + 'tdg q0;', + 'x q0;', + 'y q0;', + 'z q0;', + 'u1(0.5) q0;', + 'rx(0.5) q0;', + 'ry(0.5) q0;', + 'rz(0.5) q0;', + 'u1(-0.25) q0;', + 'x q0;', + 'c0 = measure q0;', + ] + + +def test_qasm_test_qasm_single_qubit_gates_control(): + eng = MainEngine(backend=OpenQASMBackend(), engine_list=[]) + qubit = eng.allocate_qubit() + ctrl = eng.allocate_qubit() + + with Control(eng, ctrl): + H | qubit + X | qubit + Y | qubit + Z | qubit + NOT | qubit + R(0.5) | qubit + Rz(0.5) | qubit + Ph(0.5) | qubit + All(Measure) | qubit + ctrl + eng.flush() + + qasm = eng.backend.qasm + # Note: ignoring header and footer for comparison + assert qasm[2:-1] == [ + 'qubit q0;', + 'bit c0;', + 'qubit q1;', + 'bit c1;', + 'ch q1,q0;', + 'cx q1,q0;', + 'cy q1,q0;', + 'cz q1,q0;', + 'cx q1,q0;', + 'cu1(0.5) q1,q0;', + 'crz(0.5) q1,q0;', + 'cu1(-0.25) q1,q0;', + 'c0 = measure q0;', + 'c1 = measure q1;', + ] + + # Also test invalid gates with 1 control qubits + with pytest.raises(RuntimeError): + with Control(eng, ctrl): + T | qubit + eng.flush() + + +def test_qasm_test_qasm_single_qubit_gates_controls(): + eng = MainEngine(backend=OpenQASMBackend(), engine_list=[], verbose=True) + qubit = eng.allocate_qubit() + ctrls = eng.allocate_qureg(2) + + with Control(eng, ctrls): + X | qubit + NOT | qubit + eng.flush() + + qasm = eng.backend.qasm + # Note: ignoring header and footer for comparison + assert qasm[2:-1] == [ + 'qubit q0;', + 'bit c0;', + 'qubit q1;', + 'bit c1;', + 'qubit q2;', + 'bit c2;', + 'ccx q1,q2,q0;', + 'ccx q1,q2,q0;', + ] + + # Also test invalid gates with 2 control qubits + with pytest.raises(RuntimeError): + with Control(eng, ctrls): + Y | qubit + eng.flush() + + +def test_qasm_no_collate(): + qasm_list = [] + + def _process(output): + qasm_list.append(output) + + eng = MainEngine(backend=OpenQASMBackend(collate_callback=_process, qubit_id_mapping_redux=False), engine_list=[]) + qubit = eng.allocate_qubit() + ctrls = eng.allocate_qureg(2) + + H | qubit + with Control(eng, ctrls): + X | qubit + NOT | qubit + + eng.flush() + + All(Measure) | qubit + ctrls + eng.flush() + + print(qasm_list) + assert len(qasm_list) == 2 + + # Note: ignoring header for comparison + assert qasm_list[0][2:] == [ + 'qubit q0;', + 'bit c0;', + 'qubit q1;', + 'bit c1;', + 'qubit q2;', + 'bit c2;', + 'h q0;', + 'ccx q1,q2,q0;', + 'ccx q1,q2,q0;', + ] + + # Note: ignoring header for comparison + assert qasm_list[1][2:] == [ + 'qubit q0;', + 'qubit q1;', + 'qubit q2;', + 'bit c0;', + 'bit c1;', + 'bit c2;', + 'c0 = measure q0;', + 'c1 = measure q1;', + 'c2 = measure q2;', + ] + + +def test_qasm_name_callback(): + def _qubit(index): + return f'qubit_{index}' + + def _bit(index): + return f'classical_bit_{index}' + + eng = MainEngine(backend=OpenQASMBackend(qubit_callback=_qubit, bit_callback=_bit), engine_list=[]) + + qubit = eng.allocate_qubit() + Measure | qubit + + qasm = eng.backend.qasm + assert qasm[2:] == ['qubit qubit_0;', 'bit classical_bit_0;', 'classical_bit_0 = measure qubit_0;'] diff --git a/projectq/libs/qasm/__init__.py b/projectq/libs/qasm/__init__.py new file mode 100644 index 00000000..6efdffdc --- /dev/null +++ b/projectq/libs/qasm/__init__.py @@ -0,0 +1,36 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# 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. + +"""Contains functions/classes to handle OpenQASM.""" + +try: + from ._parse_qasm_qiskit import read_qasm_file, read_qasm_str +except ImportError: # pragma: no cover + try: + from ._parse_qasm_pyparsing import read_qasm_file, read_qasm_str + except ImportError: + ERROR_MSG = ( + 'Unable to import either qiskit or pyparsing\nPlease install either of them if you want to use ' + 'projectq.libs.qasm (e.g. using the command python -m pip install projectq[qiskit])' + ) + + def read_qasm_file(eng, filename): + """Invalid implementation.""" + # pylint: disable=unused-argument + raise RuntimeError(ERROR_MSG) + + def read_qasm_str(eng, qasm_str): + """Invalid implementation.""" + # pylint: disable=unused-argument + raise RuntimeError(ERROR_MSG) diff --git a/projectq/libs/qasm/_parse_qasm_pyparsing.py b/projectq/libs/qasm/_parse_qasm_pyparsing.py new file mode 100644 index 00000000..8135793f --- /dev/null +++ b/projectq/libs/qasm/_parse_qasm_pyparsing.py @@ -0,0 +1,939 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# 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. +"""Contains the main engine of every compiler engine pipeline, called MainEngine.""" + +import operator as op +from copy import deepcopy + +from pyparsing import ( + CharsNotIn, + Empty, + Group, + Literal, + OneOrMore, + Optional, + Or, + Suppress, + Word, + ZeroOrMore, + alphanums, + alphas, + cppStyleComment, + cStyleComment, + dblQuotedString, + nestedExpr, + pyparsing_common, + removeQuotes, +) + +from projectq.ops import All, Measure + +from ._pyparsing_expr import eval_expr +from ._qiskit_conv import gates_conv_table +from ._utils import OpaqueGate, apply_gate + +# ============================================================================== + +_QISKIT_VARS = {} +_BITS_VARS = {} +_CUSTOM_GATES = {} +_OPAQUE_GATES = {} + + +class CommonTokens: + """Some general tokens.""" + + # pylint: disable = too-few-public-methods + + int_v = pyparsing_common.signed_integer + float_v = pyparsing_common.fnumber + string_v = dblQuotedString().setParseAction(removeQuotes) + + # variable names + cname = Word(alphas + "_", alphanums + '_') + + lbra, rbra = map(Suppress, "[]") + + # variable expressions (e.g. qubit[0]) + variable_expr = Group(cname + Optional(lbra + int_v + rbra)) + + +# ============================================================================== + + +class QASMVersionOp: + """OpenQASM version.""" + + def __init__(self, toks): + """ + Initialize a QASMVersionOp object. + + Args: + toks (pyparsing.Tokens): Pyparsing tokens + """ + self.version = toks[0] + + def eval(self, _): + """ + Evaluate a QASMVersionOp. + + Args: + eng (projectq.BasicEngine): ProjectQ MainEngine to use + """ + # pylint: disable=unused-argument + + def __repr__(self): # pragma: nocover + """Mainly for debugging.""" + return f'QASMVersionOp({self.version})' + + +class IncludeOp: + """Include file operation.""" + + def __init__(self, toks): + """ + Initialize an IncludeOp object. + + Args: + toks (pyparsing.Tokens): Pyparsing tokens + """ + self.fname = toks[0] + + def eval(self, _): + """ + Evaluate a IncludeOp. + + Args: + eng (projectq.BasicEngine): ProjectQ MainEngine to use + """ + if self.fname in 'qelib1.inc, stdlib.inc': + pass + else: # pragma: nocover + raise RuntimeError(f'Invalid cannot read: {self.fname}! (unsupported)') + + def __repr__(self): # pragma: nocover + """Mainly for debugging.""" + return f'IncludeOp({self.fname})' + + +# ============================================================================== + + +class QubitProxy: + """Qubit access proxy class.""" + + def __init__(self, toks): + """ + Initialize a QubitProxy object. + + Args: + toks (pyparsing.Tokens): Pyparsing tokens + """ + if len(toks) == 2: + self.name = str(toks[0]) + self.index = int(toks[1]) + else: + self.name = toks[0] + self.index = None + + def eval(self, _): + """ + Evaluate a QubitProxy. + + Args: + eng (projectq.BasicEngine): ProjectQ MainEngine to use + """ + if self.index is not None: + return _QISKIT_VARS[self.name][self.index] + return _QISKIT_VARS[self.name] + + def __repr__(self): # pragma: nocover + """Mainly for debugging.""" + if self.index is not None: + return f'Qubit({self.name}[{self.index}])' + return f'Qubit({self.name})' + + +# ============================================================================== + + +class VarDeclOp: + """Variable declaration operation.""" + + # pylint: disable = too-few-public-methods + + def __init__(self, type_t, nbits, name, init): + """ + Initialize a VarDeclOp object. + + Args: + type_t (str): Type of variable + nbits (int): Number of bits in variable + name (str): Name of variable + init (str): Initialization expression for variable + """ + self.type_t = type_t + self.nbits = nbits + self.name = name + self.init = init + + def __repr__(self): # pragma: nocover + """Mainly for debugging.""" + if self.init: + return f"{self.__class__.__name__}({self.type_t}, {self.nbits}, {self.name}) = {self.init}" + + return f"{self.__class__.__name__}({self.type_t}, {self.nbits}, {self.name})" + + +# ------------------------------------------------------------------------------ + + +class QVarOp(VarDeclOp): + """Quantum variable declaration operation.""" + + # pylint: disable = too-few-public-methods + + def eval(self, eng): + """ + Evaluate a QVarOp. + + Args: + eng (projectq.BasicEngine): ProjectQ MainEngine to use + """ + if self.name not in _QISKIT_VARS: + _QISKIT_VARS[self.name] = eng.allocate_qureg(self.nbits) + else: # pragma: nocover + raise RuntimeError(f'Variable exist already: {self.name}') + + +# ------------------------------------------------------------------------------ + + +class CVarOp(VarDeclOp): + """Classical variable declaration operation.""" + + # pylint: disable = too-few-public-methods + + def eval(self, _): + """ + Evaluate a CVarOp. + + Args: + eng (projectq.BasicEngine): ProjectQ MainEngine to use + """ + # NB: here we are cheating a bit, since we are ignoring the number of + # bits, except for classical registers... + if self.name not in _BITS_VARS: + init = 0 + if self.init: # pragma: nocover + init = parse_expr(self.init) + + # The following are OpenQASM 3.0 + if self.type_t in ('const', 'float', 'fixed', 'angle'): # pragma: nocover + _BITS_VARS[self.name] = float(init) + elif self.type_t in ('int', 'uint'): # pragma: nocover + _BITS_VARS[self.name] = int(init) + elif self.type_t == 'bool': # pragma: nocover + _BITS_VARS[self.name] = bool(init) + else: + # bit, creg + assert self.init is None + _BITS_VARS[self.name] = [False] * self.nbits + else: # pragma: nocover + raise RuntimeError(f'Variable exist already: {self.name}') + + +# ============================================================================== + + +class GateDefOp: + """Operation representing a gate definition.""" + + def __init__(self, toks): + """ + Initialize a GateDefOp object. + + Args: + toks (pyparsing.Tokens): Pyparsing tokens + """ + self.name = toks[1] + self.params = [t[0] for t in toks[2]] + self.qparams = list(toks[3]) + self.body = list(toks[4]) + if not self.body: + self.body = [] + + def eval(self, _): + """ + Evaluate a GateDefOp. + + Args: + eng (projectq.BasicEngine): ProjectQ MainEngine to use + """ + _CUSTOM_GATES[self.name] = (self.params, self.qparams, self.body) + + def __repr__(self): # pragma: nocover + """Mainly for debugging.""" + return f"GateDefOp({self.name}, {self.params}, {self.qparams})\n\t{self.body}" + + +# ============================================================================== + + +class MeasureOp: + """Measurement operations (OpenQASM 2.0 & 3.0).""" + + def __init__(self, toks): + """ + Initialize a MeasureOp object. + + Args: + toks (pyparsing.Tokens): Pyparsing tokens + """ + if toks[1] == 'measure': # pragma: nocover + # OpenQASM 3.0 + self.qubits = QubitProxy(toks[2]) + self.bits = tuple(toks[0]) + elif toks[0] == 'measure': + # OpenQASM 2.0 + self.qubits = QubitProxy(toks[1]) + self.bits = tuple(toks[2]) + else: # pragma: nocover + raise RuntimeError('Unable to normalize measurement operation!') + + def eval(self, eng): + """ + Evaluate a MeasureOp. + + Args: + eng (projectq.BasicEngine): ProjectQ MainEngine to use + """ + # pylint: disable = pointless-statement, expression-not-assigned + # pylint: disable = global-statement + + qubits = self.qubits.eval(eng) + if not isinstance(qubits, list): + Measure | qubits + else: + All(Measure) | qubits + eng.flush() + + if not isinstance(qubits, list): + if len(self.bits) == 1: + _BITS_VARS[self.bits[0]][0] = bool(qubits) + else: + _BITS_VARS[self.bits[0]][self.bits[1]] = bool(qubits) + else: + bits = _BITS_VARS[self.bits[0]] + assert len(qubits) == len(bits) + for idx, qubit in enumerate(qubits): + bits[idx] = bool(qubit) + + def __repr__(self): # pragma: nocover + """Mainly for debugging.""" + return f'MeasureOp({self.qubits}, {self.bits})' + + +# ------------------------------------------------------------------------------ + + +class OpaqueDefOp: + """Opaque gate definition operation.""" + + def __init__(self, toks): + """ + Initialize an OpaqueDefOp object. + + Args: + name (str): Name/type of gat + params (list,tuple): Parameter for the gate (may be empty) + qubits (list): List of target qubits + """ + self.name = toks[1] + self.params = toks[2] + + def eval(self, _): + """ + Evaluate a OpaqueDefOp. + + Args: + eng (projectq.BasicEngine): ProjectQ MainEngine to use + """ + _OPAQUE_GATES[self.name] = OpaqueGate(self.name, self.params) + + def __repr__(self): # pragma: nocover + """Mainly for debugging.""" + if self.params: + return f'OpaqueOp({self.name}, {self.params})' + return f'OpaqueOp({self.name})' + + +# ------------------------------------------------------------------------------ + + +class GateOp: + """Gate applied to qubits operation.""" + + def __init__(self, string, loc, toks): + """ + Initialize a GateOp object. + + Args: + toks (pyparsing.Tokens): Pyparsing tokens + """ + # self.name = toks[0].lower() + self.name = toks[0] + if len(toks) == 2: + self.params = [] + self.qubits = [QubitProxy(qubit) for qubit in toks[1]] + else: + param_str = string[loc : string.find(';', loc)] # noqa: E203 + self.params = param_str[param_str.find('(') + 1 : param_str.rfind(')')].split(',') # noqa: E203 + self.qubits = [QubitProxy(qubit) for qubit in toks[2]] + + def eval(self, eng): # pylint: disable=too-many-branches + """ + Evaluate a GateOp. + + Args: + eng (projectq.BasicEngine): ProjectQ MainEngine to use + """ + if self.name in gates_conv_table: + gate = gates_conv_table[self.name](*[parse_expr(p) for p in self.params]) + + qubits = [] + for qureg in [qubit.eval(eng) for qubit in self.qubits]: + try: + qubits.extend(qureg) + except TypeError: + qubits.append(qureg) + + apply_gate(gate, qubits) + elif self.name in _OPAQUE_GATES: + qubits = [] + for qureg in [qubit.eval(eng) for qubit in self.qubits]: + try: + qubits.extend(qureg) + except TypeError: + qubits.append(qureg) + + apply_gate(_OPAQUE_GATES[self.name], qubits) + elif self.name in _CUSTOM_GATES: + gate_params, gate_qparams, gate_body = _CUSTOM_GATES[self.name] + + if self.params: + assert gate_params + assert len(self.params) == len(gate_params) + + params_map = {p_param: p_var for p_var, p_param in zip(self.params, gate_params)} + qparams_map = {q_param: q_var for q_var, q_param in zip(self.qubits, gate_qparams)} + + # NB: this is a hack... + # Will probably not work for multiple expansions of + # variables... + # pylint: disable = global-statement + global _BITS_VARS + bits_vars_bak = deepcopy(_BITS_VARS) + _BITS_VARS.update(params_map) + + for gate in gate_body: + # Map gate parameters and quantum parameters to real variables + gate.qubits = [qparams_map[q.name] for q in gate.qubits] + gate.eval(eng) + _BITS_VARS = bits_vars_bak + else: # pragma: nocover + if self.params: + gate_str = f'{self.name}({self.params}) | {self.qubits}' + else: + gate_str = f'{self.name} | {self.qubits}' + raise RuntimeError(f'Unknown gate: {gate_str}') + + def __repr__(self): # pragma: nocover + """Mainly for debugging.""" + return f'GateOp({self.name}, {self.params}, {self.qubits})' + + +# ============================================================================== + + +class AssignOp: # pragma: nocover + """Variable assignment operation (OpenQASM 3.0 only).""" + + def __init__(self, toks): + """ + Initialize an AssignOp object. + + Args: + toks (pyparsing.Tokens): Pyparsing tokens + """ + self.var = toks[0] + self.value = toks[1][0] + + def eval(self, _): + """ + Evaluate a AssignOp. + + Args: + eng (projectq.BasicEngine): ProjectQ MainEngine to use + """ + if self.var in _BITS_VARS: + # Assigning to creg or bit is not supported yet... + assert not isinstance(_BITS_VARS[self.var], list) + value = parse_expr(self.value) + _BITS_VARS[self.var] = value + else: + raise RuntimeError(f'The variable {self.var} is not defined!') + return 0 + + def __repr__(self): + """Mainly for debugging.""" + return f'AssignOp({self.var},{self.value})' + + +# ============================================================================== + + +def _parse_if_conditional(if_str): + # pylint: disable = invalid-name + start = if_str.find('(') + 1 + + level = 1 + for idx, ch in enumerate(if_str[start:]): + if ch == '(': # pragma: nocover + # NB: only relevant for OpenQASM 3.0 + level += 1 + elif ch == ')': + level -= 1 + if level == 0: + return if_str[start : start + idx] # noqa: E203 + raise RuntimeError(f'Unbalanced parentheses in {if_str}') # pragma: nocover + + +class IfOp: + """Operation representing a conditional expression (if-expr).""" + + greater = Literal('>').addParseAction(lambda: op.gt) + greater_equal = Literal('>=').addParseAction(lambda: op.ge) + less = Literal('<').addParseAction(lambda: op.lt) + less_equal = Literal('<=').addParseAction(lambda: op.le) + equal = Literal('==').addParseAction(lambda: op.eq) + not_equal = Literal('!=').addParseAction(lambda: op.ne) + + logical_bin_op = Or([greater, greater_equal, less, less_equal, equal, not_equal]) + + cond_expr = CommonTokens.variable_expr.copy() + logical_bin_op + CharsNotIn('()') + + def __init__(self, if_str, loc, toks): + """ + Initialize an IfOp object. + + Args: + toks (pyparsing.Tokens): Pyparsing tokens + """ + # pylint: disable=unused-argument + + cond = _parse_if_conditional(if_str[loc:]) + res = IfOp.cond_expr.parseString(cond, parseAll=True) + self.binary_op = res[1] + self.comp_expr = res[2] + self.body = toks[2:] + + if len(res[0]) == 1: + self.bit = res[0][0] + else: # pragma: nocover + # OpenQASM >= 3.0 only + name, index = res[0] + self.bit = (name, index) + + def eval(self, eng): + """ + Evaluate a IfOp. + + Args: + eng (projectq.BasicEngine): ProjectQ MainEngine to use + """ + if isinstance(self.bit, tuple): # pragma: nocover + # OpenQASM >= 3.0 only + bit = _BITS_VARS[self.bit[0]][self.bit[1]] + else: + bit_val = _BITS_VARS[self.bit] + if isinstance(bit_val, list) and len(bit_val) > 1: + bit = 0 + for bit_el in reversed(bit_val): + bit = (bit << 1) | bit_el + else: + bit = bit_val[0] + + if self.binary_op(bit, parse_expr(self.comp_expr)): + for gate in self.body: + gate.eval(eng) + + def __repr__(self): # pragma: nocover + """Mainly for debugging.""" + return f"IfExpr({self.bit} {self.binary_op} {self.comp_expr}) {{ {self.body} }}" + + +# ============================================================================== + + +def create_var_decl(toks): + """ + Create either a classical or a quantum variable operation. + + Args: + toks (pyparsing.Tokens): Pyparsing tokens + """ + type_t = toks[0] + + names = [] + nbits = 1 + body = None + + if len(toks) == 3: # pragma: nocover + # OpenQASM >= 3.0 only + # qubit[NN] var, var, var; + # fixed[7, 25] num; + names = toks[2] + nbits = toks[1] + var_list = [] + + def _get_name(name): + return name + + def _get_nbits(_): + return nbits + + elif len(toks) == 2: + # qubit qa, qb[2], qc[3]; + names = toks[1] + + def _get_name(name): + return name[0] + + def _get_nbits(name): + if len(name) == 1: # pragma: nocover + # OpenQASM >= 3.0 only + return 1 + return name[1] + + else: # pragma: nocover + # OpenQASM >= 3.0 only + # const myvar = 1234; + names = [toks[2]] + nbits = toks[1] + body = toks[3][0] + assert len(toks) == 4 + assert len(names) == 1 + + def _get_name(name): + return name + + def _get_nbits(_): + return nbits + + var_list = [] + for name in names: + if type_t in ('const', 'creg', 'bit', 'uint', 'int', 'fixed', 'float', 'angle', 'bool'): + var_list.append(CVarOp(type_t, _get_nbits(name), _get_name(name), body)) + elif body is None: + var_list.append(QVarOp(type_t, _get_nbits(name), _get_name(name), body)) + else: # pragma: nocover + raise RuntimeError('Initializer for quantum variable is unsupported!') + + return var_list + + +def parse_expr(expr_str): + """ + Parse a mathematical expression. + + Args: + expr_str (str): Expression to evaluate + """ + return eval_expr(expr_str, _BITS_VARS) + + +# ============================================================================== + + +class QiskitParser: + """Qiskit parser class.""" + + def __init__(self): + """Initialize a QiskitParser object.""" + # pylint: disable = too-many-locals + # ---------------------------------------------------------------------- + # Punctuation marks + + lpar, rpar, lbra, rbra, lbrace, rbrace = map(Suppress, "()[]{}") + equal_sign, comma, end = map(Suppress, "=,;") + + # ---------------------------------------------------------------------- + # Quantum and classical types + + qubit_t = Literal("qubit") ^ Literal("qreg") + bit_t = Literal("bit") ^ Literal("creg") + bool_t = Literal("bool") + const_t = Literal("const") + int_t = Literal("int") + uint_t = Literal("uint") + angle_t = Literal("angle") + fixed_t = Literal("fixed") + float_t = Literal("float") + # length_t = Literal("length") + # stretch_t = Literal("stretch") + + # ---------------------------------------------------------------------- + # Other core types + + cname = CommonTokens.cname.copy() + float_v = CommonTokens.float_v.copy() + int_v = CommonTokens.int_v.copy() + string_v = CommonTokens.string_v.copy() + + # ---------------------------------------------------------------------- + # Variable type matching + + # Only match an exact type + var_type = Or([qubit_t, bit_t, bool_t, const_t, int_t, uint_t, angle_t, float_t]) + + # Match a type or an array of type (e.g. int vs int[10]) + type_t = (var_type + Optional(lbra + int_v + rbra, default=1)) | ( + fixed_t + Group(lbra + int_v + comma + int_v + rbra) + ) + + # ---------------------------------------------------------------------- + # (mathematical) expressions + + expr = CharsNotIn(',;') + variable_expr = CommonTokens.variable_expr + + # ---------------------------------------------------------------------- + # Variable declarations + + # e.g. qubit[10] qr, qs / int[5] i, j + variable_decl_const_bits = type_t + Group(cname + ZeroOrMore(comma + cname)) + + # e.g. qubit qr[10], qs[2] / int i[5], j[10] + variable_decl_var_bits = var_type + Group(variable_expr + ZeroOrMore(comma + variable_expr)) + + # e.g. int[10] i = 5; + variable_decl_assign = type_t + cname + equal_sign + Group(expr) + + # Putting it all together + variable_decl_statement = Or( + [variable_decl_const_bits, variable_decl_var_bits, variable_decl_assign] + ).addParseAction(create_var_decl) + + # ---------------------------------------------------------------------- + # Gate operations + + gate_op_no_param = cname + Group(variable_expr + ZeroOrMore(comma + variable_expr)) + gate_op_w_param = ( + cname + Group(nestedExpr(ignoreExpr=comma)) + Group(variable_expr + ZeroOrMore(comma + variable_expr)) + ) + + # ---------------------------------- + # Measure gate operations + + measure_op_qasm2 = Literal("measure") + variable_expr + Suppress("->") + variable_expr + measure_op_qasm3 = variable_expr + equal_sign + Literal("measure") + variable_expr + + measure_op = (measure_op_qasm2 ^ measure_op_qasm3).addParseAction(MeasureOp) + + # Putting it all together + gate_op = Or([gate_op_no_param, gate_op_w_param, measure_op]).addParseAction(GateOp) + + # ---------------------------------------------------------------------- + # If expressions + + if_expr_qasm2 = Literal("if") + nestedExpr(ignoreExpr=comma) + gate_op + end + + # NB: not exactly 100% OpenQASM 3.0 conformant... + if_expr_qasm3 = ( + Literal("if") + nestedExpr(ignoreExpr=comma) + (lbrace + OneOrMore(Group(gate_op + end)) + rbrace) + ) + if_expr = (if_expr_qasm2 ^ if_expr_qasm3).addParseAction(IfOp) + + assign_op = (cname + equal_sign + Group(expr)).addParseAction(AssignOp) + + # ---------------------------------------------------------------------- + + # NB: this is not restrictive enough and may parse some invalid code + # such as: + # gate g a + # { + # U(0,0,0) a[0]; // <- indexing of a is forbidden + # } + + param_decl_qasm2 = cname + param_decl_qasm3 = type_t + Suppress(":") + cname + + param_decl = Group(param_decl_qasm2 ^ param_decl_qasm3) + + qargs_list = Group(cname + ZeroOrMore(comma + cname)) + + gate_def_no_args = ZeroOrMore(lpar + rpar) + Group(Empty()) + qargs_list + gate_def_w_args = lpar + Group(param_decl + ZeroOrMore(comma + param_decl)) + rpar + qargs_list + gate_def_expr = ( + Literal("gate") + + cname + + (gate_def_no_args ^ gate_def_w_args) + + lbrace + + Group(ZeroOrMore(gate_op + end)) + + rbrace + ).addParseAction(GateDefOp) + + # ---------------------------------- + # Opaque gate declarations operations + + opaque_def_expr = (Literal("opaque") + cname + (gate_def_no_args ^ gate_def_w_args) + end).addParseAction( + OpaqueDefOp + ) + + # ---------------------------------------------------------------------- + # Control related expressions (OpenQASM 3.0) + + # control_var_type = Or([length_t, stretch_t + ZeroOrMore(int_v)]) + + # lengthof_arg = OneOrMore(Word(alphanums + '_+-*/%{}[]')) + # lengthof_op = Literal("lengthof") + lpar + lengthof_arg + rpar + + # units = Word(alphas) + # control_variable_decl = control_var_type + cname + # control_variable_decl_assign = ( + # control_var_type + cname + equal_sign + + # ((pyparsing_common.number + units) | lengthof_op | expr)) + + # control_variable_decl_statement = Group( + # Or([control_variable_decl, control_variable_decl_assign])) + + # control_func_arg = OneOrMore(Word(alphanums + '_+-*/%')) + # control_func_op = Literal("rotary") + + # rotary_op = (control_func_op + lpar + control_func_arg + rpar + lbra + # + ((float_v + units) ^ control_func_arg) + rbra + + # variable_expr + ZeroOrMore(comma + variable_expr)) + + # delay_arg = OneOrMore(Word(alphanums + '_+-*/%')) + # delay_op = Literal("delay") + lbra + delay_arg + rbra + Optional( + # variable_expr + ZeroOrMore(comma + variable_expr)) + + # control_op = Group(Or([rotary_op, delay_op])) + + # ---------------------------------------------------------------------- + + header = Suppress("OPENQASM") + (int_v ^ float_v).addParseAction(QASMVersionOp) + end + + include = Suppress("include") + string_v.addParseAction(IncludeOp) + end + + statement = ( + (measure_op + end) + | if_expr + | (gate_def_expr) + | opaque_def_expr + | (variable_decl_statement + end) + # | (control_variable_decl_statement + + # end).addParseAction(lambda toks: []) + | (assign_op + end) + | (gate_op + end) + # | (control_op + end).addParseAction(lambda toks: []) + ) + + self.parser = header + ZeroOrMore(include) + ZeroOrMore(statement) + self.parser.ignore(cppStyleComment) + self.parser.ignore(cStyleComment) + + def parse_str(self, qasm_str): + """ + Parse a QASM string. + + Args: + qasm_str (str): QASM string + """ + return self.parser.parseString(qasm_str, parseAll=True) + + def parse_file(self, fname): + """ + Parse a QASM file. + + Args: + fname (str): Filename + """ + return self.parser.parseFile(fname, parseAll=True) + + +def _reset(): + """Reset internal variables.""" + # pylint: disable = invalid-name, global-statement + global _QISKIT_VARS, _BITS_VARS, _CUSTOM_GATES, _OPAQUE_GATES + + _QISKIT_VARS = {} + _BITS_VARS = {} + _CUSTOM_GATES = {} + _OPAQUE_GATES = {} + + +# ============================================================================== + +parser = QiskitParser() + +# ------------------------------------------------------------------------------ + + +def read_qasm_str(eng, qasm_str): + """ + Read an OpenQASM (2.0, 3.0 is experimental) string and convert it to ProjectQ commands. + + This version of the function uses pyparsing in order to parse the *.qasm file. + + Args: + eng (MainEngine): MainEngine to use for creating qubits and commands. + filename (string): Path to *.qasm file + + Note: + At this time, we support most of OpenQASM 2.0 and some of 3.0, although the latter is still experimental. + """ + _reset() + for operation in parser.parse_str(qasm_str).asList(): + operation.eval(eng) + return _QISKIT_VARS, _BITS_VARS + + +# ------------------------------------------------------------------------------ + + +def read_qasm_file(eng, filename): + """ + Read an OpenQASM (2.0, 3.0 is experimental) and convert it to ProjectQ Commands. + + This version of the function uses pyparsing in order to parse the *.qasm file. + + Args: + eng (MainEngine): MainEngine to use for creating qubits and commands. + filename (string): Path to *.qasm file + + Note: + At this time, we support most of OpenQASM 2.0 and some of 3.0, although the latter is still experimental. + + Also note that we do not try to enforce 100% conformity to the OpenQASM standard while parsing QASM code. The + parser may allow some syntax that are actually banned by the standard. + """ + _reset() + for operation in parser.parse_file(filename).asList(): + operation.eval(eng) + return _QISKIT_VARS, _BITS_VARS + + +# ============================================================================== diff --git a/projectq/libs/qasm/_parse_qasm_pyparsing_test.py b/projectq/libs/qasm/_parse_qasm_pyparsing_test.py new file mode 100644 index 00000000..abf7f126 --- /dev/null +++ b/projectq/libs/qasm/_parse_qasm_pyparsing_test.py @@ -0,0 +1,246 @@ +# Copyright 2021 +# +# 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 platform +import tempfile + +import pytest + +from projectq.backends import CommandPrinter +from projectq.cengines import DummyEngine, MainEngine +from projectq.ops import AllocateQubitGate, HGate, MeasureGate, SGate, TGate, XGate + +# ============================================================================== + +_has_pyparsing = True +try: + import pyparsing # noqa: F401 + + from ._parse_qasm_pyparsing import read_qasm_file, read_qasm_str +except ImportError: + _has_pyparsing = False + +has_pyparsing = pytest.mark.skipif(not _has_pyparsing, reason="Qiskit is not installed") + +# ------------------------------------------------------------------------------ + + +@pytest.fixture +def eng(): + return MainEngine(backend=DummyEngine(save_commands=True), engine_list=[]) + + +@pytest.fixture +def dummy_eng(): + dummy = DummyEngine(save_commands=True) + eng = MainEngine(backend=CommandPrinter(accept_input=False, default_measure=True), engine_list=[dummy]) + return dummy, eng + + +@pytest.fixture +def iqft_example(): + return ''' +// QFT and measure, version 1 +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[4]; +creg c[4]; +h q; +barrier q; +h q[0]; + +measure q[0] -> c[0]; +if(c==1) u1(pi/2) q[1]; +h q[1]; +measure q[1] -> c[1]; +if(c==1) u1(pi/4) q[2]; +if(c==2) u1(pi/2) q[2]; +if(c==3) u1(pi/2+pi/4) q[2]; +h q[2]; +measure q[2] -> c[2]; +if(c==1) u1(pi/8) q[3]; +if(c==2) u1(pi/4) q[3]; +if(c==3) u1(pi/4+pi/8) q[3]; +if(c==4) u1(pi/2) q[3]; +if(c==5) u1(pi/2+pi/8) q[3]; +if(c==6) u1(pi/2+pi/4) q[3]; +if(c==7) u1(pi/2+pi/4+pi/8) q[3]; +h q[3]; +measure q[3] -> c[3]; +''' + + +# ============================================================================== + + +def filter_gates(dummy, gate_class): + return [cmd for cmd in dummy.received_commands if isinstance(cmd.gate, gate_class)] + + +def exclude_gates(dummy, gate_class): + return [cmd for cmd in dummy.received_commands if not isinstance(cmd.gate, gate_class)] + + +# ============================================================================== + + +@has_pyparsing +def test_read_qasm_allocation(eng): + qasm_str = ''' +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[1]; +creg c[1]; +qreg q2[3]; +creg c2[2]; +''' + qubits_map, bits_map = read_qasm_str(eng, qasm_str) + assert {'q', 'q2'} == set(qubits_map) + assert len(qubits_map['q']) == 1 + assert len(qubits_map['q2']) == 3 + assert {'c', 'c2'} == set(bits_map) + assert len(bits_map['c']) == 1 + assert len(bits_map['c2']) == 2 + assert all(isinstance(cmd.gate, AllocateQubitGate) for cmd in eng.backend.received_commands) + + +@has_pyparsing +def test_read_qasm_if_expr_single_cbit(dummy_eng): + dummy, eng = dummy_eng + qasm_str = ''' +OPENQASM 2.0; +include "qelib1.inc"; +qreg a[1]; +creg b; +if(b==1) x a; +measure a[0] -> b; +if(b==1) x a; +measure a -> b; +''' + qubits_map, bits_map = read_qasm_str(eng, qasm_str) + assert {'a'} == set(qubits_map) + assert len(qubits_map['a']) == 1 + assert {'b'} == set(bits_map) + assert len(bits_map['b']) == 1 + assert len(filter_gates(dummy, AllocateQubitGate)) == 1 + assert len(filter_gates(dummy, XGate)) == 1 + assert len(filter_gates(dummy, MeasureGate)) == 2 + + +@has_pyparsing +def test_read_qasm_custom_gate(eng): + qasm_str = ''' +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[3]; +creg c[3]; +gate empty a, b {} +gate cHH a,b { +h b; +sdg b; +cx a,b; +h b; +t b; +cx a,b; +t b; +h b; +s b; +x b; +s a; + } +cHH q[0],q[1]; +''' + + qubits_map, bits_map = read_qasm_str(eng, qasm_str) + assert {'q'} == set(qubits_map) + assert len(qubits_map['q']) == 3 + assert {'c'} == set(bits_map) + assert len(bits_map['c']) == 3 + assert len(filter_gates(eng.backend, AllocateQubitGate)) == 3 + assert len(filter_gates(eng.backend, XGate)) == 3 + assert len(filter_gates(eng.backend, HGate)) == 3 + assert len(filter_gates(eng.backend, TGate)) == 2 + assert len(filter_gates(eng.backend, SGate)) == 2 + # + 1 DaggeredGate for sdg + + +@has_pyparsing +def test_read_qasm_custom_gate_with_param(eng): + qasm_str = ''' +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[3]; +creg c[3]; +gate mygate(alpha) a,b { +rx(alpha) a; +x b; + } +mygate(1.2) q[0],q[1]; +''' + + qubits_map, bits_map = read_qasm_str(eng, qasm_str) + assert {'q'} == set(qubits_map) + assert len(qubits_map['q']) == 3 + assert {'c'} == set(bits_map) + assert len(bits_map['c']) == 3 + assert len(filter_gates(eng.backend, AllocateQubitGate)) == 3 + assert len(exclude_gates(eng.backend, AllocateQubitGate)) == 2 + + +@has_pyparsing +def test_read_qasm_opaque_gate(eng): + qasm_str = ''' +OPENQASM 2.0; +include "qelib1.inc"; + +opaque mygate q1, q2, q3; +qreg q[3]; +creg c[3]; + +mygate q[0], q[1], q[2]; +''' + qubits_map, bits_map = read_qasm_str(eng, qasm_str) + assert {'q'} == set(qubits_map) + assert len(qubits_map['q']) == 3 + assert {'c'} == set(bits_map) + assert len(bits_map['c']) == 3 + assert len(filter_gates(eng.backend, AllocateQubitGate)) == 3 + assert len(exclude_gates(eng.backend, AllocateQubitGate)) == 1 + + +@has_pyparsing +def test_read_qasm2_str(dummy_eng, iqft_example): + dummy, eng = dummy_eng + qubits_map, bits_map = read_qasm_str(eng, iqft_example) + assert {'q'} == set(qubits_map) + assert len(qubits_map['q']) == 4 + assert {'c'} == set(bits_map) + assert len(bits_map['c']) == 4 + + +@has_pyparsing +def test_read_qasm2_file(dummy_eng, iqft_example): + dummy, eng = dummy_eng + + with tempfile.NamedTemporaryFile(mode='w', delete=True if platform.system() != 'Windows' else False) as fd: + fd.write(iqft_example) + fd.flush() + qubits_map, bits_map = read_qasm_file(eng, fd.name) + + assert {'q'} == set(qubits_map) + assert len(qubits_map['q']) == 4 + assert {'c'} == set(bits_map) + assert len(bits_map['c']) == 4 diff --git a/projectq/libs/qasm/_parse_qasm_qiskit.py b/projectq/libs/qasm/_parse_qasm_qiskit.py new file mode 100644 index 00000000..9da90a0d --- /dev/null +++ b/projectq/libs/qasm/_parse_qasm_qiskit.py @@ -0,0 +1,155 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# 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. + +"""Function definitions to read OpenQASM file format (using Qiskit).""" + +from qiskit.circuit import Clbit, QuantumCircuit + +from projectq.ops import All, Measure + +from ._qiskit_conv import gates_conv_table +from ._utils import apply_gate + +# ============================================================================== + + +def apply_op(eng, gate, qubits, bits, bits_map): + """ + Apply a qiskit operation. + + This function takes care of converting between qiskit gates and ProjectQ gates, as well as handling the + translation between qiskit's and ProjectQ's qubit and bits. + + Args: + eng (MainEngine): MainEngine to use to the operation(s) + gate (qiskit.Gate): Qiskit gate to apply + qubits (list): List of ProjectQ qubits to apply the gate to + bits (list): List of classical bits to apply the gate to + """ + # pylint: disable = expression-not-assigned, protected-access + + if bits: + # Only measurement gates have classical bits + assert len(qubits) == len(bits) + All(Measure) | qubits + eng.flush() + + for idx, bit in enumerate(bits): + assert isinstance(bit, Clbit) + bits_map[bit.register.name][bit.index] = bool(qubits[idx]) + else: + if gate.name not in gates_conv_table: + if not gate._definition: + # TODO: This will silently discard opaque gates... + return + + for gate_sub, quregs_sub, bits_sub in gate.definition._data: + # OpenQASM 2.0 limitation... + assert gate.name != 'measure' and not bits_sub + apply_op( + eng, + gate_sub, + [qubits[gate.definition._qubit_indices[qubit].index] for qubit in quregs_sub], + [], + bits_map, + ) + else: + gate_projectq = gates_conv_table[gate.name](*gate.params) + + if gate.condition: + # OpenQASM 2.0 + cbit, value = gate.condition + + if cbit.size == 1: + cbit_value = bits_map[cbit.name][0] + else: + cbit_value = 0 + for bit in reversed(bits_map[cbit.name]): + cbit_value = (cbit_value << 1) | bit + + if cbit_value == value: + apply_gate(gate_projectq, qubits) + else: + apply_gate(gate_projectq, qubits) + + +# ============================================================================== + + +def _convert_qiskit_circuit(eng, circuit): + """ + Convert a QisKit circuit and convert it to ProjectQ commands. + + This function supports OpenQASM 2.0 (3.0 is experimental) + + Args: + eng (MainEngine): MainEngine to use for creating qubits and commands. + circuit (qiskit.QuantumCircuit): Quantum circuit to process + + Note: + At this time, we support most of OpenQASM 2.0 and some of 3.0, although the latter is still experimental. + """ + # Create maps between qiskit and ProjectQ for qubits and bits + qubits_map = {qureg.name: eng.allocate_qureg(qureg.size) for qureg in circuit.qregs} + bits_map = {bit.name: [False] * bit.size for bit in circuit.cregs} + + # Convert all the gates to ProjectQ commands + for gate, quregs, bits in circuit.data: + apply_op(eng, gate, [qubits_map[qubit.register.name][qubit.index] for qubit in quregs], bits, bits_map) + + return qubits_map, bits_map + + +# ============================================================================== + + +def read_qasm_str(eng, qasm_str): + """ + Read an OpenQASM (2.0, 3.0 is experimental) string and convert it to ProjectQ commands. + + This version of the function uses Qiskit in order to parse the *.qasm file. + + Args: + eng (MainEngine): MainEngine to use for creating qubits and commands. + filename (string): Path to *.qasm file + + Note: + At this time, we support most of OpenQASM 2.0 and some of 3.0, although the latter is still experimental. + """ + circuit = QuantumCircuit.from_qasm_str(qasm_str) + return _convert_qiskit_circuit(eng, circuit) + + +# ------------------------------------------------------------------------------ + + +def read_qasm_file(eng, filename): + """ + Read an OpenQASM (2.0, 3.0 is experimental) file and convert it to ProjectQ commands. + + This version of the function uses Qiskit in order to parse the *.qasm file. + + Args: + eng (MainEngine): MainEngine to use for creating qubits and commands. + filename (string): Path to *.qasm file + + Note: + At this time, we support most of OpenQASM 2.0 and some of 3.0, although the latter is still experimental. + """ + circuit = QuantumCircuit.from_qasm_file(filename) + + return _convert_qiskit_circuit(eng, circuit) + + +# ============================================================================== diff --git a/projectq/libs/qasm/_parse_qasm_qiskit_test.py b/projectq/libs/qasm/_parse_qasm_qiskit_test.py new file mode 100644 index 00000000..faff7d6d --- /dev/null +++ b/projectq/libs/qasm/_parse_qasm_qiskit_test.py @@ -0,0 +1,220 @@ +# Copyright 2021 +# +# 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 platform +import tempfile + +import pytest + +from projectq.backends import CommandPrinter +from projectq.cengines import DummyEngine, MainEngine +from projectq.ops import AllocateQubitGate, HGate, MeasureGate, SGate, TGate, XGate + +# ============================================================================== + +_has_qiskit = True +try: + import qiskit # noqa: F401 + + from ._parse_qasm_qiskit import read_qasm_file, read_qasm_str +except ImportError: + _has_qiskit = False + +has_qiskit = pytest.mark.skipif(not _has_qiskit, reason="Qiskit is not installed") + +# ------------------------------------------------------------------------------ + + +@pytest.fixture +def eng(): + return MainEngine(backend=DummyEngine(save_commands=True), engine_list=[]) + + +@pytest.fixture +def dummy_eng(): + dummy = DummyEngine(save_commands=True) + eng = MainEngine(backend=CommandPrinter(accept_input=False, default_measure=True), engine_list=[dummy]) + return dummy, eng + + +@pytest.fixture +def iqft_example(): + return ''' +// QFT and measure, version 1 +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[4]; +creg c[4]; +h q; +barrier q; +h q[0]; + +measure q[0] -> c[0]; +if(c==1) u1(pi/2) q[1]; +h q[1]; +measure q[1] -> c[1]; +if(c==1) u1(pi/4) q[2]; +if(c==2) u1(pi/2) q[2]; +if(c==3) u1(pi/2+pi/4) q[2]; +h q[2]; +measure q[2] -> c[2]; +if(c==1) u1(pi/8) q[3]; +if(c==2) u1(pi/4) q[3]; +if(c==3) u1(pi/4+pi/8) q[3]; +if(c==4) u1(pi/2) q[3]; +if(c==5) u1(pi/2+pi/8) q[3]; +if(c==6) u1(pi/2+pi/4) q[3]; +if(c==7) u1(pi/2+pi/4+pi/8) q[3]; +h q[3]; +measure q[3] -> c[3]; +''' + + +# ============================================================================== + + +def filter_gates(dummy, gate_class): + return [cmd for cmd in dummy.received_commands if isinstance(cmd.gate, gate_class)] + + +def exclude_gates(dummy, gate_class): + return [cmd for cmd in dummy.received_commands if not isinstance(cmd.gate, gate_class)] + + +# ============================================================================== + + +@has_qiskit +def test_read_qasm_allocation(eng): + qasm_str = ''' +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[1]; +creg c[1]; +qreg q2[3]; +creg c2[2]; +''' + qubits_map, bits_map = read_qasm_str(eng, qasm_str) + assert {'q', 'q2'} == set(qubits_map) + assert len(qubits_map['q']) == 1 + assert len(qubits_map['q2']) == 3 + assert {'c', 'c2'} == set(bits_map) + assert len(bits_map['c']) == 1 + assert len(bits_map['c2']) == 2 + assert all(isinstance(cmd.gate, AllocateQubitGate) for cmd in eng.backend.received_commands) + + +@has_qiskit +def test_read_qasm_if_expr_single_cbit(dummy_eng): + dummy, eng = dummy_eng + qasm_str = ''' +OPENQASM 2.0; +include "qelib1.inc"; +qreg a[1]; +creg b[1]; +if(b==1) x a; +measure a -> b; +if(b==1) x a; +measure a -> b; +''' + qubits_map, bits_map = read_qasm_str(eng, qasm_str) + assert {'a'} == set(qubits_map) + assert len(qubits_map['a']) == 1 + assert {'b'} == set(bits_map) + assert len(bits_map['b']) == 1 + assert len(filter_gates(dummy, AllocateQubitGate)) == 1 + assert len(filter_gates(dummy, XGate)) == 1 + assert len(filter_gates(dummy, MeasureGate)) == 2 + + +@has_qiskit +def test_read_qasm_custom_gate(eng): + qasm_str = ''' +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[3]; +creg c[3]; +gate cH a,b { +h b; +sdg b; +cx a,b; +h b; +t b; +cx a,b; +t b; +h b; +s b; +x b; +s a; + } +cH q[0],q[1]; +''' + + qubits_map, bits_map = read_qasm_str(eng, qasm_str) + assert {'q'} == set(qubits_map) + assert len(qubits_map['q']) == 3 + assert {'c'} == set(bits_map) + assert len(bits_map['c']) == 3 + assert len(filter_gates(eng.backend, AllocateQubitGate)) == 3 + assert len(filter_gates(eng.backend, XGate)) == 3 + assert len(filter_gates(eng.backend, HGate)) == 3 + assert len(filter_gates(eng.backend, TGate)) == 2 + assert len(filter_gates(eng.backend, SGate)) == 2 + # + 1 DaggeredGate for sdg + + +@has_qiskit +def test_read_qasm_opaque_gate(eng): + qasm_str = ''' +OPENQASM 2.0; +include "qelib1.inc"; + +opaque mygate q1, q2, q3; +qreg q[3]; +creg c[3]; + +mygate q[0], q[1], q[2]; +''' + qubits_map, bits_map = read_qasm_str(eng, qasm_str) + assert {'q'} == set(qubits_map) + assert len(qubits_map['q']) == 3 + assert {'c'} == set(bits_map) + assert len(bits_map['c']) == 3 + assert len(eng.backend.received_commands) == 3 # Only allocate gates + + +@has_qiskit +def test_read_qasm2_str(dummy_eng, iqft_example): + dummy, eng = dummy_eng + qubits_map, bits_map = read_qasm_str(eng, iqft_example) + assert {'q'} == set(qubits_map) + assert len(qubits_map['q']) == 4 + assert {'c'} == set(bits_map) + assert len(bits_map['c']) == 4 + + +@has_qiskit +def test_read_qasm2_file(dummy_eng, iqft_example): + dummy, eng = dummy_eng + + with tempfile.NamedTemporaryFile(mode='w', delete=True if platform.system() != 'Windows' else False) as fd: + fd.write(iqft_example) + fd.flush() + qubits_map, bits_map = read_qasm_file(eng, fd.name) + + assert {'q'} == set(qubits_map) + assert len(qubits_map['q']) == 4 + assert {'c'} == set(bits_map) + assert len(bits_map['c']) == 4 diff --git a/projectq/libs/qasm/_pyparsing_expr.py b/projectq/libs/qasm/_pyparsing_expr.py new file mode 100644 index 00000000..c015aa21 --- /dev/null +++ b/projectq/libs/qasm/_pyparsing_expr.py @@ -0,0 +1,267 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# 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. + +"""Helper module to parse expressions.""" + +import math +import operator +from numbers import Number + +from pyparsing import ( + CaselessKeyword, + Forward, + Group, + Literal, + Regex, + Suppress, + Word, + ZeroOrMore, + alphanums, + alphas, + delimitedList, + pyparsing_common, +) + +# ============================================================================== + +EXPR_STACK = [] + + +def push_first(toks): + """ + Push first token on top of the stack. + + Args: + toks (pyparsing.Tokens): Pyparsing tokens + """ + EXPR_STACK.append(toks[0]) + + +def push_unary_minus(toks): + """ + Push a unary minus operation on top of the stack if required. + + Args: + toks (pyparsing.Tokens): Pyparsing tokens + """ + if toks[0] == '-': + EXPR_STACK.append('unary -') + + +# ============================================================================== + + +class ExprParser: + """ + Expression parser. + + Grammar: + expop :: '^' + multop :: '*' | '/' + addop :: '+' | '-' + integer :: ['+' | '-'] '0'..'9'+ + atom :: PI | E | real | fn '(' expr ')' | '(' expr ')' + factor :: atom [ expop factor ]* + term :: factor [ multop factor ]* + expr :: term [ addop term ]* + """ + + def __init__(self): + """Initialize an ExprParser object.""" + # pylint: disable = too-many-locals + self.var_dict = {} + + # use CaselessKeyword for e and pi, to avoid accidentally matching + # functions that start with 'e' or 'pi' (such as 'exp'); Keyword + # and CaselessKeyword only match whole words + e_const = CaselessKeyword("E").addParseAction(lambda: math.e) + pi_const = (CaselessKeyword("PI") | CaselessKeyword("π")).addParseAction(lambda: math.pi) + # fnumber = Combine(Word("+-"+nums, nums) + + # Optional("." + Optional(Word(nums))) + + # Optional(e + Word("+-"+nums, nums))) + # or use provided pyparsing_common.number, but convert back to str: + # fnumber = ppc.number().addParseAction(lambda t: str(t[0])) + fnumber = Regex(r"[+-]?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?") + fnumber = pyparsing_common.number + cname = Word(alphas + "_", alphanums + '_') + int_v = pyparsing_common.integer + + plus, minus, mult, div = map(Literal, "+-*/") + lpar, rpar, lbra, rbra = map(Suppress, "()[]") + addop = plus | minus + multop = mult | div + expop = Literal("^") + + expr = Forward() + expr_list = delimitedList(Group(expr)) + + # add parse action that replaces the function identifier with a (name, + # number of args) tuple + def insert_fn_argcount_tuple(toks): + fn_name = toks.pop(0) + num_args = len(toks[0]) + toks.insert(0, (fn_name, num_args)) + + var_expr = (cname + ZeroOrMore(lbra + int_v + rbra)).addParseAction(self._eval_var_expr) + + fn_call = (cname + lpar - Group(expr_list) + rpar).setParseAction(insert_fn_argcount_tuple) + atom = ( + ZeroOrMore(addop) + + ( + (fn_call | pi_const | e_const | var_expr | fnumber | cname).setParseAction(push_first) + | Group(lpar + expr + rpar) + ) + ).setParseAction(push_unary_minus) + + # by defining exponentiation as "atom [ ^ factor ]..." instead of + # "atom [ ^ atom ]...", we get right-to-left + # exponents, instead of left-to-right that is, 2^3^2 = 2^(3^2), not + # (2^3)^2. + factor = Forward() + factor <<= atom + ZeroOrMore((expop + factor).setParseAction(push_first)) + term = factor + ZeroOrMore((multop + factor).setParseAction(push_first)) + expr <<= term + ZeroOrMore((addop + term).setParseAction(push_first)) + self.bnf = expr + + def parse(self, expr): + """ + Parse an expression. + + Args: + expr (str): Expression to evaluate + """ + return self.bnf.parseString(expr, parseAll=True) + + def set_var_dict(self, var_dict): + """ + Set the internal variable dictionary. + + Args: + var_dict (dict): Dictionary of variables with their corresponding value for substitution. + """ + self.var_dict = var_dict + + def _eval_var_expr(self, toks): + """ + Evaluate an expression containing a variable. + + Name matching keys in the internal variable dictionary have their values substituted. + + Args: + toks (pyparsing.Tokens): Pyparsing tokens + """ + if len(toks) == 1: + return self.var_dict[toks[0]] + + value, index = toks + value = self.var_dict[value] + + if isinstance(value, list): + return value[index] + + if isinstance(value, int): + # Might be faster than (value >> index) & 1 + return int(bool(value & (1 << index))) + + # TODO: Properly handle other types... + return value # pragma: no cover + + +# map operator symbols to corresponding arithmetic operations +EPSILON = 1e-12 +opn = { + "+": operator.add, + "-": operator.sub, + "*": operator.mul, + "/": operator.truediv, + "^": operator.pow, +} + +fn = { + "sin": math.sin, + "cos": math.cos, + "tan": math.tan, + "exp": math.exp, + "abs": abs, + "trunc": int, + "round": round, + "sgn": lambda a: -1 if a < -EPSILON else 1 if a > EPSILON else 0, + "all": lambda *a: all(a), + "float": float, + "int": int, + "bool": bool, +} + + +def evaluate_stack(stack): + """ + Evaluate a stack of operations. + + Args: + stack (list): Expression stack + + Returns: + Result of evaluating the operation at the top of the stack. + """ + # pylint: disable=invalid-name + + op, num_args = stack.pop(), 0 + if isinstance(op, tuple): + op, num_args = op + + if isinstance(op, Number): + return op + + if op == "unary -": + return -evaluate_stack(stack) + + if op in "+-*/^": + # note: operands are pushed onto the stack in reverse order + op2 = evaluate_stack(stack) + op1 = evaluate_stack(stack) + return opn[op](op1, op2) + + if op in fn: + # note: args are pushed onto the stack in reverse order + args = reversed([evaluate_stack(stack) for _ in range(num_args)]) + return fn[op](*args) + + try: + return int(op) + except ValueError: + return float(op) + + +_parser = ExprParser() + + +def eval_expr(expr_str, var_dict=None): + """ + Evaluate a mathematical expression. + + Args: + expr_str (str): Expression to evaluate + var_dict (dict): Dictionary of variables with their corresponding + value for substitution. + + Returns: + Result of evaluation. + """ + # pylint: disable = global-statement + global EXPR_STACK + EXPR_STACK = [] + + _parser.set_var_dict(var_dict if var_dict else {}) + _parser.parse(expr_str) + return evaluate_stack(EXPR_STACK[:]) diff --git a/projectq/libs/qasm/_pyparsing_expr_test.py b/projectq/libs/qasm/_pyparsing_expr_test.py new file mode 100644 index 00000000..9072e60d --- /dev/null +++ b/projectq/libs/qasm/_pyparsing_expr_test.py @@ -0,0 +1,44 @@ +# Copyright 2021 +# +# 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 math + +import pytest + +from ._pyparsing_expr import eval_expr + +# ============================================================================== + + +def test_eval(): + assert eval_expr('1 + 2') == 3 + assert eval_expr('1 + 2^3') == 9 + assert eval_expr('(2.2 + 1) - (2*4 + -1)') == pytest.approx((2.2 + 1) - (2 * 4 + -1)) + assert eval_expr('-1 + PI') == pytest.approx(-1 + math.pi) + assert eval_expr('-1 + E') == pytest.approx(-1 + math.e) + assert eval_expr('-1 + 2') == 1 + assert eval_expr('a', {'a': '2'}) == 2 + assert eval_expr('a', {'a': '2.2'}) == 2.2 + assert eval_expr('-1 + a', {'a': 2}) == 1 + assert eval_expr('-1 + a[1]', {'a': [2, 3]}) == 2 + assert eval_expr('-1 + a[1]', {'a': 2}) == 0 + assert eval_expr('-1 + a[1]', {'a': 1}) == -1 + assert eval_expr('sin(1.2)') == pytest.approx(math.sin(1.2)) + assert eval_expr('cos(1.2)') == pytest.approx(math.cos(1.2)) + assert eval_expr('tan(1.2)') == pytest.approx(math.tan(1.2)) + assert eval_expr('exp(1.2)') == pytest.approx(math.exp(1.2)) + assert eval_expr('abs(-1.2)') == pytest.approx(abs(-1.2)) + + +# ============================================================================== diff --git a/projectq/libs/qasm/_qiskit_conv.py b/projectq/libs/qasm/_qiskit_conv.py new file mode 100644 index 00000000..eec9abd2 --- /dev/null +++ b/projectq/libs/qasm/_qiskit_conv.py @@ -0,0 +1,71 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# 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. + +"""Definition of helper variables for the Qiskit conversion functions.""" + +from projectq.ops import ( + CNOT, + U2, + U3, + Barrier, + C, + HGate, + Rx, + Ry, + Rz, + Sdagger, + SGate, + SwapGate, + Tdagger, + TGate, + Toffoli, + XGate, + YGate, + ZGate, +) + +# ============================================================================== +# Conversion map between Qiskit gate names and ProjectQ gates + +gates_conv_table = { + 'barrier': lambda: Barrier, + 'h': HGate, + 's': SGate, + 'sdg': lambda: Sdagger, + 't': TGate, + 'tdg': lambda: Tdagger, + 'x': XGate, + 'y': YGate, + 'z': ZGate, + 'swap': SwapGate, + 'rx': Rx, + 'ry': Ry, + 'rz': Rz, + 'u1': Rz, + 'u2': U2, + 'u3': U3, + 'phase': Rz, + # Controlled gates + 'ch': lambda: C(HGate()), + 'cx': lambda: CNOT, + 'cy': lambda: C(YGate()), + 'cz': lambda: C(ZGate()), + 'cswap': lambda: C(SwapGate()), + 'crz': lambda a: C(Rz(a)), + 'cu1': lambda a: C(Rz(a)), + 'cu2': lambda phi, lamda: C(U2(phi, lamda)), + 'cu3': lambda theta, phi, lamda: C(U3(theta, phi, lamda)), + # Doubly-controlled gates + "ccx": lambda: Toffoli, +} diff --git a/projectq/libs/qasm/_utils.py b/projectq/libs/qasm/_utils.py new file mode 100644 index 00000000..4929e6ec --- /dev/null +++ b/projectq/libs/qasm/_utils.py @@ -0,0 +1,73 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# 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. + +"""Some helper utilities.""" + +from projectq.ops import BasicGate, ControlledGate, SwapGate + +# ============================================================================== + + +class OpaqueGate(BasicGate): + """Gate representing an opaque gate type.""" + + def __init__(self, name, params): + """ + Initialize an OpaqueGate object. + + Args: + name (str): Name/type of the quantum gate + params (list,tuple): Parameter for the gate (may be empty) + """ + super().__init__() + self.name = name + self.params = params + + def __str__(self): + """Return the string representation of an OpaqueGate.""" + # TODO: This is a bit crude... + if self.params: + return f'Opaque({self.name})({",".join(self.params)})' + return f'Opaque({self.name})' + + +# ============================================================================== + + +def apply_gate(gate, qubits): + """ + Apply a gate to some qubits while separating control and target qubits. + + Args: + gate (BasicGate): Instance of a ProjectQ gate + qubits (list): List of ProjectQ qubits the gate applies to. + """ + # pylint: disable = protected-access + + if isinstance(gate, ControlledGate): + ctrls = qubits[: gate._n] + qubits = qubits[gate._n :] # noqa: E203 + if isinstance(gate._gate, SwapGate): + assert len(qubits) == 2 + gate | (ctrls, qubits[0], qubits[1]) + else: + gate | (ctrls, qubits) + elif isinstance(gate, SwapGate): + assert len(qubits) == 2 + gate | (qubits[0], qubits[1]) + else: + gate | qubits + + +# ============================================================================== diff --git a/projectq/libs/qasm/_utils_test.py b/projectq/libs/qasm/_utils_test.py new file mode 100644 index 00000000..8ea16c5f --- /dev/null +++ b/projectq/libs/qasm/_utils_test.py @@ -0,0 +1,100 @@ +# Copyright 2020 ProjectQ-Framework (www.projectq.ch) +# +# 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. +"""Tests for projectq.libs.qasm._utils.py.""" + +import pytest + +from projectq.cengines import DummyEngine +from projectq.ops import ( + U2, + U3, + Barrier, + C, + H, + Ph, + R, + Rx, + Ry, + Rz, + S, + Sdagger, + Swap, + T, + Tdagger, + Toffoli, + X, + Y, + Z, +) +from projectq.types import WeakQubitRef + +from ._utils import OpaqueGate, apply_gate + +# ============================================================================== + + +def test_opaque_gate(): + gate = OpaqueGate('my_gate', None) + assert gate.name == 'my_gate' + assert not gate.params + assert str(gate) == 'Opaque(my_gate)' + + gate = OpaqueGate('my_gate', ('lambda', 'alpha')) + assert gate.name == 'my_gate' + assert gate.params == ('lambda', 'alpha') + + assert str(gate) == 'Opaque(my_gate)(lambda,alpha)' + + +# ============================================================================== + + +@pytest.mark.parametrize( + 'gate, n_qubits', + ( + [ + (x, 1) + for x in [ + X, + Y, + Z, + S, + Sdagger, + T, + Tdagger, + H, + Barrier, + Ph(1.12), + Rx(1.12), + Ry(1.12), + Rz(1.12), + R(1.12), + U2(1.12, 1.12), + U3(1.12, 1.12, 1.12), + ] + ] + + [(x, 2) for x in [C(X), C(Y), C(Z), Swap, Barrier]] + + [(x, 3) for x in [Toffoli, C(Swap), Barrier]] + + [(x, 10) for x in [Barrier]] + ), + ids=str, +) +def test_apply_gate(gate, n_qubits): + backend = DummyEngine() + backend.is_last_engine = True + + gate.engine = backend + qubits = [WeakQubitRef(backend, idx) for idx in range(n_qubits)] + + apply_gate(gate, qubits) diff --git a/projectq/ops/_basics.py b/projectq/ops/_basics.py index 85469658..861dcfd8 100755 --- a/projectq/ops/_basics.py +++ b/projectq/ops/_basics.py @@ -63,6 +63,19 @@ class NotInvertible(Exception): """ +def _round_angle(angle, mod_pi): + rounded_angle = round(float(angle) % (mod_pi * math.pi), ANGLE_PRECISION) + if rounded_angle > mod_pi * math.pi - ANGLE_TOLERANCE: + rounded_angle = 0.0 + return rounded_angle + + +def _angle_to_str(angle, symbols): + if symbols: + return f"{str(round(angle / math.pi, 3))}{unicodedata.lookup('GREEK SMALL LETTER PI')}" + return f"{str(angle)}" + + class BasicGate: """Base class of all gates. (Don't use it directly but derive from it).""" @@ -335,10 +348,7 @@ def __init__(self, angle): angle (float): Angle of rotation (saved modulo 4 * pi) """ super().__init__() - rounded_angle = round(float(angle) % (4.0 * math.pi), ANGLE_PRECISION) - if rounded_angle > 4 * math.pi - ANGLE_TOLERANCE: - rounded_angle = 0.0 - self.angle = rounded_angle + self.angle = _round_angle(angle, 4) def __str__(self): """ @@ -360,11 +370,7 @@ def to_string(self, symbols=False): symbols (bool): uses the pi character and round the angle for a more user friendly display if True, full angle written in radian otherwise. """ - if symbols: - angle = f"({str(round(self.angle / math.pi, 3))}{unicodedata.lookup('GREEK SMALL LETTER PI')})" - else: - angle = f"({str(self.angle)})" - return str(self.__class__.__name__) + angle + return str(self.__class__.__name__) + '(' + _angle_to_str(self.angle, symbols) + ')' def tex_str(self): """ @@ -418,6 +424,116 @@ def is_identity(self): return self.angle in (0.0, 4 * math.pi) +class U3Gate(BasicGate): + """ + Base class of for a general unitary single-qubit gate. + + All three angles are continuous parameters. The inverse is the same gate + with the negated argument. Rotation gates of the same class can be merged + by adding the angles. The continuous parameter are modulo 4 * pi. + """ + + def __init__(self, theta, phi, lamda): + """ + Initialize a general unitary single-qubit gate. + + Args: + theta (float): Angle of rotation (saved modulo 4 * pi) + phi (float): Angle of rotation (saved modulo 4 * pi) + lamda (float): Angle of rotation (saved modulo 4 * pi) + """ + BasicGate.__init__(self) + self.theta = _round_angle(theta, 4) + self.phi = _round_angle(phi, 4) + self.lamda = _round_angle(lamda, 4) + + def __str__(self): + """ + Return the string representation of a U3Gate. + + Returns the class name and the angle as + + .. code-block:: python + + [CLASSNAME]([ANGLE]) + """ + return self.to_string() + + def to_string(self, symbols=False): + """ + Return the string representation of a U3Gate. + + Args: + symbols (bool): uses the pi character and round the angle for a + more user friendly display if True, full angle + written in radian otherwise. + """ + return ( + str(self.__class__.__name__) + + f'({_angle_to_str(self.theta, symbols)},' + + f'{_angle_to_str(self.phi, symbols)},' + + f'{_angle_to_str(self.lamda, symbols)})' + ) + + def tex_str(self): + """ + Return the Latex string representation of a BasicRotationGate. + + Returns the class name and the angle as a subscript, i.e. + + .. code-block:: latex + + [CLASSNAME]$_[ANGLE]$ + """ + return ( + str(self.__class__.__name__) + + f'$({round(self.theta / math.pi, 3)}\\pi,' + + f'{round(self.phi / math.pi, 3)}\\pi,' + + f'{round(self.lamda / math.pi, 3)}\\pi)$' + ) + + def get_inverse(self): + """Return the inverse of this rotation gate (negate the angle, return new object).""" + if (self.theta, self.phi, self.lamda) == (0, 0, 0): + return self.__class__(0, 0, 0) + return self.__class__(-self.theta + 4 * math.pi, -self.phi + 4 * math.pi, -self.lamda + 4 * math.pi) + + def get_merged(self, other): + """ + Return self merged with another gate. + + Default implementation handles rotation gate of the same type, where + angles are simply added. + + Args: + other: Rotation gate of same type. + + Raises: + NotMergeable: For non-rotation gates or rotation gates of + different type. + + Returns: + New object representing the merged gates. + """ + if isinstance(other, self.__class__): + return self.__class__(self.theta + other.theta, self.phi + other.phi, self.lamda + other.lamda) + raise NotMergeable("Can't merge different types of rotation gates.") + + def __eq__(self, other): + """Return True if same class and same rotation angle.""" + if isinstance(other, self.__class__): + return (self.theta, self.phi, self.lamda) == (other.theta, other.phi, other.lamda) + return False + + def __hash__(self): + """Compute the hash of the object.""" + return hash(str(self)) + + def is_identity(self): + """Return True if the gate is equivalent to an Identity gate.""" + return self.theta in (0.0, 4 * math.pi) and self.phi in (0.0, 4 * math.pi) and self.lamda in (0.0, 4 * math.pi) + + class BasicPhaseGate(BasicGate): """ Base class for all phase gates. @@ -435,10 +551,7 @@ def __init__(self, angle): angle (float): Angle of rotation (saved modulo 2 * pi) """ super().__init__() - rounded_angle = round(float(angle) % (2.0 * math.pi), ANGLE_PRECISION) - if rounded_angle > 2 * math.pi - ANGLE_TOLERANCE: - rounded_angle = 0.0 - self.angle = rounded_angle + self.angle = _round_angle(angle, 2) def __str__(self): """ diff --git a/projectq/ops/_basics_test.py b/projectq/ops/_basics_test.py index 35b19986..e06a7382 100755 --- a/projectq/ops/_basics_test.py +++ b/projectq/ops/_basics_test.py @@ -212,7 +212,7 @@ def test_basic_rotation_gate_is_identity(): assert basic_rotation_gate5.is_identity() -def test_basic_rotation_gate_comparison_and_hash(): +def test_basic_gate_comparison_and_hash(): basic_rotation_gate1 = _basics.BasicRotationGate(0.5) basic_rotation_gate2 = _basics.BasicRotationGate(0.5) basic_rotation_gate3 = _basics.BasicRotationGate(0.5 + 4 * math.pi) @@ -235,6 +235,89 @@ def test_basic_rotation_gate_comparison_and_hash(): assert basic_rotation_gate2 != _basics.BasicRotationGate(0.5 + 2 * math.pi) +@pytest.mark.parametrize( + "input_angle, modulo_angle", + [(2.0, 2.0), (17.0, 4.4336293856408275), (-0.5 * math.pi, 3.5 * math.pi), (4 * math.pi, 0)], +) +def test_u3_gate_init(input_angle, modulo_angle): + # Test internal representation + gate = _basics.U3Gate(input_angle, input_angle, input_angle) + assert gate.theta == pytest.approx(modulo_angle) + assert gate.phi == pytest.approx(modulo_angle) + assert gate.lamda == pytest.approx(modulo_angle) + + +def test_u3_gate_str(): + gate = _basics.U3Gate(math.pi, math.pi, math.pi) + assert str(gate) == "U3Gate(3.14159265359,3.14159265359,3.14159265359)" + assert gate.to_string(symbols=True) == "U3Gate(1.0π,1.0π,1.0π)" + assert gate.to_string(symbols=False) == "U3Gate(3.14159265359,3.14159265359,3.14159265359)" + + +def test_u3_tex_str(): + gate = _basics.U3Gate(0.5 * math.pi, 0.5 * math.pi, 0.5 * math.pi) + assert gate.tex_str() == "U3Gate$(0.5\\pi,0.5\\pi,0.5\\pi)$" + gate = _basics.U3Gate(4 * math.pi - 1e-13, 4 * math.pi - 1e-13, 4 * math.pi - 1e-13) + assert gate.tex_str() == "U3Gate$(0.0\\pi,0.0\\pi,0.0\\pi)$" + + +@pytest.mark.parametrize("input_angle, inverse_angle", [(2.0, -2.0 + 4 * math.pi), (-0.5, 0.5), (0.0, 0)]) +def test_u3_gate_get_inverse(input_angle, inverse_angle): + u3_gate = _basics.U3Gate(input_angle, input_angle, input_angle) + inverse = u3_gate.get_inverse() + assert isinstance(inverse, _basics.U3Gate) + assert inverse.theta == pytest.approx(inverse_angle) + assert inverse.phi == pytest.approx(inverse_angle) + assert inverse.lamda == pytest.approx(inverse_angle) + + +def test_u3_gate_get_merged(): + basic_gate = _basics.BasicGate() + u3_gate1 = _basics.U3Gate(0.5, 0.5, 0.5) + u3_gate2 = _basics.U3Gate(1.0, 1.0, 1.0) + u3_gate3 = _basics.U3Gate(1.5, 1.5, 1.5) + with pytest.raises(_basics.NotMergeable): + u3_gate1.get_merged(basic_gate) + merged_gate = u3_gate1.get_merged(u3_gate2) + assert merged_gate == u3_gate3 + + +def test_u3_gate_is_identity(): + u3_gate1 = _basics.U3Gate(0.0, 0.0, 0.0) + u3_gate2 = _basics.U3Gate(1.0 * math.pi, 1.0 * math.pi, 1.0 * math.pi) + u3_gate3 = _basics.U3Gate(2.0 * math.pi, 2.0 * math.pi, 2.0 * math.pi) + u3_gate4 = _basics.U3Gate(3.0 * math.pi, 3.0 * math.pi, 3.0 * math.pi) + u3_gate5 = _basics.U3Gate(4.0 * math.pi, 4.0 * math.pi, 4.0 * math.pi) + assert u3_gate1.is_identity() + assert not u3_gate2.is_identity() + assert not u3_gate3.is_identity() + assert not u3_gate4.is_identity() + assert u3_gate5.is_identity() + + +def test_u3_gate_comparison_and_hash(): + u3gate1 = _basics.U3Gate(0.5, 0.5, 0.5) + u3gate2 = _basics.U3Gate(0.5, 0.5, 0.5) + u3gate3 = _basics.U3Gate(0.5 + 4 * math.pi, 0.5 + 4 * math.pi, 0.5 + 4 * math.pi) + assert u3gate1 == u3gate2 + assert hash(u3gate1) == hash(u3gate2) + assert u3gate1 == u3gate3 + assert hash(u3gate1) == hash(u3gate3) + u3gate4 = _basics.U3Gate(0.50000001, 0.50000001, 0.50000001) + # Test __ne__: + assert u3gate4 != u3gate1 + # Test one gate close to 4*pi the other one close to 0 + u3gate5 = _basics.U3Gate(1.0e-13, 1.0e-13, 1.0e-13) + u3gate6 = _basics.U3Gate(4 * math.pi - 1.0e-13, 4 * math.pi - 1.0e-13, 4 * math.pi - 1.0e-13) + assert u3gate5 == u3gate6 + assert u3gate6 == u3gate5 + assert hash(u3gate5) == hash(u3gate6) + # Test different types of gates + basic_gate = _basics.BasicGate() + assert not basic_gate == u3gate6 + assert u3gate2 != _basics.U3Gate(0.5 + 2 * math.pi, 0.5 + 2 * math.pi, 0.5 + 2 * math.pi) + + @pytest.mark.parametrize( "input_angle, modulo_angle", [ diff --git a/projectq/ops/_gates.py b/projectq/ops/_gates.py index 10527a86..d3f06e8c 100755 --- a/projectq/ops/_gates.py +++ b/projectq/ops/_gates.py @@ -54,6 +54,7 @@ ClassicalInstructionGate, FastForwardingGate, SelfInverseGate, + U3Gate, ) from ._command import apply_command from ._metagates import get_inverse @@ -309,6 +310,40 @@ def matrix(self): ) +class U3(U3Gate): + """U3 rotation gate class.""" + + @property + def matrix(self): + """Access to the matrix property of this gate.""" + return np.matrix( + [ + [ + cmath.exp(-0.5j * (self.phi + self.lamda)) + math.cos(0.5 * self.theta), + -cmath.exp(-0.5j * (self.phi - self.lamda)) + math.sin(0.5 * self.theta), + ], + [ + cmath.exp(0.5j * (self.phi - self.lamda)) + math.sin(0.5 * self.theta), + cmath.exp(0.5j * (self.phi + self.lamda)) + math.cos(0.5 * self.theta), + ], + ] + ) + + +class U2(U3): + """U2 rotation gate class.""" + + def __init__(self, phi, lamda): + """ + Initialize a U2 gate. + + Args: + phi (float): Angle of rotation (saved modulo 4 * pi) + lamda (float): Angle of rotation (saved modulo 4 * pi) + """ + super().__init__(math.pi / 2, phi, lamda) + + class Rxx(BasicRotationGate): """RotationXX gate class.""" diff --git a/projectq/ops/_gates_test.py b/projectq/ops/_gates_test.py index ab431b92..9f38c8e5 100755 --- a/projectq/ops/_gates_test.py +++ b/projectq/ops/_gates_test.py @@ -150,6 +150,32 @@ def test_rz(angle): assert np.allclose(gate.matrix, expected_matrix) +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) +def test_u3(angle): + gate = _gates.U3(angle, angle, angle) + expected_matrix = np.matrix( + [ + [cmath.exp(-1j * angle) + math.cos(0.5 * angle), -1 + math.sin(0.5 * angle)], + [1 + math.sin(0.5 * angle), cmath.exp(1j * angle) + math.cos(0.5 * angle)], + ] + ) + assert gate.matrix.shape == expected_matrix.shape + assert np.allclose(gate.matrix, expected_matrix) + + +@pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) +def test_u2(angle): + gate = _gates.U2(angle, angle) + expected_matrix = np.matrix( + [ + [cmath.exp(-1j * angle) + math.cos(0.25 * math.pi), -1 + math.sin(0.25 * math.pi)], + [1 + math.sin(0.25 * math.pi), cmath.exp(1j * angle) + math.cos(0.25 * math.pi)], + ] + ) + assert gate.matrix.shape == expected_matrix.shape + assert np.allclose(gate.matrix, expected_matrix) + + @pytest.mark.parametrize("angle", [0, 0.2, 2.1, 4.1, 2 * math.pi, 4 * math.pi]) def test_rxx(angle): gate = _gates.Rxx(angle) diff --git a/pyproject.toml b/pyproject.toml index 8d3b23af..3c1897a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,7 +31,7 @@ dynamic = ["version"] dependencies = [ 'matplotlib >= 2.2.3', 'networkx >= 2', - 'numpy', + 'numpy >= 1.17', 'requests', 'scipy' ] @@ -51,6 +51,18 @@ braket = [ 'boto3' ] +qiskit = [ + 'qiskit-terra>=0.17' +] + +qasm = [ + 'qiskit-terra>=0.17' +] + +pyparsing = [ + 'pyparsing' +] + revkit = [ 'revkit == 3.0a2.dev2', 'dormouse' @@ -161,7 +173,7 @@ ignore = [ ] [tool.pylint.typecheck] - ignored-modules = ['boto3', 'botocore', 'sympy'] + ignored-modules = ['boto3', 'botocore', 'sympy', 'qiskit'] [tool.pytest.ini_options]