From aa3afaf4b01bd028c19b86869141cede2873d986 Mon Sep 17 00:00:00 2001 From: XYShe <30593841+XYShe@users.noreply.github.com> Date: Wed, 16 Jun 2021 14:28:55 +0200 Subject: [PATCH] Ctrl generalize (#400) * Init * Apply control through compute engine. Added Enum class * Apply control through compute engine. Added Enum class * Apply control through compute engine, added enum class * Fix small bug due to wrong default value, clean up output * abolished decomp * Revert "abolished decomp" This reverts commit 36743a15b7e3c66a9f648735de91f690eea51121. * Apply Control through decomposition. Added Test Files * Fix a few issues with new control state - Address inconsistency with qubit state ordering when using integers as input control state - Add canonical_ctrl_state function to centralise functionality - Fix some file encoding - Fix tests * Update examples/control_tester.py * Add some missing license headers and fix some tests * Add missing docstring * Cleanup some code in AQT and IBM backends * Some code cleanup in _simulator.py * Change autoreplacer priority for control. Added Additional test for autoreplacer. Added check for canonical ctrl state func. * Update projectq/setups/default.py * Update projectq/setups/decompositions/cnu2toffoliandcu.py * Update projectq/setups/decompositions/cnu2toffoliandcu.py * Cleanup code in _replacer.py * Tweak some of the unit tests + add comments * Add more tests for canonical_ctrl_state and has_negative_control * Short pass of reformatting using black * Bug fixing for rebasing * Reformat files. Improve control_tester examples. Update change log * Dummy change to trigger CI with new state * Use pytest-mock for awsbraket client testing * Fix Linter warnings * Use pytest-mock also for awsbraket backend tests * Fix missing tests in backends and added support for IonQ * Fix linter warning * Add support for AWSBraketBackend * Fix small typo * Use backported mock instead of unittest.mock * Sort requirements_tests.txt * Fix a bunch of errors that happens at program exit Monkeypatching or patching of external may unload the patch before the MainEngine calls the last flush operations which would then call the original API although unwanted. Co-authored-by: Damien Nguyen --- .gitignore | 5 + CHANGELOG.md | 1 + examples/control_tester.py | 89 ++++++++++++++ projectq/backends/_aqt/_aqt_test.py | 10 +- projectq/backends/_awsbraket/_awsbraket.py | 24 ++-- .../_awsbraket_boto3_client_test.py | 45 +++---- .../backends/_awsbraket/_awsbraket_test.py | 54 ++++++--- projectq/backends/_ibm/_ibm.py | 19 +-- projectq/backends/_ibm/_ibm_test.py | 18 ++- projectq/backends/_ionq/_ionq.py | 5 +- projectq/backends/_ionq/_ionq_mapper_test.py | 32 ++--- projectq/backends/_ionq/_ionq_test.py | 20 +++- projectq/backends/_sim/_simulator.py | 16 ++- projectq/backends/_sim/_simulator_test.py | 17 +++ projectq/cengines/_basics.py | 1 + projectq/cengines/_main.py | 2 +- projectq/cengines/_replacer/_replacer.py | 86 ++++++++------ projectq/cengines/_replacer/_replacer_test.py | 48 ++++++++ projectq/meta/__init__.py | 2 +- projectq/meta/_control.py | 72 +++++++++++- projectq/meta/_control_test.py | 111 +++++++++++++++++- projectq/ops/__init__.py | 12 +- projectq/ops/_command.py | 63 +++++++++- projectq/ops/_command_test.py | 43 ++++++- projectq/setups/decompositions/__init__.py | 2 + .../setups/decompositions/controlstate.py | 45 +++++++ .../decompositions/controlstate_test.py | 48 ++++++++ projectq/setups/default.py | 16 +-- pyproject.toml | 1 + requirements_tests.txt | 2 + 30 files changed, 736 insertions(+), 173 deletions(-) create mode 100755 examples/control_tester.py create mode 100755 projectq/setups/decompositions/controlstate.py create mode 100755 projectq/setups/decompositions/controlstate_test.py diff --git a/.gitignore b/.gitignore index 3b349b6a1..680678f56 100644 --- a/.gitignore +++ b/.gitignore @@ -171,6 +171,9 @@ dmypy.json *.out *.app +# Others +err.txt + # ============================================================================== VERSION.txt @@ -180,3 +183,5 @@ thumbs.db # Mac OSX artifacts *.DS_Store + +# ============================================================================== diff --git a/CHANGELOG.md b/CHANGELOG.md index 154ad88ec..7fe324dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added ``pyproject.toml`` and ``setup.cfg`` - Added CHANGELOG.md - Added backend for IonQ. +- Added support for state-dependent qubit control ### Deprecated diff --git a/examples/control_tester.py b/examples/control_tester.py new file mode 100755 index 000000000..94833a70e --- /dev/null +++ b/examples/control_tester.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 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. + +from projectq.cengines import MainEngine +from projectq.meta import Control +from projectq.ops import All, X, Measure, CtrlAll + + +def run_circuit(eng, circuit_num): + qubit = eng.allocate_qureg(2) + ctrl_fail = eng.allocate_qureg(3) + ctrl_success = eng.allocate_qureg(3) + + if circuit_num == 1: + with Control(eng, ctrl_fail): + X | qubit[0] + All(X) | ctrl_success + with Control(eng, ctrl_success): + X | qubit[1] + + elif circuit_num == 2: + All(X) | ctrl_fail + with Control(eng, ctrl_fail, ctrl_state=CtrlAll.Zero): + X | qubit[0] + with Control(eng, ctrl_success, ctrl_state=CtrlAll.Zero): + X | qubit[1] + + elif circuit_num == 3: + All(X) | ctrl_fail + with Control(eng, ctrl_fail, ctrl_state='101'): + X | qubit[0] + + X | ctrl_success[0] + X | ctrl_success[2] + with Control(eng, ctrl_success, ctrl_state='101'): + X | qubit[1] + + elif circuit_num == 4: + All(X) | ctrl_fail + with Control(eng, ctrl_fail, ctrl_state=5): + X | qubit[0] + + X | ctrl_success[0] + X | ctrl_success[2] + with Control(eng, ctrl_success, ctrl_state=5): + X | qubit[1] + + All(Measure) | qubit + All(Measure) | ctrl_fail + All(Measure) | ctrl_success + eng.flush() + return qubit, ctrl_fail, ctrl_success + + +if __name__ == '__main__': + # Create a MainEngine with a unitary simulator backend + eng = MainEngine() + + # Run out quantum circuit + # 1 - Default behaviour of the control: all control qubits should be 1 + # 2 - Off-control: all control qubits should remain 0 + # 3 - Specific state given by a string + # 4 - Specific state given by an integer + + qubit, ctrl_fail, ctrl_success = run_circuit(eng, 4) + + # Measured value of the failed qubit should be 0 in all cases + print('The final value of the qubit with failed control is:') + print(int(qubit[0])) + print('with the state of control qubits are:') + print([int(qubit) for qubit in ctrl_fail], '\n') + + # Measured value of the success qubit should be 1 in all cases + print('The final value of the qubit with successful control is:') + print(int(qubit[1])) + print('with the state of control qubits are:') + print([int(qubit) for qubit in ctrl_success], '\n') diff --git a/projectq/backends/_aqt/_aqt_test.py b/projectq/backends/_aqt/_aqt_test.py index 0810cfe0a..a11293853 100644 --- a/projectq/backends/_aqt/_aqt_test.py +++ b/projectq/backends/_aqt/_aqt_test.py @@ -19,7 +19,7 @@ from projectq import MainEngine from projectq.backends._aqt import _aqt -from projectq.types import WeakQubitRef, Qubit +from projectq.types import WeakQubitRef from projectq.cengines import DummyEngine, BasicMapperEngine from projectq.ops import ( All, @@ -140,6 +140,10 @@ def test_aqt_too_many_runs(): Rx(math.pi / 2) | qubit eng.flush() + # Avoid exception at deletion + backend._num_runs = 1 + backend._circuit = [] + def test_aqt_retrieve(monkeypatch): # patch send @@ -171,7 +175,7 @@ def mock_retrieve(*args, **kwargs): assert prob_dict['00'] == pytest.approx(0.6) # Unknown qubit and no mapper - invalid_qubit = [Qubit(eng, 10)] + invalid_qubit = [WeakQubitRef(eng, 10)] with pytest.raises(RuntimeError): eng.backend.get_probabilities(invalid_qubit) @@ -227,7 +231,7 @@ def mock_send(*args, **kwargs): assert prob_dict['00'] == pytest.approx(0.6) # Unknown qubit and no mapper - invalid_qubit = [Qubit(eng, 10)] + invalid_qubit = [WeakQubitRef(eng, 10)] with pytest.raises(RuntimeError): eng.backend.get_probabilities(invalid_qubit) diff --git a/projectq/backends/_awsbraket/_awsbraket.py b/projectq/backends/_awsbraket/_awsbraket.py index 2eaf7ba00..7f158afa1 100755 --- a/projectq/backends/_awsbraket/_awsbraket.py +++ b/projectq/backends/_awsbraket/_awsbraket.py @@ -18,7 +18,7 @@ import json from projectq.cengines import BasicEngine -from projectq.meta import get_control_count, LogicalQubitIDTag +from projectq.meta import get_control_count, LogicalQubitIDTag, has_negative_control from projectq.types import WeakQubitRef from projectq.ops import ( R, @@ -176,6 +176,9 @@ def is_available(self, cmd): if gate in (Measure, Allocate, Deallocate, Barrier): return True + if has_negative_control(cmd): + return False + if self.device == 'Aspen-8': if get_control_count(cmd) == 2: return isinstance(gate, XGate) @@ -271,21 +274,24 @@ def _store(self, cmd): Args: cmd: Command to store """ + gate = cmd.gate + + # Do not clear the self._clear flag for those gates + if gate in (Deallocate, Barrier): + return + + num_controls = get_control_count(cmd) + gate_type = type(gate) if not isinstance(gate, DaggeredGate) else type(gate._gate) + if self._clear: self._probabilities = dict() self._clear = False self._circuit = "" self._allocated_qubits = set() - gate = cmd.gate - num_controls = get_control_count(cmd) - gate_type = type(gate) if not isinstance(gate, DaggeredGate) else type(gate._gate) - if gate == Allocate: self._allocated_qubits.add(cmd.qubits[0][0].id) return - if gate in (Deallocate, Barrier): - return if gate == Measure: assert len(cmd.qubits) == 1 and len(cmd.qubits[0]) == 1 qb_id = cmd.qubits[0][0].id @@ -412,6 +418,10 @@ def _run(self): # Also, AWS Braket currently does not support intermediate # measurements. + # If the clear flag is set, nothing to do here... + if self._clear: + return + # In Braket the results for the jobs are stored in S3. # You can recover the results from previous jobs using the TaskArn # (self._retrieve_execution). diff --git a/projectq/backends/_awsbraket/_awsbraket_boto3_client_test.py b/projectq/backends/_awsbraket/_awsbraket_boto3_client_test.py index 4c669d165..5faf939a8 100644 --- a/projectq/backends/_awsbraket/_awsbraket_boto3_client_test.py +++ b/projectq/backends/_awsbraket/_awsbraket_boto3_client_test.py @@ -15,7 +15,6 @@ """ Test for projectq.backends._awsbraket._awsbraket_boto3_client.py """ import pytest -from unittest.mock import patch from ._awsbraket_boto3_client_test_fixtures import * # noqa: F401,F403 @@ -34,13 +33,13 @@ @has_boto3 -@patch('boto3.client') -def test_show_devices(mock_boto3_client, show_devices_setup): +def test_show_devices(mocker, show_devices_setup): creds, search_value, device_value, devicelist_result = show_devices_setup - mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client = mocker.MagicMock(spec=['search_devices', 'get_device']) mock_boto3_client.search_devices.return_value = search_value mock_boto3_client.get_device.return_value = device_value + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) devicelist = _awsbraket_boto3_client.show_devices(credentials=creds) assert devicelist == devicelist_result @@ -85,7 +84,6 @@ def test_show_devices(mock_boto3_client, show_devices_setup): @has_boto3 -@patch('boto3.client') @pytest.mark.parametrize( "var_status, var_result", [ @@ -95,13 +93,14 @@ def test_show_devices(mock_boto3_client, show_devices_setup): ('other', other_value), ], ) -def test_retrieve(mock_boto3_client, var_status, var_result, retrieve_setup): +def test_retrieve(mocker, var_status, var_result, retrieve_setup): arntask, creds, device_value, res_completed, results_dict = retrieve_setup - mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client = mocker.MagicMock(spec=['get_quantum_task', 'get_device', 'get_object']) mock_boto3_client.get_quantum_task.return_value = var_result mock_boto3_client.get_device.return_value = device_value mock_boto3_client.get_object.return_value = results_dict + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) if var_status == 'completed': res = _awsbraket_boto3_client.retrieve(credentials=creds, taskArn=arntask) @@ -132,8 +131,7 @@ def test_retrieve(mock_boto3_client, var_status, var_result, retrieve_setup): @has_boto3 -@patch('boto3.client') -def test_retrieve_devicetypes(mock_boto3_client, retrieve_devicetypes_setup): +def test_retrieve_devicetypes(mocker, retrieve_devicetypes_setup): ( arntask, creds, @@ -142,10 +140,11 @@ def test_retrieve_devicetypes(mock_boto3_client, retrieve_devicetypes_setup): res_completed, ) = retrieve_devicetypes_setup - mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client = mocker.MagicMock(spec=['get_quantum_task', 'get_device', 'get_object']) mock_boto3_client.get_quantum_task.return_value = completed_value mock_boto3_client.get_device.return_value = device_value mock_boto3_client.get_object.return_value = results_dict + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) res = _awsbraket_boto3_client.retrieve(credentials=creds, taskArn=arntask) assert res == res_completed @@ -155,13 +154,13 @@ def test_retrieve_devicetypes(mock_boto3_client, retrieve_devicetypes_setup): @has_boto3 -@patch('boto3.client') -def test_send_too_many_qubits(mock_boto3_client, send_too_many_setup): +def test_send_too_many_qubits(mocker, send_too_many_setup): (creds, s3_folder, search_value, device_value, info_too_much) = send_too_many_setup - mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client = mocker.MagicMock(spec=['search_devices', 'get_device']) mock_boto3_client.search_devices.return_value = search_value mock_boto3_client.get_device.return_value = device_value + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) with pytest.raises(_awsbraket_boto3_client.DeviceTooSmall): _awsbraket_boto3_client.send(info_too_much, device='name2', credentials=creds, s3_folder=s3_folder) @@ -171,7 +170,6 @@ def test_send_too_many_qubits(mock_boto3_client, send_too_many_setup): @has_boto3 -@patch('boto3.client') @pytest.mark.parametrize( "var_status, var_result", [ @@ -181,7 +179,7 @@ def test_send_too_many_qubits(mock_boto3_client, send_too_many_setup): ('other', other_value), ], ) -def test_send_real_device_online_verbose(mock_boto3_client, var_status, var_result, real_device_online_setup): +def test_send_real_device_online_verbose(mocker, var_status, var_result, real_device_online_setup): ( qtarntask, @@ -194,12 +192,15 @@ def test_send_real_device_online_verbose(mock_boto3_client, var_status, var_resu results_dict, ) = real_device_online_setup - mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client = mocker.MagicMock( + spec=['search_devices', 'get_device', 'create_quantum_task', 'get_quantum_task', 'get_object'] + ) mock_boto3_client.search_devices.return_value = search_value mock_boto3_client.get_device.return_value = device_value mock_boto3_client.create_quantum_task.return_value = qtarntask mock_boto3_client.get_quantum_task.return_value = var_result mock_boto3_client.get_object.return_value = results_dict + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) # This is a ficticios situation because the job will be always queued # at the beginning. After that the status will change at some point in time @@ -243,7 +244,6 @@ def test_send_real_device_online_verbose(mock_boto3_client, var_status, var_resu @has_boto3 -@patch('boto3.client') @pytest.mark.parametrize( "var_error", [ @@ -254,16 +254,17 @@ def test_send_real_device_online_verbose(mock_boto3_client, var_status, var_resu ('ValidationException'), ], ) -def test_send_that_errors_are_caught(mock_boto3_client, var_error, send_that_error_setup): +def test_send_that_errors_are_caught(mocker, var_error, send_that_error_setup): creds, s3_folder, info, search_value, device_value = send_that_error_setup - mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client = mocker.MagicMock(spec=['search_devices', 'get_device', 'create_quantum_task']) mock_boto3_client.search_devices.return_value = search_value mock_boto3_client.get_device.return_value = device_value mock_boto3_client.create_quantum_task.side_effect = botocore.exceptions.ClientError( {"Error": {"Code": var_error, "Message": "Msg error for " + var_error}}, "create_quantum_task", ) + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) with pytest.raises(botocore.exceptions.ClientError): _awsbraket_boto3_client.send(info, device='name2', credentials=creds, s3_folder=s3_folder, num_retries=2) @@ -282,15 +283,15 @@ def test_send_that_errors_are_caught(mock_boto3_client, var_error, send_that_err @has_boto3 -@patch('boto3.client') @pytest.mark.parametrize("var_error", [('ResourceNotFoundException')]) -def test_retrieve_error_arn_not_exist(mock_boto3_client, var_error, arntask, creds): +def test_retrieve_error_arn_not_exist(mocker, var_error, arntask, creds): - mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client = mocker.MagicMock(spec=['get_quantum_task']) mock_boto3_client.get_quantum_task.side_effect = botocore.exceptions.ClientError( {"Error": {"Code": var_error, "Message": "Msg error for " + var_error}}, "get_quantum_task", ) + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) with pytest.raises(botocore.exceptions.ClientError): _awsbraket_boto3_client.retrieve(credentials=creds, taskArn=arntask) diff --git a/projectq/backends/_awsbraket/_awsbraket_test.py b/projectq/backends/_awsbraket/_awsbraket_test.py index d82274cbe..dc51283ab 100644 --- a/projectq/backends/_awsbraket/_awsbraket_test.py +++ b/projectq/backends/_awsbraket/_awsbraket_test.py @@ -15,13 +15,13 @@ """ Test for projectq.backends._awsbraket._awsbraket.py""" import pytest -from unittest.mock import patch import copy import math from projectq import MainEngine -from projectq.types import WeakQubitRef, Qubit + +from projectq.types import WeakQubitRef from projectq.cengines import ( BasicMapperEngine, DummyEngine, @@ -320,6 +320,22 @@ def test_awsbraket_backend_is_available_control_singlequbit_sv1(ctrl_singlequbit assert aws_backend.is_available(cmd) == is_available_sv1 +def test_awsbraket_backend_is_available_negative_control(): + backend = _awsbraket.AWSBraketBackend() + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + + 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, qb2])) + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='11')) + assert not backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='01')) + + @has_boto3 def test_awsbraket_backend_is_available_swap_aspen(): eng = MainEngine(backend=DummyEngine(), engine_list=[DummyEngine()]) @@ -403,18 +419,18 @@ def test_awsbraket_invalid_command(): @has_boto3 -@patch('boto3.client') -def test_awsbraket_sent_error(mock_boto3_client, sent_error_setup): +def test_awsbraket_sent_error(mocker, sent_error_setup): creds, s3_folder, search_value, device_value = sent_error_setup var_error = 'ServiceQuotaExceededException' - mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client = mocker.MagicMock(spec=['search_devices', 'get_device', 'create_quantum_task']) mock_boto3_client.search_devices.return_value = search_value mock_boto3_client.get_device.return_value = device_value mock_boto3_client.create_quantum_task.side_effect = botocore.exceptions.ClientError( {"Error": {"Code": var_error, "Message": "Msg error for " + var_error}}, "create_quantum_task", ) + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) backend = _awsbraket.AWSBraketBackend( verbose=True, @@ -461,14 +477,14 @@ def test_awsbraket_sent_error_2(): @has_boto3 -@patch('boto3.client') -def test_awsbraket_retrieve(mock_boto3_client, retrieve_setup): +def test_awsbraket_retrieve(mocker, retrieve_setup): (arntask, creds, completed_value, device_value, results_dict) = retrieve_setup - mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client = mocker.MagicMock(spec=['get_quantum_task', 'get_device', 'get_object']) mock_boto3_client.get_quantum_task.return_value = completed_value mock_boto3_client.get_device.return_value = device_value mock_boto3_client.get_object.return_value = results_dict + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) backend = _awsbraket.AWSBraketBackend(retrieve_execution=arntask, credentials=creds, num_retries=2, verbose=True) @@ -491,7 +507,7 @@ def test_awsbraket_retrieve(mock_boto3_client, retrieve_setup): assert prob_dict['010'] == 0.8 # Unknown qubit or no mapper - invalid_qubit = [Qubit(eng, 10)] + invalid_qubit = [WeakQubitRef(eng, 10)] with pytest.raises(RuntimeError): eng.backend.get_probabilities(invalid_qubit) @@ -500,8 +516,7 @@ def test_awsbraket_retrieve(mock_boto3_client, retrieve_setup): @has_boto3 -@patch('boto3.client') -def test_awsbraket_backend_functional_test(mock_boto3_client, functional_setup, mapper): +def test_awsbraket_backend_functional_test(mocker, functional_setup, mapper): ( creds, s3_folder, @@ -512,12 +527,15 @@ def test_awsbraket_backend_functional_test(mock_boto3_client, functional_setup, results_dict, ) = functional_setup - mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client = mocker.MagicMock( + spec=['search_devices', 'get_device', 'create_quantum_task', 'get_quantum_task', 'get_object'] + ) mock_boto3_client.search_devices.return_value = search_value mock_boto3_client.get_device.return_value = device_value mock_boto3_client.create_quantum_task.return_value = qtarntask mock_boto3_client.get_quantum_task.return_value = completed_value mock_boto3_client.get_object.return_value = results_dict + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) backend = _awsbraket.AWSBraketBackend( verbose=True, @@ -574,10 +592,11 @@ def test_awsbraket_backend_functional_test(mock_boto3_client, functional_setup, assert prob_dict['00'] == pytest.approx(0.84) assert prob_dict['01'] == pytest.approx(0.06) + eng.flush(deallocate_qubits=True) + @has_boto3 -@patch('boto3.client') -def test_awsbraket_functional_test_as_engine(mock_boto3_client, functional_setup): +def test_awsbraket_functional_test_as_engine(mocker, functional_setup): ( creds, s3_folder, @@ -588,12 +607,15 @@ def test_awsbraket_functional_test_as_engine(mock_boto3_client, functional_setup results_dict, ) = functional_setup - mock_boto3_client.return_value = mock_boto3_client + mock_boto3_client = mocker.MagicMock( + spec=['search_devices', 'get_device', 'create_quantum_task', 'get_quantum_task', 'get_object'] + ) mock_boto3_client.search_devices.return_value = search_value mock_boto3_client.get_device.return_value = device_value mock_boto3_client.create_quantum_task.return_value = qtarntask mock_boto3_client.get_quantum_task.return_value = completed_value mock_boto3_client.get_object.return_value = copy.deepcopy(results_dict) + mocker.patch('boto3.client', return_value=mock_boto3_client, autospec=True) backend = _awsbraket.AWSBraketBackend( verbose=True, @@ -638,3 +660,5 @@ def test_awsbraket_functional_test_as_engine(mock_boto3_client, functional_setup assert eng.backend.received_commands[7].qubits[0][0].id == qureg[0].id assert eng.backend.received_commands[8].gate == H assert eng.backend.received_commands[8].qubits[0][0].id == qureg[1].id + + eng.flush(deallocate_qubits=True) diff --git a/projectq/backends/_ibm/_ibm.py b/projectq/backends/_ibm/_ibm.py index baa4de60c..c34057817 100755 --- a/projectq/backends/_ibm/_ibm.py +++ b/projectq/backends/_ibm/_ibm.py @@ -17,19 +17,8 @@ import random from projectq.cengines import BasicEngine -from projectq.meta import get_control_count, LogicalQubitIDTag -from projectq.ops import ( - NOT, - H, - Rx, - Ry, - Rz, - Measure, - Allocate, - Deallocate, - Barrier, - FlushGate, -) +from projectq.meta import get_control_count, LogicalQubitIDTag, has_negative_control +from projectq.ops import NOT, H, Rx, Ry, Rz, Measure, Allocate, Deallocate, Barrier, FlushGate from ._ibm_http_client import send, retrieve @@ -100,7 +89,11 @@ def is_available(self, cmd): Args: cmd (Command): Command for which to check availability """ + if has_negative_control(cmd): + return False + g = cmd.gate + if g == NOT and get_control_count(cmd) == 1: return True if get_control_count(cmd) == 0: diff --git a/projectq/backends/_ibm/_ibm_test.py b/projectq/backends/_ibm/_ibm_test.py index 27249161d..204878276 100755 --- a/projectq/backends/_ibm/_ibm_test.py +++ b/projectq/backends/_ibm/_ibm_test.py @@ -16,11 +16,8 @@ import pytest import math -from projectq.setups import restrictedgateset -from projectq import MainEngine from projectq.backends._ibm import _ibm -from projectq.cengines import BasicMapperEngine, DummyEngine - +from projectq.cengines import MainEngine, BasicMapperEngine, DummyEngine from projectq.ops import ( All, Allocate, @@ -43,6 +40,8 @@ H, CNOT, ) +from projectq.setups import restrictedgateset +from projectq.types import WeakQubitRef # Insure that no HTTP request can be made in all tests in this module @@ -91,6 +90,17 @@ def test_ibm_backend_is_available_control_not(num_ctrl_qubits, is_available): assert ibm_backend.is_available(cmd) == is_available +def test_ibm_backend_is_available_negative_control(): + backend = _ibm.IBMBackend() + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + + assert backend.is_available(Command(None, NOT, qubits=([qb0],), controls=[qb1])) + assert backend.is_available(Command(None, NOT, qubits=([qb0],), controls=[qb1], control_state='1')) + assert not backend.is_available(Command(None, NOT, qubits=([qb0],), controls=[qb1], control_state='0')) + + def test_ibm_backend_init(): backend = _ibm.IBMBackend(verbose=True, use_hardware=True) assert backend.qasm == "" diff --git a/projectq/backends/_ionq/_ionq.py b/projectq/backends/_ionq/_ionq.py index 055eaab61..6dfd52d20 100644 --- a/projectq/backends/_ionq/_ionq.py +++ b/projectq/backends/_ionq/_ionq.py @@ -17,7 +17,7 @@ import random from projectq.cengines import BasicEngine -from projectq.meta import LogicalQubitIDTag, get_control_count +from projectq.meta import LogicalQubitIDTag, get_control_count, has_negative_control from projectq.ops import ( Allocate, Barrier, @@ -143,6 +143,9 @@ def is_available(self, cmd): if gate in (Measure, Allocate, Deallocate, Barrier): return True + if has_negative_control(cmd): + return False + # CNOT gates. # NOTE: IonQ supports up to 7 control qubits num_ctrl_qubits = get_control_count(cmd) diff --git a/projectq/backends/_ionq/_ionq_mapper_test.py b/projectq/backends/_ionq/_ionq_mapper_test.py index bf5784984..4f7e70605 100644 --- a/projectq/backends/_ionq/_ionq_mapper_test.py +++ b/projectq/backends/_ionq/_ionq_mapper_test.py @@ -14,18 +14,19 @@ # limitations under the License. import pytest -from projectq import MainEngine from projectq.backends import Simulator from projectq.backends._ionq._ionq_mapper import BoundedQubitMapper +from projectq.cengines import MainEngine, DummyEngine from projectq.meta import LogicalQubitIDTag from projectq.ops import AllocateQubitGate, Command, DeallocateQubitGate from projectq.types import WeakQubitRef def test_cannot_allocate_past_max(): + mapper = BoundedQubitMapper(1) engine = MainEngine( - Simulator(), - engine_list=[BoundedQubitMapper(1)], + DummyEngine(), + engine_list=[mapper], verbose=True, ) engine.allocate_qubit() @@ -34,6 +35,9 @@ def test_cannot_allocate_past_max(): assert str(excinfo.value) == "Cannot allocate more than 1 qubits!" + # Avoid double error reporting + mapper.current_mapping = {0: 0, 1: 1} + def test_cannot_reallocate_same_qubit(): engine = MainEngine( @@ -74,26 +78,22 @@ def test_cannot_deallocate_unknown_qubit(): assert str(excinfo.value) == "Cannot deallocate a qubit that is not already allocated!" # but we can still deallocate an already allocated one - qubit_id = qureg[0].id - deallocate_cmd = Command( - engine=engine, - gate=DeallocateQubitGate(), - qubits=([WeakQubitRef(engine=engine, idx=qubit_id)],), - tags=[LogicalQubitIDTag(qubit_id)], - ) - engine.send([deallocate_cmd]) + engine.deallocate_qubit(qureg[0]) + del qureg + del engine def test_cannot_deallocate_same_qubit(): + mapper = BoundedQubitMapper(1) engine = MainEngine( Simulator(), - engine_list=[BoundedQubitMapper(1)], + engine_list=[mapper], verbose=True, ) qureg = engine.allocate_qubit() - qubit = qureg[0] - qubit_id = qubit.id - engine.deallocate_qubit(qubit) + qubit_id = qureg[0].id + engine.deallocate_qubit(qureg[0]) + with pytest.raises(RuntimeError) as excinfo: deallocate_cmd = Command( engine=engine, @@ -106,7 +106,7 @@ def test_cannot_deallocate_same_qubit(): assert str(excinfo.value) == "Cannot deallocate a qubit that is not already allocated!" -def test_flush_deallocates_all_qubits(monkeypatch): +def test_flush_deallocates_all_qubits(): mapper = BoundedQubitMapper(10) engine = MainEngine( Simulator(), diff --git a/projectq/backends/_ionq/_ionq_test.py b/projectq/backends/_ionq/_ionq_test.py index 46458e603..4156edb63 100644 --- a/projectq/backends/_ionq/_ionq_test.py +++ b/projectq/backends/_ionq/_ionq_test.py @@ -54,7 +54,7 @@ Y, Z, ) -from projectq.types import Qubit, WeakQubitRef +from projectq.types import WeakQubitRef @pytest.fixture(scope='function') @@ -126,6 +126,22 @@ def test_ionq_backend_is_available_control_not(num_ctrl_qubits, is_available): assert ionq_backend.is_available(cmd) is is_available +def test_ionq_backend_is_available_negative_control(): + backend = _ionq.IonQBackend() + + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + + 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, qb2])) + assert backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='11')) + assert not backend.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='01')) + + def test_ionq_backend_init(): """Test initialized backend has an empty circuit""" backend = _ionq.IonQBackend(verbose=True, use_hardware=True) @@ -331,7 +347,7 @@ def mock_retrieve(*args, **kwargs): assert prob_dict['00'] == pytest.approx(0.6) # Unknown qubit - invalid_qubit = [Qubit(eng, 10)] + invalid_qubit = [WeakQubitRef(eng, 10)] probs = eng.backend.get_probabilities(invalid_qubit) assert {'0': 1} == probs diff --git a/projectq/backends/_sim/_simulator.py b/projectq/backends/_sim/_simulator.py index 4aca230f6..3647136b6 100755 --- a/projectq/backends/_sim/_simulator.py +++ b/projectq/backends/_sim/_simulator.py @@ -21,15 +21,8 @@ import math import random from projectq.cengines import BasicEngine -from projectq.meta import get_control_count, LogicalQubitIDTag -from projectq.ops import ( - Measure, - FlushGate, - Allocate, - Deallocate, - BasicMathGate, - TimeEvolution, -) +from projectq.meta import get_control_count, LogicalQubitIDTag, has_negative_control +from projectq.ops import Measure, FlushGate, Allocate, Deallocate, BasicMathGate, TimeEvolution from projectq.types import WeakQubitRef FALLBACK_TO_PYSIM = False @@ -104,6 +97,9 @@ def is_available(self, cmd): Returns: True if it can be simulated and False otherwise. """ + if has_negative_control(cmd): + return False + if ( cmd.gate == Measure or cmd.gate == Allocate @@ -352,6 +348,7 @@ def _handle(self, cmd): Exception: If a non-single-qubit gate needs to be processed (which should never happen due to is_available). """ + if cmd.gate == Measure: assert get_control_count(cmd) == 0 ids = [qb.id for qr in cmd.qubits for qb in qr] @@ -428,6 +425,7 @@ def _handle(self, cmd): ) ) self._simulator.apply_controlled_gate(matrix.tolist(), ids, [qb.id for qb in cmd.control_qubits]) + if not self._gate_fusion: self._simulator.run() else: diff --git a/projectq/backends/_sim/_simulator_test.py b/projectq/backends/_sim/_simulator_test.py index 0b7f6d288..0d3cae90f 100755 --- a/projectq/backends/_sim/_simulator_test.py +++ b/projectq/backends/_sim/_simulator_test.py @@ -180,6 +180,20 @@ def test_simulator_is_available(sim): assert new_cmd.gate.cnt == 0 +def test_simulator_is_available_negative_control(sim): + qb0 = WeakQubitRef(engine=None, idx=0) + qb1 = WeakQubitRef(engine=None, idx=1) + qb2 = WeakQubitRef(engine=None, idx=2) + + assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1])) + assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='1')) + assert not sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1], control_state='0')) + + assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2])) + assert sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='11')) + assert not sim.is_available(Command(None, X, qubits=([qb0],), controls=[qb1, qb2], control_state='01')) + + def test_simulator_cheat(sim): # cheat function should return a tuple assert isinstance(sim.cheat(), tuple) @@ -490,6 +504,9 @@ def test_simulator_applyqubitoperator(sim, mapper): sim.apply_qubit_operator(op_Proj1, [qureg[0]]) assert sim.get_amplitude('000', qureg) == pytest.approx(0.0) + # TODO: this is suspicious... + eng.backend.set_wavefunction([1, 0, 0, 0, 0, 0, 0, 0], qureg) + def test_simulator_time_evolution(sim): N = 8 # number of qubits diff --git a/projectq/cengines/_basics.py b/projectq/cengines/_basics.py index 6a6f6c79d..9ac53bb6f 100755 --- a/projectq/cengines/_basics.py +++ b/projectq/cengines/_basics.py @@ -196,6 +196,7 @@ def send(self, command_list): """ Forward the list of commands to the next engine in the pipeline. """ + self.next_engine.receive(command_list) diff --git a/projectq/cengines/_main.py b/projectq/cengines/_main.py index 3eaea3ef8..475207dd1 100755 --- a/projectq/cengines/_main.py +++ b/projectq/cengines/_main.py @@ -300,7 +300,7 @@ def flush(self, deallocate_qubits=False): id to -1). """ if deallocate_qubits: - while len(self.active_qubits): + while [qb for qb in self.active_qubits if qb is not None]: qb = self.active_qubits.pop() qb.__del__() self.receive([Command(self, FlushGate(), ([WeakQubitRef(self, -1)],))]) diff --git a/projectq/cengines/_replacer/_replacer.py b/projectq/cengines/_replacer/_replacer.py index 0ad153297..07ea5d3fb 100755 --- a/projectq/cengines/_replacer/_replacer.py +++ b/projectq/cengines/_replacer/_replacer.py @@ -126,10 +126,6 @@ def _process_command(self, cmd): if self.is_available(cmd): self.send([cmd]) else: - # check for decomposition rules - decomp_list = [] - potential_decomps = [] - # First check for a decomposition rules of the gate class, then # the gate class of the inverse gate. If nothing is found, do the # same for the first parent class, etc. @@ -138,40 +134,54 @@ def _process_command(self, cmd): # DaggeredGate, BasicGate, object. Hence don't check the last two inverse_mro = type(get_inverse(cmd.gate)).mro()[:-2] rules = self.decompositionRuleSet.decompositions - for level in range(max(len(gate_mro), len(inverse_mro))): - # Check for forward rules - if level < len(gate_mro): - class_name = gate_mro[level].__name__ - try: - potential_decomps = [d for d in rules[class_name]] - except KeyError: - pass - # throw out the ones which don't recognize the command - for d in potential_decomps: - if d.check(cmd): - decomp_list.append(d) - if len(decomp_list) != 0: - break - # Check for rules implementing the inverse gate - # and run them in reverse - if level < len(inverse_mro): - inv_class_name = inverse_mro[level].__name__ - try: - potential_decomps += [d.get_inverse_decomposition() for d in rules[inv_class_name]] - except KeyError: - pass - # throw out the ones which don't recognize the command - for d in potential_decomps: - if d.check(cmd): - decomp_list.append(d) - if len(decomp_list) != 0: - break - - if len(decomp_list) == 0: - raise NoGateDecompositionError("\nNo replacement found for " + str(cmd) + "!") - - # use decomposition chooser to determine the best decomposition - chosen_decomp = self._decomp_chooser(cmd, decomp_list) + + # If the decomposition rule to remove negatively controlled qubits is present in the list of potential + # decompositions, we process it immediately, before any other decompositions. + controlstate_rule = [ + rule for rule in rules.get('BasicGate', []) if rule.decompose.__name__ == '_decompose_controlstate' + ] + if controlstate_rule and controlstate_rule[0].check(cmd): + chosen_decomp = controlstate_rule[0] + else: + # check for decomposition rules + decomp_list = [] + potential_decomps = [] + + for level in range(max(len(gate_mro), len(inverse_mro))): + # Check for forward rules + if level < len(gate_mro): + class_name = gate_mro[level].__name__ + try: + potential_decomps = [d for d in rules[class_name]] + except KeyError: + pass + # throw out the ones which don't recognize the command + for d in potential_decomps: + if d.check(cmd): + decomp_list.append(d) + if len(decomp_list) != 0: + break + # Check for rules implementing the inverse gate + # and run them in reverse + if level < len(inverse_mro): + inv_class_name = inverse_mro[level].__name__ + try: + potential_decomps += [d.get_inverse_decomposition() for d in rules[inv_class_name]] + except KeyError: + pass + # throw out the ones which don't recognize the command + for d in potential_decomps: + if d.check(cmd): + decomp_list.append(d) + if len(decomp_list) != 0: + break + + if len(decomp_list) == 0: + raise NoGateDecompositionError("\nNo replacement found for " + str(cmd) + "!") + + # use decomposition chooser to determine the best decomposition + chosen_decomp = self._decomp_chooser(cmd, decomp_list) + # the decomposed command must have the same tags # (plus the ones it gets from meta-statements inside the # decomposition rule). diff --git a/projectq/cengines/_replacer/_replacer_test.py b/projectq/cengines/_replacer/_replacer_test.py index a63e43c87..b2675f191 100755 --- a/projectq/cengines/_replacer/_replacer_test.py +++ b/projectq/cengines/_replacer/_replacer_test.py @@ -23,6 +23,7 @@ ClassicalInstructionGate, Command, H, + S, NotInvertible, Rx, X, @@ -232,3 +233,50 @@ def test_gate_filter_func(self, cmd): eng.flush() received_gate = backend.received_commands[1].gate assert received_gate == X or received_gate == H + + +def test_auto_replacer_priorize_controlstate_rule(): + # Check that when a control state is given and it has negative control, + # Autoreplacer prioritizes the corresponding decomposition rule before anything else. + # (Decomposition rule should have name _decompose_controlstate) + + # Create test gate and inverse + class ControlGate(BasicGate): + pass + + def _decompose_controlstate(cmd): + S | cmd.qubits + + def _decompose_random(cmd): + H | cmd.qubits + + def control_filter(self, cmd): + if cmd.gate == ControlGate(): + return False + return True + + rule_set.add_decomposition_rule(DecompositionRule(BasicGate, _decompose_random)) + + backend = DummyEngine(save_commands=True) + eng = MainEngine( + backend=backend, engine_list=[_replacer.AutoReplacer(rule_set), _replacer.InstructionFilter(control_filter)] + ) + assert len(backend.received_commands) == 0 + qb = eng.allocate_qubit() + ControlGate() | qb + eng.flush() + assert len(backend.received_commands) == 3 + assert backend.received_commands[1].gate == H + + rule_set.add_decomposition_rule(DecompositionRule(BasicGate, _decompose_controlstate)) + + backend = DummyEngine(save_commands=True) + eng = MainEngine( + backend=backend, engine_list=[_replacer.AutoReplacer(rule_set), _replacer.InstructionFilter(control_filter)] + ) + assert len(backend.received_commands) == 0 + qb = eng.allocate_qubit() + ControlGate() | qb + eng.flush() + assert len(backend.received_commands) == 3 + assert backend.received_commands[1].gate == S diff --git a/projectq/meta/__init__.py b/projectq/meta/__init__.py index adb5719e8..ce321f0f0 100755 --- a/projectq/meta/__init__.py +++ b/projectq/meta/__init__.py @@ -25,7 +25,7 @@ from ._dirtyqubit import DirtyQubitTag from ._loop import LoopTag, Loop from ._compute import Compute, Uncompute, CustomUncompute, ComputeTag, UncomputeTag -from ._control import Control, get_control_count +from ._control import Control, get_control_count, has_negative_control, canonical_ctrl_state from ._dagger import Dagger from ._util import insert_engine, drop_engine_after from ._logicalqubit import LogicalQubitIDTag diff --git a/projectq/meta/_control.py b/projectq/meta/_control.py index b50848574..704c76374 100755 --- a/projectq/meta/_control.py +++ b/projectq/meta/_control.py @@ -28,6 +28,61 @@ from projectq.ops import ClassicalInstructionGate from projectq.types import BasicQubit from ._util import insert_engine, drop_engine_after +from projectq.ops import CtrlAll + + +def canonical_ctrl_state(ctrl_state, num_qubits): + """ + Return canonical form for control state + + Args: + ctrl_state (int,str,CtrlAll): Initial control state representation + num_qubits (int): number of control qubits + + Returns: + Canonical form of control state (currently a string composed of '0' and '1') + + Note: + In case of integer values for `ctrl_state`, the least significant bit applies to the first qubit in the qubit + register, e.g. if ctrl_state == 2, its binary representation if '10' with the least significan bit being 0. + This means in particular that the followings are equivalent: + + .. code-block:: python + + canonical_ctrl_state(6, 3) == canonical_ctrl_state(6, '110') + """ + if not num_qubits: + return '' + + if isinstance(ctrl_state, CtrlAll): + if ctrl_state == CtrlAll.One: + return '1' * num_qubits + return '0' * num_qubits + + if isinstance(ctrl_state, int): + # If the user inputs an integer, convert it to binary bit string + converted_str = '{0:b}'.format(ctrl_state).zfill(num_qubits)[::-1] + if len(converted_str) != num_qubits: + raise ValueError( + 'Control state specified as {} ({}) is higher than maximum for {} qubits: {}'.format( + ctrl_state, converted_str, num_qubits, 2 ** num_qubits - 1 + ) + ) + return converted_str + + if isinstance(ctrl_state, str): + # If the user inputs bit string, directly use it + if len(ctrl_state) != num_qubits: + raise ValueError( + 'Control state {} has different length than the number of control qubits {}'.format( + ctrl_state, num_qubits + ) + ) + if not set(ctrl_state).issubset({'0', '1'}): + raise ValueError('Control state {} has string other than 1 and 0'.format(ctrl_state)) + return ctrl_state + + raise TypeError('Input must be a string, an integer or an enum value of class State') class ControlEngine(BasicEngine): @@ -35,7 +90,7 @@ class ControlEngine(BasicEngine): Adds control qubits to all commands that have no compute / uncompute tags. """ - def __init__(self, qubits): + def __init__(self, qubits, ctrl_state=CtrlAll.One): """ Initialize the control engine. @@ -45,6 +100,7 @@ def __init__(self, qubits): """ BasicEngine.__init__(self) self._qubits = qubits + self._state = ctrl_state def _has_compute_uncompute_tag(self, cmd): """ @@ -60,7 +116,7 @@ def _has_compute_uncompute_tag(self, cmd): def _handle_command(self, cmd): if not self._has_compute_uncompute_tag(cmd) and not isinstance(cmd.gate, ClassicalInstructionGate): - cmd.add_control_qubits(self._qubits) + cmd.add_control_qubits(self._qubits, self._state) self.send([cmd]) def receive(self, command_list): @@ -79,7 +135,7 @@ class Control(object): do_something(otherqubits) """ - def __init__(self, engine, qubits): + def __init__(self, engine, qubits, ctrl_state=CtrlAll.One): """ Enter a controlled section. @@ -99,10 +155,11 @@ def __init__(self, engine, qubits): if isinstance(qubits, BasicQubit): qubits = [qubits] self._qubits = qubits + self._state = canonical_ctrl_state(ctrl_state, len(self._qubits)) def __enter__(self): if len(self._qubits) > 0: - ce = ControlEngine(self._qubits) + ce = ControlEngine(self._qubits, self._state) insert_engine(self.engine, ce) def __exit__(self, type, value, traceback): @@ -116,3 +173,10 @@ def get_control_count(cmd): Return the number of control qubits of the command object cmd """ return len(cmd.control_qubits) + + +def has_negative_control(cmd): + """ + Returns whether a command has negatively controlled qubits + """ + return get_control_count(cmd) > 0 and '0' in cmd.control_state diff --git a/projectq/meta/_control_test.py b/projectq/meta/_control_test.py index 601252e04..cc79c4591 100755 --- a/projectq/meta/_control_test.py +++ b/projectq/meta/_control_test.py @@ -13,13 +13,64 @@ # See the License for the specific language governing permissions and # limitations under the License. """Tests for projectq.meta._control.py""" +import pytest from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.ops import Command, H, Rx +from projectq.ops import Command, H, Rx, CtrlAll, X, IncompatibleControlState from projectq.meta import DirtyQubitTag, ComputeTag, UncomputeTag, Compute, Uncompute from projectq.meta import _control +from projectq.types import WeakQubitRef + + +def test_canonical_representation(): + assert _control.canonical_ctrl_state(0, 0) == '' + for num_qubits in range(4): + assert _control.canonical_ctrl_state(0, num_qubits) == '0' * num_qubits + + num_qubits = 4 + for i in range(2 ** num_qubits): + state = '{0:0b}'.format(i).zfill(num_qubits) + assert _control.canonical_ctrl_state(i, num_qubits) == state[::-1] + assert _control.canonical_ctrl_state(state, num_qubits) == state + + for num_qubits in range(10): + assert _control.canonical_ctrl_state(CtrlAll.Zero, num_qubits) == '0' * num_qubits + assert _control.canonical_ctrl_state(CtrlAll.One, num_qubits) == '1' * num_qubits + + with pytest.raises(TypeError): + _control.canonical_ctrl_state(1.1, 2) + + with pytest.raises(ValueError): + _control.canonical_ctrl_state('1', 2) + + with pytest.raises(ValueError): + _control.canonical_ctrl_state('11111', 2) + + with pytest.raises(ValueError): + _control.canonical_ctrl_state('1a', 2) + + with pytest.raises(ValueError): + _control.canonical_ctrl_state(4, 2) + + +def test_has_negative_control(): + qubit0 = WeakQubitRef(None, 0) + qubit1 = WeakQubitRef(None, 0) + qubit2 = WeakQubitRef(None, 0) + qubit3 = WeakQubitRef(None, 0) + assert not _control.has_negative_control(Command(None, H, ([qubit0],))) + assert not _control.has_negative_control(Command(None, H, ([qubit0],), [qubit1])) + assert not _control.has_negative_control(Command(None, H, ([qubit0],), [qubit1], control_state=CtrlAll.One)) + assert _control.has_negative_control(Command(None, H, ([qubit0],), [qubit1], control_state=CtrlAll.Zero)) + assert _control.has_negative_control( + Command(None, H, ([qubit0],), [qubit1, qubit2, qubit3], control_state=CtrlAll.Zero) + ) + assert not _control.has_negative_control( + Command(None, H, ([qubit0],), [qubit1, qubit2, qubit3], control_state='111') + ) + assert _control.has_negative_control(Command(None, H, ([qubit0],), [qubit1, qubit2, qubit3], control_state='101')) def test_control_engine_has_compute_tag(): @@ -31,7 +82,7 @@ def test_control_engine_has_compute_tag(): test_cmd0.tags = [DirtyQubitTag(), ComputeTag(), DirtyQubitTag()] test_cmd1.tags = [DirtyQubitTag(), UncomputeTag(), DirtyQubitTag()] test_cmd2.tags = [DirtyQubitTag()] - control_eng = _control.ControlEngine("MockEng") + control_eng = _control.ControlEngine("MockEng", ctrl_state=CtrlAll.One) assert control_eng._has_compute_uncompute_tag(test_cmd0) assert control_eng._has_compute_uncompute_tag(test_cmd1) assert not control_eng._has_compute_uncompute_tag(test_cmd2) @@ -62,3 +113,59 @@ def test_control(): assert backend.received_commands[4].control_qubits[0].id == qureg[0].id assert backend.received_commands[4].control_qubits[1].id == qureg[1].id assert backend.received_commands[6].control_qubits[0].id == qureg[0].id + + +def test_control_state(): + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[DummyEngine()]) + + qureg = eng.allocate_qureg(3) + xreg = eng.allocate_qureg(3) + X | qureg[1] + with _control.Control(eng, qureg[0], '0'): + with Compute(eng): + X | xreg[0] + + X | xreg[1] + Uncompute(eng) + + with _control.Control(eng, qureg[1:], 2): + X | xreg[2] + eng.flush() + + assert len(backend.received_commands) == 6 + 5 + 1 + assert len(backend.received_commands[0].control_qubits) == 0 + assert len(backend.received_commands[1].control_qubits) == 0 + assert len(backend.received_commands[2].control_qubits) == 0 + assert len(backend.received_commands[3].control_qubits) == 0 + assert len(backend.received_commands[4].control_qubits) == 0 + assert len(backend.received_commands[5].control_qubits) == 0 + + assert len(backend.received_commands[6].control_qubits) == 0 + assert len(backend.received_commands[7].control_qubits) == 0 + assert len(backend.received_commands[8].control_qubits) == 1 + assert len(backend.received_commands[9].control_qubits) == 0 + assert len(backend.received_commands[10].control_qubits) == 2 + + assert len(backend.received_commands[11].control_qubits) == 0 + + assert backend.received_commands[8].control_qubits[0].id == qureg[0].id + assert backend.received_commands[8].control_state == '0' + assert backend.received_commands[10].control_qubits[0].id == qureg[1].id + assert backend.received_commands[10].control_qubits[1].id == qureg[2].id + assert backend.received_commands[10].control_state == '01' + + assert _control.has_negative_control(backend.received_commands[8]) + assert _control.has_negative_control(backend.received_commands[10]) + + +def test_control_state_contradiction(): + backend = DummyEngine(save_commands=True) + eng = MainEngine(backend=backend, engine_list=[DummyEngine()]) + qureg = eng.allocate_qureg(1) + with pytest.raises(IncompatibleControlState): + with _control.Control(eng, qureg[0], '0'): + qubit = eng.allocate_qubit() + with _control.Control(eng, qureg[0], '1'): + H | qubit + eng.flush() diff --git a/projectq/ops/__init__.py b/projectq/ops/__init__.py index 79f50e32b..21e4e4031 100755 --- a/projectq/ops/__init__.py +++ b/projectq/ops/__init__.py @@ -25,16 +25,8 @@ BasicMathGate, BasicPhaseGate, ) -from ._command import apply_command, Command -from ._metagates import ( - DaggeredGate, - get_inverse, - is_identity, - ControlledGate, - C, - Tensor, - All, -) +from ._command import apply_command, Command, CtrlAll, IncompatibleControlState +from ._metagates import DaggeredGate, get_inverse, is_identity, ControlledGate, C, Tensor, All from ._gates import * from ._qftgate import QFT, QFTGate from ._qubit_operator import QubitOperator diff --git a/projectq/ops/_command.py b/projectq/ops/_command.py index 626b0b233..8cd4061d4 100755 --- a/projectq/ops/_command.py +++ b/projectq/ops/_command.py @@ -40,9 +40,24 @@ """ from copy import deepcopy +import itertools import projectq from projectq.types import WeakQubitRef, Qureg +from enum import IntEnum + + +class IncompatibleControlState(Exception): + """ + Exception thrown when trying to set two incompatible states for a control qubit. + """ + + pass + + +class CtrlAll(IntEnum): + Zero = 0 + One = 1 def apply_command(cmd): @@ -84,7 +99,7 @@ class Command(object): all_qubits: A tuple of control_qubits + qubits """ - def __init__(self, engine, gate, qubits, controls=(), tags=()): + def __init__(self, engine, gate, qubits, controls=(), tags=(), control_state=CtrlAll.One): """ Initialize a Command object. @@ -106,6 +121,8 @@ def __init__(self, engine, gate, qubits, controls=(), tags=()): Qubits that condition the command. tags (list[object]): Tags associated with the command. + control_state(int,str,projectq.meta.CtrlAll) + Control state for any control qubits """ qubits = tuple([WeakQubitRef(qubit.engine, qubit.id) for qubit in qreg] for qreg in qubits) @@ -115,6 +132,7 @@ def __init__(self, engine, gate, qubits, controls=(), tags=()): self.qubits = qubits # property self.control_qubits = controls # property self.engine = engine # property + self.control_state = control_state # property @property def qubits(self): @@ -235,7 +253,24 @@ def control_qubits(self, qubits): self._control_qubits = [WeakQubitRef(qubit.engine, qubit.id) for qubit in qubits] self._control_qubits = sorted(self._control_qubits, key=lambda x: x.id) - def add_control_qubits(self, qubits): + @property + def control_state(self): + return self._control_state + + @control_state.setter + def control_state(self, state): + """ + Set control_state to state + + Args: + state (int,str,projectq.meta.CtrtAll): state of control qubit (ie. positive or negative) + """ + # NB: avoid circular imports + from projectq.meta import canonical_ctrl_state + + self._control_state = canonical_ctrl_state(state, len(self._control_qubits)) + + def add_control_qubits(self, qubits, state=CtrlAll.One): """ Add (additional) control qubits to this command object. @@ -244,13 +279,29 @@ def add_control_qubits(self, qubits): thus early deallocation of qubits. Args: - qubits (list of Qubit objects): List of qubits which control this - gate, i.e., the gate is only executed if all qubits are - in state 1. + qubits (list of Qubit objects): List of qubits which control this gate + state (int,str,CtrlAll): Control state (ie. positive or negative) for the qubits being added as + control qubits. """ + # NB: avoid circular imports + from projectq.meta import canonical_ctrl_state + assert isinstance(qubits, list) self._control_qubits.extend([WeakQubitRef(qubit.engine, qubit.id) for qubit in qubits]) - self._control_qubits = sorted(self._control_qubits, key=lambda x: x.id) + self._control_state += canonical_ctrl_state(state, len(qubits)) + + zipped = sorted(zip(self._control_qubits, self._control_state), key=lambda x: x[0].id) + unzipped_qubit, unzipped_state = zip(*zipped) + self._control_qubits, self._control_state = list(unzipped_qubit), ''.join(unzipped_state) + + # Make sure that we do not have contradicting control states for any control qubits + for _, data in itertools.groupby(zipped, key=lambda x: x[0].id): + qubits, states = list(zip(*data)) + assert len(set(qubits)) == 1 # This should be by design... + if len(set(states)) != 1: + raise IncompatibleControlState( + 'Control qubits {} cannot have conflicting control states: {}'.format(list(qubits), states) + ) @property def all_qubits(self): diff --git a/projectq/ops/_command_test.py b/projectq/ops/_command_test.py index d4823df66..eb4cb6681 100755 --- a/projectq/ops/_command_test.py +++ b/projectq/ops/_command_test.py @@ -21,8 +21,8 @@ from projectq import MainEngine from projectq.cengines import DummyEngine -from projectq.meta import ComputeTag -from projectq.ops import BasicGate, Rx, NotMergeable +from projectq.meta import ComputeTag, canonical_ctrl_state +from projectq.ops import BasicGate, Rx, NotMergeable, CtrlAll from projectq.types import Qubit, Qureg, WeakQubitRef from projectq.ops import _command @@ -183,14 +183,45 @@ def test_command_interchangeable_qubit_indices(main_engine): ) -def test_commmand_add_control_qubits(main_engine): +@pytest.mark.parametrize( + 'state', + [0, 1, '0', '1', CtrlAll.One, CtrlAll.Zero], + ids=['int(0)', 'int(1)', 'str(0)', 'str(1)', 'CtrlAll.One', 'CtrlAll.Zero'], +) +def test_commmand_add_control_qubits_one(main_engine, state): qubit0 = Qureg([Qubit(main_engine, 0)]) qubit1 = Qureg([Qubit(main_engine, 1)]) - qubit2 = Qureg([Qubit(main_engine, 2)]) cmd = _command.Command(main_engine, Rx(0.5), (qubit0,)) - cmd.add_control_qubits(qubit2 + qubit1) + cmd.add_control_qubits(qubit1, state=state) + assert cmd.control_qubits[0].id == 1 + assert cmd.control_state == canonical_ctrl_state(state, 1) + + +@pytest.mark.parametrize( + 'state', + [0, 1, 2, 3, '00', '01', '10', '11', CtrlAll.One, CtrlAll.Zero], + ids=[ + 'int(0)', + 'int(1)', + 'int(2)', + 'int(3)', + 'str(00)', + 'str(01)', + 'str(10)', + 'str(1)', + 'CtrlAll.One', + 'CtrlAll.Zero', + ], +) +def test_commmand_add_control_qubits_two(main_engine, state): + qubit0 = Qureg([Qubit(main_engine, 0)]) + qubit1 = Qureg([Qubit(main_engine, 1)]) + qubit2 = Qureg([Qubit(main_engine, 2)]) + qubit3 = Qureg([Qubit(main_engine, 3)]) + cmd = _command.Command(main_engine, Rx(0.5), (qubit0,), qubit1) + cmd.add_control_qubits(qubit2 + qubit3, state) assert cmd.control_qubits[0].id == 1 - assert cmd.control_qubits[1].id == 2 + assert cmd.control_state == '1' + canonical_ctrl_state(state, 2) def test_command_all_qubits(main_engine): diff --git a/projectq/setups/decompositions/__init__.py b/projectq/setups/decompositions/__init__.py index 5fabb8dbd..cca7d08c9 100755 --- a/projectq/setups/decompositions/__init__.py +++ b/projectq/setups/decompositions/__init__.py @@ -21,6 +21,7 @@ cnot2rxx, cnot2cz, cnu2toffoliandcu, + controlstate, entangle, globalphase, h2rx, @@ -51,6 +52,7 @@ cnot2rxx, cnot2cz, cnu2toffoliandcu, + controlstate, entangle, globalphase, h2rx, diff --git a/projectq/setups/decompositions/controlstate.py b/projectq/setups/decompositions/controlstate.py new file mode 100755 index 000000000..26a3e5ae7 --- /dev/null +++ b/projectq/setups/decompositions/controlstate.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 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. + +""" +Register a decomposition to replace turn negatively controlled qubits into positively controlled qubits by applying X +gates. +""" + +from copy import deepcopy +from projectq.cengines import DecompositionRule +from projectq.meta import Compute, Uncompute, has_negative_control +from projectq.ops import BasicGate, X + + +def _decompose_controlstate(cmd): + """ + Decompose commands with control qubits in negative state (ie. control + qubits with state '0' instead of '1') + """ + with Compute(cmd.engine): + for state, ctrl in zip(cmd.control_state, cmd.control_qubits): + if state == '0': + X | ctrl + + # Resend the command with the `control_state` cleared + cmd.ctrl_state = '1' * len(cmd.control_state) + orig_engine = cmd.engine + cmd.engine.receive([deepcopy(cmd)]) # NB: deepcopy required here to workaround infinite recursion detection + Uncompute(orig_engine) + + +#: Decomposition rules +all_defined_decomposition_rules = [DecompositionRule(BasicGate, _decompose_controlstate, has_negative_control)] diff --git a/projectq/setups/decompositions/controlstate_test.py b/projectq/setups/decompositions/controlstate_test.py new file mode 100755 index 000000000..a74538b58 --- /dev/null +++ b/projectq/setups/decompositions/controlstate_test.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Copyright 2021 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 the controlstate decomposition rule. +""" + +from projectq import MainEngine +from projectq.cengines import DummyEngine, AutoReplacer, InstructionFilter, DecompositionRuleSet +from projectq.meta import Control, has_negative_control +from projectq.ops import X +from projectq.setups.decompositions import controlstate, cnot2cz + + +def filter_func(eng, cmd): + if has_negative_control(cmd): + return False + return True + + +def test_controlstate_priority(): + saving_backend = DummyEngine(save_commands=True) + rule_set = DecompositionRuleSet(modules=[cnot2cz, controlstate]) + eng = MainEngine(backend=saving_backend, engine_list=[AutoReplacer(rule_set), InstructionFilter(filter_func)]) + qubit1 = eng.allocate_qubit() + qubit2 = eng.allocate_qubit() + qubit3 = eng.allocate_qubit() + with Control(eng, qubit2, ctrl_state='0'): + X | qubit1 + with Control(eng, qubit3, ctrl_state='1'): + X | qubit1 + eng.flush() + + assert len(saving_backend.received_commands) == 8 + for cmd in saving_backend.received_commands: + assert not has_negative_control(cmd) diff --git a/projectq/setups/default.py b/projectq/setups/default.py index 8f9edeb30..b31d98fcf 100755 --- a/projectq/setups/default.py +++ b/projectq/setups/default.py @@ -12,6 +12,7 @@ # 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. + """ Defines the default setup which provides an `engine_list` for the `MainEngine` @@ -21,20 +22,9 @@ import projectq import projectq.setups.decompositions -from projectq.cengines import ( - TagRemover, - LocalOptimizer, - AutoReplacer, - DecompositionRuleSet, -) +from projectq.cengines import TagRemover, LocalOptimizer, AutoReplacer, DecompositionRuleSet def get_engine_list(): rule_set = DecompositionRuleSet(modules=[projectq.setups.decompositions]) - return [ - TagRemover(), - LocalOptimizer(10), - AutoReplacer(rule_set), - TagRemover(), - LocalOptimizer(10), - ] + return [TagRemover(), LocalOptimizer(10), AutoReplacer(rule_set), TagRemover(), LocalOptimizer(10)] diff --git a/pyproject.toml b/pyproject.toml index c7034d6fe..e2d959ca0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -73,6 +73,7 @@ build-backend = "setuptools.build_meta" minversion = '6.0' addopts = '-pno:warnings' testpaths = ['projectq'] +mock_use_standalone_module = true [tool.setuptools_scm] diff --git a/requirements_tests.txt b/requirements_tests.txt index ab10bc7de..ea01acbbc 100644 --- a/requirements_tests.txt +++ b/requirements_tests.txt @@ -1,3 +1,5 @@ flaky +mock pytest >= 6.0 pytest-cov +pytest-mock