From ed2ec1233652e4f8a2e60dec63f3789d38587337 Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Tue, 6 May 2025 16:41:36 -0400 Subject: [PATCH 01/18] initial commit, feature branch From 5c914980aefb2715e3813cb706867912624d102b Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Tue, 6 May 2025 16:57:16 -0400 Subject: [PATCH 02/18] removed legacy tests --- .../ops/op_math/test_controlled_ops.py | 1288 ----------------- .../ops/op_math/test_symbolic.py | 598 -------- .../ops/qubit/test_non_parametric_ops.py | 834 ----------- .../qubit/test_parametric_ops_multi_qubit.py | 551 ------- .../qubit/test_parametric_ops_single_qubit.py | 460 ------ .../ops/qubit/test_qchem_ops.py | 326 ----- .../resource_estimation/ops/test_identity.py | 191 --- .../templates/test_resource_basisrotation.py | 99 -- .../templates/test_resource_prepselprep.py | 190 --- .../templates/test_resource_qpe.py | 143 -- .../templates/test_resource_qubitization.py | 176 --- .../templates/test_resource_reflection.py | 106 -- .../templates/test_resource_select.py | 190 --- .../templates/test_resource_stateprep.py | 396 ----- .../templates/test_resource_trotter.py | 321 ---- .../templates/test_subroutines.py | 1041 ------------- .../test_resource_container.py | 392 ----- .../test_resource_operator.py | 132 -- .../test_resource_tracking.py | 357 ----- 19 files changed, 7791 deletions(-) delete mode 100644 pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py delete mode 100644 pennylane/labs/tests/resource_estimation/ops/op_math/test_symbolic.py delete mode 100644 pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py delete mode 100644 pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_multi_qubit.py delete mode 100644 pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py delete mode 100644 pennylane/labs/tests/resource_estimation/ops/qubit/test_qchem_ops.py delete mode 100644 pennylane/labs/tests/resource_estimation/ops/test_identity.py delete mode 100644 pennylane/labs/tests/resource_estimation/templates/test_resource_basisrotation.py delete mode 100644 pennylane/labs/tests/resource_estimation/templates/test_resource_prepselprep.py delete mode 100644 pennylane/labs/tests/resource_estimation/templates/test_resource_qpe.py delete mode 100644 pennylane/labs/tests/resource_estimation/templates/test_resource_qubitization.py delete mode 100644 pennylane/labs/tests/resource_estimation/templates/test_resource_reflection.py delete mode 100644 pennylane/labs/tests/resource_estimation/templates/test_resource_select.py delete mode 100644 pennylane/labs/tests/resource_estimation/templates/test_resource_stateprep.py delete mode 100644 pennylane/labs/tests/resource_estimation/templates/test_resource_trotter.py delete mode 100644 pennylane/labs/tests/resource_estimation/templates/test_subroutines.py delete mode 100644 pennylane/labs/tests/resource_estimation/test_resource_container.py delete mode 100644 pennylane/labs/tests/resource_estimation/test_resource_operator.py delete mode 100644 pennylane/labs/tests/resource_estimation/test_resource_tracking.py diff --git a/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py b/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py deleted file mode 100644 index b41bacb935e..00000000000 --- a/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py +++ /dev/null @@ -1,1288 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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 controlled resource operators. -""" -import pytest - -import pennylane.labs.resource_estimation as re - -# pylint: disable=no-self-use, use-implicit-booleaness-not-comparison,too-many-arguments,too-many-positional-arguments - - -class TestResourceCH: - """Test the ResourceCH operation""" - - op = re.ResourceCH(wires=[0, 1]) - - def test_resources(self): - """Test that the resources method produces the expected resources.""" - - expected_resources = { - re.ResourceRY.resource_rep(): 2, - re.ResourceHadamard.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 1, - } - assert self.op.resources(**self.op.resource_params) == expected_resources - - def test_resource_rep(self): - """Test the resource_rep produces the correct compressed representation.""" - expected_rep = re.CompressedResourceOp(re.ResourceCH, {}) - assert self.op.resource_rep(**self.op.resource_params) == expected_rep - - def test_resource_params(self): - """Test that the resource_params are produced as expected.""" - expected_params = {} - assert self.op.resource_params == expected_params - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - expected_res = {self.op.resource_rep(): 1} - op2 = re.ResourceAdjoint(self.op) - - assert self.op.adjoint_resource_decomp() == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceControlled.resource_rep(re.ResourceHadamard, {}, 2, 0, 0): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - {re.ResourceControlled.resource_rep(re.ResourceHadamard, {}, 3, 0, 1): 1}, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceControlled.resource_rep(re.ResourceHadamard, {}, 4, 2, 2): 1}, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op2 = re.ResourceControlled( - self.op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {op.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (5, {op.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op2 = re.ResourcePow(self.op, z) - - assert self.op.pow_resource_decomp(z) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - -class TestResourceCY: - """Test the ResourceCY operation""" - - op = re.ResourceCY(wires=[0, 1]) - - def test_resources(self): - """Test that the resources method produces the expected resources.""" - - expected_resources = { - re.ResourceS.resource_rep(): 1, - re.ResourceCNOT.resource_rep(): 1, - re.ResourceAdjoint.resource_rep(re.ResourceS, {}): 1, - } - assert self.op.resources(**self.op.resource_params) == expected_resources - - def test_resource_rep(self): - """Test the resource_rep produces the correct compressed representation.""" - expected_rep = re.CompressedResourceOp(re.ResourceCY, {}) - assert self.op.resource_rep(**self.op.resource_params) == expected_rep - - def test_resource_params(self): - """Test that the resource_params are produced as expected.""" - expected_params = {} - assert self.op.resource_params == expected_params - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - expected_res = {self.op.resource_rep(): 1} - op2 = re.ResourceAdjoint(self.op) - - assert self.op.adjoint_resource_decomp() == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceControlled.resource_rep(re.ResourceY, {}, 2, 0, 0): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - {re.ResourceControlled.resource_rep(re.ResourceY, {}, 3, 0, 1): 1}, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceControlled.resource_rep(re.ResourceY, {}, 4, 2, 2): 1}, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op2 = re.ResourceControlled( - self.op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {op.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (5, {op.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op2 = re.ResourcePow(self.op, z) - - assert self.op.pow_resource_decomp(z) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - -class TestResourceCZ: - """Test the ResourceCZ operation""" - - op = re.ResourceCZ(wires=[0, 1]) - - def test_resources(self): - """Test that the resources method produces the expected resources.""" - - expected_resources = { - re.ResourceHadamard.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 1, - } - assert self.op.resources(**self.op.resource_params) == expected_resources - - def test_resource_rep(self): - """Test the resource_rep produces the correct compressed representation.""" - expected_rep = re.CompressedResourceOp(re.ResourceCZ, {}) - assert self.op.resource_rep() == expected_rep - - def test_resource_params(self): - """Test that the resource_params are produced as expected.""" - expected_params = {} - assert self.op.resource_params == expected_params - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - expected_res = {self.op.resource_rep(): 1} - op2 = re.ResourceAdjoint(self.op) - - assert self.op.adjoint_resource_decomp() == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceCCZ.resource_rep(): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - {re.ResourceControlled.resource_rep(re.ResourceZ, {}, 3, 0, 1): 1}, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceControlled.resource_rep(re.ResourceZ, {}, 4, 2, 2): 1}, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op2 = re.ResourceControlled( - self.op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {op.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (5, {op.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op2 = re.ResourcePow(self.op, z) - - assert self.op.pow_resource_decomp(z) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - -class TestResourceCSWAP: - """Test the ResourceCSWAP operation""" - - op = re.ResourceCSWAP(wires=[0, 1, 2]) - - def test_resources(self): - """Test that the resources method produces the expected resources.""" - expected_resources = { - re.ResourceToffoli.resource_rep(): 1, - re.ResourceCNOT.resource_rep(): 2, - } - assert self.op.resources(**self.op.resource_params) == expected_resources - - def test_resource_rep(self): - """Test the resource_rep produces the correct compressed representation.""" - expected_rep = re.CompressedResourceOp(re.ResourceCSWAP, {}) - assert self.op.resource_rep(**self.op.resource_params) == expected_rep - - def test_resource_params(self): - """Test that the resource_params are produced as expected.""" - expected_params = {} - assert self.op.resource_params == expected_params - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - expected_res = {self.op.resource_rep(): 1} - op2 = re.ResourceAdjoint(self.op) - - assert self.op.adjoint_resource_decomp() == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceControlled.resource_rep(re.ResourceSWAP, {}, 2, 0, 0): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - {re.ResourceControlled.resource_rep(re.ResourceSWAP, {}, 3, 0, 1): 1}, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceControlled.resource_rep(re.ResourceSWAP, {}, 4, 2, 2): 1}, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op2 = re.ResourceControlled( - self.op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {op.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (5, {op.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op2 = re.ResourcePow(self.op, z) - - assert self.op.pow_resource_decomp(z) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - -class TestResourceCCZ: - """Test the ResourceCZZ operation""" - - op = re.ResourceCCZ(wires=[0, 1, 2]) - - def test_resources(self): - """Test that the resources method produces the expected resources.""" - expected_resources = { - re.ResourceHadamard.resource_rep(): 2, - re.ResourceToffoli.resource_rep(): 1, - } - assert self.op.resources(**self.op.resource_params) == expected_resources - - def test_resource_rep(self): - """Test the resource_rep produces the correct compressed representation.""" - expected_rep = re.CompressedResourceOp(re.ResourceCCZ, {}) - assert self.op.resource_rep(**self.op.resource_params) == expected_rep - - def test_resource_params(self): - """Test that the resource_params are produced as expected.""" - expected_params = {} - assert self.op.resource_params == expected_params - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - expected_res = {self.op.resource_rep(): 1} - op2 = re.ResourceAdjoint(self.op) - - assert self.op.adjoint_resource_decomp() == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceControlled.resource_rep(re.ResourceZ, {}, 3, 0, 0): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - {re.ResourceControlled.resource_rep(re.ResourceZ, {}, 4, 0, 1): 1}, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceControlled.resource_rep(re.ResourceZ, {}, 5, 2, 2): 1}, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op2 = re.ResourceControlled( - self.op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {op.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (5, {op.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op2 = re.ResourcePow(self.op, z) - - assert self.op.pow_resource_decomp(z) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - -class TestResourceCNOT: - """Test ResourceCNOT operation""" - - op = re.ResourceCNOT([0, 1]) - - def test_resources(self): - """Test that the resources method is not implemented""" - with pytest.raises(re.ResourcesNotDefined): - self.op.resources() - - def test_resource_rep(self): - """Test the resource_rep produces the correct compressed representation.""" - expected = re.CompressedResourceOp(re.ResourceCNOT, {}) - assert self.op.resource_rep() == expected - - def test_resource_params(self): - """Test that the resource_params are produced as expected.""" - expected_params = {} - assert self.op.resource_params == expected_params - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - expected_res = {self.op.resource_rep(): 1} - op2 = re.ResourceAdjoint(self.op) - - assert self.op.adjoint_resource_decomp() == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceToffoli.resource_rep(): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - {re.ResourceMultiControlledX.resource_rep(3, 0, 1): 1}, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceMultiControlledX.resource_rep(4, 2, 2): 1}, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op2 = re.ResourceControlled( - self.op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {op.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (5, {op.resource_rep(): 1}), - (8, {re.ResourceIdentity.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op2 = re.ResourcePow(self.op, z) - - assert self.op.pow_resource_decomp(z) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - -class TestResourceToffoli: - """Test the ResourceToffoli operation""" - - op = re.ResourceToffoli(wires=[0, 1, 2]) - - def test_resources(self): - """Test that the resources method produces the expected resources.""" - - expected_resources = { - re.ResourceS.resource_rep(): 1, - re.ResourceT.resource_rep(): 2, - re.ResourceAdjoint.resource_rep(re.ResourceT, {}): 2, - re.ResourceCZ.resource_rep(): 1, - re.ResourceCNOT.resource_rep(): 9, - re.ResourceHadamard.resource_rep(): 3, - } - assert self.op.resources(**self.op.resource_params) == expected_resources - - def test_resource_rep(self): - """Test the resource_rep produces the correct compressed representation.""" - expected_rep = re.CompressedResourceOp(re.ResourceToffoli, {}) - assert self.op.resource_rep(**self.op.resource_params) == expected_rep - - def test_resource_params(self): - """Test that the resource_params are produced as expected.""" - expected_params = {} - assert self.op.resource_params == expected_params - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - expected_res = {self.op.resource_rep(): 1} - op2 = re.ResourceAdjoint(self.op) - - assert self.op.adjoint_resource_decomp() == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceMultiControlledX.resource_rep(3, 0, 0): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - {re.ResourceMultiControlledX.resource_rep(4, 0, 1): 1}, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceMultiControlledX.resource_rep(5, 2, 2): 1}, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op2 = re.ResourceControlled( - self.op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {op.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (5, {op.resource_rep(): 1}), - (8, {re.ResourceIdentity.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op2 = re.ResourcePow(self.op, z) - - assert self.op.pow_resource_decomp(z) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - -class TestResourceMultiControlledX: - """Test the ResourceMultiControlledX operation""" - - res_ops = ( - re.ResourceMultiControlledX(wires=[0, "t"], control_values=[1]), - re.ResourceMultiControlledX(wires=[0, 1, "t"], control_values=[1, 1]), - re.ResourceMultiControlledX(wires=[0, 1, 2, "t"], control_values=[1, 1, 1]), - re.ResourceMultiControlledX(wires=[0, 1, 2, 3, 4, "t"], control_values=[1, 1, 1, 1, 1]), - re.ResourceMultiControlledX(wires=[0, "t"], control_values=[0], work_wires=["w1"]), - re.ResourceMultiControlledX( - wires=[0, 1, "t"], control_values=[1, 0], work_wires=["w1", "w2"] - ), - re.ResourceMultiControlledX(wires=[0, 1, 2, "t"], control_values=[0, 0, 1]), - re.ResourceMultiControlledX( - wires=[0, 1, 2, 3, 4, "t"], - control_values=[1, 0, 0, 1, 0], - work_wires=["w1"], - ), - ) - - res_params = ( - (1, 0, 0), - (2, 0, 0), - (3, 0, 0), - (5, 0, 0), - (1, 1, 1), - (2, 1, 2), - (3, 2, 0), - (5, 3, 1), - ) - - expected_resources = ( - {re.ResourceCNOT.resource_rep(): 1}, - {re.ResourceToffoli.resource_rep(): 1}, - { - re.ResourceCNOT.resource_rep(): 2, - re.ResourceToffoli.resource_rep(): 1, - }, - {re.ResourceCNOT.resource_rep(): 69}, - { - re.ResourceX.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 1, - }, - { - re.ResourceX.resource_rep(): 2, - re.ResourceToffoli.resource_rep(): 1, - }, - { - re.ResourceX.resource_rep(): 4, - re.ResourceCNOT.resource_rep(): 2, - re.ResourceToffoli.resource_rep(): 1, - }, - { - re.ResourceX.resource_rep(): 6, - re.ResourceCNOT.resource_rep(): 69, - }, - ) - - @staticmethod - def _prep_params(num_control, num_control_values, num_work_wires): - return { - "num_ctrl_wires": num_control, - "num_ctrl_values": num_control_values, - "num_work_wires": num_work_wires, - } - - @pytest.mark.parametrize("params, expected_res", zip(res_params, expected_resources)) - def test_resources(self, params, expected_res): - """Test that the resources method produces the expected resources.""" - op_resource_params = self._prep_params(*params) - assert re.ResourceMultiControlledX.resources(**op_resource_params) == expected_res - - @pytest.mark.parametrize("op, params", zip(res_ops, res_params)) - def test_resource_rep(self, op, params): - """Test the resource_rep produces the correct compressed representation.""" - op_resource_params = self._prep_params(*params) - expected_rep = re.CompressedResourceOp(re.ResourceMultiControlledX, op_resource_params) - assert op.resource_rep(**op.resource_params) == expected_rep - - @pytest.mark.parametrize("op, params", zip(res_ops, res_params)) - def test_resource_params(self, op, params): - """Test that the resource_params are produced as expected.""" - expected_params = self._prep_params(*params) - assert op.resource_params == expected_params - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - op = re.ResourceMultiControlledX( - wires=[0, 1, 2, 3, 4, "t"], - control_values=[1, 0, 0, 1, 0], - work_wires=["w1"], - ) - - expected_res = {op.resource_rep(**op.resource_params): 1} - op2 = re.ResourceAdjoint(op) - - assert op.adjoint_resource_decomp(**op.resource_params) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceToffoli.resource_rep(): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["work1"], - { - re.ResourceCNOT.resource_rep(): 2, - re.ResourceToffoli.resource_rep(): 1, - }, - ), - ( - ["c1", "c2", "c3", "c4"], - [1, 0, 0, 1], - ["work1", "work2"], - { - re.ResourceX.resource_rep(): 4, - re.ResourceCNOT.resource_rep(): 69, - }, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - op = re.ResourceMultiControlledX(wires=[0, "t"], control_values=[1]) - - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires, **op.resource_params - ) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {re.ResourceMultiControlledX.resource_rep(5, 3, 1): 1}), - (2, {}), - (5, {re.ResourceMultiControlledX.resource_rep(5, 3, 1): 1}), - (6, {}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op = re.ResourceMultiControlledX( - wires=[0, 1, 2, 3, 4, "t"], - control_values=[1, 0, 0, 1, 0], - work_wires=["w1"], - ) - - op2 = re.ResourcePow(op, z) - - assert op.pow_resource_decomp(z, **op.resource_params) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - -class TestResourceCRX: - """Test the ResourceCRX operation""" - - op = re.ResourceCRX(phi=1.23, wires=[0, 1]) - - def test_resources(self): - """Test that the resources method produces the expected resources.""" - - expected_resources = { - re.ResourceRZ.resource_rep(): 2, - re.ResourceHadamard.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 2, - } - assert self.op.resources(**self.op.resource_params) == expected_resources - - def test_resource_rep(self): - """Test the resource_rep produces the correct compressed representation.""" - expected_rep = re.CompressedResourceOp(re.ResourceCRX, {}) - assert self.op.resource_rep(**self.op.resource_params) == expected_rep - - def test_resource_params(self): - """Test that the resource_params are produced as expected.""" - expected_params = {} - assert self.op.resource_params == expected_params - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - expected_res = {self.op.resource_rep(): 1} - op2 = re.ResourceAdjoint(self.op) - - assert self.op.adjoint_resource_decomp() == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceControlled.resource_rep(re.ResourceRX, {}, 2, 0, 0): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - {re.ResourceControlled.resource_rep(re.ResourceRX, {}, 3, 0, 1): 1}, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceControlled.resource_rep(re.ResourceRX, {}, 4, 2, 2): 1}, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op2 = re.ResourceControlled( - self.op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {op.resource_rep(): 1}), - (2, {op.resource_rep(): 1}), - (5, {op.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op2 = re.ResourcePow(self.op, z) - - assert self.op.pow_resource_decomp(z) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - -class TestResourceCRY: - """Test the ResourceCRY operation""" - - op = re.ResourceCRY(phi=1.23, wires=[0, 1]) - - def test_resources(self): - """Test that the resources method produces the expected resources.""" - - expected_resources = { - re.ResourceRY.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 2, - } - assert self.op.resources(**self.op.resource_params) == expected_resources - - def test_resource_rep(self): - """Test the resource_rep produces the correct compressed representation.""" - expected_rep = re.CompressedResourceOp(re.ResourceCRY, {}) - assert self.op.resource_rep(**self.op.resource_params) == expected_rep - - def test_resource_params(self): - """Test that the resource_params are produced as expected.""" - expected_params = {} - assert self.op.resource_params == expected_params - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - expected_res = {self.op.resource_rep(): 1} - op2 = re.ResourceAdjoint(self.op) - - assert self.op.adjoint_resource_decomp() == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceControlled.resource_rep(re.ResourceRY, {}, 2, 0, 0): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - {re.ResourceControlled.resource_rep(re.ResourceRY, {}, 3, 0, 1): 1}, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceControlled.resource_rep(re.ResourceRY, {}, 4, 2, 2): 1}, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op2 = re.ResourceControlled( - self.op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {op.resource_rep(): 1}), - (2, {op.resource_rep(): 1}), - (5, {op.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op2 = re.ResourcePow(self.op, z) - - assert self.op.pow_resource_decomp(z) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - -class TestResourceCRZ: - """Test the ResourceCRZ operation""" - - op = re.ResourceCRZ(phi=1.23, wires=[0, 1]) - - def test_resources(self): - """Test that the resources method produces the expected resources.""" - - expected_resources = { - re.ResourceRZ.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 2, - } - assert self.op.resources(**self.op.resource_params) == expected_resources - - def test_resource_rep(self): - """Test the resource_rep produces the correct compressed representation.""" - expected_rep = re.CompressedResourceOp(re.ResourceCRZ, {}) - assert self.op.resource_rep(**self.op.resource_params) == expected_rep - - def test_resource_params(self): - """Test that the resource_params are produced as expected.""" - expected_params = {} - assert self.op.resource_params == expected_params - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - expected_res = {self.op.resource_rep(): 1} - op2 = re.ResourceAdjoint(self.op) - - assert self.op.adjoint_resource_decomp() == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceControlled.resource_rep(re.ResourceRZ, {}, 2, 0, 0): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - {re.ResourceControlled.resource_rep(re.ResourceRZ, {}, 3, 0, 1): 1}, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceControlled.resource_rep(re.ResourceRZ, {}, 4, 2, 2): 1}, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op2 = re.ResourceControlled( - self.op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {op.resource_rep(): 1}), - (2, {op.resource_rep(): 1}), - (5, {op.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op2 = re.ResourcePow(self.op, z) - - assert self.op.pow_resource_decomp(z) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - -class TestResourceCRot: - """Test the ResourceCRot operation""" - - op = re.ResourceCRot(0.1, 0.2, 0.3, wires=[0, 1]) - - def test_resources(self): - """Test that the resources method produces the expected resources.""" - expected_resources = { - re.ResourceRY.resource_rep(): 2, - re.ResourceRZ.resource_rep(): 3, - re.ResourceCNOT.resource_rep(): 2, - } - assert self.op.resources(**self.op.resource_params) == expected_resources - - def test_resource_rep(self): - """Test the resource_rep produces the correct compressed representation.""" - expected_rep = re.CompressedResourceOp(re.ResourceCRot, {}) - assert self.op.resource_rep(**self.op.resource_params) == expected_rep - - def test_resource_params(self): - """Test that the resource_params are produced as expected.""" - expected_params = {} - assert self.op.resource_params == expected_params - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - expected_res = {self.op.resource_rep(): 1} - op2 = re.ResourceAdjoint(self.op) - - assert self.op.adjoint_resource_decomp() == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceControlled.resource_rep(re.ResourceRot, {}, 2, 0, 0): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - {re.ResourceControlled.resource_rep(re.ResourceRot, {}, 3, 0, 1): 1}, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceControlled.resource_rep(re.ResourceRot, {}, 4, 2, 2): 1}, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op2 = re.ResourceControlled( - self.op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {op.resource_rep(): 1}), - (2, {op.resource_rep(): 1}), - (5, {op.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op2 = re.ResourcePow(self.op, z) - - assert self.op.pow_resource_decomp(z) == expected_res - assert op2.resources(**op2.resource_params) == expected_res - - -class TestResourceControlledPhaseShift: - """Test ResourceControlledPhaseShift""" - - params = [(1.2, [0, 1]), (2.4, [2, 3])] - - @pytest.mark.parametrize("phi, wires", params) - def test_resources(self, phi, wires): - """Test the resources method""" - - op = re.ResourceControlledPhaseShift(phi, wires) - - expected = { - re.CompressedResourceOp(re.ResourceCNOT, {}): 2, - re.CompressedResourceOp(re.ResourceRZ, {}): 3, - } - - assert op.resources(**op.resource_params) == expected - - @pytest.mark.parametrize("phi, wires", params) - def test_resource_params(self, phi, wires): - """Test the resource parameters""" - - op = re.ResourceControlledPhaseShift(phi, wires) - assert op.resource_params == {} # pylint: disable=use-implicit-booleaness-not-comparison - - @pytest.mark.parametrize("phi, wires", params) - def test_resource_rep(self, phi, wires): - """Test the compressed representation""" - - op = re.ResourceControlledPhaseShift(phi, wires) - expected = re.CompressedResourceOp(re.ResourceControlledPhaseShift, {}) - - assert op.resource_rep() == expected - - @pytest.mark.parametrize("phi, wires", params) - def test_resource_rep_from_op(self, phi, wires): - """Test resource_rep_from_op method""" - - op = re.ResourceControlledPhaseShift(phi, wires) - assert op.resource_rep_from_op() == re.ResourceControlledPhaseShift.resource_rep( - **op.resource_params - ) - - @pytest.mark.parametrize("phi, wires", params) - def test_resources_from_rep(self, phi, wires): - """Compute the resources from the compressed representation""" - - op = re.ResourceControlledPhaseShift(phi, wires) - - expected = { - re.CompressedResourceOp(re.ResourceCNOT, {}): 2, - re.CompressedResourceOp(re.ResourceRZ, {}): 3, - } - - op_compressed_rep = op.resource_rep_from_op() - op_resource_params = op_compressed_rep.params - op_compressed_rep_type = op_compressed_rep.op_type - - assert op_compressed_rep_type.resources(**op_resource_params) == expected - - @pytest.mark.parametrize("phi, wires", params) - def test_adjoint_decomp(self, phi, wires): - """Test that the adjoint resources are correct.""" - - op = re.ResourceControlledPhaseShift(phi, wires) - adjoint = re.ResourceAdjoint(op) - - assert re.get_resources(op) == re.get_resources(adjoint) - - @pytest.mark.parametrize("phi, wires", params) - def test_pow_decomp(self, phi, wires): - """Test that the adjoint resources are correct.""" - - op = re.ResourceControlledPhaseShift(phi, wires) - pow = re.ResourcePow(op, 2) - - assert re.get_resources(op) == re.get_resources(pow) - - ctrl_data = ( - ( - ["c1"], - [1], - [], - {re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 2, 0, 0): 1}, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - {re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 3, 0, 1): 1}, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 4, 2, 2): 1}, - ), - ) - - @pytest.mark.parametrize("phi, wires", params) - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled( - self, phi, wires, ctrl_wires, ctrl_values, work_wires, expected_res - ): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourceControlledPhaseShift(phi, wires) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res diff --git a/pennylane/labs/tests/resource_estimation/ops/op_math/test_symbolic.py b/pennylane/labs/tests/resource_estimation/ops/op_math/test_symbolic.py deleted file mode 100644 index 8877d5bda57..00000000000 --- a/pennylane/labs/tests/resource_estimation/ops/op_math/test_symbolic.py +++ /dev/null @@ -1,598 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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 symbolic resource operators. -""" - -import pytest - -import pennylane as qml -import pennylane.labs.resource_estimation as re -from pennylane.labs.resource_estimation.ops.op_math.symbolic import ( - _extract_exp_params, - _resources_from_pauli_sentence, -) -from pennylane.operation import Operation -from pennylane.pauli import PauliSentence, PauliWord - -# pylint: disable=protected-access,no-self-use,arguments-differ - - -class DummyOp(re.ResourceOperator, Operation): - """Dummy ResourceOperator child class which implements the - :code:`exp_resource_decomp` method.""" - - def __init__(self, a, b, wires=(0,)): - self.a = a - self.b = b - super().__init__(wires=wires) - - @staticmethod - def _resource_decomp(a, b, **kwargs): - h = re.ResourceHadamard.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - - return {h: a, cnot: b} - - @classmethod - def exp_resource_decomp(cls, coeff, num_steps, a, b): # pylint: disable=unused-argument - return cls.resources(a + 1, b + 1) - - @property - def resource_params(self) -> dict: - return {"a": self.a, "b": self.b} - - @classmethod - def resource_rep(cls, a, b): - return re.CompressedResourceOp(cls, {"a": a, "b": b}) - - -z_pauli_rep = qml.Z(0).pauli_rep -lc_op = qml.ops.LinearCombination( - [1.11, 0.12, -3.4, 5], [qml.X(0) @ qml.X(1), qml.Z(2), qml.Y(0) @ qml.Y(1), qml.I((0, 1, 2))] -) -exp_params_data = ( - ( - lc_op, - { - "base_class": qml.ops.LinearCombination, - "base_params": {}, - "base_pauli_rep": lc_op.pauli_rep, - "coeff": 1.2j, - "num_steps": 3, - }, - ), - ( - re.ResourceQFT(range(10)), - { - "base_class": re.ResourceQFT, - "base_params": {"num_wires": 10}, - "base_pauli_rep": None, - "coeff": 1.2j, - "num_steps": 3, - }, - ), -) - - -class TestResourceAdjoint: - """Tests for ResourceAdjoint""" - - adjoint_ops = [ - re.ResourceAdjoint(re.ResourceQFT([0, 1])), - re.ResourceAdjoint(re.ResourceAdjoint(re.ResourceQFT([0, 1]))), - re.ResourceAdjoint(re.ResourcePow(re.ResourceX(0), 5)), - ] - - expected_params = [ - {"base_class": re.ResourceQFT, "base_params": {"num_wires": 2}}, - { - "base_class": re.ResourceAdjoint, - "base_params": {"base_class": re.ResourceQFT, "base_params": {"num_wires": 2}}, - }, - { - "base_class": re.ResourcePow, - "base_params": {"base_class": re.ResourceX, "base_params": {}, "z": 5}, - }, - ] - - @pytest.mark.parametrize("op, expected", zip(adjoint_ops, expected_params)) - def test_resource_params(self, op, expected): - """Test that the resources are correct""" - assert op.resource_params == expected - - expected_names = [ - "Adjoint(QFT(2))", - "Adjoint(Adjoint(QFT(2)))", - "Adjoint(Pow(X, 5))", - ] - - @pytest.mark.parametrize("op, expected", zip(adjoint_ops, expected_names)) - def test_tracking_name(self, op, expected): - """Test that the tracking name is correct""" - name = op.tracking_name_from_op() - assert name == expected - - @pytest.mark.parametrize( - "nested_op, expected_op", - [ - ( - re.ResourceAdjoint(re.ResourceAdjoint(re.ResourceQFT([0, 1, 2]))), - re.ResourceQFT([0, 1, 2]), - ), - ( - re.ResourceAdjoint( - re.ResourceAdjoint(re.ResourceAdjoint(re.ResourceQFT([0, 1, 2]))) - ), - re.ResourceAdjoint(re.ResourceQFT([0, 1, 2])), - ), - ( - re.ResourceAdjoint( - re.ResourceAdjoint( - re.ResourceAdjoint(re.ResourceAdjoint(re.ResourceQFT([0, 1, 2]))) - ) - ), - re.ResourceQFT([0, 1, 2]), - ), - ( - re.ResourceAdjoint( - re.ResourceAdjoint( - re.ResourceAdjoint( - re.ResourceAdjoint(re.ResourceAdjoint(re.ResourceQFT([0, 1, 2]))) - ) - ) - ), - re.ResourceAdjoint(re.ResourceQFT([0, 1, 2])), - ), - ], - ) - def test_nested_adjoints(self, nested_op, expected_op): - """Test the resources of nested Adjoints.""" - assert re.get_resources(nested_op) == re.get_resources(expected_op) - - expected_resources = [ - re.Resources(gate_types={"Adjoint(QFT(2))": 1}, num_gates=1, num_wires=2), - re.Resources(gate_types={"Adjoint(Adjoint(QFT(2)))": 1}, num_gates=1, num_wires=2), - re.Resources(gate_types={"Adjoint(Pow(X, 5))": 1}, num_gates=1, num_wires=1), - ] - - @pytest.mark.parametrize("op, expected", zip(adjoint_ops, expected_resources)) - def test_tracking(self, op, expected): - """Test that adjoints can be tracked.""" - tracking_name = op.tracking_name_from_op() - gate_set = {tracking_name} - - assert re.get_resources(op, gate_set=gate_set) == expected - - -class TestResourceControlled: - """Tests for ResourceControlled""" - - controlled_ops = [ - re.ResourceControlled(re.ResourceQFT([0, 1]), control_wires=[2]), - re.ResourceControlled( - re.ResourceControlled(re.ResourceQFT([0, 1]), control_wires=[2]), control_wires=[3] - ), - re.ResourceControlled(re.ResourceQFT([0, 1]), control_wires=[2, 3], control_values=[0, 1]), - re.ResourceControlled( - re.ResourceAdjoint(re.ResourceQFT([0, 1])), - control_wires=[2, 3], - control_values=[0, 1], - work_wires=[4], - ), - ] - - expected_params = [ - { - "base_class": re.ResourceQFT, - "base_params": {"num_wires": 2}, - "num_ctrl_wires": 1, - "num_ctrl_values": 0, - "num_work_wires": 0, - }, - { - "base_class": re.ResourceControlled, - "base_params": { - "base_class": re.ResourceQFT, - "base_params": {"num_wires": 2}, - "num_ctrl_wires": 1, - "num_ctrl_values": 0, - "num_work_wires": 0, - }, - "num_ctrl_wires": 1, - "num_ctrl_values": 0, - "num_work_wires": 0, - }, - { - "base_class": re.ResourceQFT, - "base_params": {"num_wires": 2}, - "num_ctrl_wires": 2, - "num_ctrl_values": 1, - "num_work_wires": 0, - }, - { - "base_class": re.ResourceAdjoint, - "base_params": {"base_class": re.ResourceQFT, "base_params": {"num_wires": 2}}, - "num_ctrl_wires": 2, - "num_ctrl_values": 1, - "num_work_wires": 1, - }, - ] - - @pytest.mark.parametrize("op, expected", zip(controlled_ops, expected_params)) - def test_resource_params(self, op, expected): - """Test that the resources are correct""" - assert op.resource_params == expected - - expected_names = [ - "C(QFT(2),1,0,0)", - "C(C(QFT(2),1,0,0),1,0,0)", - "C(QFT(2),2,1,0)", - "C(Adjoint(QFT(2)),2,1,1)", - ] - - @pytest.mark.parametrize("op, expected", zip(controlled_ops, expected_names)) - def test_tracking_name(self, op, expected): - """Test that the tracking name is correct""" - name = op.tracking_name_from_op() - assert name == expected - - @pytest.mark.parametrize( - "nested_op, expected_op", - [ - ( - re.ResourceControlled( - re.ResourceControlled(re.ResourceX(0), control_wires=[1]), control_wires=[2] - ), - re.ResourceToffoli([0, 1, 2]), - ), - ( - re.ResourceControlled( - re.ResourceControlled(re.ResourceX(0), control_wires=[1]), control_wires=[2] - ), - re.ResourceControlled(re.ResourceX(0), control_wires=[1, 2]), - ), - ], - ) - def test_nested_controls(self, nested_op, expected_op): - """Test the resources for nested Controlled operators.""" - assert re.get_resources(nested_op) == re.get_resources(expected_op) - - expected_resources = [ - re.Resources(gate_types={"C(QFT(2),1,0,0)": 1}, num_gates=1, num_wires=3), - re.Resources(gate_types={"C(C(QFT(2),1,0,0),1,0,0)": 1}, num_gates=1, num_wires=4), - re.Resources(gate_types={"C(QFT(2),2,1,0)": 1}, num_gates=1, num_wires=4), - re.Resources( - gate_types={"C(Adjoint(QFT(2)),2,1,1)": 1}, num_gates=1, num_wires=4 - ), # PL does not count work wires for controlled operators - ] - - @pytest.mark.parametrize("op, expected", zip(controlled_ops, expected_resources)) - def test_tracking(self, op, expected): - """Test that adjoints can be tracked.""" - tracking_name = op.tracking_name_from_op() - gate_set = {tracking_name} - - assert re.get_resources(op, gate_set=gate_set) == expected - - -class TestResourcePow: - """Tests for ResourcePow""" - - pow_ops = [ - re.ResourcePow(re.ResourceQFT([0, 1]), 2), - re.ResourcePow(re.ResourceAdjoint(re.ResourceQFT([0, 1])), 2), - re.ResourcePow(re.ResourcePow(re.ResourceQFT([0, 1]), 2), 3), - ] - - expected_params = [ - {"base_class": re.ResourceQFT, "base_params": {"num_wires": 2}, "z": 2}, - { - "base_class": re.ResourceAdjoint, - "base_params": {"base_class": re.ResourceQFT, "base_params": {"num_wires": 2}}, - "z": 2, - }, - { - "base_class": re.ResourcePow, - "base_params": {"base_class": re.ResourceQFT, "base_params": {"num_wires": 2}, "z": 2}, - "z": 3, - }, - ] - - @pytest.mark.parametrize("op, expected", zip(pow_ops, expected_params)) - def test_resource_params(self, op, expected): - """Test that the resources are correct""" - assert op.resource_params == expected - - expected_names = [ - "Pow(QFT(2), 2)", - "Pow(Adjoint(QFT(2)), 2)", - "Pow(Pow(QFT(2), 2), 3)", - ] - - @pytest.mark.parametrize("op, expected", zip(pow_ops, expected_names)) - def test_tracking_name(self, op, expected): - """Test that the tracking name is correct""" - rep = op.resource_rep_from_op() - name = rep.op_type.tracking_name(**rep.params) - assert name == expected - - expected_resources = [ - re.Resources(gate_types={"Pow(QFT(2), 2)": 1}, num_gates=1, num_wires=2), - re.Resources(gate_types={"Pow(Adjoint(QFT(2)), 2)": 1}, num_gates=1, num_wires=2), - re.Resources(gate_types={"Pow(Pow(QFT(2), 2), 3)": 1}, num_gates=1, num_wires=2), - ] - - @pytest.mark.parametrize("op, expected", zip(pow_ops, expected_resources)) - def test_tracking(self, op, expected): - """Test that adjoints can be tracked.""" - tracking_name = op.tracking_name_from_op() - gate_set = {tracking_name} - - assert re.get_resources(op, gate_set=gate_set) == expected - - @pytest.mark.parametrize( - "nested_op, expected_op", - [ - ( - re.ResourcePow(re.ResourcePow(re.ResourceQFT([0, 1]), 2), 2), - re.ResourcePow(re.ResourceQFT([0, 1]), 4), - ), - ( - re.ResourcePow(re.ResourcePow(re.ResourcePow(re.ResourceQFT([0, 1]), 2), 2), 2), - re.ResourcePow(re.ResourceQFT([0, 1]), 8), - ), - ( - re.ResourcePow( - re.ResourcePow(re.ResourcePow(re.ResourcePow(re.ResourceQFT([0, 1]), 2), 2), 2), - 2, - ), - re.ResourcePow(re.ResourceQFT([0, 1]), 16), - ), - ], - ) - def test_nested_pow(self, nested_op, expected_op): - """Test the resources for nested Pow operators.""" - assert re.get_resources(nested_op) == re.get_resources(expected_op) - - -class TestResourceProd: - """Test ResourceProd""" - - op_data = ( - re.ResourceProd(re.ResourceX(0), re.ResourceHadamard(1)), - re.ResourceProd( - re.ResourceQFT(wires=[0, 1, 2]), - re.ResourceAdjoint(re.ResourceZ(0)), - re.ResourceControlled(re.ResourcePhaseShift(1.23, 0), control_wires=["c1", "c2"]), - ), - re.ResourceProd( - re.ResourceCNOT([0, 1]), - re.ResourceExp(re.ResourceZ(0), 0.1j), - re.ResourceProd(re.ResourceX(1), re.ResourceY(2)), - re.ResourceZ(3), - ), - ) - - resource_data = ( - { - re.ResourceX.resource_rep(): 1, - re.ResourceHadamard.resource_rep(): 1, - }, - { - re.ResourceQFT.resource_rep(3): 1, - re.ResourceAdjoint.resource_rep(re.ResourceZ, {}): 1, - re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 2, 0, 0): 1, - }, - { - re.ResourceCNOT.resource_rep(): 1, - re.ResourceExp.resource_rep(re.ResourceZ, {}, z_pauli_rep, 0.1j, None): 1, - re.ResourceProd.resource_rep( - cmpr_factors=(re.ResourceX.resource_rep(), re.ResourceY.resource_rep()) - ): 1, - re.ResourceZ.resource_rep(): 1, - }, - ) - - resource_params_data = ( - { - "cmpr_factors": ( - re.ResourceX.resource_rep(), - re.ResourceHadamard.resource_rep(), - ), - }, - { - "cmpr_factors": ( - re.ResourceQFT.resource_rep(3), - re.ResourceAdjoint.resource_rep(re.ResourceZ, {}), - re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 2, 0, 0), - ), - }, - { - "cmpr_factors": ( - re.ResourceCNOT.resource_rep(), - re.ResourceExp.resource_rep(re.ResourceZ, {}, z_pauli_rep, 0.1j, None), - re.ResourceProd.resource_rep( - cmpr_factors=(re.ResourceX.resource_rep(), re.ResourceY.resource_rep()) - ), - re.ResourceZ.resource_rep(), - ), - }, - ) - - @pytest.mark.parametrize( - "op, params, expected_res", zip(op_data, resource_params_data, resource_data) - ) - def test_resources(self, op, params, expected_res): - """Test the resources method returns the correct dictionary""" - res_from_op = op.resources(**op.resource_params) - res_from_func = re.ResourceProd.resources(**params) - - assert res_from_op == expected_res - assert res_from_func == expected_res - - @pytest.mark.parametrize("op, expected_params", zip(op_data, resource_params_data)) - def test_resource_params(self, op, expected_params): - """Test that the resource params are correct""" - assert op.resource_params == expected_params - - @pytest.mark.parametrize("expected_params", resource_params_data) - def test_resource_rep(self, expected_params): - """Test the resource_rep returns the correct CompressedResourceOp""" - expected = re.CompressedResourceOp(re.ResourceProd, expected_params) - assert re.ResourceProd.resource_rep(**expected_params) == expected - - -class TestResourceExp: - """Test for ResourceExp""" - - @pytest.mark.parametrize("op, expected", exp_params_data) - def test_resource_params(self, op, expected): - """Test that the resource_params method produces the expected parameters.""" - exp_op = re.ResourceExp(op, 1.2j, num_steps=3) - extracted_params = exp_op.resource_params - assert extracted_params == expected - - @pytest.mark.parametrize("op, expected_params", exp_params_data) - def test_resource_rep(self, op, expected_params): - """Test that the resource_rep method produces the correct compressed representation.""" - exp_op = re.ResourceExp(op, 1.2j, num_steps=3) - computed_rep = exp_op.resource_rep(**expected_params) - expected_rep = re.CompressedResourceOp( - re.ResourceExp, expected_params, name=exp_op.tracking_name_from_op() - ) - - assert expected_rep == computed_rep - - exp_res_data = ( - ( - re.ResourceExp(lc_op, 1.5j), - { - re.ResourcePauliRot.resource_rep("XX"): 1, - re.ResourcePauliRot.resource_rep("YY"): 1, - re.ResourcePauliRot.resource_rep("Z"): 1, - re.ResourcePauliRot.resource_rep(""): 1, - }, - ), - ( - re.ResourceExp(DummyOp(2, 3, wires=[1, 2, 3]), 0.1j, num_steps=5), - { - re.ResourceHadamard.resource_rep(): 3, - re.ResourceCNOT.resource_rep(): 4, - }, - ), - ) - - @pytest.mark.parametrize("op, expected_resources", exp_res_data) - def test_resources_decomp(self, op, expected_resources): - """Test that the _resources_decomp method works as expected.""" - computed_resources = op._resource_decomp(**op.resource_params) - assert computed_resources == expected_resources - - @pytest.mark.parametrize( - "op, z, expected_resources", - ( - ( - re.ResourceExp(lc_op, 1.5j), - 1, - {re.ResourceExp.resource_rep(type(lc_op), {}, lc_op.pauli_rep, 1.5j, None): 1}, - ), - ( - re.ResourceExp(lc_op, 1.5j), - 2, - {re.ResourceExp.resource_rep(type(lc_op), {}, lc_op.pauli_rep, 3j, None): 1}, - ), - ( - re.ResourceExp(lc_op, 1.5j), - 7, - {re.ResourceExp.resource_rep(type(lc_op), {}, lc_op.pauli_rep, 10.5j, None): 1}, - ), - ( - re.ResourceExp(DummyOp(2, 3, wires=[1, 2, 3]), 0.1j, num_steps=5), - 1, - {re.ResourceExp.resource_rep(DummyOp, {"a": 2, "b": 3}, None, 0.1j, 5): 1}, - ), - ( - re.ResourceExp(DummyOp(2, 3, wires=[1, 2, 3]), 0.1j, num_steps=5), - 4, - {re.ResourceExp.resource_rep(DummyOp, {"a": 2, "b": 3}, None, 0.4j, 5): 1}, - ), - ( - re.ResourceExp(DummyOp(2, 3, wires=[1, 2, 3]), 0.1j, num_steps=5), - 8, - {re.ResourceExp.resource_rep(DummyOp, {"a": 2, "b": 3}, None, 0.8j, 5): 1}, - ), - ), - ) - def test_pow_resources(self, op, z, expected_resources): - """Test that the pow resource decomp method works as expected.""" - params = op.resource_params - computed_resources = op.pow_resource_decomp(z, **params) - assert computed_resources == expected_resources - - -@pytest.mark.parametrize("base_op, expected_params", exp_params_data) -def test_extract_exp_params(base_op, expected_params): - """Test the private _extract_exp_params method behaves as expected""" - extracted_params = _extract_exp_params(base_op, scalar=1.2j, num_steps=3) - assert extracted_params == expected_params - - -def test_extract_exp_params_raises_error(): - """Test that the private _extract_exp_params method raises an error if the base operator - isnt compatible with ResourceExp.""" - with pytest.raises(ValueError, match="Cannot obtain resources for the exponential of"): - _ = _extract_exp_params(qml.QFT(range(10)), 1j, 5) - - -@pytest.mark.parametrize( - "ps, expected_res", - ( - (PauliSentence({}), {}), - ( - PauliSentence( - { - PauliWord({0: "I", 2: "I", 3: "I"}): 0.12, - PauliWord({0: "X", 2: "I", 3: "I"}): -3.4, - PauliWord({0: "I", 2: "Y", 3: "I"}): 56, - PauliWord({0: "I", 2: "I", 3: "Z"}): 0.78, - } - ), - { - re.ResourcePauliRot.resource_rep(""): 1, - re.ResourcePauliRot.resource_rep("X"): 1, - re.ResourcePauliRot.resource_rep("Y"): 1, - re.ResourcePauliRot.resource_rep("Z"): 1, - }, - ), - ( - PauliSentence( - { - PauliWord({0: "X", 2: "X", 3: "X"}): 0.12, - PauliWord({0: "Y", 2: "Y", 3: "Y"}): -3.4, - PauliWord({0: "X", 2: "Y", 3: "Z"}): 56, - } - ), - { - re.ResourcePauliRot.resource_rep("XXX"): 1, - re.ResourcePauliRot.resource_rep("YYY"): 1, - re.ResourcePauliRot.resource_rep("XYZ"): 1, - }, - ), - ), -) -def test_resources_from_pauli_sentence(ps, expected_res): - """Test that the private function resources_from_pauli_sentence works correcty""" - extracted_res = _resources_from_pauli_sentence(ps) - assert extracted_res == expected_res diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py deleted file mode 100644 index e23c3ecac9f..00000000000 --- a/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py +++ /dev/null @@ -1,834 +0,0 @@ -# Copyright 2024 Xanadu Quantum Technologies Inc. - -# 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 non parametric resource operators. -""" -import pytest - -import pennylane.labs.resource_estimation as re - -# pylint: disable=no-self-use,use-implicit-booleaness-not-comparison - - -class TestHadamard: - """Tests for ResourceHadamard""" - - def test_resources(self): - """Test that ResourceHadamard does not implement a decomposition""" - op = re.ResourceHadamard(0) - with pytest.raises(re.ResourcesNotDefined): - op.resources() - - def test_resource_params(self): - """Test that the resource params are correct""" - op = re.ResourceHadamard(0) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compact representation is correct""" - expected = re.CompressedResourceOp(re.ResourceHadamard, {}) - assert re.ResourceHadamard.resource_rep() == expected - - def test_adjoint_decomp(self): - """Test that the adjoint decomposition is correct.""" - h = re.ResourceHadamard(0) - h_dag = re.ResourceAdjoint(re.ResourceHadamard(0)) - - assert re.get_resources(h) == re.get_resources(h_dag) - - ctrl_data = ( - ( - ["c1"], - [1], - [], - { - re.ResourceCH.resource_rep(): 1, - }, - ), - ( - ["c1"], - [0], - [], - { - re.ResourceCH.resource_rep(): 1, - re.ResourceX.resource_rep(): 2, - }, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - { - re.ResourceRY.resource_rep(): 2, - re.ResourceHadamard.resource_rep(): 2, - re.ResourceMultiControlledX.resource_rep(2, 0, 1): 1, - }, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - { - re.ResourceRY.resource_rep(): 2, - re.ResourceHadamard.resource_rep(): 2, - re.ResourceMultiControlledX.resource_rep(3, 2, 2): 1, - }, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourceHadamard(0) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {re.ResourceHadamard.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (3, {re.ResourceHadamard.resource_rep(): 1}), - (4, {re.ResourceIdentity.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_pow_decomp(self, z, expected_res): - """Test that the pow decomposition is correct.""" - op = re.ResourceHadamard(0) - assert op.pow_resource_decomp(z) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res - - -class TestSWAP: - """Tests for ResourceSWAP""" - - def test_resources(self): - """Test that SWAP decomposes into three CNOTs""" - op = re.ResourceSWAP([0, 1]) - cnot = re.ResourceCNOT.resource_rep() - expected = {cnot: 3} - - assert op.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct""" - op = re.ResourceSWAP([0, 1]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test the compact representation""" - expected = re.CompressedResourceOp(re.ResourceSWAP, {}) - assert re.ResourceSWAP.resource_rep() == expected - - def test_resources_from_rep(self): - """Test that the resources can be computed from the compressed representation""" - - op = re.ResourceSWAP([0, 1]) - expected = {re.ResourceCNOT.resource_rep(): 3} - - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - def test_adjoint_decomp(self): - """Test that the adjoint decomposition is correct.""" - swap = re.ResourceSWAP([0, 1]) - swap_dag = re.ResourceAdjoint(re.ResourceSWAP([0, 1])) - - assert re.get_resources(swap) == re.get_resources(swap_dag) - - ctrl_data = ( - ( - ["c1"], - [1], - [], - { - re.ResourceCSWAP.resource_rep(): 1, - }, - ), - ( - ["c1"], - [0], - [], - { - re.ResourceCSWAP.resource_rep(): 1, - re.ResourceX.resource_rep(): 2, - }, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - { - re.ResourceCNOT.resource_rep(): 2, - re.ResourceMultiControlledX.resource_rep(2, 0, 1): 1, - }, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - { - re.ResourceCNOT.resource_rep(): 2, - re.ResourceMultiControlledX.resource_rep(3, 2, 2): 1, - }, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourceSWAP([0, 1]) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {re.ResourceSWAP.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (3, {re.ResourceSWAP.resource_rep(): 1}), - (4, {re.ResourceIdentity.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_pow_decomp(self, z, expected_res): - """Test that the pow decomposition is correct.""" - op = re.ResourceSWAP([0, 1]) - assert op.pow_resource_decomp(z) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res - - -class TestS: - """Tests for ResourceS""" - - def test_resources(self): - """Test that S decomposes into two Ts""" - op = re.ResourceS(0) - expected = {re.CompressedResourceOp(re.ResourceT, {}): 2} - assert op.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct""" - op = re.ResourceS(0) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct""" - expected = re.CompressedResourceOp(re.ResourceS, {}) - assert re.ResourceS.resource_rep() == expected - - def test_resources_from_rep(self): - """Test that the resources can be computed from the compressed representation""" - - op = re.ResourceS(0) - expected = {re.ResourceT.resource_rep(): 2} - - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - def test_adjoint_decomposition(self): - """Test that the adjoint resources are correct.""" - expected = {re.ResourceS.resource_rep(): 3} - assert re.ResourceS.adjoint_resource_decomp() == expected - - s = re.ResourceS(0) - s_dag = re.ResourceAdjoint(s) - - r1 = re.get_resources(s) * 3 - r2 = re.get_resources(s_dag) - assert r1 == r2 - - pow_data = ( - (1, {re.ResourceS.resource_rep(): 1}), - (2, {re.ResourceS.resource_rep(): 2}), - (3, {re.ResourceS.resource_rep(): 3}), - (4, {re.ResourceIdentity.resource_rep(): 1}), - (7, {re.ResourceS.resource_rep(): 3}), - (8, {re.ResourceIdentity.resource_rep(): 1}), - (14, {re.ResourceS.resource_rep(): 2}), - (15, {re.ResourceS.resource_rep(): 3}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_pow_decomp(self, z, expected_res): - """Test that the pow decomposition is correct.""" - op = re.ResourceS(0) - assert op.pow_resource_decomp(z) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res - - ctrl_data = ( - ( - ["c1"], - [1], - [], - { - re.ResourceControlledPhaseShift.resource_rep(): 1, - }, - ), - ( - ["c1"], - [0], - [], - { - re.ResourceControlledPhaseShift.resource_rep(): 1, - re.ResourceX.resource_rep(): 2, - }, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - { - re.ResourceControlledPhaseShift.resource_rep(): 1, - re.ResourceMultiControlledX.resource_rep(2, 0, 1): 2, - }, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - { - re.ResourceControlledPhaseShift.resource_rep(): 1, - re.ResourceMultiControlledX.resource_rep(3, 2, 2): 2, - }, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourceS(0) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - -class TestT: - """Tests for ResourceT""" - - def test_resources(self): - """Test that there is no further decomposition of the T gate.""" - op = re.ResourceT(0) - with pytest.raises(re.ResourcesNotDefined): - op.resources() - - def test_resource_params(self): - """Test that the resource params are correct""" - op = re.ResourceT(0) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compact representation is correct""" - expected = re.CompressedResourceOp(re.ResourceT, {}) - assert re.ResourceT.resource_rep() == expected - - def test_adjoint_decomposition(self): - """Test that the adjoint resources are correct.""" - expected = {re.ResourceT.resource_rep(): 7} - assert re.ResourceT.adjoint_resource_decomp() == expected - - t = re.ResourceT(0) - t_dag = re.ResourceAdjoint(t) - - r1 = re.get_resources(t) * 7 - r2 = re.get_resources(t_dag) - assert r1 == r2 - - ctrl_data = ( - ( - ["c1"], - [1], - [], - { - re.ResourceControlledPhaseShift.resource_rep(): 1, - }, - ), - ( - ["c1"], - [0], - [], - { - re.ResourceControlledPhaseShift.resource_rep(): 1, - re.ResourceX.resource_rep(): 2, - }, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - { - re.ResourceControlledPhaseShift.resource_rep(): 1, - re.ResourceMultiControlledX.resource_rep(2, 0, 1): 2, - }, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - { - re.ResourceControlledPhaseShift.resource_rep(): 1, - re.ResourceMultiControlledX.resource_rep(3, 2, 2): 2, - }, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourceT(0) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {re.ResourceT.resource_rep(): 1}), - (2, {re.ResourceT.resource_rep(): 2}), - (3, {re.ResourceT.resource_rep(): 3}), - (7, {re.ResourceT.resource_rep(): 7}), - (8, {re.ResourceIdentity.resource_rep(): 1}), - (14, {re.ResourceT.resource_rep(): 6}), - (15, {re.ResourceT.resource_rep(): 7}), - (16, {re.ResourceIdentity.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_pow_decomp(self, z, expected_res): - """Test that the pow decomposition is correct.""" - op = re.ResourceT(0) - assert op.pow_resource_decomp(z) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res - - -class TestX: - """Tests for the ResourceX gate""" - - def test_resources(self): - """Tests for the ResourceX gate""" - expected = { - re.ResourceS.resource_rep(): 2, - re.ResourceHadamard.resource_rep(): 2, - } - assert re.ResourceX.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct""" - op = re.ResourceX(0) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compact representation is correct""" - expected = re.CompressedResourceOp(re.ResourceX, {}) - assert re.ResourceX.resource_rep() == expected - - ctrl_data = ( - ( - ["c1"], - [1], - [], - { - re.ResourceCNOT.resource_rep(): 1, - }, - ), - ( - ["c1"], - [0], - [], - { - re.ResourceCNOT.resource_rep(): 1, - re.ResourceX.resource_rep(): 2, - }, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - { - re.ResourceToffoli.resource_rep(): 1, - }, - ), - ( - ["c1", "c2"], - [0, 0], - ["w1"], - { - re.ResourceToffoli.resource_rep(): 1, - re.ResourceX.resource_rep(): 4, - }, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - { - re.ResourceMultiControlledX.resource_rep(3, 2, 2): 1, - }, - ), - ( - ["c1", "c2", "c3", "c4"], - [1, 0, 0, 1], - ["w1", "w2"], - { - re.ResourceMultiControlledX.resource_rep(4, 2, 2): 1, - }, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourceX(0) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - def test_adjoint_decomposition(self): - """Test that the adjoint resources are correct.""" - expected = {re.ResourceX.resource_rep(): 1} - assert re.ResourceX.adjoint_resource_decomp() == expected - - x = re.ResourceX(0) - x_dag = re.ResourceAdjoint(x) - - r1 = re.get_resources(x) - r2 = re.get_resources(x_dag) - assert r1 == r2 - - pow_data = ( - (1, {re.ResourceX.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (3, {re.ResourceX.resource_rep(): 1}), - (4, {re.ResourceIdentity.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_pow_decomp(self, z, expected_res): - """Test that the pow decomposition is correct.""" - op = re.ResourceX(0) - assert op.pow_resource_decomp(z) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res - - -class TestY: - """Tests for the ResourceY gate""" - - def test_resources(self): - """Test that ResourceT does not implement a decomposition""" - expected = { - re.ResourceS.resource_rep(): 6, - re.ResourceHadamard.resource_rep(): 2, - } - assert re.ResourceY.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct""" - op = re.ResourceY(0) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compact representation is correct""" - expected = re.CompressedResourceOp(re.ResourceY, {}) - assert re.ResourceY.resource_rep() == expected - - ctrl_data = ( - ( - ["c1"], - [1], - [], - { - re.ResourceCY.resource_rep(): 1, - }, - ), - ( - ["c1"], - [0], - [], - { - re.ResourceCY.resource_rep(): 1, - re.ResourceX.resource_rep(): 2, - }, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - { - re.ResourceS.resource_rep(): 1, - re.ResourceAdjoint.resource_rep(re.ResourceS, {}): 1, - re.ResourceMultiControlledX.resource_rep(2, 0, 1): 1, - }, - ), - ( - ["c1", "c2"], - [0, 0], - ["w1"], - { - re.ResourceS.resource_rep(): 1, - re.ResourceAdjoint.resource_rep(re.ResourceS, {}): 1, - re.ResourceMultiControlledX.resource_rep(2, 2, 1): 1, - }, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - { - re.ResourceS.resource_rep(): 1, - re.ResourceAdjoint.resource_rep(re.ResourceS, {}): 1, - re.ResourceMultiControlledX.resource_rep(3, 2, 2): 1, - }, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourceY(0) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - def test_adjoint_decomposition(self): - """Test that the adjoint resources are correct.""" - expected = {re.ResourceY.resource_rep(): 1} - assert re.ResourceY.adjoint_resource_decomp() == expected - - y = re.ResourceY(0) - y_dag = re.ResourceAdjoint(y) - - r1 = re.get_resources(y) - r2 = re.get_resources(y_dag) - assert r1 == r2 - - pow_data = ( - (1, {re.ResourceY.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (3, {re.ResourceY.resource_rep(): 1}), - (4, {re.ResourceIdentity.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_pow_decomp(self, z, expected_res): - """Test that the pow decomposition is correct.""" - op = re.ResourceY(0) - assert op.pow_resource_decomp(z) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res - - -class TestZ: - """Tests for the ResourceZ gate""" - - def test_resources(self): - """Test that ResourceT does not implement a decomposition""" - expected = { - re.ResourceS.resource_rep(): 2, - } - assert re.ResourceZ.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct""" - op = re.ResourceZ(0) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compact representation is correct""" - expected = re.CompressedResourceOp(re.ResourceZ, {}) - assert re.ResourceZ.resource_rep() == expected - - ctrl_data = ( - ( - ["c1"], - [1], - [], - { - re.ResourceCZ.resource_rep(): 1, - }, - ), - ( - ["c1"], - [0], - [], - { - re.ResourceCZ.resource_rep(): 1, - re.ResourceX.resource_rep(): 2, - }, - ), - ( - ["c1", "c2"], - [1, 1], - ["w1"], - { - re.ResourceCCZ.resource_rep(): 1, - }, - ), - ( - ["c1", "c2"], - [0, 0], - ["w1"], - { - re.ResourceCCZ.resource_rep(): 1, - re.ResourceX.resource_rep(): 4, - }, - ), - ( - ["c1", "c2", "c3"], - [1, 0, 0], - ["w1", "w2"], - { - re.ResourceHadamard.resource_rep(): 2, - re.ResourceMultiControlledX.resource_rep(3, 2, 2): 1, - }, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", - ctrl_data, - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourceZ(0) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - def test_adjoint_decomposition(self): - """Test that the adjoint resources are correct.""" - expected = {re.ResourceZ.resource_rep(): 1} - assert re.ResourceZ.adjoint_resource_decomp() == expected - - z = re.ResourceZ(0) - z_dag = re.ResourceAdjoint(z) - - r1 = re.get_resources(z) - r2 = re.get_resources(z_dag) - assert r1 == r2 - - pow_data = ( - (1, {re.ResourceZ.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (3, {re.ResourceZ.resource_rep(): 1}), - (4, {re.ResourceIdentity.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_pow_decomp(self, z, expected_res): - """Test that the pow decomposition is correct.""" - op = re.ResourceZ(0) - assert op.pow_resource_decomp(z) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_multi_qubit.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_multi_qubit.py deleted file mode 100644 index 24f37ec1e0e..00000000000 --- a/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_multi_qubit.py +++ /dev/null @@ -1,551 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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 parametric multi qubit resource operators.""" - -import pytest - -import pennylane.labs.resource_estimation as re - -# pylint: disable=use-implicit-booleaness-not-comparison,no-self-use,too-many-arguments - - -class TestMultiRZ: - """Test the ResourceMultiRZ class.""" - - @pytest.mark.parametrize("num_wires", range(1, 10)) - def test_resource_params(self, num_wires): - """Test that the resource params are correct.""" - op = re.ResourceMultiRZ(0.5, range(num_wires)) - assert op.resource_params == {"num_wires": num_wires} - - @pytest.mark.parametrize("num_wires", range(1, 10)) - def test_resource_rep(self, num_wires): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceMultiRZ, {"num_wires": num_wires}) - assert re.ResourceMultiRZ.resource_rep(num_wires) == expected - - @pytest.mark.parametrize("num_wires", range(1, 10)) - def test_resources(self, num_wires): - """Test that the resources are correct.""" - expected = { - re.ResourceCNOT.resource_rep(): 2 * (num_wires - 1), - re.ResourceRZ.resource_rep(): 1, - } - assert re.ResourceMultiRZ.resources(num_wires) == expected - - @pytest.mark.parametrize("num_wires", range(1, 10)) - def test_resources_from_rep(self, num_wires): - """Test that the resources can be computed from the compressed representation and params.""" - op = re.ResourceMultiRZ(0.5, wires=range(num_wires)) - expected = { - re.ResourceCNOT.resource_rep(): 2 * (num_wires - 1), - re.ResourceRZ.resource_rep(): 1, - } - - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - @pytest.mark.parametrize("num_wires", range(1, 5)) - def test_adjoint_decomp(self, num_wires): - """Test that the adjoint decomposition is correct.""" - expected = {re.ResourceMultiRZ.resource_rep(num_wires=num_wires): 1} - assert re.ResourceMultiRZ.adjoint_resource_decomp(num_wires=num_wires) == expected - - multi_rz = re.ResourceMultiRZ(0.123, wires=range(num_wires)) - multi_rz_dag = re.ResourceAdjoint(multi_rz) - - assert re.get_resources(multi_rz) == re.get_resources(multi_rz_dag) - - ctrl_data = ( - ( - [1], - [1], - [], - { - re.ResourceCNOT.resource_rep(): 4, - re.ResourceControlled.resource_rep(re.ResourceRZ, {}, 1, 0, 0): 1, - }, - ), - ( - [1], - [0], - [], - { - re.ResourceControlled.resource_rep( - re.ResourceMultiRZ, {"num_wires": 3}, 1, 0, 0 - ): 1, - re.ResourceX.resource_rep(): 2, - }, - ), - ( - [1, 2], - [1, 1], - ["w1"], - { - re.ResourceCNOT.resource_rep(): 4, - re.ResourceControlled.resource_rep(re.ResourceRZ, {}, 2, 0, 1): 1, - }, - ), - ( - [1, 2, 3], - [1, 0, 0], - ["w1", "w2"], - { - re.ResourceControlled.resource_rep( - re.ResourceMultiRZ, {"num_wires": 3}, 3, 0, 2 - ): 1, - re.ResourceX.resource_rep(): 4, - }, - ), - ) - - @pytest.mark.parametrize("ctrl_wires, ctrl_values, work_wires, expected_res", ctrl_data) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourceMultiRZ(1.24, wires=range(5, 8)) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - if num_ctrl_values != 0: - with pytest.raises(re.ResourcesNotDefined): - op.controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires, **op.resource_params - ) - else: - assert ( - op.controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires, **op.resource_params - ) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (1, {re.ResourceMultiRZ.resource_rep(num_wires=4): 1}), - (2, {re.ResourceMultiRZ.resource_rep(num_wires=4): 1}), - (3, {re.ResourceMultiRZ.resource_rep(num_wires=4): 1}), - (4, {re.ResourceMultiRZ.resource_rep(num_wires=4): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_pow_decomp(self, z, expected_res): - """Test that the pow decomposition is correct.""" - op = re.ResourceMultiRZ(1.23, wires=range(4)) - assert op.pow_resource_decomp(z, **op.resource_params) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res - - -class TestPauliRot: - """Test the ResourcePauliRot class.""" - - pauli_words = ("I", "XYZ", "XXX", "XIYIZIX", "III") - - @pytest.mark.parametrize("pauli_string", pauli_words) - def test_resource_params(self, pauli_string): - """Test that the resource params are correct.""" - op = re.ResourcePauliRot(theta=0.5, pauli_word=pauli_string, wires=range(len(pauli_string))) - assert op.resource_params == {"pauli_string": pauli_string} - - @pytest.mark.parametrize("pauli_string", pauli_words) - def test_resource_rep(self, pauli_string): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourcePauliRot, {"pauli_string": pauli_string}) - assert re.ResourcePauliRot.resource_rep(pauli_string) == expected - - expected_h_count = (0, 4, 6, 6, 0) - expected_s_count = (0, 1, 0, 1, 0) - params = zip(pauli_words, expected_h_count, expected_s_count) - - @pytest.mark.parametrize("pauli_string, expected_h_count, expected_s_count", params) - def test_resources(self, pauli_string, expected_h_count, expected_s_count): - """Test that the resources are correct.""" - active_wires = len(pauli_string.replace("I", "")) - - if set(pauli_string) == {"I"}: - expected = {re.ResourceGlobalPhase.resource_rep(): 1} - else: - expected = { - re.ResourceRZ.resource_rep(): 1, - re.ResourceCNOT.resource_rep(): 2 * (active_wires - 1), - } - - if expected_h_count: - expected[re.ResourceHadamard.resource_rep()] = expected_h_count - - if expected_s_count: - expected[re.ResourceS.resource_rep()] = expected_s_count - expected[re.ResourceAdjoint.resource_rep(re.ResourceS, {})] = expected_s_count - - assert re.ResourcePauliRot.resources(pauli_string) == expected - - def test_resources_empty_pauli_string(self): - """Test that the resources method produces the correct result for an empty pauli string.""" - expected = {re.ResourceGlobalPhase.resource_rep(): 1} - assert re.ResourcePauliRot.resources(pauli_string="") == expected - - @pytest.mark.parametrize("pauli_string, expected_h_count, expected_s_count", params) - def test_resources_from_rep(self, pauli_string, expected_h_count, expected_s_count): - """Test that the resources can be computed from the compressed representation and params.""" - op = re.ResourcePauliRot(0.5, pauli_string, wires=range(len(pauli_string))) - active_wires = len(pauli_string.replace("I", "")) - - if (set(pauli_string) == {"I"}) or (pauli_string == ""): - expected = {re.ResourceGlobalPhase.resource_rep(): 1} - - else: - expected = { - re.ResourceRZ.resource_rep(): 1, - re.ResourceCNOT.resource_rep(): 2 * (active_wires - 1), - } - - if expected_h_count: - expected[re.ResourceHadamard.resource_rep()] = expected_h_count - - if expected_s_count: - expected[re.ResourceS.resource_rep()] = expected_s_count - expected[re.ResourceAdjoint.resource_rep(re.ResourceS, {})] = expected_s_count - - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - @pytest.mark.parametrize("pauli_word", pauli_words) - def test_adjoint_decomp(self, pauli_word): - """Test that the adjoint decomposition is correct.""" - expected = {re.ResourcePauliRot.resource_rep(pauli_string=pauli_word): 1} - assert re.ResourcePauliRot.adjoint_resource_decomp(pauli_string=pauli_word) == expected - - op = re.ResourcePauliRot(theta=0.5, pauli_word=pauli_word, wires=range(len(pauli_word))) - op_dag = re.ResourceAdjoint(op) - - assert re.get_resources(op) == re.get_resources(op_dag) - - ctrl_data = ( - ( - "XXX", - [1], - [1], - [], - { - re.ResourceHadamard.resource_rep(): 6, - re.ResourceControlled.resource_rep(re.ResourceRZ, {}, 1, 0, 0): 1, - re.ResourceCNOT.resource_rep(): 4, - }, - ), - ( - "XXX", - [1], - [0], - [], - { - re.ResourceHadamard.resource_rep(): 6, - re.ResourceControlled.resource_rep(re.ResourceRZ, {}, 1, 1, 0): 1, - re.ResourceCNOT.resource_rep(): 4, - }, - ), - ( - "XXX", - [1, 2], - [1, 1], - ["w1"], - { - re.ResourceHadamard.resource_rep(): 6, - re.ResourceControlled.resource_rep(re.ResourceRZ, {}, 2, 0, 1): 1, - re.ResourceCNOT.resource_rep(): 4, - }, - ), - ( - "XIYIZIX", - [1], - [1], - [], - { - re.ResourceHadamard.resource_rep(): 6, - re.ResourceS.resource_rep(): 1, - re.ResourceAdjoint.resource_rep(re.ResourceS, {}): 1, - re.ResourceControlled.resource_rep(re.ResourceRZ, {}, 1, 0, 0): 1, - re.ResourceCNOT.resource_rep(): 6, - }, - ), - ( - "XIYIZIX", - [1], - [0], - [], - { - re.ResourceHadamard.resource_rep(): 6, - re.ResourceS.resource_rep(): 1, - re.ResourceAdjoint.resource_rep(re.ResourceS, {}): 1, - re.ResourceControlled.resource_rep(re.ResourceRZ, {}, 1, 1, 0): 1, - re.ResourceCNOT.resource_rep(): 6, - }, - ), - ( - "XIYIZIX", - [1, 2], - [1, 1], - ["w1"], - { - re.ResourceHadamard.resource_rep(): 6, - re.ResourceS.resource_rep(): 1, - re.ResourceAdjoint.resource_rep(re.ResourceS, {}): 1, - re.ResourceControlled.resource_rep(re.ResourceRZ, {}, 2, 0, 1): 1, - re.ResourceCNOT.resource_rep(): 6, - }, - ), - ( - "III", - [1], - [1], - [], - {re.ResourceControlled.resource_rep(re.ResourceGlobalPhase, {}, 1, 0, 0): 1}, - ), - ( - "X", - [1], - [0], - [], - {re.ResourceControlled.resource_rep(re.ResourceRX, {}, 1, 1, 0): 1}, - ), - ( - "Y", - [1, 2], - [1, 1], - ["w1"], - {re.ResourceControlled.resource_rep(re.ResourceRY, {}, 2, 0, 1): 1}, - ), - ) - - @pytest.mark.parametrize( - "pauli_word, ctrl_wires, ctrl_values, work_wires, expected_res", ctrl_data - ) - def test_resource_controlled( - self, ctrl_wires, ctrl_values, work_wires, pauli_word, expected_res - ): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourcePauliRot( - 1.24, pauli_word, wires=list(f"wire_{i}" for i in range(len(pauli_word))) - ) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires, **op.resource_params - ) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - @pytest.mark.parametrize("z", range(1, 5)) - @pytest.mark.parametrize("pauli_word", pauli_words) - def test_pow_decomp(self, z, pauli_word): - """Test that the pow decomposition is correct.""" - op = re.ResourcePauliRot(theta=0.5, pauli_word=pauli_word, wires=range(len(pauli_word))) - expected_res = {re.ResourcePauliRot.resource_rep(pauli_string=pauli_word): 1} - assert op.pow_resource_decomp(z, **op.resource_params) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res - - -class TestIsingXX: - """Test the IsingXX class.""" - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourceIsingXX(0.5, wires=[0, 1]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceIsingXX, {}) - assert re.ResourceIsingXX.resource_rep() == expected - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceCNOT.resource_rep(): 2, - re.ResourceRX.resource_rep(): 1, - } - assert re.ResourceIsingXX.resources() == expected - - def test_resources_from_rep(self): - """Test that the resources can be computed from the compressed representation and params.""" - op = re.ResourceIsingXX(0.5, wires=[0, 1]) - expected = { - re.ResourceCNOT.resource_rep(): 2, - re.ResourceRX.resource_rep(): 1, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - -class TestIsingXY: - """Test the IsingXY class.""" - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourceIsingXY(0.5, wires=[0, 1]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceIsingXY, {}) - assert re.ResourceIsingXY.resource_rep() == expected - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceHadamard.resource_rep(): 2, - re.ResourceCY.resource_rep(): 2, - re.ResourceRY.resource_rep(): 1, - re.ResourceRX.resource_rep(): 1, - } - assert re.ResourceIsingXY.resources() == expected - - def test_resources_from_rep(self): - """Test that the resources can be computed from the compressed representation and params.""" - op = re.ResourceIsingXY(0.5, wires=[0, 1]) - expected = { - re.ResourceHadamard.resource_rep(): 2, - re.ResourceCY.resource_rep(): 2, - re.ResourceRY.resource_rep(): 1, - re.ResourceRX.resource_rep(): 1, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - -class TestIsingYY: - """Test the IsingYY class.""" - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourceIsingYY(0.5, wires=[0, 1]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceIsingYY, {}) - assert re.ResourceIsingYY.resource_rep() == expected - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceCY.resource_rep(): 2, - re.ResourceRY.resource_rep(): 1, - } - assert re.ResourceIsingYY.resources() == expected - - def test_resources_from_rep(self): - """Test that the resources can be computed from the compressed representation and params.""" - op = re.ResourceIsingYY(0.5, wires=[0, 1]) - expected = { - re.ResourceCY.resource_rep(): 2, - re.ResourceRY.resource_rep(): 1, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - -class TestIsingZZ: - """Test the IsingZZ class.""" - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourceIsingZZ(0.5, wires=[0, 1]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceIsingZZ, {}) - assert re.ResourceIsingZZ.resource_rep() == expected - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceCNOT.resource_rep(): 2, - re.ResourceRZ.resource_rep(): 1, - } - assert re.ResourceIsingZZ.resources() == expected - - def test_resources_from_rep(self): - """Test that the resources can be computed from the compressed representation and params.""" - op = re.ResourceIsingZZ(0.5, wires=[0, 1]) - expected = { - re.ResourceCNOT.resource_rep(): 2, - re.ResourceRZ.resource_rep(): 1, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - -class TestPSWAP: - """Test the PSWAP class.""" - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourcePSWAP(0.5, wires=[0, 1]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourcePSWAP, {}) - assert re.ResourcePSWAP.resource_rep() == expected - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceSWAP.resource_rep(): 1, - re.ResourceCNOT.resource_rep(): 2, - re.ResourcePhaseShift.resource_rep(): 1, - } - assert re.ResourcePSWAP.resources() == expected - - def test_resources_from_rep(self): - """Test that the resources can be computed from the compressed representation and params.""" - op = re.ResourcePSWAP(0.5, wires=[0, 1]) - expected = { - re.ResourceSWAP.resource_rep(): 1, - re.ResourceCNOT.resource_rep(): 2, - re.ResourcePhaseShift.resource_rep(): 1, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py deleted file mode 100644 index 64bfe0d3d7c..00000000000 --- a/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py +++ /dev/null @@ -1,460 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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 parametric single qubit resource operators. -""" -import copy - -import pytest - -import pennylane.labs.resource_estimation as re -from pennylane.labs.resource_estimation.ops.qubit.parametric_ops_single_qubit import ( - _rotation_resources, -) - -# pylint: disable=no-self-use, use-implicit-booleaness-not-comparison,too-many-arguments - -params = list(zip([10e-3, 10e-4, 10e-5], [17, 21, 24])) - - -@pytest.mark.parametrize("epsilon, expected", params) -def test_rotation_resources(epsilon, expected): - """Test the hardcoded resources used for RX, RY, RZ""" - gate_types = {} - - t = re.CompressedResourceOp(re.ResourceT, {}) - gate_types[t] = expected - assert gate_types == _rotation_resources(epsilon=epsilon) - - -class TestPauliRotation: - """Test ResourceRX, ResourceRY, and ResourceRZ""" - - params_classes = [re.ResourceRX, re.ResourceRY, re.ResourceRZ] - params_errors = [10e-3, 10e-4, 10e-5] - params_ctrl_res = [ - { - re.ResourceHadamard.resource_rep(): 2, - re.ResourceRZ.resource_rep(): 2, - }, - { - re.ResourceRY.resource_rep(): 2, - }, - { - re.ResourceRZ.resource_rep(): 2, - }, - ] - - @pytest.mark.parametrize("resource_class", params_classes) - @pytest.mark.parametrize("epsilon", params_errors) - def test_resources(self, resource_class, epsilon): - """Test the resources method""" - - label = "error_" + resource_class.__name__.replace("Resource", "").lower() - config = {label: epsilon} - op = resource_class(1.24, wires=0) - assert op.resources(config) == _rotation_resources(epsilon=epsilon) - - @pytest.mark.parametrize("resource_class", params_classes) - @pytest.mark.parametrize("epsilon", params_errors) - def test_resource_rep(self, resource_class, epsilon): # pylint: disable=unused-argument - """Test the compact representation""" - op = resource_class(1.24, wires=0) - expected = re.CompressedResourceOp(resource_class, {}) - assert op.resource_rep() == expected - - @pytest.mark.parametrize("resource_class", params_classes) - @pytest.mark.parametrize("epsilon", params_errors) - def test_resources_from_rep(self, resource_class, epsilon): - """Test the resources can be obtained from the compact representation""" - - label = "error_" + resource_class.__name__.replace("Resource", "").lower() - config = {label: epsilon} - op = resource_class(1.24, wires=0) - expected = _rotation_resources(epsilon=epsilon) - - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params, config=config) == expected - - @pytest.mark.parametrize("resource_class", params_classes) - @pytest.mark.parametrize("epsilon", params_errors) - def test_resource_params(self, resource_class, epsilon): # pylint: disable=unused-argument - """Test that the resource params are correct""" - op = resource_class(1.24, wires=0) - assert op.resource_params == {} - - @pytest.mark.parametrize("resource_class", params_classes) - @pytest.mark.parametrize("epsilon", params_errors) - def test_adjoint_decomposition(self, resource_class, epsilon): - """Test that the adjoint decompositions are correct.""" - - expected = {resource_class.resource_rep(): 1} - assert resource_class.adjoint_resource_decomp() == expected - - op = resource_class(1.24, wires=0) - dag = re.ResourceAdjoint(op) - - label = "error_" + resource_class.__name__.replace("Resource", "").lower() - config = {label: epsilon} - - r1 = re.get_resources(op, config=config) - r2 = re.get_resources(dag, config=config) - - assert r1 == r2 - - @pytest.mark.parametrize("resource_class", params_classes) - @pytest.mark.parametrize("epsilon", params_errors) - @pytest.mark.parametrize("z", list(range(0, 10))) - def test_pow_decomposition(self, resource_class, epsilon, z): - """Test that the pow decompositions are correct.""" - - expected = ( - {resource_class.resource_rep(): 1} if z else {re.ResourceIdentity.resource_rep(): 1} - ) - assert resource_class.pow_resource_decomp(z) == expected - - op = resource_class(1.24, wires=0) if z else re.ResourceIdentity(wires=0) - dag = re.ResourcePow(op, z) - - label = "error_" + resource_class.__name__.replace("Resource", "").lower() - config = {label: epsilon} - - r1 = re.get_resources(op, config=config) - r2 = re.get_resources(dag, config=config) - - assert r1 == r2 - - params_ctrl_classes = ( - (re.ResourceRX, re.ResourceCRX), - (re.ResourceRY, re.ResourceCRY), - (re.ResourceRZ, re.ResourceCRZ), - ) - - @pytest.mark.parametrize("resource_class, controlled_class", params_ctrl_classes) - @pytest.mark.parametrize("epsilon", params_errors) - def test_controlled_decomposition_single_control( - self, resource_class, controlled_class, epsilon - ): - """Test that the controlled decompositions are correct.""" - expected = {controlled_class.resource_rep(): 1} - assert resource_class.controlled_resource_decomp(1, 0, 0) == expected - - expected = {controlled_class.resource_rep(): 1, re.ResourceX.resource_rep(): 2} - assert resource_class.controlled_resource_decomp(1, 1, 0) == expected - - op = resource_class(1.24, wires=0) - c_op = re.ResourceControlled(op, control_wires=[1]) - - c = controlled_class(1.24, wires=[0, 1]) - - config = {"error_rx": epsilon, "error_ry": epsilon, "error_rz": epsilon} - - r1 = re.get_resources(c, config=config) - r2 = re.get_resources(c_op, config=config) - - assert r1 == r2 - - ctrl_res_data = ( - ( - [1, 2], - [1, 1], - ["w1"], - {re.ResourceMultiControlledX.resource_rep(2, 0, 1): 2}, - ), - ( - [1, 2], - [1, 0], - [], - {re.ResourceMultiControlledX.resource_rep(2, 1, 0): 2}, - ), - ( - [1, 2, 3], - [1, 0, 0], - ["w1", "w2"], - {re.ResourceMultiControlledX.resource_rep(3, 2, 2): 2}, - ), - ) - - @pytest.mark.parametrize("resource_class, local_res", zip(params_classes, params_ctrl_res)) - @pytest.mark.parametrize("ctrl_wires, ctrl_values, work_wires, general_res", ctrl_res_data) - def test_controlled_decomposition_multi_controlled( - self, resource_class, local_res, ctrl_wires, ctrl_values, work_wires, general_res - ): - """Test that the controlled docomposition is correct when controlled on multiple wires.""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = resource_class(1.23, wires=0) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - expected_resources = copy.copy(local_res) - for k, v in general_res.items(): - expected_resources[k] = v - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_resources - ) - assert op2.resources(**op2.resource_params) == expected_resources - - # pylint: disable=unused-argument, import-outside-toplevel - @pytest.mark.parametrize("resource_class", params_classes) - @pytest.mark.parametrize("epsilon", params_errors) - def test_sparse_matrix_format(self, resource_class, epsilon): - """Test that the sparse matrix accepts the format parameter.""" - from scipy.sparse import coo_matrix, csc_matrix, csr_matrix, lil_matrix - - op = resource_class(1.24, wires=0) - assert isinstance(op.sparse_matrix(), csr_matrix) - assert isinstance(op.sparse_matrix(format="csc"), csc_matrix) - assert isinstance(op.sparse_matrix(format="lil"), lil_matrix) - assert isinstance(op.sparse_matrix(format="coo"), coo_matrix) - - -class TestRot: - """Test ResourceRot""" - - def test_resources(self): - """Test the resources method""" - op = re.ResourceRot(0.1, 0.2, 0.3, wires=0) - ry = re.ResourceRY.resource_rep() - rz = re.ResourceRZ.resource_rep() - expected = {ry: 1, rz: 2} - - assert op.resources() == expected - - def test_resource_rep(self): - """Test the compressed representation""" - op = re.ResourceRot(0.1, 0.2, 0.3, wires=0) - expected = re.CompressedResourceOp(re.ResourceRot, {}) - assert op.resource_rep() == expected - - def test_resources_from_rep(self): - """Test that the resources can be obtained from the compact representation""" - op = re.ResourceRot(0.1, 0.2, 0.3, wires=0) - ry = re.ResourceRY.resource_rep() - rz = re.ResourceRZ.resource_rep() - expected = {ry: 1, rz: 2} - - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - def test_resource_params(self): - """Test that the resource params are correct""" - op = re.ResourceRot(0.1, 0.2, 0.3, wires=0) - assert op.resource_params == {} - - def test_adjoint_decomp(self): - """Test that the adjoint decomposition is correct""" - - expected = {re.ResourceRot.resource_rep(): 1} - assert re.ResourceRot.adjoint_resource_decomp() == expected - - op = re.ResourceRot(1.24, 1.25, 1.26, wires=0) - dag = re.ResourceAdjoint(op) - - r1 = re.get_resources(op) - r2 = re.get_resources(dag) - - assert r1 == r2 - - ctrl_data = ( - ([1], [1], [], {re.ResourceCRot.resource_rep(): 1}), - ( - [1], - [0], - [], - { - re.ResourceCRot.resource_rep(): 1, - re.ResourceX.resource_rep(): 2, - }, - ), - ( - [1, 2], - [1, 1], - ["w1"], - { - re.ResourceRZ.resource_rep(): 3, - re.ResourceRY.resource_rep(): 2, - re.ResourceMultiControlledX.resource_rep(2, 0, 1): 2, - }, - ), - ( - [1, 2, 3], - [1, 0, 0], - ["w1", "w2"], - { - re.ResourceRZ.resource_rep(): 3, - re.ResourceRY.resource_rep(): 2, - re.ResourceMultiControlledX.resource_rep(3, 2, 2): 2, - }, - ), - ) - - @pytest.mark.parametrize("ctrl_wires, ctrl_values, work_wires, expected_res", ctrl_data) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourceRot(1.24, 1.25, 1.26, wires=0) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (0, {re.ResourceIdentity.resource_rep(): 1}), - (1, {re.ResourceRot.resource_rep(): 1}), - (2, {re.ResourceRot.resource_rep(): 1}), - (5, {re.ResourceRot.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op = re.ResourceRot(1.24, 1.25, 1.26, wires=0) - assert op.pow_resource_decomp(z) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res - - -class TestPhaseShift: - """Test ResourcePhaseShift""" - - def test_resources(self): - """Test the resources method""" - op = re.ResourcePhaseShift(0.1, wires=0) - rz = re.ResourceRZ.resource_rep() - global_phase = re.ResourceGlobalPhase.resource_rep() - - expected = {rz: 1, global_phase: 1} - - assert op.resources() == expected - - def test_resource_rep(self): - """Test the compressed representation""" - op = re.ResourcePhaseShift(0.1, wires=0) - expected = re.CompressedResourceOp(re.ResourcePhaseShift, {}) - assert op.resource_rep() == expected - - def test_resources_from_rep(self): - """Test that the resources can be obtained from the compact representation""" - op = re.ResourcePhaseShift(0.1, wires=0) - global_phase = re.ResourceGlobalPhase.resource_rep() - rz = re.ResourceRZ.resource_rep() - expected = {global_phase: 1, rz: 1} - - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - def test_resource_params(self): - """Test that the resource params are correct""" - op = re.ResourcePhaseShift(0.1, wires=0) - assert op.resource_params == {} - - def test_adjoint_decomp(self): - """Test that the adjoint decomposition is correct""" - - expected = {re.ResourcePhaseShift.resource_rep(): 1} - assert re.ResourcePhaseShift.adjoint_resource_decomp() == expected - - op = re.ResourcePhaseShift(0.1, wires=0) - dag = re.ResourceAdjoint(op) - - r1 = re.get_resources(op) - r2 = re.get_resources(dag) - - assert r1 == r2 - - ctrl_data = ( - ([1], [1], [], {re.ResourceControlledPhaseShift.resource_rep(): 1}), - ( - [1], - [0], - [], - { - re.ResourceControlledPhaseShift.resource_rep(): 1, - re.ResourceX.resource_rep(): 2, - }, - ), - ( - [1, 2], - [1, 1], - ["w1"], - { - re.ResourceControlledPhaseShift.resource_rep(): 1, - re.ResourceMultiControlledX.resource_rep(2, 0, 1): 2, - }, - ), - ( - [1, 2, 3], - [1, 0, 0], - ["w1", "w2"], - { - re.ResourceControlledPhaseShift.resource_rep(): 1, - re.ResourceMultiControlledX.resource_rep(3, 2, 2): 2, - }, - ), - ) - - @pytest.mark.parametrize("ctrl_wires, ctrl_values, work_wires, expected_res", ctrl_data) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourcePhaseShift(0.1, wires=0) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - pow_data = ( - (0, {re.ResourceIdentity.resource_rep(): 1}), - (1, {re.ResourcePhaseShift.resource_rep(): 1}), - (2, {re.ResourcePhaseShift.resource_rep(): 1}), - (5, {re.ResourcePhaseShift.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op = re.ResourcePhaseShift(0.1, wires=0) - assert op.pow_resource_decomp(z) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_qchem_ops.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_qchem_ops.py deleted file mode 100644 index 364502f2056..00000000000 --- a/pennylane/labs/tests/resource_estimation/ops/qubit/test_qchem_ops.py +++ /dev/null @@ -1,326 +0,0 @@ -# Copyright 2024 Xanadu Quantum Technologies Inc. - -# 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 qchem ops resource operators.""" - -import pennylane.labs.resource_estimation as re - -# pylint: disable=use-implicit-booleaness-not-comparison,no-self-use - - -class TestSingleExcitation: - """Tests for the ResourceSingleExcitation class.""" - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceAdjoint.resource_rep(re.ResourceT, {}): 2, - re.ResourceHadamard.resource_rep(): 4, - re.ResourceS.resource_rep(): 2, - re.ResourceAdjoint.resource_rep(re.ResourceS, {}): 2, - re.ResourceCNOT.resource_rep(): 2, - re.ResourceRZ.resource_rep(): 1, - re.ResourceRY.resource_rep(): 1, - re.ResourceT.resource_rep(): 2, - } - assert re.ResourceSingleExcitation.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourceSingleExcitation(0.5, wires=[0, 1]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceSingleExcitation, {}) - assert re.ResourceSingleExcitation.resource_rep() == expected - - def test_resources_from_rep(self): - """Test that the resources can be obtained from the compressed representation.""" - op = re.ResourceSingleExcitation(0.5, wires=[0, 1]) - expected = { - re.ResourceAdjoint.resource_rep(re.ResourceT, {}): 2, - re.ResourceHadamard.resource_rep(): 4, - re.ResourceS.resource_rep(): 2, - re.ResourceAdjoint.resource_rep(re.ResourceS, {}): 2, - re.ResourceCNOT.resource_rep(): 2, - re.ResourceRZ.resource_rep(): 1, - re.ResourceRY.resource_rep(): 1, - re.ResourceT.resource_rep(): 2, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - -class TestSingleExcitationMinus: - """Tests for the ResourceSingleExcitationMinus class.""" - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceX.resource_rep(): 4, - re.ResourceControlledPhaseShift.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 2, - re.ResourceCRY.resource_rep(): 1, - } - assert re.ResourceSingleExcitationMinus.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourceSingleExcitationMinus(0.5, wires=[0, 1]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceSingleExcitationMinus, {}) - assert re.ResourceSingleExcitationMinus.resource_rep() == expected - - def test_resources_from_rep(self): - """Test that the resources can be obtained from the compressed representation.""" - op = re.ResourceSingleExcitationMinus(0.5, wires=[0, 1]) - expected = { - re.ResourceX.resource_rep(): 4, - re.ResourceControlledPhaseShift.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 2, - re.ResourceCRY.resource_rep(): 1, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - -class TestSingleExcitationPlus: - """Tests for the ResourceSingleExcitationPlus class.""" - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceX.resource_rep(): 4, - re.ResourceControlledPhaseShift.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 2, - re.ResourceCRY.resource_rep(): 1, - } - assert re.ResourceSingleExcitationPlus.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourceSingleExcitationPlus(0.5, wires=[0, 1]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceSingleExcitationPlus, {}) - assert re.ResourceSingleExcitationPlus.resource_rep() == expected - - def test_resources_from_rep(self): - """Test that the resources can be obtained from the compressed representation.""" - op = re.ResourceSingleExcitationPlus(0.5, wires=[0, 1]) - expected = { - re.ResourceX.resource_rep(): 4, - re.ResourceControlledPhaseShift.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 2, - re.ResourceCRY.resource_rep(): 1, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - -class TestDoubleExcitation: - """Tests for the ResourceDoubleExcitation class.""" - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceHadamard.resource_rep(): 6, - re.ResourceRY.resource_rep(): 8, - re.ResourceCNOT.resource_rep(): 14, - } - assert re.ResourceDoubleExcitation.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourceDoubleExcitation(0.5, wires=[0, 1, 2, 3]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceDoubleExcitation, {}) - assert re.ResourceDoubleExcitation.resource_rep() == expected - - def test_resources_from_rep(self): - """Test that the resources can be obtained from the compressed representation.""" - op = re.ResourceDoubleExcitation(0.5, wires=[0, 1, 2, 3]) - expected = { - re.ResourceHadamard.resource_rep(): 6, - re.ResourceRY.resource_rep(): 8, - re.ResourceCNOT.resource_rep(): 14, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - -class TestDoubleExcitationMinus: - """Tests for the ResourceDoubleExcitationMinus class.""" - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceGlobalPhase.resource_rep(): 1, - re.ResourceDoubleExcitation.resource_rep(): 1, - re.ResourceControlled.resource_rep(re.ResourceZ, {}, 3, 1, 0): 2, - re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 3, 1, 0): 2, - } - assert re.ResourceDoubleExcitationMinus.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourceDoubleExcitationMinus(0.5, wires=[0, 1, 2, 3]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceDoubleExcitationMinus, {}) - assert re.ResourceDoubleExcitationMinus.resource_rep() == expected - - def test_resources_from_rep(self): - """Test that the resources can be obtained from the compressed representation.""" - op = re.ResourceDoubleExcitationMinus(0.5, wires=[0, 1, 2, 3]) - expected = { - re.ResourceGlobalPhase.resource_rep(): 1, - re.ResourceDoubleExcitation.resource_rep(): 1, - re.ResourceControlled.resource_rep(re.ResourceZ, {}, 3, 1, 0): 2, - re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 3, 1, 0): 2, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - -class TestDoubleExcitationPlus: - """Tests for the ResourceDoubleExcitationPlus class.""" - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceGlobalPhase.resource_rep(): 1, - re.ResourceDoubleExcitation.resource_rep(): 1, - re.ResourceControlled.resource_rep(re.ResourceZ, {}, 3, 1, 0): 2, - re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 3, 1, 0): 2, - } - assert re.ResourceDoubleExcitationPlus.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourceDoubleExcitationPlus(0.5, wires=[0, 1, 3, 4]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceDoubleExcitationPlus, {}) - assert re.ResourceDoubleExcitationPlus.resource_rep() == expected - - def test_resources_from_rep(self): - """Test that the resources can be obtained from the compressed representation.""" - op = re.ResourceDoubleExcitationPlus(0.5, wires=[0, 1, 3, 4]) - expected = { - re.ResourceGlobalPhase.resource_rep(): 1, - re.ResourceDoubleExcitation.resource_rep(): 1, - re.ResourceControlled.resource_rep(re.ResourceZ, {}, 3, 1, 0): 2, - re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 3, 1, 0): 2, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - -class TestOrbitalRotation: - """Tests for the ResourceOrbitalRotation class.""" - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceFermionicSWAP.resource_rep(): 2, - re.ResourceSingleExcitation.resource_rep(): 2, - } - assert re.ResourceOrbitalRotation.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourceOrbitalRotation(0.5, wires=[0, 1, 3, 4]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceOrbitalRotation, {}) - assert re.ResourceOrbitalRotation.resource_rep() == expected - - def test_resources_from_rep(self): - """Test that the resources can be obtained from the compressed representation.""" - op = re.ResourceOrbitalRotation(0.5, wires=[0, 1, 3, 4]) - expected = { - re.ResourceFermionicSWAP.resource_rep(): 2, - re.ResourceSingleExcitation.resource_rep(): 2, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - -class TestFermionicSWAP: - """Tests for the ResourceFermionicSWAP class.""" - - def test_resources(self): - """Test that the resources are correct.""" - expected = { - re.ResourceHadamard.resource_rep(): 4, - re.ResourceMultiRZ.resource_rep(num_wires=2): 2, - re.ResourceRX.resource_rep(): 4, - re.ResourceRZ.resource_rep(): 2, - re.ResourceGlobalPhase.resource_rep(): 1, - } - assert re.ResourceFermionicSWAP.resources() == expected - - def test_resource_params(self): - """Test that the resource params are correct.""" - op = re.ResourceFermionicSWAP(0.5, wires=[0, 1]) - assert op.resource_params == {} - - def test_resource_rep(self): - """Test that the compressed representation is correct.""" - expected = re.CompressedResourceOp(re.ResourceFermionicSWAP, {}) - assert re.ResourceFermionicSWAP.resource_rep() == expected - - def test_resources_from_rep(self): - """Test that the resources can be obtained from the compressed representation.""" - op = re.ResourceFermionicSWAP(0.5, wires=[0, 1]) - expected = { - re.ResourceHadamard.resource_rep(): 4, - re.ResourceMultiRZ.resource_rep(num_wires=2): 2, - re.ResourceRX.resource_rep(): 4, - re.ResourceRZ.resource_rep(): 2, - re.ResourceGlobalPhase.resource_rep(): 1, - } - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected diff --git a/pennylane/labs/tests/resource_estimation/ops/test_identity.py b/pennylane/labs/tests/resource_estimation/ops/test_identity.py deleted file mode 100644 index 0cfb385c429..00000000000 --- a/pennylane/labs/tests/resource_estimation/ops/test_identity.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright 2024 Xanadu Quantum Technologies Inc. - -# 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 identity resource operators -""" -import pytest - -import pennylane.labs.resource_estimation as re - -# pylint: disable=no-self-use,use-implicit-booleaness-not-comparison - - -class TestIdentity: - """Test ResourceIdentity""" - - def test_resources(self): - """ResourceIdentity should have empty resources""" - op = re.ResourceIdentity() - assert op.resources() == {} - - def test_resource_rep(self): - """Test the compressed representation""" - expected = re.CompressedResourceOp(re.ResourceIdentity, {}) - assert re.ResourceIdentity.resource_rep() == expected - - def test_resource_params(self): - """Test the resource params are correct""" - op = re.ResourceIdentity(0) - assert op.resource_params == {} - - def test_resources_from_rep(self): - """Test that the resources can be computed from the compressed representation""" - op = re.ResourceIdentity() - expected = {} - - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - op = re.ResourceIdentity(0) - op2 = re.ResourceAdjoint(op) - assert op.adjoint_resource_decomp() == {re.ResourceIdentity.resource_rep(): 1} - assert op2.resources(**op2.resource_params) == {re.ResourceIdentity.resource_rep(): 1} - - identity_ctrl_data = ( - ([1], [1], [], {re.ResourceIdentity.resource_rep(): 1}), - ([1, 2], [1, 1], ["w1"], {re.ResourceIdentity.resource_rep(): 1}), - ([1, 2, 3], [1, 0, 0], ["w1", "w2"], {re.ResourceIdentity.resource_rep(): 1}), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", identity_ctrl_data - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_work_wires = len(work_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - - op = re.ResourceIdentity(0) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - identity_pow_data = ( - (1, {re.ResourceIdentity.resource_rep(): 1}), - (2, {re.ResourceIdentity.resource_rep(): 1}), - (5, {re.ResourceIdentity.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", identity_pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op = re.ResourceIdentity(0) - assert op.pow_resource_decomp(z) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res - - -class TestGlobalPhase: - """Test ResourceGlobalPhase""" - - def test_resources(self): - """ResourceGlobalPhase should have empty resources""" - op = re.ResourceGlobalPhase(0.1, wires=0) - assert op.resources() == {} - - def test_resource_rep(self): - """Test the compressed representation""" - expected = re.CompressedResourceOp(re.ResourceGlobalPhase, {}) - assert re.ResourceGlobalPhase.resource_rep() == expected - - def test_resource_params(self): - """Test the resource params are correct""" - op = re.ResourceGlobalPhase(0.1, wires=0) - assert op.resource_params == {} - - def test_resources_from_rep(self): - """Test that the resources can be computed from the compressed representation""" - op = re.ResourceGlobalPhase(0.1, wires=0) - expected = {} - - op_compressed_rep = op.resource_rep_from_op() - op_resource_type = op_compressed_rep.op_type - op_resource_params = op_compressed_rep.params - assert op_resource_type.resources(**op_resource_params) == expected - - def test_resource_adjoint(self): - """Test that the adjoint resources are as expected""" - op = re.ResourceGlobalPhase(0.1, wires=0) - op2 = re.ResourceAdjoint(op) - assert op.adjoint_resource_decomp() == {re.ResourceGlobalPhase.resource_rep(): 1} - assert op2.resources(**op2.resource_params) == {re.ResourceGlobalPhase.resource_rep(): 1} - - globalphase_ctrl_data = ( - ([1], [1], [], {re.ResourcePhaseShift.resource_rep(): 1}), - ( - [1, 2], - [1, 1], - ["w1"], - { - re.ResourcePhaseShift.resource_rep(): 1, - re.ResourceMultiControlledX.resource_rep(2, 0, 1): 2, - }, - ), - ( - [1, 2, 3], - [1, 0, 0], - ["w1", "w2"], - { - re.ResourcePhaseShift.resource_rep(): 1, - re.ResourceMultiControlledX.resource_rep(3, 2, 2): 2, - }, - ), - ) - - @pytest.mark.parametrize( - "ctrl_wires, ctrl_values, work_wires, expected_res", globalphase_ctrl_data - ) - def test_resource_controlled(self, ctrl_wires, ctrl_values, work_wires, expected_res): - """Test that the controlled resources are as expected""" - num_ctrl_wires = len(ctrl_wires) - num_ctrl_values = len([v for v in ctrl_values if not v]) - num_work_wires = len(work_wires) - - op = re.ResourceGlobalPhase(0.1, wires=0) - op2 = re.ResourceControlled( - op, control_wires=ctrl_wires, control_values=ctrl_values, work_wires=work_wires - ) - - assert ( - op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires) - == expected_res - ) - assert op2.resources(**op2.resource_params) == expected_res - - globalphase_pow_data = ( - (1, {re.ResourceGlobalPhase.resource_rep(): 1}), - (2, {re.ResourceGlobalPhase.resource_rep(): 1}), - (5, {re.ResourceGlobalPhase.resource_rep(): 1}), - ) - - @pytest.mark.parametrize("z, expected_res", globalphase_pow_data) - def test_resource_pow(self, z, expected_res): - """Test that the pow resources are as expected""" - op = re.ResourceGlobalPhase(0.1, wires=0) - assert op.pow_resource_decomp(z) == expected_res - - op2 = re.ResourcePow(op, z) - assert op2.resources(**op2.resource_params) == expected_res diff --git a/pennylane/labs/tests/resource_estimation/templates/test_resource_basisrotation.py b/pennylane/labs/tests/resource_estimation/templates/test_resource_basisrotation.py deleted file mode 100644 index 54cb70f5902..00000000000 --- a/pennylane/labs/tests/resource_estimation/templates/test_resource_basisrotation.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -""" -Test the ResourceBasisRotation class -""" -import pytest - -import pennylane as qml -import pennylane.labs.resource_estimation as re - -# pylint: disable=no-self-use - - -class TestBasisRotation: - """Test the ResourceBasisRotation class""" - - op_data = ( - re.ResourceBasisRotation(unitary_matrix=qml.matrix(qml.X(0) @ qml.Y(1)), wires=range(4)), - re.ResourceBasisRotation( - unitary_matrix=qml.matrix(qml.RX(1.23, 0) @ qml.RY(4.56, 1) @ qml.Z(2)), wires=range(8) - ), - re.ResourceBasisRotation( - unitary_matrix=qml.matrix( - qml.Hadamard(0) @ qml.Hadamard(1) @ qml.Hadamard(2) @ qml.Hadamard(3) - ), - wires=range(16), - ), - ) - - resource_data = ( - { - re.ResourcePhaseShift.resource_rep(): 10, - re.ResourceSingleExcitation.resource_rep(): 6, - }, - { - re.ResourcePhaseShift.resource_rep(): 36, - re.ResourceSingleExcitation.resource_rep(): 28, - }, - { - re.ResourcePhaseShift.resource_rep(): 136, - re.ResourceSingleExcitation.resource_rep(): 120, - }, - ) - - resource_params_data = ( - { - "dim_N": 4, - }, - { - "dim_N": 8, - }, - { - "dim_N": 16, - }, - ) - - name_data = ( - "BasisRotation(4)", - "BasisRotation(8)", - "BasisRotation(16)", - ) - - @pytest.mark.parametrize( - "op, params, expected_res", zip(op_data, resource_params_data, resource_data) - ) - def test_resources(self, op, params, expected_res): - """Test the resources method returns the correct dictionary""" - res_from_op = op.resources(**op.resource_params) - res_from_func = re.ResourceBasisRotation.resources(**params) - - assert res_from_op == expected_res - assert res_from_func == expected_res - - @pytest.mark.parametrize("op, expected_params", zip(op_data, resource_params_data)) - def test_resource_params(self, op, expected_params): - """Test that the resource params are correct""" - assert op.resource_params == expected_params - - @pytest.mark.parametrize("expected_params", resource_params_data) - def test_resource_rep(self, expected_params): - """Test the resource_rep returns the correct CompressedResourceOp""" - expected = re.CompressedResourceOp(re.ResourceBasisRotation, expected_params) - assert re.ResourceBasisRotation.resource_rep(**expected_params) == expected - - @pytest.mark.parametrize("params, expected_name", zip(resource_params_data, name_data)) - def test_tracking_name(self, params, expected_name): - """Test that the tracking name is correct.""" - assert re.ResourceBasisRotation.tracking_name(**params) == expected_name diff --git a/pennylane/labs/tests/resource_estimation/templates/test_resource_prepselprep.py b/pennylane/labs/tests/resource_estimation/templates/test_resource_prepselprep.py deleted file mode 100644 index ffd2cc7a907..00000000000 --- a/pennylane/labs/tests/resource_estimation/templates/test_resource_prepselprep.py +++ /dev/null @@ -1,190 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -""" -Test the ResourcePrepSelPrep class -""" -import copy - -import pytest - -import pennylane.labs.resource_estimation as re -from pennylane.ops import LinearCombination - -# pylint: disable=no-self-use - - -class TestPrepSelPrep: - """Test the ResourcePrepSelPrep class""" - - op_data = ( - re.ResourcePrepSelPrep( - LinearCombination([1.23, -4.5], [re.ResourceX(0), re.ResourceZ(0)]), - control=["c1"], - ), - re.ResourcePrepSelPrep( - LinearCombination( - [1.0, 1.0, 1.0, 1.0], - [ - re.ResourceRX(1.2, 0), - re.ResourceRZ(-3.4, 1), - re.ResourceCNOT([0, 1]), - re.ResourceHadamard(0), - ], - ), - control=["c1", "c2"], - ), - re.ResourcePrepSelPrep( - LinearCombination( - (0.1, -2.3, 4.5, -6, 0.78), - ( - re.ResourceProd(re.ResourceZ(0), re.ResourceZ(1)), - re.ResourceProd(re.ResourceX(0), re.ResourceX(2)), - re.ResourceProd(re.ResourceY(2), re.ResourceY(1)), - re.ResourceAdjoint( - re.ResourceProd(re.ResourceX(0), re.ResourceY(1), re.ResourceZ(2)) - ), - re.ResourceQFT([0, 1, 2]), - ), - ), - control=["c1", "c2", "c3"], - ), - ) - - resource_data = ( - { - re.ResourceStatePrep.resource_rep(num_wires=1): 1, - re.ResourceSelect.resource_rep( - cmpr_ops=( - re.ResourceX.resource_rep(), - re.ResourceZ.resource_rep(), - ), - ): 1, - re.ResourceAdjoint.resource_rep(re.ResourceStatePrep, base_params={"num_wires": 1}): 1, - }, - { - re.ResourceStatePrep.resource_rep(num_wires=2): 1, - re.ResourceSelect.resource_rep( - cmpr_ops=( - re.ResourceRX.resource_rep(), - re.ResourceRZ.resource_rep(), - re.ResourceCNOT.resource_rep(), - re.ResourceHadamard.resource_rep(), - ), - ): 1, - re.ResourceAdjoint.resource_rep(re.ResourceStatePrep, base_params={"num_wires": 2}): 1, - }, - { - re.ResourceStatePrep.resource_rep(num_wires=3): 1, - re.ResourceSelect.resource_rep( - cmpr_ops=( - re.ResourceProd.resource_rep( - (re.ResourceZ.resource_rep(), re.ResourceZ.resource_rep()) - ), - re.ResourceProd.resource_rep( - (re.ResourceX.resource_rep(), re.ResourceX.resource_rep()) - ), - re.ResourceProd.resource_rep( - (re.ResourceY.resource_rep(), re.ResourceY.resource_rep()) - ), - re.ResourceAdjoint.resource_rep( - base_class=re.ResourceProd, - base_params={ - "cmpr_factors": ( - re.ResourceX.resource_rep(), - re.ResourceY.resource_rep(), - re.ResourceZ.resource_rep(), - ) - }, - ), - re.ResourceQFT.resource_rep(num_wires=3), - ), - ): 1, - re.ResourceAdjoint.resource_rep(re.ResourceStatePrep, base_params={"num_wires": 3}): 1, - }, - ) - - resource_params_data = ( - { - "cmpr_ops": ( - re.ResourceX.resource_rep(), - re.ResourceZ.resource_rep(), - ), - }, - { - "cmpr_ops": ( - re.ResourceRX.resource_rep(), - re.ResourceRZ.resource_rep(), - re.ResourceCNOT.resource_rep(), - re.ResourceHadamard.resource_rep(), - ), - }, - { - "cmpr_ops": ( - re.ResourceProd.resource_rep( - (re.ResourceZ.resource_rep(), re.ResourceZ.resource_rep()) - ), - re.ResourceProd.resource_rep( - (re.ResourceX.resource_rep(), re.ResourceX.resource_rep()) - ), - re.ResourceProd.resource_rep( - (re.ResourceY.resource_rep(), re.ResourceY.resource_rep()) - ), - re.ResourceAdjoint.resource_rep( - base_class=re.ResourceProd, - base_params={ - "cmpr_factors": ( - re.ResourceX.resource_rep(), - re.ResourceY.resource_rep(), - re.ResourceZ.resource_rep(), - ) - }, - ), - re.ResourceQFT.resource_rep(num_wires=3), - ), - }, - ) - - @pytest.mark.parametrize( - "op, params, expected_res", zip(op_data, resource_params_data, resource_data) - ) - def test_resources(self, op, params, expected_res): - """Test the resources method returns the correct dictionary""" - res_from_op = op.resources(**op.resource_params) - res_from_func = re.ResourcePrepSelPrep.resources(**params) - - assert res_from_op == expected_res - assert res_from_func == expected_res - - @pytest.mark.parametrize("op, expected_params", zip(op_data, resource_params_data)) - def test_resource_params(self, op, expected_params): - """Test that the resource params are correct""" - assert op.resource_params == expected_params - - @pytest.mark.parametrize("expected_params", resource_params_data) - def test_resource_rep(self, expected_params): - """Test the resource_rep returns the correct CompressedResourceOp""" - expected = re.CompressedResourceOp(re.ResourcePrepSelPrep, expected_params) - assert re.ResourcePrepSelPrep.resource_rep(**expected_params) == expected - - @pytest.mark.parametrize("z", [2, 3, 5]) - @pytest.mark.parametrize("params, base_res", zip(resource_params_data, resource_data)) - def test_pow_resources(self, params, base_res, z): - """Test the pow_resources method returns the correct dictionary""" - pow_select = re.ResourcePow.resource_rep(re.ResourceSelect, base_params=params, z=z) - - expected_res = copy.deepcopy(base_res) - select_res = expected_res.pop(re.ResourceSelect.resource_rep(**params)) - - expected_res[pow_select] = select_res - assert re.ResourcePrepSelPrep.pow_resource_decomp(z=z, **params) == expected_res diff --git a/pennylane/labs/tests/resource_estimation/templates/test_resource_qpe.py b/pennylane/labs/tests/resource_estimation/templates/test_resource_qpe.py deleted file mode 100644 index 52653e12be3..00000000000 --- a/pennylane/labs/tests/resource_estimation/templates/test_resource_qpe.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -""" -Test the ResourceQuantumPhaseEstimation class -""" -import pytest - -import pennylane as qml -import pennylane.labs.resource_estimation as re - -# pylint: disable=no-self-use - - -class TestQuantumPhaseEstimation: - """Test the ResourceQuantumPhaseEstimation class""" - - input_data = ( - ( - re.ResourceHadamard(0), - [1, 2], - ), - ( - re.ResourceRX(1.23, 1), - [2, 3, 4], - ), - ( - re.ResourceCRY(1.23, [0, 1]), - [2, 3, 4, 5], - ), - ( - re.ResourceQFT([0, 1, 2]), - [4, 5], - ), - ) - - resource_data = ( - { - re.ResourceHadamard.resource_rep(): 2, - re.ResourceAdjoint.resource_rep(re.ResourceQFT, {"num_wires": 2}): 1, - re.ResourceControlled.resource_rep(re.ResourceHadamard, {}, 1, 0, 0): 3, - }, - { - re.ResourceRX.resource_rep(): 3, - re.ResourceAdjoint.resource_rep(re.ResourceQFT, {"num_wires": 3}): 1, - re.ResourceControlled.resource_rep(re.ResourceRX, {}, 1, 0, 0): 7, - }, - { - re.ResourceCRY.resource_rep(): 4, - re.ResourceAdjoint.resource_rep(re.ResourceQFT, {"num_wires": 4}): 1, - re.ResourceControlled.resource_rep(re.ResourceCRY, {}, 1, 0, 0): 15, - }, - { - re.ResourceQFT.resource_rep(num_wires=3): 2, - re.ResourceAdjoint.resource_rep(re.ResourceQFT, {"num_wires": 2}): 1, - re.ResourceControlled.resource_rep(re.ResourceQFT, {"num_wires": 3}, 1, 0, 0): 3, - }, - ) - - resource_params_data = ( - { - "base_class": re.ResourceHadamard, - "base_params": {}, - "num_estimation_wires": 2, - }, - { - "base_class": re.ResourceRX, - "base_params": {}, - "num_estimation_wires": 3, - }, - { - "base_class": re.ResourceCRY, - "base_params": {}, - "num_estimation_wires": 4, - }, - { - "base_class": re.ResourceQFT, - "base_params": {"num_wires": 3}, - "num_estimation_wires": 2, - }, - ) - - name_data = ( - "QPE(Hadamard, 2)", - "QPE(RX, 3)", - "QPE(CRY, 4)", - "QPE(QFT(3), 2)", - ) - - @pytest.mark.parametrize( - "num_wires, num_hadamard, num_swap, num_ctrl_phase_shift", - [ - (1, 1, 0, 0), - (2, 2, 1, 1), - (3, 3, 1, 3), - (4, 4, 2, 6), - ], - ) - def test_resources(self, num_wires, num_hadamard, num_swap, num_ctrl_phase_shift): - """Test the resources method returns the correct dictionary""" - hadamard = re.CompressedResourceOp(re.ResourceHadamard, {}) - swap = re.CompressedResourceOp(re.ResourceSWAP, {}) - ctrl_phase_shift = re.CompressedResourceOp(re.ResourceControlledPhaseShift, {}) - - expected = {hadamard: num_hadamard, swap: num_swap, ctrl_phase_shift: num_ctrl_phase_shift} - - assert re.ResourceQFT.resources(num_wires) == expected - - @pytest.mark.parametrize( - "unitary_and_wires, expected_params", zip(input_data, resource_params_data) - ) - def test_resource_params(self, unitary_and_wires, expected_params): - """Test that the resource params are correct""" - unitary, estimation_wires = unitary_and_wires - op = re.ResourceQuantumPhaseEstimation(unitary, estimation_wires=estimation_wires) - assert op.resource_params == expected_params - - def test_resource_params_error(self): - """Test that an error is raised if a resource operator is not provided.""" - with pytest.raises(TypeError, match="Can't obtain QPE resources when"): - op = re.ResourceQuantumPhaseEstimation(qml.Hadamard(0), estimation_wires=[1, 2, 3]) - op.resource_params # pylint: disable=pointless-statement - - @pytest.mark.parametrize("expected_params", resource_params_data) - def test_resource_rep(self, expected_params): - """Test the resource_rep returns the correct CompressedResourceOp""" - expected = re.CompressedResourceOp(re.ResourceQuantumPhaseEstimation, expected_params) - assert re.ResourceQuantumPhaseEstimation.resource_rep(**expected_params) == expected - - @pytest.mark.parametrize("params, expected_name", zip(resource_params_data, name_data)) - def test_tracking_name(self, params, expected_name): - """Test that the tracking name is correct.""" - assert re.ResourceQuantumPhaseEstimation.tracking_name(**params) == expected_name diff --git a/pennylane/labs/tests/resource_estimation/templates/test_resource_qubitization.py b/pennylane/labs/tests/resource_estimation/templates/test_resource_qubitization.py deleted file mode 100644 index 2a9b6fc72a2..00000000000 --- a/pennylane/labs/tests/resource_estimation/templates/test_resource_qubitization.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -""" -Test the ResourceQubitization class -""" -import pytest - -import pennylane.labs.resource_estimation as re -from pennylane.ops import LinearCombination - -# pylint: disable=no-self-use - - -class TestQubitization: - """Test the ResourceQubitization class""" - - op_data = ( - re.ResourceQubitization( - LinearCombination([1.23, -4.5], [re.ResourceX(0), re.ResourceZ(0)]), - control=["c1"], - ), - re.ResourceQubitization( - LinearCombination( - [1.0, 1.0, 1.0, 1.0], - [ - re.ResourceRX(1.2, 0), - re.ResourceRZ(-3.4, 1), - re.ResourceCNOT([0, 1]), - re.ResourceHadamard(0), - ], - ), - control=["c1", "c2"], - ), - re.ResourceQubitization( - LinearCombination( - (0.1, -2.3, 4.5, -6, 0.78), - ( - re.ResourceProd(re.ResourceZ(0), re.ResourceZ(1)), - re.ResourceProd(re.ResourceX(0), re.ResourceX(2)), - re.ResourceProd(re.ResourceY(2), re.ResourceY(1)), - re.ResourceAdjoint( - re.ResourceProd(re.ResourceX(0), re.ResourceY(1), re.ResourceZ(2)) - ), - re.ResourceQFT([0, 1, 2]), - ), - ), - control=["c1", "c2", "c3"], - ), - ) - - resource_data = ( - { - re.ResourceReflection.resource_rep(re.ResourceIdentity, {}, 1): 1, - re.ResourcePrepSelPrep.resource_rep( - cmpr_ops=( - re.ResourceX.resource_rep(), - re.ResourceZ.resource_rep(), - ), - ): 1, - }, - { - re.ResourceReflection.resource_rep(re.ResourceIdentity, {}, 2): 1, - re.ResourcePrepSelPrep.resource_rep( - cmpr_ops=( - re.ResourceRX.resource_rep(), - re.ResourceRZ.resource_rep(), - re.ResourceCNOT.resource_rep(), - re.ResourceHadamard.resource_rep(), - ), - ): 1, - }, - { - re.ResourceReflection.resource_rep(re.ResourceIdentity, {}, 3): 1, - re.ResourcePrepSelPrep.resource_rep( - cmpr_ops=( - re.ResourceProd.resource_rep( - (re.ResourceZ.resource_rep(), re.ResourceZ.resource_rep()) - ), - re.ResourceProd.resource_rep( - (re.ResourceX.resource_rep(), re.ResourceX.resource_rep()) - ), - re.ResourceProd.resource_rep( - (re.ResourceY.resource_rep(), re.ResourceY.resource_rep()) - ), - re.ResourceAdjoint.resource_rep( - base_class=re.ResourceProd, - base_params={ - "cmpr_factors": ( - re.ResourceX.resource_rep(), - re.ResourceY.resource_rep(), - re.ResourceZ.resource_rep(), - ) - }, - ), - re.ResourceQFT.resource_rep(num_wires=3), - ), - ): 1, - }, - ) - - resource_params_data = ( - { - "cmpr_ops": ( - re.ResourceX.resource_rep(), - re.ResourceZ.resource_rep(), - ), - "num_ctrl_wires": 1, - }, - { - "cmpr_ops": ( - re.ResourceRX.resource_rep(), - re.ResourceRZ.resource_rep(), - re.ResourceCNOT.resource_rep(), - re.ResourceHadamard.resource_rep(), - ), - "num_ctrl_wires": 2, - }, - { - "cmpr_ops": ( - re.ResourceProd.resource_rep( - (re.ResourceZ.resource_rep(), re.ResourceZ.resource_rep()) - ), - re.ResourceProd.resource_rep( - (re.ResourceX.resource_rep(), re.ResourceX.resource_rep()) - ), - re.ResourceProd.resource_rep( - (re.ResourceY.resource_rep(), re.ResourceY.resource_rep()) - ), - re.ResourceAdjoint.resource_rep( - base_class=re.ResourceProd, - base_params={ - "cmpr_factors": ( - re.ResourceX.resource_rep(), - re.ResourceY.resource_rep(), - re.ResourceZ.resource_rep(), - ) - }, - ), - re.ResourceQFT.resource_rep(num_wires=3), - ), - "num_ctrl_wires": 3, - }, - ) - - @pytest.mark.parametrize( - "op, params, expected_res", zip(op_data, resource_params_data, resource_data) - ) - def test_resources(self, op, params, expected_res): - """Test the resources method returns the correct dictionary""" - res_from_op = op.resources(**op.resource_params) - res_from_func = re.ResourceQubitization.resources(**params) - - assert res_from_op == expected_res - assert res_from_func == expected_res - - @pytest.mark.parametrize("op, expected_params", zip(op_data, resource_params_data)) - def test_resource_params(self, op, expected_params): - """Test that the resource params are correct""" - assert op.resource_params == expected_params - - @pytest.mark.parametrize("expected_params", resource_params_data) - def test_resource_rep(self, expected_params): - """Test the resource_rep returns the correct CompressedResourceOp""" - expected = re.CompressedResourceOp(re.ResourceQubitization, expected_params) - assert re.ResourceQubitization.resource_rep(**expected_params) == expected diff --git a/pennylane/labs/tests/resource_estimation/templates/test_resource_reflection.py b/pennylane/labs/tests/resource_estimation/templates/test_resource_reflection.py deleted file mode 100644 index 3b1cb708ad9..00000000000 --- a/pennylane/labs/tests/resource_estimation/templates/test_resource_reflection.py +++ /dev/null @@ -1,106 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -""" -Test the ResourceReflection class -""" -import pytest - -import pennylane.labs.resource_estimation as re - -# pylint: disable=no-self-use - - -class TestReflection: - """Test the ResourceReflection class""" - - op_data = ( - re.ResourceReflection(re.ResourceHadamard(0)), - re.ResourceReflection(re.ResourceProd(re.ResourceX(0), re.ResourceY(1)), alpha=1.23), - re.ResourceReflection(re.ResourceQFT(range(4)), reflection_wires=range(3)), - ) - - resource_data = ( - { - re.ResourceX.resource_rep(): 2, - re.ResourceGlobalPhase.resource_rep(): 1, - re.ResourceHadamard.resource_rep(): 1, - re.ResourceAdjoint.resource_rep(base_class=re.ResourceHadamard, base_params={}): 1, - re.ResourcePhaseShift.resource_rep(): 1, - }, - { - re.ResourceX.resource_rep(): 2, - re.ResourceGlobalPhase.resource_rep(): 1, - re.ResourceProd.resource_rep( - cmpr_factors=(re.ResourceX.resource_rep(), re.ResourceY.resource_rep()) - ): 1, - re.ResourceAdjoint.resource_rep( - base_class=re.ResourceProd, - base_params={ - "cmpr_factors": (re.ResourceX.resource_rep(), re.ResourceY.resource_rep()) - }, - ): 1, - re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 1, 1, 0): 1, - }, - { - re.ResourceX.resource_rep(): 2, - re.ResourceGlobalPhase.resource_rep(): 1, - re.ResourceQFT.resource_rep(num_wires=4): 1, - re.ResourceAdjoint.resource_rep( - base_class=re.ResourceQFT, base_params={"num_wires": 4} - ): 1, - re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 2, 2, 0): 1, - }, - ) - - resource_params_data = ( - { - "base_class": re.ResourceHadamard, - "base_params": {}, - "num_ref_wires": 1, - }, - { - "base_class": re.ResourceProd, - "base_params": { - "cmpr_factors": (re.ResourceX.resource_rep(), re.ResourceY.resource_rep()) - }, - "num_ref_wires": 2, - }, - { - "base_class": re.ResourceQFT, - "base_params": {"num_wires": 4}, - "num_ref_wires": 3, - }, - ) - - @pytest.mark.parametrize( - "op, params, expected_res", zip(op_data, resource_params_data, resource_data) - ) - def test_resources(self, op, params, expected_res): - """Test the resources method returns the correct dictionary""" - res_from_op = op.resources(**op.resource_params) - res_from_func = re.ResourceReflection.resources(**params) - - assert res_from_op == expected_res - assert res_from_func == expected_res - - @pytest.mark.parametrize("op, expected_params", zip(op_data, resource_params_data)) - def test_resource_params(self, op, expected_params): - """Test that the resource params are correct""" - assert op.resource_params == expected_params - - @pytest.mark.parametrize("expected_params", resource_params_data) - def test_resource_rep(self, expected_params): - """Test the resource_rep returns the correct CompressedResourceOp""" - expected = re.CompressedResourceOp(re.ResourceReflection, expected_params) - assert re.ResourceReflection.resource_rep(**expected_params) == expected diff --git a/pennylane/labs/tests/resource_estimation/templates/test_resource_select.py b/pennylane/labs/tests/resource_estimation/templates/test_resource_select.py deleted file mode 100644 index 0d88801d756..00000000000 --- a/pennylane/labs/tests/resource_estimation/templates/test_resource_select.py +++ /dev/null @@ -1,190 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -""" -Test the ResourceSelect class -""" -import pytest - -import pennylane.labs.resource_estimation as re - -# pylint: disable=no-self-use - - -class TestSelect: - """Test the ResourceSelect class""" - - op_data = ( - re.ResourceSelect( - [re.ResourceX(0), re.ResourceY(1)], - control=["c1"], - ), - re.ResourceSelect( - [re.ResourceCNOT((0, 1)), re.ResourceHadamard(1), re.ResourceQFT(range(3))], - control=["c1", "c2"], - ), - re.ResourceSelect( - [ - re.ResourceCNOT((0, 1)), - re.ResourceHadamard(1), - re.ResourceT(2), - re.ResourceS(3), - re.ResourceZ(0), - re.ResourceY(1), - re.ResourceX(3), - ], - control=["c1", "c2", "c3"], - ), - ) - - resource_params_data = ( - { - "cmpr_ops": (re.ResourceX.resource_rep(), re.ResourceY.resource_rep()), - }, - { - "cmpr_ops": ( - re.ResourceCNOT.resource_rep(), - re.ResourceHadamard.resource_rep(), - re.ResourceQFT.resource_rep(num_wires=3), - ), - }, - { - "cmpr_ops": ( - re.ResourceCNOT.resource_rep(), - re.ResourceHadamard.resource_rep(), - re.ResourceT.resource_rep(), - re.ResourceS.resource_rep(), - re.ResourceZ.resource_rep(), - re.ResourceY.resource_rep(), - re.ResourceX.resource_rep(), - ), - }, - ) - - resource_data = ( - { - re.ResourceX.resource_rep(): 2, - re.ResourceControlled.resource_rep( - re.ResourceX, - {}, - 1, - 0, - 0, - ): 1, - re.ResourceControlled.resource_rep( - re.ResourceY, - {}, - 1, - 0, - 0, - ): 1, - }, - { - re.ResourceX.resource_rep(): 4, - re.ResourceControlled.resource_rep( - re.ResourceCNOT, - {}, - 2, - 0, - 0, - ): 1, - re.ResourceControlled.resource_rep( - re.ResourceHadamard, - {}, - 2, - 0, - 0, - ): 1, - re.ResourceControlled.resource_rep( - re.ResourceQFT, - {"num_wires": 3}, - 2, - 0, - 0, - ): 1, - }, - { - re.ResourceX.resource_rep(): 8, - re.ResourceControlled.resource_rep( - re.ResourceCNOT, - {}, - 3, - 0, - 0, - ): 1, - re.ResourceControlled.resource_rep( - re.ResourceHadamard, - {}, - 3, - 0, - 0, - ): 1, - re.ResourceControlled.resource_rep( - re.ResourceT, - {}, - 3, - 0, - 0, - ): 1, - re.ResourceControlled.resource_rep( - re.ResourceS, - {}, - 3, - 0, - 0, - ): 1, - re.ResourceControlled.resource_rep( - re.ResourceZ, - {}, - 3, - 0, - 0, - ): 1, - re.ResourceControlled.resource_rep( - re.ResourceY, - {}, - 3, - 0, - 0, - ): 1, - re.ResourceControlled.resource_rep( - re.ResourceX, - {}, - 3, - 0, - 0, - ): 1, - }, - ) - - @pytest.mark.parametrize( - "op, params, expected_res", zip(op_data, resource_params_data, resource_data) - ) - def test_resources(self, op, params, expected_res): - """Test the resources method returns the correct dictionary""" - res_from_op = op.resources(**op.resource_params) - res_from_func = re.ResourceSelect.resources(**params) - - assert res_from_op == expected_res - assert res_from_func == expected_res - - @pytest.mark.parametrize("op, expected_params", zip(op_data, resource_params_data)) - def test_resource_params(self, op, expected_params): - """Test that the resource params are correct""" - assert op.resource_params == expected_params - - @pytest.mark.parametrize("expected_params", resource_params_data) - def test_resource_rep(self, expected_params): - """Test the resource_rep returns the correct CompressedResourceOp""" - expected = re.CompressedResourceOp(re.ResourceSelect, expected_params) - assert re.ResourceSelect.resource_rep(**expected_params) == expected diff --git a/pennylane/labs/tests/resource_estimation/templates/test_resource_stateprep.py b/pennylane/labs/tests/resource_estimation/templates/test_resource_stateprep.py deleted file mode 100644 index f2feb2d2e49..00000000000 --- a/pennylane/labs/tests/resource_estimation/templates/test_resource_stateprep.py +++ /dev/null @@ -1,396 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -""" -Test the ResourceStatePrep class -""" -import math - -import pytest - -import pennylane.labs.resource_estimation as re -from pennylane import numpy as qnp - -# pylint: disable=no-self-use - - -class TestStatePrep: - """Test the ResourceStatePrep class""" - - op_data = ( - re.ResourceStatePrep([1, 0], wires=[0]), - re.ResourceStatePrep(qnp.random.rand(2**3), wires=range(3), normalize=True), - re.ResourceStatePrep(qnp.random.rand(10), wires=range(4), normalize=True, pad_with=0), - re.ResourceStatePrep(qnp.random.rand(2**5), wires=range(5), normalize=True), - ) - - resource_data = ( - { - re.ResourceMottonenStatePreparation.resource_rep(1): 1, - }, - { - re.ResourceMottonenStatePreparation.resource_rep(3): 1, - }, - { - re.ResourceMottonenStatePreparation.resource_rep(4): 1, - }, - { - re.ResourceMottonenStatePreparation.resource_rep(5): 1, - }, - ) - - resource_params_data = ( - { - "num_wires": 1, - }, - { - "num_wires": 3, - }, - { - "num_wires": 4, - }, - { - "num_wires": 5, - }, - ) - - name_data = ( - "StatePrep(1)", - "StatePrep(3)", - "StatePrep(4)", - "StatePrep(5)", - ) - - @pytest.mark.parametrize( - "op, params, expected_res", zip(op_data, resource_params_data, resource_data) - ) - def test_resources(self, op, params, expected_res): - """Test the resources method returns the correct dictionary""" - res_from_op = op.resources(**op.resource_params) - res_from_func = re.ResourceStatePrep.resources(**params) - - assert res_from_op == expected_res - assert res_from_func == expected_res - - @pytest.mark.parametrize("op, expected_params", zip(op_data, resource_params_data)) - def test_resource_params(self, op, expected_params): - """Test that the resource params are correct""" - assert op.resource_params == expected_params - - @pytest.mark.parametrize("expected_params", resource_params_data) - def test_resource_rep(self, expected_params): - """Test the resource_rep returns the correct CompressedResourceOp""" - expected = re.CompressedResourceOp(re.ResourceStatePrep, expected_params) - assert re.ResourceStatePrep.resource_rep(**expected_params) == expected - - @pytest.mark.parametrize("params, expected_name", zip(resource_params_data, name_data)) - def test_tracking_name(self, params, expected_name): - """Test that the tracking name is correct.""" - assert re.ResourceStatePrep.tracking_name(**params) == expected_name - - -class TestResourceBasisState: - """Test the ResourceBasisState class""" - - @pytest.mark.parametrize( - "num_bit_flips, num_x", - [(4, 4), (5, 5), (6, 6)], - ) - def test_resources(self, num_bit_flips, num_x): - """Test that the resources are correct""" - expected = {} - x = re.CompressedResourceOp(re.ResourceX, {}) - expected[x] = num_x - - assert re.ResourceBasisState.resources(num_bit_flips) == expected - - @pytest.mark.parametrize( - "state, wires", - [ - ( - [1, 1], - range(2), - ), - ], - ) - def test_resource_params(self, state, wires): - """Test that the resource params are correct""" - op = re.ResourceBasisState(state, wires=wires) - - assert op.resource_params == {"num_bit_flips": 2} - - @pytest.mark.parametrize( - "num_bit_flips", - [(4, 4), (5, 5), (6, 6)], - ) - def test_resource_rep(self, num_bit_flips): - """Test the resource_rep returns the correct CompressedResourceOp""" - - expected = re.CompressedResourceOp( - re.ResourceBasisState, - {"num_bit_flips": num_bit_flips}, - ) - assert expected == re.ResourceBasisState.resource_rep(num_bit_flips) - - @pytest.mark.parametrize( - "num_bit_flips, num_x", - [(4, 4), (5, 5), (6, 6)], - ) - def test_resources_from_rep(self, num_bit_flips, num_x): - """Test that computing the resources from a compressed representation works""" - rep = re.ResourceBasisState.resource_rep(num_bit_flips) - actual = rep.op_type.resources(**rep.params) - expected = {} - x = re.CompressedResourceOp(re.ResourceX, {}) - expected[x] = num_x - - assert actual == expected - - @pytest.mark.parametrize( - "num_bit_flips", - [(4), (5), (6)], - ) - def test_tracking_name(self, num_bit_flips): - """Test that the tracking name is correct.""" - assert re.ResourceBasisState.tracking_name(num_bit_flips) == f"BasisState({num_bit_flips})" - - -class TestResourceSuperposition: - """Test the ResourceSuperposition class""" - - @pytest.mark.parametrize( - "num_stateprep_wires, num_basis_states, size_basis_state", - [(4, 2, 2), (4, 5, 2), (4, 5, 0)], - ) - def test_resources(self, num_stateprep_wires, num_basis_states, size_basis_state): - """Test that the resources are correct""" - expected = {} - msp = re.CompressedResourceOp( - re.ResourceMottonenStatePreparation, {"num_wires": num_stateprep_wires} - ) - expected[msp] = 1 - - cnot = re.CompressedResourceOp(re.ResourceCNOT, {}) - num_zero_ctrls = size_basis_state // 2 - multi_x = re.CompressedResourceOp( - re.ResourceMultiControlledX, - { - "num_ctrl_wires": size_basis_state, - "num_ctrl_values": num_zero_ctrls, - "num_work_wires": 0, - }, - ) - - basis_size = 2**size_basis_state - prob_matching_basis_states = num_basis_states / basis_size - num_permutes = round(num_basis_states * (1 - prob_matching_basis_states)) - - if num_permutes: - expected[cnot] = num_permutes * ( - size_basis_state // 2 - ) # average number of bits to flip - expected[multi_x] = 2 * num_permutes # for compute and uncompute - - assert ( - re.ResourceSuperposition.resources( - num_stateprep_wires, num_basis_states, size_basis_state - ) - == expected - ) - - @pytest.mark.parametrize( - "coeffs, bases, wires, work_wire", - [ - ( - qnp.sqrt(qnp.array([1 / 3, 1 / 3, 1 / 3])), - qnp.array([[1, 1, 1], [0, 1, 0], [0, 0, 0]]), - [0, 1, 2], - [3], - ), - ], - ) - def test_resource_params(self, coeffs, bases, wires, work_wire): - """Test that the resource params are correct""" - op = re.ResourceSuperposition(coeffs, bases, wires, work_wire) - - num_basis_states = len(bases) - size_basis_state = len(bases[0]) # assuming they are all the same size - num_stateprep_wires = math.ceil(math.log2(len(coeffs))) - - assert op.resource_params == { - "num_stateprep_wires": num_stateprep_wires, - "num_basis_states": num_basis_states, - "size_basis_state": size_basis_state, - } - - @pytest.mark.parametrize( - "num_stateprep_wires, num_basis_states, size_basis_state", - [(4, 2, 2), (4, 5, 2), (4, 5, 0)], - ) - def test_resource_rep(self, num_stateprep_wires, num_basis_states, size_basis_state): - """Test the resource_rep returns the correct CompressedResourceOp""" - - expected = re.CompressedResourceOp( - re.ResourceSuperposition, - { - "num_stateprep_wires": num_stateprep_wires, - "num_basis_states": num_basis_states, - "size_basis_state": size_basis_state, - }, - ) - assert expected == re.ResourceSuperposition.resource_rep( - num_stateprep_wires, num_basis_states, size_basis_state - ) - - @pytest.mark.parametrize( - "num_stateprep_wires, num_basis_states, size_basis_state", - [(4, 2, 2), (4, 5, 2), (4, 5, 0)], - ) - def test_resources_from_rep(self, num_stateprep_wires, num_basis_states, size_basis_state): - """Test that computing the resources from a compressed representation works""" - expected = {} - rep = re.ResourceSuperposition.resource_rep( - num_stateprep_wires, num_basis_states, size_basis_state - ) - actual = rep.op_type.resources(**rep.params) - - expected = {} - msp = re.CompressedResourceOp( - re.ResourceMottonenStatePreparation, {"num_wires": num_stateprep_wires} - ) - expected[msp] = 1 - - cnot = re.CompressedResourceOp(re.ResourceCNOT, {}) - num_zero_ctrls = size_basis_state // 2 - multi_x = re.CompressedResourceOp( - re.ResourceMultiControlledX, - { - "num_ctrl_wires": size_basis_state, - "num_ctrl_values": num_zero_ctrls, - "num_work_wires": 0, - }, - ) - - basis_size = 2**size_basis_state - prob_matching_basis_states = num_basis_states / basis_size - num_permutes = round(num_basis_states * (1 - prob_matching_basis_states)) - - if num_permutes: - expected[cnot] = num_permutes * ( - size_basis_state // 2 - ) # average number of bits to flip - expected[multi_x] = 2 * num_permutes # for compute and uncompute - - assert actual == expected - - def test_tracking_name(self): - """Test that the tracking name is correct.""" - assert re.ResourceSuperposition.tracking_name() == "Superposition" - - -class TestResourceMottonenStatePreparation: - """Test the ResourceMottonenStatePreparation class""" - - @pytest.mark.parametrize( - "num_wires", - [(4), (5), (6)], - ) - def test_resources(self, num_wires): - """Test that the resources are correct""" - expected = {} - rz = re.CompressedResourceOp(re.ResourceRZ, {}) - cnot = re.CompressedResourceOp(re.ResourceCNOT, {}) - - r_count = 2 ** (num_wires + 2) - 5 - cnot_count = 2 ** (num_wires + 2) - 4 * num_wires - 4 - - if r_count: - expected[rz] = r_count - - if cnot_count: - expected[cnot] = cnot_count - assert re.ResourceMottonenStatePreparation.resources(num_wires) == expected - - @pytest.mark.parametrize( - "state_vector, wires", - [ - ( - qnp.array( - [ - 0.070014 + 0.0j, - 0.0 + 0.14002801j, - 0.21004201 + 0.0j, - 0.0 + 0.28005602j, - 0.35007002 + 0.0j, - 0.0 + 0.42008403j, - 0.49009803 + 0.0j, - 0.0 + 0.56011203j, - ] - ), - range(3), - ), - ], - ) - def test_resource_params(self, state_vector, wires): - """Test that the resource params are correct""" - op = re.ResourceMottonenStatePreparation(state_vector=state_vector, wires=wires) - - assert op.resource_params == {"num_wires": len(wires)} - - @pytest.mark.parametrize( - "num_wires", - [(4), (5), (6)], - ) - def test_resource_rep(self, num_wires): - """Test the resource_rep returns the correct CompressedResourceOp""" - - expected = re.CompressedResourceOp( - re.ResourceMottonenStatePreparation, - {"num_wires": num_wires}, - ) - assert expected == re.ResourceMottonenStatePreparation.resource_rep(num_wires) - - @pytest.mark.parametrize( - "num_wires", - [(4), (5), (6)], - ) - def test_resources_from_rep(self, num_wires): - """Test that computing the resources from a compressed representation works""" - rep = re.ResourceMottonenStatePreparation.resource_rep(num_wires) - actual = rep.op_type.resources(**rep.params) - - expected = {} - rz = re.CompressedResourceOp(re.ResourceRZ, {}) - cnot = re.CompressedResourceOp(re.ResourceCNOT, {}) - - r_count = 2 ** (num_wires + 2) - 5 - cnot_count = 2 ** (num_wires + 2) - 4 * num_wires - 4 - - if r_count: - expected[rz] = r_count - - if cnot_count: - expected[cnot] = cnot_count - - assert actual == expected - - @pytest.mark.parametrize( - "num_wires", - [(4), (5), (6)], - ) - def test_tracking_name(self, num_wires): - """Test that the tracking name is correct.""" - assert ( - re.ResourceMottonenStatePreparation.tracking_name(num_wires) - == f"MottonenStatePrep({num_wires})" - ) diff --git a/pennylane/labs/tests/resource_estimation/templates/test_resource_trotter.py b/pennylane/labs/tests/resource_estimation/templates/test_resource_trotter.py deleted file mode 100644 index f99dfd001ce..00000000000 --- a/pennylane/labs/tests/resource_estimation/templates/test_resource_trotter.py +++ /dev/null @@ -1,321 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -""" -Test the Resource classes associated with TrotterProduct and TrotterizedQfunc -""" -import pytest - -import pennylane as qml -import pennylane.labs.resource_estimation as re -from pennylane.operation import Operation -from pennylane.ops.op_math import LinearCombination, SProd - -# pylint: disable=no-self-use,arguments-differ - - -def qfunc_1(time, wires): - """A quantum function which queues operations to be trotterized.""" - re.ResourceRX(1.23 * time, wires[0]) - re.ResourceRY(-4.5 * time, wires[0]) - - -def qfunc_2(time, arg1, wires): - """A quantum function which queues operations to be trotterized.""" - for w in wires: - re.ResourceHadamard(w) - - re.ResourceQFT(wires) - re.ResourceRX(arg1 * time, wires[0]) - - -def qfunc_3(time, arg1, arg2, kwarg1=False, wires=None): - """A quantum function which queues operations to be trotterized.""" - re.ResourceControlled( - re.ResourceRot(arg1 * time, arg2 * time, time * (arg1 + arg2), wires=wires[1]), - control_wires=wires[0], - ) - if kwarg1: - for w in wires: - re.ResourceHadamard(w) - - -trotterized_qfunc_op_data = ( - re.ResourceTrotterizedQfunc(1, qfunc=qfunc_1, n=5, order=2, reverse=False, wires=[0]), - re.ResourceTrotterizedQfunc( - 1, *(1.23,), qfunc=qfunc_2, n=10, order=2, reverse=False, wires=[0, 1, 2] - ), - re.ResourceTrotterizedQfunc( - 1, - *(1.23, -4.56), - qfunc=qfunc_3, - n=10, - order=4, - reverse=False, - wires=[0, 1], - **{"kwarg1": True}, - ), -) - - -class DummyOp(re.ResourceOperator, Operation): - """A Dummy ResourceOperator child class which implements the - :code:`exp_resource_decomp` method.""" - - def __init__(self, a, b, wires=(0,)): - self.a = a - self.b = b - super().__init__(wires=wires) - - @staticmethod - def _resource_decomp(a, b, **kwargs): - h = re.ResourceHadamard.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - - return {h: a, cnot: b} - - @classmethod - def exp_resource_decomp(cls, coeff, num_steps, a, b): # pylint: disable=unused-argument - return cls.resources(a + 1, b + 1) - - @property - def resource_params(self) -> dict: - return {"a": self.a, "b": self.b} - - @classmethod - def resource_rep(cls, a, b): - return re.CompressedResourceOp(cls, {"a": a, "b": b}) - - -class TestResourceTrotterProduct: - """Test the ResourceTrotterProduct class""" - - op_data = ( - re.ResourceTrotterProduct( - qml.sum( - DummyOp(a=1, b=2), - re.ResourceProd(re.ResourceZ(0), re.ResourceZ(1)), - LinearCombination( - [1.2, 3.4, 5.6], [re.ResourceX(0), re.ResourceY(0), re.ResourceZ(1)] - ), - ), - time=0.1, - check_hermitian=False, - ), - re.ResourceTrotterProduct( - LinearCombination( - coeffs=[0.1, 0.2], - observables=[ - re.ResourceX(0), - re.ResourceProd(re.ResourceZ(0), re.ResourceZ(1)), - ], - ), - time=0.1, - n=5, - order=2, - ), - re.ResourceTrotterProduct( - LinearCombination( - coeffs=[1.2, -3.4], - observables=[re.ResourceZ(0), re.ResourceX(1)], - ), - time=0.1, - n=10, - order=4, - ), - ) - - resource_params_data = ( - { - "n": 1, - "order": 1, - "first_order_expansion": [ - re.ResourceExp.resource_rep(DummyOp, {"a": 1, "b": 2}, None, 1j, 1), - re.ResourceExp.resource_rep( - re.ResourceProd, - {"cmpr_factors": (re.ResourceZ.resource_rep(), re.ResourceZ.resource_rep())}, - (qml.Z(0) @ qml.Z(1)).pauli_rep, - 1j, - 1, - ), - re.ResourceExp.resource_rep( - LinearCombination, - {}, - (1.2 * qml.X(0) + 3.4 * qml.Y(0) + 5.6 * qml.Z(1)).pauli_rep, - 1j, - 1, - ), - ], - }, - { - "n": 5, - "order": 2, - "first_order_expansion": [ - re.ResourceExp.resource_rep(SProd, {}, (0.1 * qml.X(0)).pauli_rep, 1j, 1), - re.ResourceExp.resource_rep( - SProd, {}, (0.2 * qml.Z(0) @ qml.Z(1)).pauli_rep, 1j, 1 - ), - ], - }, - { - "n": 10, - "order": 4, - "first_order_expansion": [ - re.ResourceExp.resource_rep(SProd, {}, (1.2 * qml.Z(0)).pauli_rep, 1j, 1), - re.ResourceExp.resource_rep(SProd, {}, (-3.4 * qml.X(1)).pauli_rep, 1j, 1), - ], - }, - ) - - resource_data = ( - { - re.ResourceExp.resource_rep(DummyOp, {"a": 1, "b": 2}, None, 1j, 1): 1, - re.ResourceExp.resource_rep( - re.ResourceProd, - {"cmpr_factors": (re.ResourceZ.resource_rep(), re.ResourceZ.resource_rep())}, - (qml.Z(0) @ qml.Z(1)).pauli_rep, - 1j, - 1, - ): 1, - re.ResourceExp.resource_rep( - LinearCombination, - {}, - (1.2 * qml.X(0) + 3.4 * qml.Y(0) + 5.6 * qml.Z(1)).pauli_rep, - 1j, - 1, - ): 1, - }, - { - re.ResourceExp.resource_rep(SProd, {}, (0.1 * qml.X(0)).pauli_rep, 1j, 1): 6, - re.ResourceExp.resource_rep(SProd, {}, (0.2 * qml.Z(0) @ qml.Z(1)).pauli_rep, 1j, 1): 5, - }, - { - re.ResourceExp.resource_rep(SProd, {}, (1.2 * qml.Z(0)).pauli_rep, 1j, 1): 51, - re.ResourceExp.resource_rep(SProd, {}, (-3.4 * qml.X(1)).pauli_rep, 1j, 1): 50, - }, - ) - - @pytest.mark.parametrize("op, expected_res", zip(op_data, resource_data)) - def test_resources(self, op, expected_res): - """Test the resources method returns the correct dictionary""" - op_rep = op.resource_rep_from_op() - computed_res = op_rep.op_type.resources(**op_rep.params) - assert computed_res == expected_res - - @pytest.mark.parametrize("op, expected_params", zip(op_data, resource_params_data)) - def test_resource_params(self, op, expected_params): - """Test that the resource params are correct""" - assert op.resource_params == expected_params - - @pytest.mark.parametrize("params", resource_params_data) - def test_resource_rep(self, params): - """Test the resource_rep returns the correct CompressedResourceOp""" - expected = re.CompressedResourceOp(re.ResourceTrotterProduct, params) - assert re.ResourceTrotterProduct.resource_rep(**params) == expected - - -class TestResourceTrotterizedQfunc: - """Test the ResourceTrotterizedQfunc class""" - - resource_params_data = ( - { - "n": 5, - "order": 2, - "qfunc_compressed_reps": ( - re.ResourceRX.resource_rep(), - re.ResourceRY.resource_rep(), - ), - }, - { - "n": 10, - "order": 2, - "qfunc_compressed_reps": ( - re.ResourceHadamard.resource_rep(), - re.ResourceHadamard.resource_rep(), - re.ResourceHadamard.resource_rep(), - re.ResourceQFT.resource_rep(num_wires=3), - re.ResourceRX.resource_rep(), - ), - }, - { - "n": 10, - "order": 4, - "qfunc_compressed_reps": ( - re.ResourceControlled.resource_rep(re.ResourceRot, {}, 1, 0, 0), - re.ResourceHadamard.resource_rep(), - re.ResourceHadamard.resource_rep(), - ), - }, - ) - - resource_data = ( - { - re.ResourceRX.resource_rep(): 10, - re.ResourceRY.resource_rep(): 10, - }, - { - re.ResourceHadamard.resource_rep(): 60, - re.ResourceQFT.resource_rep(num_wires=3): 20, - re.ResourceRX.resource_rep(): 20, - }, - { - re.ResourceControlled.resource_rep(re.ResourceRot, {}, 1, 0, 0): 100, - re.ResourceHadamard.resource_rep(): 200, - }, - ) - - @pytest.mark.parametrize("op, expected_res", zip(trotterized_qfunc_op_data, resource_data)) - def test_resources(self, op, expected_res): - """Test the resources method returns the correct dictionary""" - op_rep = op.resource_rep_from_op() - computed_res = op_rep.op_type.resources(**op_rep.params) - assert computed_res == expected_res - - @pytest.mark.parametrize( - "op, expected_params", zip(trotterized_qfunc_op_data, resource_params_data) - ) - def test_resource_params(self, op, expected_params): - """Test that the resource params are correct""" - assert op.resource_params == expected_params - - @pytest.mark.parametrize("params", resource_params_data) - def test_resource_rep(self, params): - """Test the resource_rep returns the correct CompressedResourceOp""" - expected = re.CompressedResourceOp(re.ResourceTrotterizedQfunc, params) - assert re.ResourceTrotterizedQfunc.resource_rep(**params) == expected - - -@pytest.mark.parametrize( - "qfunc, args_n_kwargs, hyperparams, expected_op", - zip( - (qfunc_1, qfunc_2, qfunc_3), - ( - ((1,), {"wires": [0]}), - ((1, 1.23), {"wires": [0, 1, 2]}), - ((1, 1.23, -4.56), {"kwarg1": True, "wires": [0, 1]}), - ), - ( - {"n": 5, "order": 2}, - {"n": 10, "order": 2}, - {"n": 10, "order": 4}, - ), - trotterized_qfunc_op_data, - ), -) -def test_resource_trotterize(qfunc, args_n_kwargs, hyperparams, expected_op): - """Test that the resource_trotterize wrapper function generates an instance of - ResourceTrotterizedQfunc.""" - args, kwargs = args_n_kwargs - assert qml.equal( - re.resource_trotterize(qfunc=qfunc, **hyperparams)(*args, **kwargs), expected_op - ) diff --git a/pennylane/labs/tests/resource_estimation/templates/test_subroutines.py b/pennylane/labs/tests/resource_estimation/templates/test_subroutines.py deleted file mode 100644 index 846981e9186..00000000000 --- a/pennylane/labs/tests/resource_estimation/templates/test_subroutines.py +++ /dev/null @@ -1,1041 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -""" -Test the ResourceQFT class -""" -import math - -import numpy as np -import pytest - -import pennylane as qml -import pennylane.labs.resource_estimation as re - -# pylint: disable=no-self-use, too-many-arguments, protected-access - - -class TestQFT: - """Test the ResourceQFT class""" - - @pytest.mark.parametrize( - "num_wires, num_hadamard, num_swap, num_ctrl_phase_shift", - [ - (1, 1, 0, 0), - (2, 2, 1, 1), - (3, 3, 1, 3), - (4, 4, 2, 6), - ], - ) - def test_resources(self, num_wires, num_hadamard, num_swap, num_ctrl_phase_shift): - """Test the resources method returns the correct dictionary""" - hadamard = re.CompressedResourceOp(re.ResourceHadamard, {}) - swap = re.CompressedResourceOp(re.ResourceSWAP, {}) - ctrl_phase_shift = re.CompressedResourceOp(re.ResourceControlledPhaseShift, {}) - - expected = {hadamard: num_hadamard, swap: num_swap, ctrl_phase_shift: num_ctrl_phase_shift} - - assert re.ResourceQFT.resources(num_wires) == expected - - @pytest.mark.parametrize("wires", [range(1), range(2), range(3), range(4)]) - def test_resource_params(self, wires): - """Test that the resource params are correct""" - op = re.ResourceQFT(wires) - assert op.resource_params == {"num_wires": len(wires)} - - @pytest.mark.parametrize("num_wires", [1, 2, 3, 4]) - def test_resource_rep(self, num_wires): - """Test the resource_rep returns the correct CompressedResourceOp""" - - expected = re.CompressedResourceOp(re.ResourceQFT, {"num_wires": num_wires}) - assert re.ResourceQFT.resource_rep(num_wires) == expected - - @pytest.mark.parametrize( - "num_wires, num_hadamard, num_swap, num_ctrl_phase_shift", - [ - (1, 1, 0, 0), - (2, 2, 1, 1), - (3, 3, 1, 3), - (4, 4, 2, 6), - ], - ) - def test_resources_from_rep(self, num_wires, num_hadamard, num_swap, num_ctrl_phase_shift): - """Test that computing the resources from a compressed representation works""" - - hadamard = re.CompressedResourceOp(re.ResourceHadamard, {}) - swap = re.CompressedResourceOp(re.ResourceSWAP, {}) - ctrl_phase_shift = re.CompressedResourceOp(re.ResourceControlledPhaseShift, {}) - - expected = {hadamard: num_hadamard, swap: num_swap, ctrl_phase_shift: num_ctrl_phase_shift} - - rep = re.ResourceQFT.resource_rep(num_wires) - actual = rep.op_type.resources(**rep.params) - - assert actual == expected - - @pytest.mark.parametrize("num_wires", range(10)) - def test_tracking_name(self, num_wires): - """Test that the tracking name is correct.""" - assert re.ResourceQFT.tracking_name(num_wires + 1) == f"QFT({num_wires+1})" - - -class TestControlledSequence: - """Test the ResourceControlledSequence class""" - - @pytest.mark.parametrize( - "base_class, base_params, num_ctrl_wires, num_cseq", - [(re.ResourceHadamard, {}, 1, 1), (re.ResourceRX, {}, 3, 7)], - ) - def test_resources(self, base_class, base_params, num_ctrl_wires, num_cseq): - """Test the resources method returns the correct dictionary""" - resource_controlled_sequence = re.CompressedResourceOp( - re.ResourceControlled, - { - "base_class": base_class, - "base_params": base_params, - "num_ctrl_wires": 1, - "num_ctrl_values": 0, - "num_work_wires": 0, - }, - ) - expected = {resource_controlled_sequence: num_cseq} - - assert ( - re.ResourceControlledSequence.resources(base_class, base_params, num_ctrl_wires) - == expected - ) - - @pytest.mark.parametrize( - "base, control", - [(re.ResourceHadamard(3), [0, 1, 2]), (re.ResourceRX(0.25, 2), [0, 1])], - ) - def test_resource_params(self, base, control): - """Test that the resource params are correct""" - op = re.ResourceControlledSequence(base=base, control=control) - - assert op.resource_params == { - "base_class": type(base), - "base_params": base.resource_params, - "num_ctrl_wires": len(control), - } - - @pytest.mark.parametrize( - "base_class, base_params, num_ctrl_wires", - [(re.ResourceHadamard, {}, 1), (re.ResourceRX, {}, 3)], - ) - def test_resource_rep(self, base_class, base_params, num_ctrl_wires): - """Test the resource_rep returns the correct CompressedResourceOp""" - - expected = re.CompressedResourceOp( - re.ResourceControlledSequence, - { - "base_class": base_class, - "base_params": base_params, - "num_ctrl_wires": num_ctrl_wires, - }, - ) - assert expected == re.ResourceControlledSequence.resource_rep( - base_class, base_params, num_ctrl_wires - ) - - @pytest.mark.parametrize( - "base_class, base_params, num_ctrl_wires, num_cseq", - [(re.ResourceHadamard, {}, 1, 1), (re.ResourceRX, {}, 3, 7)], - ) - def test_resources_from_rep(self, base_class, base_params, num_ctrl_wires, num_cseq): - """Test that computing the resources from a compressed representation works""" - - resource_controlled_sequence = re.CompressedResourceOp( - re.ResourceControlled, - { - "base_class": base_class, - "base_params": base_params, - "num_ctrl_wires": 1, - "num_ctrl_values": 0, - "num_work_wires": 0, - }, - ) - - expected = {resource_controlled_sequence: num_cseq} - - rep = re.ResourceControlledSequence.resource_rep(base_class, base_params, num_ctrl_wires) - actual = rep.op_type.resources(**rep.params) - - assert actual == expected - - @pytest.mark.parametrize( - "base_class, base_params, num_ctrl_wires", - [(re.ResourceHadamard, {}, 1), (re.ResourceRX, {}, 3)], - ) - def test_tracking_name(self, base_class, base_params, num_ctrl_wires): - """Test that the tracking name is correct.""" - base_name = base_class.tracking_name(**base_params) - assert ( - re.ResourceControlledSequence.tracking_name(base_class, base_params, num_ctrl_wires) - == f"ControlledSequence({base_name}, {num_ctrl_wires})" - ) - - -class TestPhaseAdder: - """Test the ResourcePhaseAdder class""" - - @pytest.mark.parametrize( - "mod, num_x_wires", - [(8, 3), (7, 3)], - ) - def test_resources(self, mod, num_x_wires): - """Test the resources method returns the correct dictionary""" - if mod == 2**num_x_wires: - resource_phase_shift = re.CompressedResourceOp(re.ResourcePhaseShift, {}) - expected = {resource_phase_shift: num_x_wires} - assert re.ResourcePhaseAdder.resources(mod, num_x_wires) == expected - return - - qft = re.CompressedResourceOp(re.ResourceQFT, {"num_wires": num_x_wires}) - qft_dag = re.CompressedResourceOp( - re.ResourceAdjoint, - {"base_class": re.ResourceQFT, "base_params": {"num_wires": num_x_wires}}, - ) - - phase_shift = re.CompressedResourceOp(re.ResourcePhaseShift, {}) - phase_shift_dag = re.CompressedResourceOp( - re.ResourceAdjoint, {"base_class": re.ResourcePhaseShift, "base_params": {}} - ) - ctrl_phase_shift = re.CompressedResourceOp(re.ResourceControlledPhaseShift, {}) - - cnot = re.CompressedResourceOp(re.ResourceCNOT, {}) - multix = re.CompressedResourceOp( - re.ResourceMultiControlledX, - {"num_ctrl_wires": 1, "num_ctrl_values": 0, "num_work_wires": 1}, - ) - - expected = {} - expected[qft] = 2 - expected[qft_dag] = 2 - expected[phase_shift] = 2 * num_x_wires - expected[phase_shift_dag] = 2 * num_x_wires - expected[ctrl_phase_shift] = num_x_wires - expected[cnot] = 1 - expected[multix] = 1 - - assert re.ResourcePhaseAdder.resources(mod, num_x_wires) == expected - - @pytest.mark.parametrize( - "mod, x_wires", - [(8, [0, 1, 2]), (3, [0, 1])], - ) - def test_resource_params(self, mod, x_wires): - """Test that the resource params are correct""" - op = re.ResourcePhaseAdder(k=3, mod=mod, x_wires=x_wires, work_wire=[5]) - - assert op.resource_params == { - "mod": mod, - "num_x_wires": len(x_wires), - } - - @pytest.mark.parametrize( - "mod, num_x_wires", - [(8, 3), (3, 2)], - ) - def test_resource_rep(self, mod, num_x_wires): - """Test the resource_rep returns the correct CompressedResourceOp""" - - expected = re.CompressedResourceOp( - re.ResourcePhaseAdder, - { - "mod": mod, - "num_x_wires": num_x_wires, - }, - ) - assert expected == re.ResourcePhaseAdder.resource_rep(mod=mod, num_x_wires=num_x_wires) - - @pytest.mark.parametrize( - "mod, num_x_wires", - [(8, 3), (7, 3)], - ) - def test_resources_from_rep(self, mod, num_x_wires): - """Test that computing the resources from a compressed representation works""" - rep = re.ResourcePhaseAdder.resource_rep(mod=mod, num_x_wires=num_x_wires) - actual = rep.op_type.resources(**rep.params) - if mod == 2**num_x_wires: - resource_phase_shift = re.CompressedResourceOp(re.ResourcePhaseShift, {}) - expected = {resource_phase_shift: num_x_wires} - assert expected == actual - return - - qft = re.CompressedResourceOp(re.ResourceQFT, {"num_wires": num_x_wires}) - qft_dag = re.CompressedResourceOp( - re.ResourceAdjoint, - {"base_class": re.ResourceQFT, "base_params": {"num_wires": num_x_wires}}, - ) - - phase_shift = re.CompressedResourceOp(re.ResourcePhaseShift, {}) - phase_shift_dag = re.CompressedResourceOp( - re.ResourceAdjoint, {"base_class": re.ResourcePhaseShift, "base_params": {}} - ) - ctrl_phase_shift = re.CompressedResourceOp(re.ResourceControlledPhaseShift, {}) - - cnot = re.CompressedResourceOp(re.ResourceCNOT, {}) - multix = re.CompressedResourceOp( - re.ResourceMultiControlledX, - {"num_ctrl_wires": 1, "num_ctrl_values": 0, "num_work_wires": 1}, - ) - - expected = {} - expected[qft] = 2 - expected[qft_dag] = 2 - expected[phase_shift] = 2 * num_x_wires - expected[phase_shift_dag] = 2 * num_x_wires - expected[ctrl_phase_shift] = num_x_wires - expected[cnot] = 1 - expected[multix] = 1 - - assert actual == expected - - def test_tracking_name(self): - """Test that the tracking name is correct.""" - assert re.ResourcePhaseAdder.tracking_name() == "PhaseAdder" - - -class TestResourceMultiplier: - """Test the ResourceMultiplier class""" - - @pytest.mark.parametrize( - "mod, num_work_wires, num_x_wires", - [(7, 5, 3), (8, 5, 3)], - ) - def test_resources(self, mod, num_work_wires, num_x_wires): - """Test the resources method returns the correct dictionary""" - if mod == 2**num_x_wires: - num_aux_wires = num_x_wires - num_aux_swap = num_x_wires - else: - num_aux_wires = num_work_wires - 1 - num_aux_swap = num_aux_wires - 1 - - qft = re.CompressedResourceOp(re.ResourceQFT, {"num_wires": num_aux_wires}) - qft_dag = re.CompressedResourceOp( - re.ResourceAdjoint, - {"base_class": re.ResourceQFT, "base_params": {"num_wires": num_aux_wires}}, - ) - - sequence = re.CompressedResourceOp( - re.ResourceControlledSequence, - { - "base_class": re.ResourcePhaseAdder, - "base_params": {}, - "num_ctrl_wires": num_x_wires, - }, - ) - - sequence_dag = re.CompressedResourceOp( - re.ResourceAdjoint, - { - "base_class": re.ResourceControlledSequence, - "base_params": { - "base_class": re.ResourcePhaseAdder, - "base_params": {}, - "num_ctrl_wires": num_x_wires, - }, - }, - ) - - cnot = re.CompressedResourceOp(re.ResourceCNOT, {}) - - expected = {} - expected[qft] = 2 - expected[qft_dag] = 2 - expected[sequence] = 1 - expected[sequence_dag] = 1 - expected[cnot] = min(num_x_wires, num_aux_swap) - - assert re.ResourceMultiplier.resources(mod, num_work_wires, num_x_wires) == expected - - @pytest.mark.parametrize( - "k, mod, work_wires, x_wires", - [(4, 7, [3, 4, 5, 6, 7], [0, 1, 2]), (5, 8, [3, 4, 5, 6, 7], [0, 1, 2])], - ) - def test_resource_params(self, k, mod, work_wires, x_wires): - """Test that the resource params are correct""" - op = re.ResourceMultiplier( - k=k, - x_wires=x_wires, - mod=mod, - work_wires=work_wires, - ) - - assert op.resource_params == { - "mod": mod, - "num_work_wires": len(work_wires), - "num_x_wires": len(x_wires), - } - - @pytest.mark.parametrize( - "mod, num_work_wires, num_x_wires", - [(7, 5, 3), (8, 5, 3)], - ) - def test_resource_rep(self, mod, num_work_wires, num_x_wires): - """Test the resource_rep returns the correct CompressedResourceOp""" - - expected = re.CompressedResourceOp( - re.ResourceMultiplier, - { - "mod": mod, - "num_work_wires": num_work_wires, - "num_x_wires": num_x_wires, - }, - ) - assert expected == re.ResourceMultiplier.resource_rep(mod, num_work_wires, num_x_wires) - - @pytest.mark.parametrize( - "mod, num_work_wires, num_x_wires", - [(7, 5, 3), (8, 5, 3)], - ) - def test_resources_from_rep(self, mod, num_work_wires, num_x_wires): - """Test that computing the resources from a compressed representation works""" - - if mod == 2**num_x_wires: - num_aux_wires = num_x_wires - num_aux_swap = num_x_wires - else: - num_aux_wires = num_work_wires - 1 - num_aux_swap = num_aux_wires - 1 - - qft = re.CompressedResourceOp(re.ResourceQFT, {"num_wires": num_aux_wires}) - qft_dag = re.CompressedResourceOp( - re.ResourceAdjoint, - {"base_class": re.ResourceQFT, "base_params": {"num_wires": num_aux_wires}}, - ) - - sequence = re.CompressedResourceOp( - re.ResourceControlledSequence, - { - "base_class": re.ResourcePhaseAdder, - "base_params": {}, - "num_ctrl_wires": num_x_wires, - }, - ) - - sequence_dag = re.CompressedResourceOp( - re.ResourceAdjoint, - { - "base_class": re.ResourceControlledSequence, - "base_params": { - "base_class": re.ResourcePhaseAdder, - "base_params": {}, - "num_ctrl_wires": num_x_wires, - }, - }, - ) - - cnot = re.CompressedResourceOp(re.ResourceCNOT, {}) - - expected = {} - expected[qft] = 2 - expected[qft_dag] = 2 - expected[sequence] = 1 - expected[sequence_dag] = 1 - expected[cnot] = min(num_x_wires, num_aux_swap) - - rep = re.ResourceMultiplier.resource_rep(mod, num_work_wires, num_x_wires) - actual = rep.op_type.resources(**rep.params) - - assert actual == expected - - def test_tracking_name(self): - """Test that the tracking name is correct.""" - assert re.ResourceMultiplier.tracking_name() == "Multiplier" - - -class TestResourceModExp: - """Test the ResourceModExp class""" - - @pytest.mark.parametrize( - "mod, num_output_wires, num_work_wires, num_x_wires", - [(7, 5, 5, 3), (8, 5, 5, 3)], - ) - def test_resources(self, mod, num_output_wires, num_work_wires, num_x_wires): - """Test that the resources are correct""" - mult_resources = re.ResourceMultiplier._resource_decomp( - mod, num_work_wires, num_output_wires - ) - expected = {} - - for comp_rep, _ in mult_resources.items(): - new_rep = re.CompressedResourceOp( - re.ResourceControlled, - { - "base_class": comp_rep.op_type, - "base_params": comp_rep.params, - "num_ctrl_wires": 1, - "num_ctrl_values": 0, - "num_work_wires": 0, - }, - ) - - if comp_rep._name in ("QFT", "Adjoint(QFT)"): - expected[new_rep] = 1 - else: - expected[new_rep] = mult_resources[comp_rep] * ((2**num_x_wires) - 1) - - assert ( - re.ResourceModExp.resources(mod, num_output_wires, num_work_wires, num_x_wires) - == expected - ) - - @pytest.mark.parametrize( - "k, mod, work_wires, x_wires", - [(4, 7, [3, 4, 5, 6, 7], [0, 1, 2]), (5, 8, [3, 4, 5, 6, 7], [0, 1, 2])], - ) - def test_resource_params(self, k, mod, work_wires, x_wires): - """Test that the resource params are correct""" - op = re.ResourceMultiplier( - k=k, - x_wires=x_wires, - mod=mod, - work_wires=work_wires, - ) - - assert op.resource_params == { - "mod": mod, - "num_work_wires": len(work_wires), - "num_x_wires": len(x_wires), - } - - @pytest.mark.parametrize( - "mod, num_output_wires, num_work_wires, num_x_wires", - [(7, 5, 5, 3), (8, 5, 5, 3)], - ) - def test_resource_rep(self, mod, num_output_wires, num_work_wires, num_x_wires): - """Test the resource_rep returns the correct CompressedResourceOp""" - - expected = re.CompressedResourceOp( - re.ResourceModExp, - { - "mod": mod, - "num_output_wires": num_output_wires, - "num_work_wires": num_work_wires, - "num_x_wires": num_x_wires, - }, - ) - assert expected == re.ResourceModExp.resource_rep( - mod, num_output_wires, num_work_wires, num_x_wires - ) - - @pytest.mark.parametrize( - "mod, num_output_wires, num_work_wires, num_x_wires", - [(7, 5, 5, 3), (8, 5, 5, 3)], - ) - def test_resources_from_rep(self, num_output_wires, mod, num_work_wires, num_x_wires): - """Test that computing the resources from a compressed representation works""" - mult_resources = re.ResourceMultiplier._resource_decomp( - mod, num_work_wires, num_output_wires - ) - expected = {} - - for comp_rep, _ in mult_resources.items(): - new_rep = re.CompressedResourceOp( - re.ResourceControlled, - { - "base_class": comp_rep.op_type, - "base_params": comp_rep.params, - "num_ctrl_wires": 1, - "num_ctrl_values": 0, - "num_work_wires": 0, - }, - ) - - if comp_rep._name in ("QFT", "Adjoint(QFT)"): - expected[new_rep] = 1 - else: - expected[new_rep] = mult_resources[comp_rep] * ((2**num_x_wires) - 1) - - rep = re.ResourceModExp.resource_rep(mod, num_output_wires, num_work_wires, num_x_wires) - actual = rep.op_type.resources(**rep.params) - - assert actual == expected - - def test_tracking_name(self): - """Test that the tracking name is correct.""" - assert re.ResourceModExp.tracking_name() == "ModExp" - - -class TestResourceQROM: - """Test the ResourceQROM class""" - - @pytest.mark.parametrize( - "num_bitstrings, num_bit_flips, num_control_wires, num_work_wires, size_bitstring, clean", - [(4, 2, 2, 3, 3, True), (4, 5, 2, 3, 3, False), (4, 5, 0, 3, 3, False)], - ) - def test_resources( - self, - num_bitstrings, - num_bit_flips, - num_control_wires, - num_work_wires, - size_bitstring, - clean, - ): - """Test that the resources are correct""" - expected = {} - x = re.CompressedResourceOp(re.ResourceX, {}) - - if num_control_wires == 0: - expected[x] = num_bit_flips - assert ( - re.ResourceQROM.resources( - num_bitstrings, - num_bit_flips, - num_control_wires, - num_work_wires, - size_bitstring, - clean, - ) - == expected - ) - return - - cnot = re.CompressedResourceOp(re.ResourceCNOT, {}) - hadamard = re.CompressedResourceOp(re.ResourceHadamard, {}) - - num_parallel_computations = (num_work_wires + size_bitstring) // size_bitstring - num_parallel_computations = min(num_parallel_computations, num_bitstrings) - - num_swap_wires = math.floor(math.log2(num_parallel_computations)) - num_select_wires = math.ceil(math.log2(math.ceil(num_bitstrings / (2**num_swap_wires)))) - assert num_swap_wires + num_select_wires <= num_control_wires - - swap_clean_prefactor = 1 - select_clean_prefactor = 1 - - if clean: - expected[hadamard] = 2 * size_bitstring - swap_clean_prefactor = 4 - select_clean_prefactor = 2 - - # SELECT cost: - expected[cnot] = num_bit_flips # each unitary in the select is just a CNOT - - multi_x = re.CompressedResourceOp( - re.ResourceMultiControlledX, - { - "num_ctrl_wires": num_select_wires, - "num_ctrl_values": 0, - "num_work_wires": 0, - }, - ) - - num_total_ctrl_possibilities = 2**num_select_wires - expected[multi_x] = select_clean_prefactor * ( - 2 * num_total_ctrl_possibilities # two applications targetting the aux qubit - ) - num_zero_controls = (2 * num_total_ctrl_possibilities * num_select_wires) // 2 - expected[x] = select_clean_prefactor * ( - num_zero_controls * 2 # conjugate 0 controls on the multi-qubit x gates from above - ) - # SWAP cost: - ctrl_swap = re.CompressedResourceOp(re.ResourceCSWAP, {}) - expected[ctrl_swap] = swap_clean_prefactor * ((2**num_swap_wires) - 1) * size_bitstring - - assert ( - re.ResourceQROM.resources( - num_bitstrings, - num_bit_flips, - num_control_wires, - num_work_wires, - size_bitstring, - clean, - ) - == expected - ) - - @pytest.mark.parametrize( - "bitstrings, control_wires, work_wires, target_wires, clean", - [ - (["000", "001", "010", "100"], [0, 1], [2, 3, 4], [5, 6, 7], True), - (["010", "111", "110", "000"], [0, 1], [2, 3, 4], [5, 6, 7], False), - ], - ) - def test_resource_params(self, bitstrings, control_wires, work_wires, target_wires, clean): - """Test that the resource params are correct""" - op = re.ResourceQROM( - bitstrings=bitstrings, - control_wires=control_wires, - target_wires=work_wires, - work_wires=target_wires, - clean=clean, - ) - - num_bitstrings = len(bitstrings) - - num_bit_flips = 0 - for bit_string in bitstrings: - num_bit_flips += bit_string.count("1") - - num_work_wires = len(work_wires) - size_bitstring = len(target_wires) - num_control_wires = len(control_wires) - - assert op.resource_params == { - "num_bitstrings": num_bitstrings, - "num_bit_flips": num_bit_flips, - "num_control_wires": num_control_wires, - "num_work_wires": num_work_wires, - "size_bitstring": size_bitstring, - "clean": clean, - } - - @pytest.mark.parametrize( - "num_bitstrings, num_bit_flips, num_control_wires, num_work_wires, size_bitstring, clean", - [(4, 2, 2, 3, 3, True), (4, 5, 2, 3, 3, False), (4, 5, 0, 3, 3, False)], - ) - def test_resource_rep( - self, - num_bitstrings, - num_bit_flips, - num_control_wires, - num_work_wires, - size_bitstring, - clean, - ): - """Test the resource_rep returns the correct CompressedResourceOp""" - - expected = re.CompressedResourceOp( - re.ResourceQROM, - { - "num_bitstrings": num_bitstrings, - "num_bit_flips": num_bit_flips, - "num_control_wires": num_control_wires, - "num_work_wires": num_work_wires, - "size_bitstring": size_bitstring, - "clean": clean, - }, - ) - assert expected == re.ResourceQROM.resource_rep( - num_bitstrings, num_bit_flips, num_control_wires, num_work_wires, size_bitstring, clean - ) - - @pytest.mark.parametrize( - "num_bitstrings, num_bit_flips, num_control_wires, num_work_wires, size_bitstring, clean", - [(4, 2, 2, 3, 3, True), (4, 5, 2, 3, 3, False), (4, 5, 0, 3, 3, False)], - ) - def test_resources_from_rep( - self, - num_bitstrings, - num_bit_flips, - num_control_wires, - num_work_wires, - size_bitstring, - clean, - ): - """Test that computing the resources from a compressed representation works""" - expected = {} - rep = re.ResourceQROM.resource_rep( - num_bitstrings, num_bit_flips, num_control_wires, num_work_wires, size_bitstring, clean - ) - actual = rep.op_type.resources(**rep.params) - - x = re.CompressedResourceOp(re.ResourceX, {}) - - if num_control_wires == 0: - expected[x] = num_bit_flips - assert actual == expected - return - - cnot = re.CompressedResourceOp(re.ResourceCNOT, {}) - hadamard = re.CompressedResourceOp(re.ResourceHadamard, {}) - - num_parallel_computations = (num_work_wires + size_bitstring) // size_bitstring - num_parallel_computations = min(num_parallel_computations, num_bitstrings) - - num_swap_wires = math.floor(math.log2(num_parallel_computations)) - num_select_wires = math.ceil(math.log2(math.ceil(num_bitstrings / (2**num_swap_wires)))) - assert num_swap_wires + num_select_wires <= num_control_wires - - swap_clean_prefactor = 1 - select_clean_prefactor = 1 - - if clean: - expected[hadamard] = 2 * size_bitstring - swap_clean_prefactor = 4 - select_clean_prefactor = 2 - - # SELECT cost: - expected[cnot] = num_bit_flips # each unitary in the select is just a CNOT - - multi_x = re.CompressedResourceOp( - re.ResourceMultiControlledX, - { - "num_ctrl_wires": num_select_wires, - "num_ctrl_values": 0, - "num_work_wires": 0, - }, - ) - - num_total_ctrl_possibilities = 2**num_select_wires - expected[multi_x] = select_clean_prefactor * ( - 2 * num_total_ctrl_possibilities # two applications targetting the aux qubit - ) - num_zero_controls = (2 * num_total_ctrl_possibilities * num_select_wires) // 2 - expected[x] = select_clean_prefactor * ( - num_zero_controls * 2 # conjugate 0 controls on the multi-qubit x gates from above - ) - # SWAP cost: - ctrl_swap = re.CompressedResourceOp(re.ResourceCSWAP, {}) - expected[ctrl_swap] = swap_clean_prefactor * ((2**num_swap_wires) - 1) * size_bitstring - - assert actual == expected - - def test_tracking_name(self): - """Test that the tracking name is correct.""" - assert re.ResourceQROM.tracking_name() == "QROM" - - -class TestResourceStatePrep: - """Test the ResourceStatePrep class""" - - @pytest.mark.parametrize( - "num_wires", - [(4), (5), (6)], - ) - def test_resources(self, num_wires): - """Test that the resources are correct""" - expected = { - re.CompressedResourceOp( - re.ResourceMottonenStatePreparation, {"num_wires": num_wires} - ): 1 - } - - assert re.ResourceStatePrep.resources(num_wires) == expected - - @pytest.mark.parametrize( - "state, wires", - [ - ( - np.array([1 / 2, 1 / 2, 1 / 2, 1 / 2]), - range(2), - ), - ], - ) - def test_resource_params(self, state, wires): - """Test that the resource params are correct""" - op = re.ResourceStatePrep(state=state, wires=wires, normalize=True) - - assert op.resource_params == {"num_wires": len(wires)} - - @pytest.mark.parametrize( - "num_wires", - [(4), (5), (6)], - ) - def test_resource_rep(self, num_wires): - """Test the resource_rep returns the correct CompressedResourceOp""" - - expected = re.CompressedResourceOp( - re.ResourceStatePrep, - {"num_wires": num_wires}, - ) - assert expected == re.ResourceStatePrep.resource_rep(num_wires) - - @pytest.mark.parametrize( - "num_wires", - [(4), (5), (6)], - ) - def test_resources_from_rep(self, num_wires): - """Test that computing the resources from a compressed representation works""" - rep = re.ResourceStatePrep.resource_rep(num_wires) - actual = rep.op_type.resources(**rep.params) - - expected = { - re.CompressedResourceOp( - re.ResourceMottonenStatePreparation, {"num_wires": num_wires} - ): 1 - } - - assert actual == expected - - @pytest.mark.parametrize( - "num_wires", - [(4), (5), (6)], - ) - def test_tracking_name(self, num_wires): - """Test that the tracking name is correct.""" - assert re.ResourceStatePrep.tracking_name(num_wires) == f"StatePrep({num_wires})" - - -class TestResourceAmplitudeAmplification: - """Test the ResourceAmplitudeAmplification class""" - - @pytest.mark.parametrize( - "U_op, U_params, O_op,O_params,iters,num_ref_wires,fixed_point", - [(re.ResourceHadamard, {}, re.ResourceRX, {}, 5, 3, True)], - ) - def test_resources(self, U_op, U_params, O_op, O_params, iters, num_ref_wires, fixed_point): - """Test that the resources are correct""" - expected = {} - reflection = re.ResourceReflection.resource_rep( - base_class=U_op, base_params=U_params, num_ref_wires=num_ref_wires - ) - - if not fixed_point: - oracles = re.CompressedResourceOp(O_op, params=O_params) - expected[oracles] = iters - expected[reflection] = iters - - assert ( - re.ResourceAmplitudeAmplification.resources( - U_op, - U_params, - O_op, - O_params, - iters, - num_ref_wires, - fixed_point, - ) - == expected - ) - iters = iters // 2 - ctrl = re.ResourceControlled.resource_rep( - base_class=O_op, - base_params=O_params, - num_ctrl_wires=1, - num_ctrl_values=0, - num_work_wires=0, - ) - phase_shift = re.ResourcePhaseShift.resource_rep() - hadamard = re.ResourceHadamard.resource_rep() - - expected[ctrl] = iters * 2 - expected[phase_shift] = iters - expected[hadamard] = iters * 4 - expected[reflection] = iters - - assert ( - re.ResourceAmplitudeAmplification.resources( - U_op, U_params, O_op, O_params, 5, num_ref_wires, fixed_point - ) - == expected - ) - - def test_resource_params(self): - """Test that the resource params are correct""" - - @qml.prod - def generator(wires): - for wire in wires: - qml.Hadamard(wires=wire) - - U = generator(wires=range(3)) - O = qml.FlipSign(2, wires=range(3)) - - expected = { - "U_op": type(U), - "U_params": {}, - "O_op": type(O), - "O_params": {}, - "iters": 5, - "num_ref_wires": 3, - "fixed_point": True, - } - - op = re.ResourceAmplitudeAmplification(U=U, O=O, iters=5, fixed_point=True, work_wire=3) - - assert op.resource_params == expected - - @pytest.mark.parametrize( - "U_op, U_params, O_op,O_params,iters, num_ref_wires,fixed_point", - [(re.ResourceHadamard, {}, re.ResourceRX, {}, 5, 3, True)], - ) - def test_resource_rep(self, U_op, U_params, O_op, O_params, iters, num_ref_wires, fixed_point): - """Test the resource_rep returns the correct CompressedResourceOp""" - - expected = re.CompressedResourceOp( - re.ResourceAmplitudeAmplification, - { - "U_op": U_op, - "U_params": U_params, - "O_op": O_op, - "O_params": O_params, - "iters": iters, - "num_ref_wires": num_ref_wires, - "fixed_point": fixed_point, - }, - ) - assert expected == re.ResourceAmplitudeAmplification.resource_rep( - U_op, U_params, O_op, O_params, iters, num_ref_wires, fixed_point - ) - - @pytest.mark.parametrize( - "U_op, U_params, O_op,O_params,iters,num_ref_wires,fixed_point", - [(re.ResourceHadamard, {}, re.ResourceRX, {}, 5, 3, True)], - ) - def test_resources_from_rep( - self, U_op, U_params, O_op, O_params, iters, num_ref_wires, fixed_point - ): - """Test that computing the resources from a compressed representation works""" - rep = re.ResourceAmplitudeAmplification.resource_rep( - U_op, U_params, O_op, O_params, iters, num_ref_wires, fixed_point - ) - actual = rep.op_type.resources(**rep.params) - - expected = {} - reflection = re.ResourceReflection.resource_rep( - base_class=U_op, base_params=U_params, num_ref_wires=num_ref_wires - ) - - if not fixed_point: - oracles = re.CompressedResourceOp(O_op, params=O_params) - expected[oracles] = iters - expected[reflection] = iters - - assert ( - re.ResourceAmplitudeAmplification.resources( - U_op, - U_params, - O_op, - O_params, - iters, - num_ref_wires, - fixed_point, - ) - == expected - ) - iters = iters // 2 - ctrl = re.ResourceControlled.resource_rep( - base_class=O_op, - base_params=O_params, - num_ctrl_wires=1, - num_ctrl_values=0, - num_work_wires=0, - ) - phase_shift = re.ResourcePhaseShift.resource_rep() - hadamard = re.ResourceHadamard.resource_rep() - - expected[ctrl] = iters * 2 - expected[phase_shift] = iters - expected[hadamard] = iters * 4 - expected[reflection] = iters - - assert actual == expected - - @pytest.mark.parametrize( - "U_op, U_params, O_op,O_params,iters,num_ref_wires,fixed_point", - [(re.ResourceHadamard, {}, re.ResourceRX, {}, 5, 3, True)], - ) - def test_tracking_name(self, U_op, U_params, O_op, O_params, iters, num_ref_wires, fixed_point): - """Test that the tracking name is correct.""" - assert ( - re.ResourceAmplitudeAmplification.tracking_name( - U_op, U_params, O_op, O_params, iters, num_ref_wires, fixed_point - ) - == "AmplitudeAmplification" - ) diff --git a/pennylane/labs/tests/resource_estimation/test_resource_container.py b/pennylane/labs/tests/resource_estimation/test_resource_container.py deleted file mode 100644 index 4c3983929df..00000000000 --- a/pennylane/labs/tests/resource_estimation/test_resource_container.py +++ /dev/null @@ -1,392 +0,0 @@ -# Copyright 2024 Xanadu Quantum Technologies Inc. - -# 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. -""" -Test base Resource class and its associated methods -""" -# pylint:disable=protected-access, no-self-use, too-few-public-methods -import copy -from collections import defaultdict - -import pytest - -import pennylane as qml -from pennylane.labs.resource_estimation.resource_container import ( - CompressedResourceOp, - Resources, - _combine_dict, - _scale_dict, - add_in_parallel, - add_in_series, - mul_in_parallel, - mul_in_series, - substitute, -) -from pennylane.labs.resource_estimation.resource_operator import ResourceOperator -from pennylane.operation import Operator - - -class ResourceDummyX(Operator, ResourceOperator): - """Dummy testing class representing X gate""" - - -class ResourceDummyQFT(Operator, ResourceOperator): - """Dummy testing class representing QFT gate""" - - -class ResourceDummyQSVT(Operator, ResourceOperator): - """Dummy testing class representing QSVT gate""" - - -class ResourceDummyTrotterProduct(Operator, ResourceOperator): - """Dummy testing class representing TrotterProduct gate""" - - -class ResourceDummyAdjoint(Operator, ResourceOperator): - """Dummy testing class representing the Adjoint symbolic operator""" - - -class TestCompressedResourceOp: - """Testing the methods and attributes of the CompressedResourceOp class""" - - test_hamiltonian = qml.dot([1, -1, 0.5], [qml.X(0), qml.Y(1), qml.Z(0) @ qml.Z(1)]) - compressed_ops_and_params_lst = ( - ("DummyX", ResourceDummyX, {"num_wires": 1}, None), - ("DummyQFT", ResourceDummyQFT, {"num_wires": 5}, None), - ("DummyQSVT", ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}, None), - ( - "DummyTrotterProduct", - ResourceDummyTrotterProduct, - {"Hamiltonian": test_hamiltonian, "num_steps": 5, "order": 2}, - None, - ), - ("X", ResourceDummyX, {"num_wires": 1}, "X"), - ) - - compressed_op_reprs = ( - "DummyX", - "DummyQFT", - "DummyQSVT", - "DummyTrotterProduct", - "X", - ) - - @pytest.mark.parametrize("name, op_type, parameters, name_param", compressed_ops_and_params_lst) - def test_init(self, name, op_type, parameters, name_param): - """Test that we can correctly instantiate CompressedResourceOp""" - cr_op = CompressedResourceOp(op_type, parameters, name=name_param) - - assert cr_op._name == name - assert cr_op.op_type is op_type - assert cr_op.params == parameters - assert cr_op._hashable_params == tuple(sorted((str(k), v) for k, v in parameters.items())) - - def test_hash(self): - """Test that the hash method behaves as expected""" - CmprssedQSVT1 = CompressedResourceOp(ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}) - CmprssedQSVT2 = CompressedResourceOp(ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}) - Other = CompressedResourceOp(ResourceDummyQFT, {"num_wires": 3}) - - assert hash(CmprssedQSVT1) == hash(CmprssedQSVT1) # compare same object - assert hash(CmprssedQSVT1) == hash(CmprssedQSVT2) # compare identical instance - assert hash(CmprssedQSVT1) != hash(Other) - - # test dictionary as parameter - CmprssedAdjoint1 = CompressedResourceOp( - ResourceDummyAdjoint, {"base_class": ResourceDummyQFT, "base_params": {"num_wires": 1}} - ) - CmprssedAdjoint2 = CompressedResourceOp( - ResourceDummyAdjoint, {"base_class": ResourceDummyQFT, "base_params": {"num_wires": 1}} - ) - Other = CompressedResourceOp( - ResourceDummyAdjoint, {"base_class": ResourceDummyQFT, "base_params": {"num_wires": 2}} - ) - - assert hash(CmprssedAdjoint1) == hash(CmprssedAdjoint1) - assert hash(CmprssedAdjoint1) == hash(CmprssedAdjoint2) - assert hash(CmprssedAdjoint1) != hash(Other) - - def test_equality(self): - """Test that the equality methods behaves as expected""" - CmprssedQSVT1 = CompressedResourceOp(ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}) - CmprssedQSVT2 = CompressedResourceOp(ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}) - CmprssedQSVT3 = CompressedResourceOp(ResourceDummyQSVT, {"num_angles": 5, "num_wires": 3}) - Other = CompressedResourceOp(ResourceDummyQFT, {"num_wires": 3}) - - assert CmprssedQSVT1 == CmprssedQSVT2 # compare identical instance - assert CmprssedQSVT1 == CmprssedQSVT3 # compare swapped parameters - assert CmprssedQSVT1 != Other - - @pytest.mark.parametrize("args, repr", zip(compressed_ops_and_params_lst, compressed_op_reprs)) - def test_repr(self, args, repr): - """Test that the repr method behaves as expected.""" - _, op_type, parameters, name_param = args - cr_op = CompressedResourceOp(op_type, parameters, name=name_param) - - assert str(cr_op) == repr - - -class TestResources: - """Test the methods and attributes of the Resource class""" - - resource_quantities = ( - Resources(), - Resources(5, 0, defaultdict(int, {})), - Resources(1, 3, defaultdict(int, {"Hadamard": 1, "PauliZ": 2})), - Resources(4, 2, defaultdict(int, {"Hadamard": 1, "CNOT": 1})), - ) - - resource_parameters = ( - (0, 0, defaultdict(int, {})), - (5, 0, defaultdict(int, {})), - (1, 3, defaultdict(int, {"Hadamard": 1, "PauliZ": 2})), - (4, 2, defaultdict(int, {"Hadamard": 1, "CNOT": 1})), - ) - - @pytest.mark.parametrize("r, attribute_tup", zip(resource_quantities, resource_parameters)) - def test_init(self, r, attribute_tup): - """Test that the Resource class is instantiated as expected.""" - num_wires, num_gates, gate_types = attribute_tup - - assert r.num_wires == num_wires - assert r.num_gates == num_gates - assert r.gate_types == gate_types - - expected_results_add_series = ( - Resources(2, 6, defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1})), - Resources(5, 6, defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1})), - Resources( - 2, 9, defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 2, "PauliZ": 2}) - ), - Resources(4, 8, defaultdict(int, {"RZ": 2, "CNOT": 2, "RY": 2, "Hadamard": 2})), - ) - - @pytest.mark.parametrize("in_place", (False, True)) - @pytest.mark.parametrize( - "resource_obj, expected_res_obj", zip(resource_quantities, expected_results_add_series) - ) - def test_add_in_series(self, resource_obj, expected_res_obj, in_place): - """Test the add_in_series function works with Resoruces""" - resource_obj = copy.deepcopy(resource_obj) - other_obj = Resources( - num_wires=2, - num_gates=6, - gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), - ) - - resultant_obj = add_in_series(resource_obj, other_obj, in_place=in_place) - assert resultant_obj == expected_res_obj - - if in_place: - assert resultant_obj is resource_obj - - expected_results_add_parallel = ( - Resources(2, 6, defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1})), - Resources(7, 6, defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1})), - Resources( - 3, 9, defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 2, "PauliZ": 2}) - ), - Resources(6, 8, defaultdict(int, {"RZ": 2, "CNOT": 2, "RY": 2, "Hadamard": 2})), - ) - - @pytest.mark.parametrize("in_place", (False, True)) - @pytest.mark.parametrize( - "resource_obj, expected_res_obj", zip(resource_quantities, expected_results_add_parallel) - ) - def test_add_in_parallel(self, resource_obj, expected_res_obj, in_place): - """Test the add_in_parallel function works with Resoruces""" - resource_obj = copy.deepcopy(resource_obj) - other_obj = Resources( - num_wires=2, - num_gates=6, - gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), - ) - - resultant_obj = add_in_parallel(resource_obj, other_obj, in_place=in_place) - assert resultant_obj == expected_res_obj - - if in_place: - assert resultant_obj is resource_obj - - expected_results_mul_series = ( - Resources( - num_wires=2, - num_gates=6, - gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), - ), - Resources( - num_wires=2, - num_gates=12, - gate_types=defaultdict(int, {"RZ": 4, "CNOT": 2, "RY": 4, "Hadamard": 2}), - ), - Resources( - num_wires=2, - num_gates=18, - gate_types=defaultdict(int, {"RZ": 6, "CNOT": 3, "RY": 6, "Hadamard": 3}), - ), - Resources( - num_wires=2, - num_gates=30, - gate_types=defaultdict(int, {"RZ": 10, "CNOT": 5, "RY": 10, "Hadamard": 5}), - ), - ) - - @pytest.mark.parametrize("in_place", (False, True)) - @pytest.mark.parametrize( - "scalar, expected_res_obj", zip((1, 2, 3, 5), expected_results_mul_series) - ) - def test_mul_in_series(self, scalar, expected_res_obj, in_place): - """Test the mul_in_series function works with Resoruces""" - resource_obj = Resources( - num_wires=2, - num_gates=6, - gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), - ) - - resultant_obj = mul_in_series(resource_obj, scalar, in_place=in_place) - assert resultant_obj == expected_res_obj - - if in_place: - assert resultant_obj is resource_obj - assert True - - expected_results_mul_parallel = ( - Resources( - num_wires=2, - num_gates=6, - gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), - ), - Resources( - num_wires=4, - num_gates=12, - gate_types=defaultdict(int, {"RZ": 4, "CNOT": 2, "RY": 4, "Hadamard": 2}), - ), - Resources( - num_wires=6, - num_gates=18, - gate_types=defaultdict(int, {"RZ": 6, "CNOT": 3, "RY": 6, "Hadamard": 3}), - ), - Resources( - num_wires=10, - num_gates=30, - gate_types=defaultdict(int, {"RZ": 10, "CNOT": 5, "RY": 10, "Hadamard": 5}), - ), - ) - - @pytest.mark.parametrize("in_place", (False, True)) - @pytest.mark.parametrize( - "scalar, expected_res_obj", zip((1, 2, 3, 5), expected_results_mul_parallel) - ) - def test_mul_in_parallel(self, scalar, expected_res_obj, in_place): - """Test the mul_in_parallel function works with Resoruces""" - resource_obj = Resources( - num_wires=2, - num_gates=6, - gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), - ) - - resultant_obj = mul_in_parallel(resource_obj, scalar, in_place=in_place) - assert resultant_obj == expected_res_obj - - if in_place: - assert resultant_obj is resource_obj - assert True - - test_str_data = ( - ("wires: 0\n" + "gates: 0\n" + "gate_types:\n" + "{}"), - ("wires: 5\n" + "gates: 0\n" + "gate_types:\n" + "{}"), - ("wires: 1\n" + "gates: 3\n" + "gate_types:\n" + "{'Hadamard': 1, 'PauliZ': 2}"), - ("wires: 4\n" + "gates: 2\n" + "gate_types:\n" + "{'Hadamard': 1, 'CNOT': 1}"), - ) - - @pytest.mark.parametrize("r, rep", zip(resource_quantities, test_str_data)) - def test_str(self, r, rep): - """Test the string representation of a Resources instance.""" - assert str(r) == rep - - @pytest.mark.parametrize("r, rep", zip(resource_quantities, test_str_data)) - def test_ipython_display(self, r, rep, capsys): - """Test that the ipython display prints the string representation of a Resources instance.""" - r._ipython_display_() # pylint: disable=protected-access - captured = capsys.readouterr() - assert rep in captured.out - - gate_names = ("RX", "RZ") - expected_results_sub = ( - Resources( - num_wires=2, - num_gates=6, - gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), - ), - Resources( - num_wires=2, - num_gates=14, - gate_types=defaultdict(int, {"RX": 10, "CNOT": 1, "RY": 2, "Hadamard": 1}), - ), - ) - - @pytest.mark.parametrize("gate_name, expected_res_obj", zip(gate_names, expected_results_sub)) - def test_substitute(self, gate_name, expected_res_obj): - """Test the substitute function""" - resource_obj = Resources( - num_wires=2, - num_gates=6, - gate_types=defaultdict(int, {"RZ": 2, "CNOT": 1, "RY": 2, "Hadamard": 1}), - ) - - sub_obj = Resources( - num_wires=1, - num_gates=5, - gate_types=defaultdict(int, {"RX": 5}), - ) - - resultant_obj1 = substitute(resource_obj, gate_name, sub_obj, in_place=False) - assert resultant_obj1 == expected_res_obj - - resultant_obj2 = substitute(resource_obj, gate_name, sub_obj, in_place=True) - assert resultant_obj2 == expected_res_obj - assert resultant_obj2 is resource_obj - - -@pytest.mark.parametrize("in_place", [False, True]) -def test_combine_dict(in_place): - """Test that we can combine dictionaries as expected.""" - d1 = defaultdict(int, {"a": 2, "b": 4, "c": 6}) - d2 = defaultdict(int, {"a": 1, "b": 2, "d": 3}) - - result = _combine_dict(d1, d2, in_place=in_place) - expected = defaultdict(int, {"a": 3, "b": 6, "c": 6, "d": 3}) - - assert result == expected - - if in_place: - assert result is d1 - else: - assert result is not d1 - - -@pytest.mark.parametrize("scalar", (1, 2, 3)) -@pytest.mark.parametrize("in_place", (False, True)) -def test_scale_dict(scalar, in_place): - """Test that we can scale the values of a dictionary as expected.""" - d1 = defaultdict(int, {"a": 2, "b": 4, "c": 6}) - - expected = defaultdict(int, {k: scalar * v for k, v in d1.items()}) - result = _scale_dict(d1, scalar, in_place=in_place) - - assert result == expected - - if in_place: - assert result is d1 - else: - assert result is not d1 diff --git a/pennylane/labs/tests/resource_estimation/test_resource_operator.py b/pennylane/labs/tests/resource_estimation/test_resource_operator.py deleted file mode 100644 index 68406d6723a..00000000000 --- a/pennylane/labs/tests/resource_estimation/test_resource_operator.py +++ /dev/null @@ -1,132 +0,0 @@ -# Copyright 2024 Xanadu Quantum Technologies Inc. - -# 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. -""" -Test the abstract ResourceOperator class -""" -import pytest - -import pennylane.labs.resource_estimation as re - -# pylint: disable=abstract-class-instantiated,arguments-differ,missing-function-docstring,too-few-public-methods - - -def test_abstract_resource_decomp(): - """Test that the _resource_decomp method is abstract.""" - - class DummyClass(re.ResourceOperator): - """Dummy class for testing""" - - @property - def resource_params(self): - return - - @staticmethod - def resource_rep(): - return - - with pytest.raises( - TypeError, - match="Can't instantiate abstract class DummyClass with abstract method _resource_decomp", - ): - DummyClass() - - -def test_abstract_resource_params(): - """Test that the resource_params method is abstract""" - - class DummyClass(re.ResourceOperator): - """Dummy class for testing""" - - @staticmethod - def _resource_decomp(): - return - - def resource_rep(self): - return - - with pytest.raises( - TypeError, - match="Can't instantiate abstract class DummyClass with abstract method resource_params", - ): - DummyClass() - - -def test_abstract_resource_rep(): - """Test that the resource_rep method is abstract""" - - class DummyClass(re.ResourceOperator): - """Dummy class for testing""" - - @staticmethod - def _resource_decomp(): - return - - @property - def resource_params(self): - return - - with pytest.raises( - TypeError, - match="Can't instantiate abstract class DummyClass with abstract method resource_rep", - ): - DummyClass() - - -def test_set_resources(): - """Test that the resources method can be overriden""" - - class DummyClass(re.ResourceOperator): - """Dummy class for testing""" - - @property - def resource_params(self): - return - - @staticmethod - def resource_rep(): - return - - @staticmethod - def _resource_decomp(): - return - - dummy = DummyClass() - DummyClass.set_resources(lambda _: 5) - assert DummyClass.resources(10) == 5 - - -def test_resource_rep_from_op(): - """Test that the resource_rep_from_op method is the composition of resource_params and resource_rep""" - - class DummyClass(re.ResourceQFT, re.ResourceOperator): - """Dummy class for testing""" - - @staticmethod - def _resource_decomp(): - return - - @property - def resource_params(self): - return {"foo": 1, "bar": 2} - - @classmethod - def resource_rep(cls, foo, bar): - return re.CompressedResourceOp(cls, {"foo": foo, "bar": bar}) - - @staticmethod - def tracking_name(foo, bar): - return f"DummyClass({foo}, {bar})" - - op = DummyClass(wires=[1, 2, 3]) - assert op.resource_rep_from_op() == op.__class__.resource_rep(**op.resource_params) diff --git a/pennylane/labs/tests/resource_estimation/test_resource_tracking.py b/pennylane/labs/tests/resource_estimation/test_resource_tracking.py deleted file mode 100644 index 27fdbf46ff1..00000000000 --- a/pennylane/labs/tests/resource_estimation/test_resource_tracking.py +++ /dev/null @@ -1,357 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -"""Test the core resource tracking pipeline.""" -from collections import defaultdict -from copy import copy - -# pylint:disable=protected-access, no-self-use -import pytest - -import pennylane as qml -import pennylane.labs.resource_estimation as re -from pennylane.labs.resource_estimation.resource_tracking import ( - DefaultGateSet, - _clean_gate_counts, - _counts_from_compressed_res_op, - _operations_to_compressed_reps, - _StandardGateSet, - get_resources, - resource_config, - resources_from_operation, - resources_from_qfunc, - resources_from_tape, -) - - -class DummyOperation(qml.operation.Operation): - """Dummy class to test _operations_to_compressed_reps function.""" - - def __init__(self, wires=None): - super().__init__(wires=wires) - - def decomposition(self): - decomp = [ - re.ResourceHadamard(wires=self.wires[0]), - re.ResourceHadamard(wires=self.wires[1]), - re.ResourceCNOT(wires=[self.wires[0], self.wires[1]]), - ] - - return decomp - - -class TestGetResources: - """Test the core resource tracking pipeline""" - - compressed_rep_data = ( - ( - [ - re.ResourceHadamard(0), - re.ResourceRX(1.23, 1), - re.ResourceCNOT(wires=[1, 2]), - ], - [ - re.CompressedResourceOp(re.ResourceHadamard, {}), - re.CompressedResourceOp(re.ResourceRX, {}), - re.CompressedResourceOp(re.ResourceCNOT, {}), - ], - ), - ( - [ - re.ResourceQFT(wires=[1, 2, 3]), - re.ResourceIdentity(wires=[1, 2, 3]), - re.ResourceRot(1.23, 0.45, -6, wires=0), - re.ResourceQFT(wires=[1, 2]), - ], - [ - re.CompressedResourceOp(re.ResourceQFT, {"num_wires": 3}), - re.CompressedResourceOp(re.ResourceIdentity, {}), - re.CompressedResourceOp(re.ResourceRot, {}), - re.CompressedResourceOp(re.ResourceQFT, {"num_wires": 2}), - ], - ), - ( - [ - re.ResourceQFT(wires=[0, 1]), - DummyOperation(wires=["a", "b"]), - re.ResourceRY(-0.5, wires=1), - ], - [ - re.CompressedResourceOp(re.ResourceQFT, {"num_wires": 2}), - re.CompressedResourceOp(re.ResourceHadamard, {}), - re.CompressedResourceOp(re.ResourceHadamard, {}), - re.CompressedResourceOp(re.ResourceCNOT, {}), - re.CompressedResourceOp(re.ResourceRY, {}), - ], - ), # Test decomposition logic - ) - - @pytest.mark.parametrize("ops_lst, compressed_reps", compressed_rep_data) - def test_operations_to_compressed_reps(self, ops_lst, compressed_reps): - """Test that we can transform a list of operations into compressed reps""" - computed_compressed_reps = _operations_to_compressed_reps(ops_lst) - for computed_cr, expected_cr in zip(computed_compressed_reps, compressed_reps): - assert computed_cr == expected_cr - - compressed_rep_counts = ( - ( - re.ResourceHadamard(wires=0).resource_rep_from_op(), - defaultdict(int, {re.CompressedResourceOp(re.ResourceHadamard, {}): 1}), - ), - ( - re.ResourceRX(1.23, wires=0).resource_rep_from_op(), - defaultdict(int, {re.CompressedResourceOp(re.ResourceT, {}): 17}), - ), - ( - re.ResourceIdentity(wires=[1, 2, 3]).resource_rep_from_op(), - defaultdict(int, {}), # Identity has no resources - ), - ( - re.ResourceControlledPhaseShift(1.23, wires=[0, 1]).resource_rep_from_op(), - defaultdict( - int, - { - re.CompressedResourceOp(re.ResourceT, {}): 51, - re.CompressedResourceOp(re.ResourceCNOT, {}): 2, - }, - ), - ), - ( - re.ResourceQFT(wires=[1, 2, 3, 4]).resource_rep_from_op(), - defaultdict( - int, - { - re.CompressedResourceOp(re.ResourceT, {}): 306, - re.CompressedResourceOp(re.ResourceCNOT, {}): 18, - re.CompressedResourceOp(re.ResourceHadamard, {}): 4, - }, - ), - ), - ) - - @pytest.mark.parametrize("op_in_gate_set", [True, False]) - @pytest.mark.parametrize("scalar", [1, 2, 5]) - @pytest.mark.parametrize("compressed_rep, expected_counts", compressed_rep_counts) - def test_counts_from_compressed_res( - self, scalar, compressed_rep, expected_counts, op_in_gate_set - ): - """Test that we can obtain counts from a compressed resource op""" - - if op_in_gate_set: - # Test that we add op directly to counts if its in the gate_set - custom_gate_set = {compressed_rep._name} - - base_gate_counts = defaultdict(int) - _counts_from_compressed_res_op( - compressed_rep, - gate_counts_dict=base_gate_counts, - gate_set=custom_gate_set, - scalar=scalar, - ) - - assert base_gate_counts == defaultdict(int, {compressed_rep: scalar}) - - else: - expected_counts = copy(expected_counts) - for resource_op, counts in expected_counts.items(): # scale expected counts - expected_counts[resource_op] = scalar * counts - - base_gate_counts = defaultdict(int) - _counts_from_compressed_res_op( - compressed_rep, - gate_counts_dict=base_gate_counts, - gate_set=DefaultGateSet, - scalar=scalar, - ) - - assert base_gate_counts == expected_counts - - @pytest.mark.parametrize( - "custom_config, num_T_gates", - ( - ( - { - "error_rz": 10e-2, - }, - 13, - ), - ( - { - "error_rz": 10e-3, - }, - 17, - ), - ( - { - "error_rz": 10e-4, - }, - 21, - ), - ), - ) - def test_counts_from_compressed_res_custom_config(self, custom_config, num_T_gates): - """Test that the function works with custom configs and a non-empty gate_counts_dict""" - base_gate_counts = defaultdict( - int, {re.ResourceT.resource_rep(): 3, re.ResourceS.resource_rep(): 5} - ) - - _counts_from_compressed_res_op( - re.ResourceRZ.resource_rep(), - base_gate_counts, - gate_set=DefaultGateSet, - config=custom_config, - ) - expected_counts = defaultdict( - int, {re.ResourceT.resource_rep(): 3 + num_T_gates, re.ResourceS.resource_rep(): 5} - ) - - assert base_gate_counts == expected_counts - - def test_clean_gate_counts(self): - """Test that the function groups operations by name instead of compressed representation.""" - - gate_counts = defaultdict( - int, - { - re.ResourceQFT.resource_rep(5): 1, - re.ResourceHadamard.resource_rep(): 3, - re.ResourceCNOT.resource_rep(): 1, - re.ResourceQFT.resource_rep(3): 4, - }, - ) - - expected_clean_counts = defaultdict( - int, {"CNOT": 1, "Hadamard": 3, "QFT(5)": 1, "QFT(3)": 4} - ) - - assert _clean_gate_counts(gate_counts) == expected_clean_counts - - @pytest.mark.parametrize( - "op, expected_resources", - ( - (re.ResourceHadamard(wires=0), re.Resources(1, 1, defaultdict(int, {"Hadamard": 1}))), - (re.ResourceRX(1.23, wires=1), re.Resources(1, 17, defaultdict(int, {"T": 17}))), - ( - re.ResourceQFT(wires=range(5)), - re.Resources(5, 541, defaultdict(int, {"Hadamard": 5, "CNOT": 26, "T": 510})), - ), - ), - ) - def test_resources_from_operation(self, op, expected_resources): - """Test that we can extract the resources from an Operation.""" - computed_resources = resources_from_operation( - op - ) # add tests that don't use default gate_set and config - assert computed_resources == expected_resources - - @staticmethod - def my_qfunc(): - """Dummy qfunc used to test resources_from_qfunc function.""" - for w in range(2): - re.ResourceHadamard(w) - - re.ResourceCNOT([0, 1]) - re.ResourceRX(1.23, 0) - re.ResourceRY(-4.56, 1) - - re.ResourceQFT(wires=[0, 1, 2]) - return qml.expval(re.ResourceHadamard(2)) - - def test_resources_from_qfunc(self): - """Test the we can extract the resources from a quantum function.""" - expected_resources_standard = re.Resources( - num_wires=3, - num_gates=24, - gate_types=defaultdict( - int, {"Hadamard": 5, "CNOT": 7, "SWAP": 1, "RX": 1, "RY": 1, "RZ": 9} - ), - ) - - computed_resources = resources_from_qfunc(self.my_qfunc, gate_set=_StandardGateSet)() - assert computed_resources == expected_resources_standard - - expected_resources_custom = re.Resources( - num_wires=3, - num_gates=190, - gate_types=defaultdict(int, {"Hadamard": 5, "CNOT": 10, "T": 175}), - ) - - my_resource_config = copy(resource_config) - my_resource_config["error_rx"] = 10e-1 - my_resource_config["error_ry"] = 10e-2 - computed_resources = resources_from_qfunc( - self.my_qfunc, gate_set=DefaultGateSet, config=my_resource_config - )() - - assert computed_resources == expected_resources_custom - - my_tape = qml.tape.QuantumScript( - ops=[ - re.ResourceHadamard(0), - re.ResourceHadamard(1), - re.ResourceCNOT([0, 1]), - re.ResourceRX(1.23, 0), - re.ResourceRY(-4.56, 1), - re.ResourceQFT(wires=[0, 1, 2]), - ], - measurements=[qml.expval(re.ResourceHadamard(2))], - ) - - def test_resources_from_tape(self): - """Test that we can extract the resources from a quantum tape""" - expected_resources_standard = re.Resources( - num_wires=3, - num_gates=24, - gate_types=defaultdict( - int, {"Hadamard": 5, "CNOT": 7, "SWAP": 1, "RX": 1, "RY": 1, "RZ": 9} - ), - ) - - computed_resources = resources_from_tape(self.my_tape, gate_set=_StandardGateSet) - assert computed_resources == expected_resources_standard - - expected_resources_custom = re.Resources( - num_wires=3, - num_gates=190, - gate_types=defaultdict(int, {"Hadamard": 5, "CNOT": 10, "T": 175}), - ) - - my_resource_config = copy(resource_config) - my_resource_config["error_rx"] = 10e-1 - my_resource_config["error_ry"] = 10e-2 - computed_resources = resources_from_tape( - self.my_tape, gate_set=DefaultGateSet, config=my_resource_config - ) - - assert computed_resources == expected_resources_custom - - def test_get_resources(self): - """Test that we can dispatch between each of the implementations above""" - op = re.ResourceControlledPhaseShift(1.23, wires=[0, 1]) - tape = qml.tape.QuantumScript(ops=[op], measurements=[qml.expval(re.ResourceHadamard(0))]) - - def circuit(): - re.ResourceControlledPhaseShift(1.23, wires=[0, 1]) - return qml.expval(re.ResourceHadamard(0)) - - res_from_op = get_resources(op) - res_from_tape = get_resources(tape) - res_from_circuit = get_resources(circuit)() - - expected_resources = re.Resources( - num_wires=2, num_gates=53, gate_types=defaultdict(int, {"CNOT": 2, "T": 51}) - ) - - assert res_from_op == expected_resources - assert res_from_tape == expected_resources - assert res_from_circuit == expected_resources From 95232dfce56d42b91c0abffc1d0def6515cb67d2 Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Wed, 7 May 2025 15:58:18 -0400 Subject: [PATCH 03/18] remove source code --- .../labs/resource_estimation/__init__.py | 201 -- .../labs/resource_estimation/ops/__init__.py | 70 - .../labs/resource_estimation/ops/identity.py | 229 --- .../ops/op_math/__init__.py | 17 - .../ops/op_math/controlled_ops.py | 1812 ----------------- .../ops/op_math/symbolic.py | 1251 ------------ .../resource_estimation/ops/qubit/__init__.py | 19 - .../ops/qubit/non_parametric_ops.py | 1024 ---------- .../ops/qubit/parametric_ops_multi_qubit.py | 1223 ----------- .../ops/qubit/parametric_ops_single_qubit.py | 795 -------- .../ops/qubit/qchem_ops.py | 689 ------- .../resource_estimation/resource_container.py | 347 ---- .../resource_estimation/resource_operator.py | 187 -- .../resource_estimation/resource_tracking.py | 319 --- .../resource_estimation/templates/__init__.py | 40 - .../templates/stateprep.py | 393 ---- .../templates/subroutines.py | 1635 --------------- .../resource_estimation/templates/trotter.py | 494 ----- 18 files changed, 10745 deletions(-) delete mode 100644 pennylane/labs/resource_estimation/ops/__init__.py delete mode 100644 pennylane/labs/resource_estimation/ops/identity.py delete mode 100644 pennylane/labs/resource_estimation/ops/op_math/__init__.py delete mode 100644 pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py delete mode 100644 pennylane/labs/resource_estimation/ops/op_math/symbolic.py delete mode 100644 pennylane/labs/resource_estimation/ops/qubit/__init__.py delete mode 100644 pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py delete mode 100644 pennylane/labs/resource_estimation/ops/qubit/parametric_ops_multi_qubit.py delete mode 100644 pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py delete mode 100644 pennylane/labs/resource_estimation/ops/qubit/qchem_ops.py delete mode 100644 pennylane/labs/resource_estimation/resource_container.py delete mode 100644 pennylane/labs/resource_estimation/resource_operator.py delete mode 100644 pennylane/labs/resource_estimation/resource_tracking.py delete mode 100644 pennylane/labs/resource_estimation/templates/__init__.py delete mode 100644 pennylane/labs/resource_estimation/templates/stateprep.py delete mode 100644 pennylane/labs/resource_estimation/templates/subroutines.py delete mode 100644 pennylane/labs/resource_estimation/templates/trotter.py diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 7ae769f66e3..7d70e810181 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -28,209 +28,8 @@ .. autosummary:: :toctree: api - ~Resources ~ResourceOperator -Operators -~~~~~~~~~ - -.. autosummary:: - :toctree: api - - ~ResourceCCZ - ~ResourceCH - ~ResourceCNOT - ~ResourceControlledPhaseShift - ~ResourceCRot - ~ResourceCRX - ~ResourceCRY - ~ResourceCRZ - ~ResourceCSWAP - ~ResourceCY - ~ResourceCZ - ~ResourceDoubleExcitation - ~ResourceDoubleExcitationMinus - ~ResourceDoubleExcitationPlus - ~ResourceFermionicSWAP - ~ResourceGlobalPhase - ~ResourceHadamard - ~ResourceIdentity - ~ResourceIsingXX - ~ResourceIsingXY - ~ResourceIsingYY - ~ResourceIsingZZ - ~ResourceMultiControlledX - ~ResourceMultiRZ - ~ResourceOrbitalRotation - ~ResourcePauliRot - ~ResourcePhaseShift - ~ResourcePSWAP - ~ResourceRot - ~ResourceRX - ~ResourceRY - ~ResourceRZ - ~ResourceS - ~ResourceSingleExcitation - ~ResourceSingleExcitationMinus - ~ResourceSingleExcitationPlus - ~ResourceSWAP - ~ResourceT - ~ResourceToffoli - ~ResourceX - ~ResourceY - ~ResourceZ - -Symbolic Operators -~~~~~~~~~~~~~~~~~~ - -.. autosummary:: - :toctree: api - - ~ResourceAdjoint - ~ResourceControlled - ~ResourceExp - ~ResourcePow - ~ResourceProd - -Templates -~~~~~~~~~ - -.. autosummary:: - :toctree: api - - ~ResourceAmplitudeAmplification - ~ResourceBasisRotation - ~ResourcePrepSelPrep - ~ResourceQFT - ~ResourceQPE - ~ResourceQuantumPhaseEstimation - ~ResourceQubitization - ~ResourceQROM - ~ResourceReflection - ~ResourceSelect - ~ResourceTrotterProduct - ~ResourceTrotterizedQfunc - ~resource_trotterize - ~ResourceControlledSequence - ~ResourceModExp - ~ResourceMultiplier - ~ResourcePhaseAdder - -State Preparation Templates -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autosummary:: - :toctree: api - - ~ResourceBasisState - ~ResourceStatePrep - ~ResourceSuperposition - ~ResourceMottonenStatePreparation - -Tracking Resources -~~~~~~~~~~~~~~~~~~ - -.. autosummary:: - :toctree: api - - ~get_resources - -Resource Object Functions: -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autosummary:: - :toctree: api - - ~add_in_series - ~add_in_parallel - ~mul_in_series - ~mul_in_parallel - ~substitute """ from .resource_operator import ResourceOperator, ResourcesNotDefined -from .resource_tracking import DefaultGateSet, get_resources, resource_config - -from .resource_container import ( - CompressedResourceOp, - Resources, - add_in_series, - add_in_parallel, - mul_in_series, - mul_in_parallel, - substitute, -) - -from .ops import ( - ResourceAdjoint, - ResourceCCZ, - ResourceCH, - ResourceCNOT, - ResourceControlled, - ResourceControlledPhaseShift, - ResourceCRot, - ResourceCRX, - ResourceCRY, - ResourceCRZ, - ResourceCSWAP, - ResourceCY, - ResourceCZ, - ResourceDoubleExcitation, - ResourceDoubleExcitationMinus, - ResourceDoubleExcitationPlus, - ResourceExp, - ResourceFermionicSWAP, - ResourceGlobalPhase, - ResourceHadamard, - ResourceIdentity, - ResourceIsingXX, - ResourceIsingXY, - ResourceIsingYY, - ResourceIsingZZ, - ResourceMultiControlledX, - ResourceMultiRZ, - ResourceOrbitalRotation, - ResourcePauliRot, - ResourcePow, - ResourcePSWAP, - ResourcePhaseShift, - ResourceProd, - ResourceRot, - ResourceRX, - ResourceRY, - ResourceRZ, - ResourceS, - ResourceSingleExcitation, - ResourceSingleExcitationMinus, - ResourceSingleExcitationPlus, - ResourceSWAP, - ResourceT, - ResourceToffoli, - ResourceX, - ResourceY, - ResourceZ, -) - -from .templates import ( - ResourceControlledSequence, - ResourceModExp, - ResourceMultiplier, - ResourcePhaseAdder, - ResourceBasisRotation, - ResourcePrepSelPrep, - ResourceQFT, - ResourceQPE, - ResourceQuantumPhaseEstimation, - ResourceQubitization, - ResourceQROM, - ResourceReflection, - ResourceSelect, - ResourceStatePrep, - ResourceTrotterProduct, - ResourceTrotterizedQfunc, - resource_trotterize, - ResourceMottonenStatePreparation, - ResourceSuperposition, - ResourceAmplitudeAmplification, - ResourceBasisState, -) diff --git a/pennylane/labs/resource_estimation/ops/__init__.py b/pennylane/labs/resource_estimation/ops/__init__.py deleted file mode 100644 index f0aa872a9a6..00000000000 --- a/pennylane/labs/resource_estimation/ops/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""This module contains resource operators for PennyLane Operators""" - -from .identity import ( - ResourceGlobalPhase, - ResourceIdentity, -) - -from .qubit import ( - ResourceDoubleExcitation, - ResourceDoubleExcitationMinus, - ResourceDoubleExcitationPlus, - ResourceFermionicSWAP, - ResourceHadamard, - ResourceIsingXX, - ResourceIsingXY, - ResourceIsingYY, - ResourceIsingZZ, - ResourceMultiRZ, - ResourceOrbitalRotation, - ResourcePauliRot, - ResourcePhaseShift, - ResourcePSWAP, - ResourceRot, - ResourceRX, - ResourceRY, - ResourceRZ, - ResourceS, - ResourceSingleExcitation, - ResourceSingleExcitationMinus, - ResourceSingleExcitationPlus, - ResourceSWAP, - ResourceT, - ResourceX, - ResourceY, - ResourceZ, -) - -from .op_math import ( - ResourceAdjoint, - ResourceCY, - ResourceCH, - ResourceCZ, - ResourceCSWAP, - ResourceCCZ, - ResourceCRot, - ResourceCRX, - ResourceCRY, - ResourceCRZ, - ResourceExp, - ResourceToffoli, - ResourceMultiControlledX, - ResourceCNOT, - ResourceControlled, - ResourceControlledPhaseShift, - ResourcePow, - ResourceProd, -) diff --git a/pennylane/labs/resource_estimation/ops/identity.py b/pennylane/labs/resource_estimation/ops/identity.py deleted file mode 100644 index 76ea1ab9e16..00000000000 --- a/pennylane/labs/resource_estimation/ops/identity.py +++ /dev/null @@ -1,229 +0,0 @@ -# Copyright 2024 Xanadu Quantum Technologies Inc. - -# 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. -r"""Resource operators for identity operations.""" -from typing import Dict - -import pennylane as qml -import pennylane.labs.resource_estimation as re - -# pylint: disable=arguments-differ,no-self-use,too-many-ancestors - - -class ResourceIdentity(qml.Identity, re.ResourceOperator): - r"""Resource class for the Identity gate. - - Args: - wires (Iterable[Any] or Any): Wire label(s) that the identity acts on. - id (str): custom label given to an operator instance, - can be useful for some applications where the instance has to be identified. - - Resources: - The Identity gate is treated as a free gate and thus it cannot be decomposed - further. Requesting the resources of this gate returns an empty dictionary. - - .. seealso:: :class:`~.Identity` - - """ - - @staticmethod - def _resource_decomp(*args, **kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The Identity gate is treated as a free gate and thus it cannot be decomposed - further. Requesting the resources of this gate returns an empty dictionary. - - Returns: - dict: empty dictionary - """ - return {} - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls, **kwargs) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation is also an empty dictionary. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated values are the counts. - """ - return {cls.resource_rep(): 1} - - @classmethod - def controlled_resource_decomp( - cls, num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): The number of control qubits, that are triggered when in the :math:`|0\rangle` state. - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The Identity gate acts trivially when controlled. The resources of this operation are - the original (un-controlled) operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated values are the counts. - """ - return {cls.resource_rep(): 1} - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - The Identity gate acts trivially when raised to a power. The resources of this - operation are the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated values are the counts. - """ - return {cls.resource_rep(): 1} - - -class ResourceGlobalPhase(qml.GlobalPhase, re.ResourceOperator): - r"""Resource class for the GlobalPhase gate. - - Args: - phi (TensorLike): the global phase - wires (Iterable[Any] or Any): unused argument - the operator is applied to all wires - id (str): custom label given to an operator instance, - can be useful for some applications where the instance has to be identified. - - Resources: - The GlobalPhase gate is treated as a free gate and thus it cannot be decomposed - further. Requesting the resources of this gate returns an empty dictionary. - - .. seealso:: :class:`~.GlobalPhase` - - """ - - @staticmethod - def _resource_decomp(*args, **kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The GlobalPhase gate is treated as a free gate and thus it cannot be decomposed - further. Requesting the resources of this gate returns an empty dictionary. - - Returns: - dict: empty dictionary - """ - return {} - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls, **kwargs) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @staticmethod - def adjoint_resource_decomp() -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of a global phase operator changes the sign of the phase, thus - the resources of the adjoint operation is the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated values are the counts. - """ - return {re.ResourceGlobalPhase.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are generated from the fact that a global phase controlled on a - single qubit is equivalent to a local phase shift on that control qubit. - - This idea can be generalized to a multi-qubit global phase by introducing one - 'clean' auxilliary qubit which gets reset at the end of the computation. In this - case, we sandwich the phase shift operation with two multi-controlled X gates. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated values are the counts. - """ - if num_ctrl_wires == 1: - gate_types = {re.ResourcePhaseShift.resource_rep(): 1} - - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 - - return gate_types - - ps = re.ResourcePhaseShift.resource_rep() - mcx = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - return {ps: 1, mcx: 2} - - @staticmethod - def pow_resource_decomp(z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a global phase produces a sum of global phases. - The resources simplify to just one total global phase operator. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated values are the counts. - """ - return {re.ResourceGlobalPhase.resource_rep(): 1} diff --git a/pennylane/labs/resource_estimation/ops/op_math/__init__.py b/pennylane/labs/resource_estimation/ops/op_math/__init__.py deleted file mode 100644 index 1bfa7b8dc0f..00000000000 --- a/pennylane/labs/resource_estimation/ops/op_math/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2024 Xanadu Quantum Technologies Inc. - -# 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. -r"""This module contains experimental resource estimation functionality.""" - -from .controlled_ops import * -from .symbolic import * diff --git a/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py b/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py deleted file mode 100644 index bf722bf5f50..00000000000 --- a/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py +++ /dev/null @@ -1,1812 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""Resource operators for controlled operations.""" -from collections import defaultdict -from typing import Dict - -import pennylane as qml -import pennylane.labs.resource_estimation as re - -# pylint: disable=arguments-differ,too-many-ancestors,too-many-arguments,too-many-positional-arguments - - -class ResourceCH(qml.CH, re.ResourceOperator): - r"""Resource class for the CH gate. - - Args: - wires (Sequence[int]): the wires the operation acts on - - Resources: - The resources are derived from the following identities (as presented in this - `blog post `_): - - .. math:: - - \begin{align} - \hat{H} &= \hat{R}_{y}(\frac{\pi}{4}) \cdot \hat{Z} \cdot \hat{R}_{y}(\frac{-\pi}{4}), \\ - \hat{Z} &= \hat{H} \cdot \hat{X} \cdot \hat{H}. - \end{align} - - Specifically, the resources are defined as two :class:`~.ResourceRY`, two - :class:`~.ResourceHadamard` and one :class:`~.ResourceCNOT` gates. - - .. seealso:: :class:`~.CH` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceCH.resources() - {Hadamard: 2, RY: 2, CNOT: 1} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are derived from the following identities (as presented in this - `blog post `_): - - .. math:: - - \begin{align} - \hat{H} &= \hat{R}_{y}(\frac{\pi}{4}) \cdot \hat{Z} \cdot \hat{R}_{y}(\frac{-\pi}{4}), \\ - \hat{Z} &= \hat{H} \cdot \hat{X} \cdot \hat{H}. - \end{align} - - Specifically, the resources are defined as two :class:`~.ResourceRY`, two - :class:`~.ResourceHadamard` and one :class:`~.ResourceCNOT` gates. - """ - gate_types = {} - - ry = re.ResourceRY.resource_rep() - h = re.ResourceHadamard.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - - gate_types[h] = 2 - gate_types[ry] = 2 - gate_types[cnot] = 1 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources - are computed according to the :code:`controlled_resource_decomp()` of the base - :class:`~.ResourceHadamard` class. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return { - re.ResourceControlled.resource_rep( - re.ResourceHadamard, {}, num_ctrl_wires + 1, num_ctrl_values, num_work_wires - ): 1 - } - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - This operation is self-inverse, thus when raised to even integer powers acts like - the identity operator and raised to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {re.ResourceIdentity.resource_rep(): 1} if z % 2 == 0 else {cls.resource_rep(): 1} - - -class ResourceCY(qml.CY, re.ResourceOperator): - r"""Resource class for the CY gate. - - Args: - wires (Sequence[int]): the wires the operation acts on - id (str): custom label given to an operator instance, - can be useful for some applications where the instance has to be identified. - - Resources: - The resources are derived from the following identity: - - .. math:: \hat{Y} = \hat{S} \cdot \hat{X} \cdot \hat{S}^{\dagger}. - - By replacing the :class:`~.ResourceX` gate with a :class:`~.ResourceCNOT` we - obtain the controlled decomposition. Specifically, the resources are defined as a - :class:`~.ResourceCNOT` gate conjugated by a pair of :class:`~.ResourceS` gates. - - .. seealso:: :class:`~.CY` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceCY.resources() - {CNOT: 1, S: 1, Adjoint(S): 1} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are derived from the following identity: - - .. math:: \hat{Y} = \hat{S} \cdot \hat{X} \cdot \hat{S}^{\dagger}. - - By replacing the :class:`~.ResourceX` gate with a :class:`~.ResourceCNOT` we - obtain the controlled decomposition. Specifically, the resources are defined as a - :class:`~.ResourceCNOT` gate conjugated by a pair of :class:`~.ResourceS` gates. - """ - gate_types = {} - - cnot = re.ResourceCNOT.resource_rep() - s = re.ResourceS.resource_rep() - s_dag = re.ResourceAdjoint.resource_rep(re.ResourceS, {}) - - gate_types[cnot] = 1 - gate_types[s] = 1 - gate_types[s_dag] = 1 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources - are computed according to the :code:`controlled_resource_decomp()` of the base - :class:`~.ResourceY` class. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return { - re.ResourceControlled.resource_rep( - re.ResourceY, {}, num_ctrl_wires + 1, num_ctrl_values, num_work_wires - ): 1 - } - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - This operation is self-inverse, thus when raised to even integer powers acts like - the identity operator and raised to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {re.ResourceIdentity.resource_rep(): 1} if z % 2 == 0 else {cls.resource_rep(): 1} - - -class ResourceCZ(qml.CZ, re.ResourceOperator): - r"""Resource class for the CZ gate. - - Args: - wires (Sequence[int]): the wires the operation acts on - - Resources: - The resources are derived from the following identity: - - .. math:: \hat{Z} = \hat{H} \cdot \hat{X} \cdot \hat{H}. - - By replacing the :class:`~.ResourceX` gate with a :class:`~.ResourceCNOT` we obtain - the controlled decomposition. Specifically, the resources are defined as a - :class:`~.ResourceCNOT` gate conjugated by a pair of :class:`~.ResourceHadamard` gates. - - .. seealso:: :class:`~.CZ` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceCZ.resources() - {CNOT: 1, Hadamard: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are derived from the following identity: - - .. math:: \hat{Z} = \hat{H} \cdot \hat{X} \cdot \hat{H}. - - By replacing the :class:`~.ResourceX` gate with a :class:`~.ResourceCNOT` we obtain - the controlled decomposition. Specifically, the resources are defined as a - :class:`~.ResourceCNOT` gate conjugated by a pair of :class:`~.ResourceHadamard` gates. - """ - gate_types = {} - - cnot = re.ResourceCNOT.resource_rep() - h = re.ResourceHadamard.resource_rep() - - gate_types[cnot] = 1 - gate_types[h] = 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources - are computed according to the :code:`controlled_resource_decomp()` of the base - :class:`~.ResourceZ` class. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires == 1 and num_ctrl_values == 0 and num_work_wires == 0: - return {re.ResourceCCZ.resource_rep(): 1} - - return { - re.ResourceControlled.resource_rep( - re.ResourceZ, {}, num_ctrl_wires + 1, num_ctrl_values, num_work_wires - ): 1 - } - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - This operation is self-inverse, thus when raised to even integer powers acts like - the identity operator and raised to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {re.ResourceIdentity.resource_rep(): 1} if z % 2 == 0 else {cls.resource_rep(): 1} - - -class ResourceCSWAP(qml.CSWAP, re.ResourceOperator): - r"""Resource class for the CSWAP gate. - - Resources: - The resources are taken from Figure 1d of `arXiv:2305.18128 `_. - - The circuit which applies the SWAP operation on wires (1, 2) and controlled on wire (0) is - defined as: - - .. code-block:: bash - - 0: ────╭●────┤ - 1: ─╭X─├●─╭X─┤ - 2: ─╰●─╰X─╰●─┤ - - .. seealso:: :class:`~.CSWAP` - - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are taken from Figure 1d of `arXiv:2305.18128 `_. - - The circuit which applies the SWAP operation on wires (1, 2) and controlled on wire (0) is - defined as: - - .. code-block:: bash - - 0: ────╭●────┤ - 1: ─╭X─├●─╭X─┤ - 2: ─╰●─╰X─╰●─┤ - """ - gate_types = {} - - tof = re.ResourceToffoli.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - - gate_types[tof] = 1 - gate_types[cnot] = 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources - are computed according to the :code:`controlled_resource_decomp()` of the base - :class:`~.ResourceSWAP` class. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return { - re.ResourceControlled.resource_rep( - re.ResourceSWAP, {}, num_ctrl_wires + 1, num_ctrl_values, num_work_wires - ): 1 - } - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - This operation is self-inverse, thus when raised to even integer powers acts like - the identity operator and raised to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {re.ResourceIdentity.resource_rep(): 1} if z % 2 == 0 else {cls.resource_rep(): 1} - - -class ResourceCCZ(qml.CCZ, re.ResourceOperator): - r"""Resource class for the CCZ gate. - - Args: - wires (Sequence[int]): the subsystem the gate acts on - - Resources: - The resources are derived from the following identity: - - .. math:: \hat{Z} = \hat{H} \cdot \hat{X} \cdot \hat{H}. - - By replacing the :class:`~.ResourceX` gate with a :class:`~.ResourceToffoli` we obtain - the controlled decomposition. Specifically, the resources are defined as a - :class:`~.ResourceToffoli` gate conjugated by a pair of :class:`~.ResourceHadamard` gates. - - .. seealso:: :class:`~.CCZ` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceCCZ.resources() - {Toffoli: 1, Hadamard: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are derived from the following identity: - - .. math:: \hat{Z} = \hat{H} \cdot \hat{X} \cdot \hat{H}. - - By replacing the :class:`~.ResourceX` gate with a :class:`~.ResourceToffoli` we obtain - the controlled decomposition. Specifically, the resources are defined as a - :class:`~.ResourceToffoli` gate conjugated by a pair of :class:`~.ResourceHadamard` gates. - """ - gate_types = {} - - toffoli = re.ResourceToffoli.resource_rep() - h = re.ResourceHadamard.resource_rep() - - gate_types[toffoli] = 1 - gate_types[h] = 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls, **kwargs): - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources - are computed according to the :code:`controlled_resource_decomp()` of the base - :class:`~.ResourceZ` class. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return { - re.ResourceControlled.resource_rep( - re.ResourceZ, {}, num_ctrl_wires + 2, num_ctrl_values, num_work_wires - ): 1 - } - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - This operation is self-inverse, thus when raised to even integer powers acts like - the identity operator and raised to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {re.ResourceIdentity.resource_rep(): 1} if z % 2 == 0 else {cls.resource_rep(): 1} - - -class ResourceCNOT(qml.CNOT, re.ResourceOperator): - r"""Resource class for the CNOT gate. - - Args: - wires (Sequence[int]): the wires the operation acts on - - Resources: - The CNOT gate is treated as a fundamental gate and thus it cannot be decomposed - further. Requesting the resources of this gate raises a :code:`ResourcesNotDefined` error. - - .. seealso:: :class:`~.CNOT` - - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The CNOT gate is treated as a fundamental gate and thus it cannot be decomposed - further. Requesting the resources of this gate raises a :code:`ResourcesNotDefined` error. - - Raises: - ResourcesNotDefined: This gate is fundamental, no further decomposition defined. - """ - raise re.ResourcesNotDefined - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @classmethod - def controlled_resource_decomp( - cls, num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are expressed as one general :class:`~.ResourceMultiControlledX` gate. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires == 1 and num_ctrl_values == 0 and num_work_wires == 0: - return {re.ResourceToffoli.resource_rep(): 1} - - return { - re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires + 1, num_ctrl_values, num_work_wires - ): 1 - } - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - This operation is self-inverse, thus when raised to even integer powers acts like - the identity operator and raised to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {re.ResourceIdentity.resource_rep(): 1} if z % 2 == 0 else {cls.resource_rep(): 1} - - -class ResourceToffoli(qml.Toffoli, re.ResourceOperator): - r"""Resource class for the Toffoli gate. - - Args: - wires (Sequence[int]): the subsystem the gate acts on - - Resources: - The resources are obtained from Figure 1 of `Jones 2012 `_. - - The circuit which applies the Toffoli gate on target wire 'target' with control wires - ('c1', 'c2') is defined as: - - .. code-block:: bash - - c1: ─╭●────╭X──T†────────╭X────╭●───────────────╭●─┤ - c2: ─│──╭X─│──╭●───T†─╭●─│──╭X─│────────────────╰Z─┤ - aux1: ─╰X─│──│──╰X───T──╰X─│──│──╰X────────────────║─┤ - aux2: ──H─╰●─╰●──T─────────╰●─╰●──H──S─╭●──H──┤↗├──║─┤ - target: ─────────────────────────────────╰X──────║───║─┤ - ╚═══╝ - - Specifically, the resources are defined as nine :class:`~.ResourceCNOT` gates, three - :class:`~.ResourceHadamard` gates, one :class:`~.ResourceCZ` gate, one :class:`~.ResourceS` - gate, two :class:`~.ResourceT` gates and two adjoint :class:`~.ResourceT` gates. - - .. seealso:: :class:`~.Toffoli` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceToffoli.resources() - {CNOT: 9, Hadamard: 3, S: 1, CZ: 1, T: 2, Adjoint(T): 2} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are obtained from Figure 1 of `Jones 2012 `_. - - The circuit which applies the Toffoli gate on target wire 'target' with control wires - ('c1', 'c2') is defined as: - - .. code-block:: bash - - c1: ─╭●────╭X──T†────────╭X────╭●───────────────╭●─┤ - c2: ─│──╭X─│──╭●───T†─╭●─│──╭X─│────────────────╰Z─┤ - aux1: ─╰X─│──│──╰X───T──╰X─│──│──╰X────────────────║─┤ - aux2: ──H─╰●─╰●──T─────────╰●─╰●──H──S─╭●──H──┤↗├──║─┤ - target: ─────────────────────────────────╰X──────║───║─┤ - ╚═══╝ - - Specifically, the resources are defined as nine :class:`~.ResourceCNOT` gates, three - :class:`~.ResourceHadamard` gates, one :class:`~.ResourceCZ` gate, one :class:`~.ResourceS` - gate, two :class:`~.ResourceT` gates and two adjoint :class:`~.ResourceT` gates. - """ - gate_types = {} - - cnot = re.ResourceCNOT.resource_rep() - t = re.ResourceT.resource_rep() - h = re.ResourceHadamard.resource_rep() - s = re.ResourceS.resource_rep() - cz = re.ResourceCZ.resource_rep() - t_dag = re.ResourceAdjoint.resource_rep(re.ResourceT, {}) - - gate_types[cnot] = 9 - gate_types[h] = 3 - gate_types[s] = 1 - gate_types[cz] = 1 - gate_types[t] = 2 - gate_types[t_dag] = 2 - - return gate_types - - @staticmethod - def textbook_resource_decomp() -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are taken from Figure 4.9 of `Nielsen, M. A., & Chuang, I. L. (2010) `_. - - The circuit is defined as: - - .. code-block:: bash - - 0: ───────────╭●───────────╭●────╭●──T──╭●─┤ - 1: ────╭●─────│─────╭●─────│───T─╰X──T†─╰X─┤ - 2: ──H─╰X──T†─╰X──T─╰X──T†─╰X──T──H────────┤ - - Specifically, the resources are defined as six :class:`~.ResourceCNOT` gates, two - :class:`~.ResourceHadamard` gates, four :class:`~.ResourceT` gates and three adjoint - :class:`~.ResourceT` gates. - """ - gate_types = {} - - cnot = re.ResourceCNOT.resource_rep() - t = re.ResourceT.resource_rep() - h = re.ResourceHadamard.resource_rep() - t_dag = re.ResourceAdjoint.resource_rep(re.ResourceT, {}) - - gate_types[cnot] = 6 - gate_types[h] = 2 - gate_types[t] = 4 - gate_types[t_dag] = 3 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are expressed as one general :class:`~.ResourceMultiControlledX` gate. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return { - re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires + 2, num_ctrl_values, num_work_wires - ): 1 - } - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - This operation is self-inverse, thus when raised to even integer powers acts like - the identity operator and raised to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {re.ResourceIdentity.resource_rep(): 1} if z % 2 == 0 else {cls.resource_rep(): 1} - - -class ResourceMultiControlledX(qml.MultiControlledX, re.ResourceOperator): - r"""Resource class for the MultiControlledX gate. - - Args: - wires (Union[Wires, Sequence[int], or int]): control wire(s) followed by a single target wire (the last entry of ``wires``) where - the operation acts on - control_values (Union[bool, list[bool], int, list[int]]): The value(s) the control wire(s) - should take. Integers other than 0 or 1 will be treated as ``int(bool(x))``. - work_wires (Union[Wires, Sequence[int], or int]): optional work wires used to decompose - the operation into a series of :class:`~.Toffoli` gates - - Resource Parameters: - * num_ctrl_wires (int): the number of qubits the operation is controlled on - * num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - * num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are obtained from Table 3 of `Claudon, B., Zylberman, J., Feniou, C. et al. - `_. Specifically, the - resources are defined as the following rules: - - * If there is only one control qubit, treat the resources as a :class:`~.ResourceCNOT` gate. - - * If there are two control qubits, treat the resources as a :class:`~.ResourceToffoli` gate. - - * If there are three control qubits, the resources are two :class:`~.ResourceCNOT` gates and one :class:`~.ResourceToffoli` gate. - - * If there are more than three control qubits (:math:`n`), the resources are defined as :math:`36n - 111` :class:`~.ResourceCNOT` gates. - - .. seealso:: :class:`~.MultiControlledX` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceMultiControlledX.resources(num_ctrl_wires=5, num_ctrl_values=2, num_work_wires=3) - {X: 4, CNOT: 69} - """ - - @staticmethod - def _resource_decomp( - num_ctrl_wires, - num_ctrl_values, - num_work_wires, - **kwargs, # pylint: disable=unused-argument - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are obtained from Table 3 of `Claudon, B., Zylberman, J., Feniou, C. et al. - `_. Specifically, the - resources are defined as the following rules: - - * If there are no control qubits, treat the operation as a :class:`~.ResourceX` gate. - - * If there is only one control qubit, treat the resources as a :class:`~.ResourceCNOT` gate. - - * If there are two control qubits, treat the resources as a :class:`~.ResourceToffoli` gate. - - * If there are three control qubits, the resources are two :class:`~.ResourceCNOT` gates and - one :class:`~.ResourceToffoli` gate. - - * If there are more than three control qubits (:math:`n`), the resources are defined as - :math:`36n - 111` :class:`~.ResourceCNOT` gates. - """ - gate_types = defaultdict(int) - - x = re.ResourceX.resource_rep() - if num_ctrl_values: - gate_types[x] = num_ctrl_values * 2 - - if num_ctrl_wires == 0: - gate_types[x] += 1 - return gate_types - - cnot = re.ResourceCNOT.resource_rep() - if num_ctrl_wires == 1: - gate_types[cnot] = 1 - return gate_types - - toffoli = re.ResourceToffoli.resource_rep() - if num_ctrl_wires == 2: - gate_types[toffoli] = 1 - return gate_types - - if num_ctrl_wires == 3: - gate_types[cnot] = 2 - gate_types[toffoli] = 1 - return gate_types - - gate_types[cnot] = 36 * num_ctrl_wires - 111 - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_ctrl_wires (int): the number of qubits the operation is controlled on - * num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - * num_work_wires (int): the number of additional qubits that can be used for decomposition - """ - num_control = len(self.hyperparameters["control_wires"]) - num_work_wires = len(self.hyperparameters["work_wires"]) - - num_control_values = len([val for val in self.hyperparameters["control_values"] if not val]) - - return { - "num_ctrl_wires": num_control, - "num_ctrl_values": num_control_values, - "num_work_wires": num_work_wires, - } - - @classmethod - def resource_rep( - cls, num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp( - cls, - { - "num_ctrl_wires": num_ctrl_wires, - "num_ctrl_values": num_ctrl_values, - "num_work_wires": num_work_wires, - }, - ) - - @classmethod - def adjoint_resource_decomp( - cls, num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(num_ctrl_wires, num_ctrl_values, num_work_wires): 1} - - @classmethod - def controlled_resource_decomp( - cls, - outer_num_ctrl_wires, - outer_num_ctrl_values, - outer_num_work_wires, - num_ctrl_wires, - num_ctrl_values, - num_work_wires, - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - outer_num_ctrl_wires (int): The number of control qubits to further control the base - controlled operation upon. - outer_num_ctrl_values (int): The subset of those control qubits, which further control - the base controlled operation, which are controlled when in the :math:`|0\rangle` state. - outer_num_work_wires (int): the number of additional qubits that can be used in the - decomposition for the further controlled, base control oepration. - num_ctrl_wires (int): the number of control qubits of the operation - num_ctrl_values (int): The subset of control qubits of the operation, that are controlled - when in the :math:`|0\rangle` state. - num_work_wires (int): The number of additional qubits that can be used for the - decomposition of the operation. - - Resources: - The resources are derived by combining the control qubits, control-values and - work qubits into a single instance of :class:`~.ResourceMultiControlledX` gate, controlled - on the whole set of control-qubits. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return cls.resources( - outer_num_ctrl_wires + num_ctrl_wires, - outer_num_ctrl_values + num_ctrl_values, - outer_num_work_wires + num_work_wires, - ) - - @classmethod - def pow_resource_decomp( - cls, z, num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - This operation is self-inverse, thus when raised to even integer powers acts like - the identity operator and raised to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return ( - {} - if z % 2 == 0 - else {cls.resource_rep(num_ctrl_wires, num_ctrl_values, num_work_wires): 1} - ) - - -class ResourceCRX(qml.CRX, re.ResourceOperator): - r"""Resource class for the CRX gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int]): the wire the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay - `_. In combination with the following identity: - - .. math:: \hat{RX} = \hat{H} \cdot \hat{RZ} \cdot \hat{H}, - - we can express the :code:`CRX` gate as a :code:`CRZ` gate conjugated by :code:`Hadamard` - gates. The expression for controlled-RZ gates is used as defined in the reference above. - Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates, two - :class:`~.ResourceHadamard` gates and two :class:`~.ResourceRZ` gates. - - .. seealso:: :class:`~.CRX` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceCRX.resources() - {CNOT: 2, RZ: 2, Hadamard: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay - `_. In combination with the following identity: - - .. math:: \hat{RX} = \hat{H} \cdot \hat{RZ} \cdot \hat{H}, - - we can express the :code:`CRX` gate as a :code:`CRZ` gate conjugated by :code:`Hadamard` - gates. The expression for controlled-RZ gates is used as defined in the reference above. - Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates, two - :class:`~.ResourceHadamard` gates and two :class:`~.ResourceRZ` gates. - """ - gate_types = {} - - h = re.ResourceHadamard.resource_rep() - rz = re.ResourceRZ.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - - gate_types[cnot] = 2 - gate_types[rz] = 2 - gate_types[h] = 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of a single qubit rotation changes the sign of the rotation angle, - thus the resources of the adjoint operation result in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources - are computed according to the :code:`controlled_resource_decomp()` of the base - :class:`~.ResourceRX` class. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return { - re.ResourceControlled.resource_rep( - re.ResourceRX, {}, num_ctrl_wires + 1, num_ctrl_values, num_work_wires - ): 1 - } - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a single qubit rotation produces a sum of rotations. - The resources simplify to just one total single qubit rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - -class ResourceCRY(qml.CRY, re.ResourceOperator): - r"""Resource class for the CRY gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int]): the wire the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay - `_. In combination with the following identity: - - .. math:: \hat{RY}(\theta) = \hat{X} \cdot \hat{RY}(- \theta) \cdot \hat{X}. - - By replacing the :class:`~.ResourceX` gates with :class:`~.ResourceCNOT` gates, we obtain a - controlled-version of this identity. Thus we are able to constructively or destructively - interfere the gates based on the value of the control qubit. Specifically, the resources are - defined as two :class:`~.ResourceCNOT` gates and two :class:`~.ResourceRY` gates. - - .. seealso:: :class:`~.CRY` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceCRY.resources() - {CNOT: 2, RY: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay - `_. In combination with the following identity: - - .. math:: \hat{RY}(\theta) = \hat{X} \cdot \hat{RY}(- \theta) \cdot \hat{X}. - - By replacing the :code:`X` gates with :code:`CNOT` gates, we obtain a controlled-version of this - identity. Thus we are able to constructively or destructively interfere the gates based on the value - of the control qubit. Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates - and two :class:`~.ResourceRY` gates. - """ - gate_types = {} - - cnot = re.ResourceCNOT.resource_rep() - ry = re.ResourceRY.resource_rep() - - gate_types[cnot] = 2 - gate_types[ry] = 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of a single qubit rotation changes the sign of the rotation angle, - thus the resources of the adjoint operation result in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources - are computed according to the :code:`controlled_resource_decomp()` of the base - :class:`~.ResourceRY` class. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return { - re.ResourceControlled.resource_rep( - re.ResourceRY, {}, num_ctrl_wires + 1, num_ctrl_values, num_work_wires - ): 1 - } - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a single qubit rotation produces a sum of rotations. - The resources simplify to just one total single qubit rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - -class ResourceCRZ(qml.CRZ, re.ResourceOperator): - r"""Resource class for the CRZ gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int]): the wire the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay - `_. In combination with the following identity: - - .. math:: \hat{RZ}(\theta) = \hat{X} \cdot \hat{RZ}(- \theta) \cdot \hat{X}. - - By replacing the :code:`X` gates with :code:`CNOT` gates, we obtain a controlled-version of this - identity. Thus we are able to constructively or destructively interfere the gates based on the value - of the control qubit. Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates - and two :class:`~.ResourceRZ` gates. - - .. seealso:: :class:`~.CRZ` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceCRZ.resources() - {CNOT: 2, RZ: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay - `_. In combination with the following identity: - - .. math:: \hat{RZ}(\theta) = \hat{X} \cdot \hat{RZ}(- \theta) \cdot \hat{X}. - - By replacing the :code:`X` gates with :code:`CNOT` gates, we obtain a controlled-version of this - identity. Thus we are able to constructively or destructively interfere the gates based on the value - of the control qubit. Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates - and two :class:`~.ResourceRZ` gates. - """ - gate_types = {} - - cnot = re.ResourceCNOT.resource_rep() - rz = re.ResourceRZ.resource_rep() - - gate_types[cnot] = 2 - gate_types[rz] = 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of a single qubit rotation changes the sign of the rotation angle, - thus the resources of the adjoint operation result in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources - are computed according to the :code:`controlled_resource_decomp()` of the base - :class:`~.ResourceRZ` class. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return { - re.ResourceControlled.resource_rep( - re.ResourceRZ, {}, num_ctrl_wires + 1, num_ctrl_values, num_work_wires - ): 1 - } - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a single qubit rotation produces a sum of rotations. - The resources simplify to just one total single qubit rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - -class ResourceCRot(qml.CRot, re.ResourceOperator): - r"""Resource class for the CRot gate. - - Args: - phi (float): rotation angle :math:`\phi` - theta (float): rotation angle :math:`\theta` - omega (float): rotation angle :math:`\omega` - wires (Sequence[int]): the wire the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay - `_. In combination with the following identity: - - .. math:: - - \begin{align} - \hat{RZ}(\theta) = \hat{X} \cdot \hat{RZ}(- \theta) \cdot \hat{X}, \\ - \hat{RY}(\theta) = \hat{X} \cdot \hat{RY}(- \theta) \cdot \hat{X}. - \end{align} - - This identity is applied along with some clever choices for the angle values to combine rotation; - the final circuit takes the form: - - .. code-block:: bash - - ctrl: ─────╭●─────────╭●─────────┤ - trgt: ──RZ─╰X──RZ──RY─╰X──RY──RZ─┤ - - .. seealso:: :class:`~.CRot` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceCRot.resources() - {CNOT: 2, RZ: 3, RY: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay - `_. In combination with the following identity: - - .. math:: - - \begin{align} - \hat{RZ}(\theta) = \hat{X} \cdot \hat{RZ}(- \theta) \cdot \hat{X}, \\ - \hat{RY}(\theta) = \hat{X} \cdot \hat{RY}(- \theta) \cdot \hat{X}. - \end{align} - - This identity is applied along with some clever choices for the angle values to combine rotation; - the final circuit takes the form: - - .. code-block:: bash - - ctrl: ─────╭●─────────╭●─────────┤ - trgt: ──RZ─╰X──RZ──RY─╰X──RY──RZ─┤ - - """ - gate_types = {} - - cnot = re.ResourceCNOT.resource_rep() - rz = re.ResourceRZ.resource_rep() - ry = re.ResourceRY.resource_rep() - - gate_types[cnot] = 2 - gate_types[rz] = 3 - gate_types[ry] = 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of a general rotation flips the sign of the rotation angle, - thus the resources of the adjoint operation result in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources - are computed according to the :code:`controlled_resource_decomp()` of the base - :class:`~.ResourceRot` class. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return { - re.ResourceControlled.resource_rep( - re.ResourceRot, {}, num_ctrl_wires + 1, num_ctrl_values, num_work_wires - ): 1 - } - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a general single qubit rotation produces a sum of rotations. - The resources simplify to just one total single qubit rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - -class ResourceControlledPhaseShift(qml.ControlledPhaseShift, re.ResourceOperator): - r"""Resource class for the ControlledPhaseShift gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int]): the wire the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are derived using the fact that a :class:`~.ResourcePhaseShift` gate is - identical to the :class:`~.ResourceRZ` gate up to some global phase. Furthermore, a controlled - global phase simplifies to a :class:`~.ResourcePhaseShift` gate. This gives rise to the - following identity: - - .. math:: CR_\phi(\phi) = (R_\phi(\phi/2) \otimes I) \cdot CNOT \cdot (I \otimes R_\phi(-\phi/2)) \cdot CNOT \cdot (I \otimes R_\phi(\phi/2)) - - Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates and three - :class:`~.ResourceRZ` gates. - - .. seealso:: :class:`~.ControlledPhaseShift` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceControlledPhaseShift.resources() - {CNOT: 2, RZ: 3} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - gate_types = {} - - cnot = re.ResourceCNOT.resource_rep() - rz = re.ResourceRZ.resource_rep() - - gate_types[cnot] = 2 - gate_types[rz] = 3 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of a phase shift just flips the sign of the phase angle, - thus the resources of the adjoint operation result in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources - are computed according to the :code:`controlled_resource_decomp()` of the base - :class:`~.ResourcePhaseShift` class. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return { - re.ResourceControlled.resource_rep( - re.ResourcePhaseShift, {}, num_ctrl_wires + 1, num_ctrl_values, num_work_wires - ): 1 - } - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a phase shift produces a sum of shifts. - The resources simplify to just one total phase shift operator. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} diff --git a/pennylane/labs/resource_estimation/ops/op_math/symbolic.py b/pennylane/labs/resource_estimation/ops/op_math/symbolic.py deleted file mode 100644 index a1f7d246375..00000000000 --- a/pennylane/labs/resource_estimation/ops/op_math/symbolic.py +++ /dev/null @@ -1,1251 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""Resource operators for symbolic operations.""" -from collections import defaultdict -from typing import Dict - -import pennylane.labs.resource_estimation as re -from pennylane import math -from pennylane.labs.resource_estimation.resource_container import _scale_dict -from pennylane.operation import Operation -from pennylane.ops.op_math.adjoint import AdjointOperation -from pennylane.ops.op_math.controlled import ControlledOp -from pennylane.ops.op_math.exp import Exp -from pennylane.ops.op_math.pow import PowOperation -from pennylane.ops.op_math.prod import Prod -from pennylane.pauli import PauliSentence - -# pylint: disable=too-many-ancestors,arguments-differ,protected-access,too-many-arguments,too-many-positional-arguments - - -class ResourceAdjoint(AdjointOperation, re.ResourceOperator): - r"""Resource class for the symbolic AdjointOperation. - - A symbolic class used to represent the adjoint of some base operation. - - Args: - base (~.operation.Operator): The operator that we want the adjoint of. - - Resource Parameters: - * base_class (Type[~.ResourceOperator]): the class type of the base operator that we want the adjoint of - * base_params (dict): the resource parameters required to extract the cost of the base operator - - Resources: - This symbolic operation represents the adjoint of some base operation. The resources are - determined as follows. If the base operation class :code:`base_class` implements the - :code:`.adjoint_resource_decomp()` method, then the resources are obtained from this. - - Otherwise, the adjoint resources are given as the adjoint of each operation in the - base operation's resources (via :code:`.resources()`). - - .. seealso:: :class:`~.ops.op_math.adjoint.AdjointOperation` - - **Example** - - The adjoint operation can be constructed like this: - - >>> qft = re.ResourceQFT(wires=range(3)) - >>> adjoint_qft = re.ResourceAdjoint(qft) - >>> adjoint_qft.resources(**adjoint_qft.resource_params) - defaultdict(, {Adjoint(Hadamard): 3, Adjoint(SWAP): 1, - Adjoint(ControlledPhaseShift): 3}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourceAdjoint.resources( - ... base_class = re.ResourceQFT, - ... base_params = {"num_wires": 3}, - ... ) - defaultdict(, {Adjoint(Hadamard): 3, Adjoint(SWAP): 1, - Adjoint(ControlledPhaseShift): 3}) - - .. details:: - :title: Usage Details - - We can configure the resources for the adjoint of a base operation by modifying - its :code:`.adjoint_resource_decomp(**resource_params)` method. Consider for example this - custom PauliZ class, where the adjoint resources are not defined (this is the default - for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def adjoint_resource_decomp(cls): - raise re.ResourcesNotDefined - - When this method is not defined, the adjoint resources are computed by taking the - adjoint of the resources of the operation. - - >>> CustomZ.resources() - {S: 2} - >>> re.ResourceAdjoint.resources(CustomZ, {}) - defaultdict(, {Adjoint(S): 2}) - - We can update the adjoint resources with the observation that the PauliZ gate is self-adjoint, - so the resources should just be the same as the base operation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def adjoint_resource_decomp(cls): - return {cls.resource_rep(): 1} - - >>> re.ResourceAdjoint.resources(CustomZ, {}) - {CustomZ: 1} - - """ - - @classmethod - def _resource_decomp( - cls, base_class, base_params, **kwargs - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (Type[~.ResourceOperator]): the class type of the base operator that we want the adjoint of - base_params (dict): the resource parameters required to extract the cost of the base operator - - Resources: - This symbolic operation represents the adjoint of some base operation. The resources are - determined as follows. If the base operation class :code:`base_class` implements the - :code:`.adjoint_resource_decomp()` method, then the resources are obtained from this. - - Otherwise, the adjoint resources are given as the adjoint of each operation in the - base operation's resources (via :code:`.resources()`). - - **Example** - - The adjoint operation can be constructed like this: - - >>> qft = re.ResourceQFT(wires=range(3)) - >>> adjoint_qft = re.ResourceAdjoint(qft) - >>> adjoint_qft.resources(**adjoint_qft.resource_params) - defaultdict(, {Adjoint(Hadamard): 3, Adjoint(SWAP): 1, - Adjoint(ControlledPhaseShift): 3}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourceAdjoint.resources( - ... base_class = re.ResourceQFT, - ... base_params = {"num_wires": 3}, - ... ) - defaultdict(, {Adjoint(Hadamard): 3, Adjoint(SWAP): 1, - Adjoint(ControlledPhaseShift): 3}) - - .. details:: - :title: Usage Details - - We can configure the resources for the adjoint of a base operation by modifying - its :code:`.adjoint_resource_decomp(**resource_params)` method. Consider for example this - custom PauliZ class, where the adjoint resources are not defined (this is the default - for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def adjoint_resource_decomp(cls): - raise re.ResourcesNotDefined - - When this method is not defined, the adjoint resources are computed by taking the - adjoint of the resources of the operation. - - >>> CustomZ.resources() - {S: 2} - >>> re.ResourceAdjoint.resources(CustomZ, {}) - defaultdict(, {Adjoint(S): 2}) - - We can update the adjoint resources with the observation that the PauliZ gate is self-adjoint, - so the resources should just be the same as the base operation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def adjoint_resource_decomp(cls): - return {cls.resource_rep(): 1} - - >>> re.ResourceAdjoint.resources(CustomZ, {}) - {CustomZ: 1} - """ - try: - return base_class.adjoint_resource_decomp(**base_params) - except re.ResourcesNotDefined: - gate_types = defaultdict(int) - decomp = base_class.resources(**base_params, **kwargs) - for gate, count in decomp.items(): - rep = cls.resource_rep(gate.op_type, gate.params) - gate_types[rep] = count - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (Type[~.ResourceOperator]): the class type of the base operator that we want the adjoint of - * base_params (dict): the resource parameters required to extract the cost of the base operator - - """ - return {"base_class": type(self.base), "base_params": self.base.resource_params} - - @classmethod - def resource_rep(cls, base_class, base_params) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (Type[~.ResourceOperator]): the class type of the base operator that we want the adjoint of - base_params (dict): the resource parameters required to extract the cost of the base operator - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp(cls, {"base_class": base_class, "base_params": base_params}) - - @staticmethod - def adjoint_resource_decomp(base_class, base_params) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Args: - base_class (Type[~.ResourceOperator]): the class type of the base operator that we want the adjoint of - base_params (dict): the resource parameters required to extract the cost of the base operator - - Resources: - The adjoint of an adjointed operation is just the original operation. The resources - are given as one instance of the base operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {base_class.resource_rep(**base_params): 1} - - @staticmethod - def tracking_name(base_class, base_params) -> str: - r"""Returns the tracking name built with the operator's parameters.""" - base_name = base_class.tracking_name(**base_params) - return f"Adjoint({base_name})" - - -class ResourceControlled(ControlledOp, re.ResourceOperator): - r"""Resource class for the symbolic ControlledOp. - - A symbolic class used to represent the application of some base operation controlled on the state - of some control qubits. - - Args: - base (~.operation.Operator): the operator that is controlled - control_wires (Any): The wires to control on. - control_values (Iterable[Bool]): The values to control on. Must be the same - length as ``control_wires``. Defaults to ``True`` for all control wires. - Provided values are converted to `Bool` internally. - work_wires (Any): Any auxiliary wires that can be used in the decomposition - - Resource Parameters: - * base_class (Type[~.ResourceOperator]): the class type of the base operator to be controlled - * base_params (dict): the resource parameters required to extract the cost of the base operator - * num_ctrl_wires (int): the number of qubits the operation is controlled on - * num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - * num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are determined as follows. If the base operation class :code:`base_class` - implements the :code:`.controlled_resource_decomp()` method, then the resources are obtained - directly from this. - - Otherwise, the controlled resources are given in two steps. Firstly, any control qubits which - should be triggered when in the :math:`|0\rangle` state, are flipped. This corresponds to an additional - cost of two :class:`~.ResourceX` gates per :code:`num_ctrl_values`. Secondly, the base operation - resources are extracted (via :code:`.resources()`) and we add to the cost the controlled - variant of each operation in the resources. - - .. seealso:: :class:`~.ops.op_math.controlled.ControlledOp` - - **Example** - - The controlled operation can be constructed like this: - - >>> qft = re.ResourceQFT(wires=range(3)) - >>> controlled_qft = re.ResourceControlled( - ... qft, control_wires=['c0', 'c1', 'c2'], control_values=[1, 1, 1], work_wires=['w1', 'w2'], - ... ) - >>> controlled_qft.resources(**controlled_qft.resource_params) - defaultdict(, {C(Hadamard,3,0,2): 3, C(SWAP,3,0,2): 1, C(ControlledPhaseShift,3,0,2): 3}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourceControlled.resources( - ... base_class = re.ResourceQFT, - ... base_params = {"num_wires": 3}, - ... num_ctrl_wires = 3, - ... num_ctrl_values = 0, - ... num_work_wires = 2, - ... ) - defaultdict(, {C(Hadamard,3,0,2): 3, C(SWAP,3,0,2): 1, C(ControlledPhaseShift,3,0,2): 3}) - - .. details:: - :title: Usage Details - - We can configure the resources for the controlled of a base operation by modifying - its :code:`.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires, - **resource_params)` method. Consider for example this custom PauliZ class, where the - controlled resources are not defined (this is the default for a general :class:`~.ResourceOperator`). - - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def controlled_resource_decomp(cls, num_ctrl_wires, num_ctrl_values, num_work_wires): - raise re.ResourcesNotDefined - - When this method is not defined, the controlled resources are computed by taking the - controlled of each operation in the resources of the base operation. - - >>> CustomZ.resources() - {S: 2} - >>> re.ResourceControlled.resources(CustomZ, {}, num_ctrl_wires=1, num_ctrl_values=0, num_work_wires=0) - defaultdict(, {C(S,2,0,3): 2}) - - We can update the controlled resources with the observation that the PauliZ gate when controlled - on a single wire is equivalent to :math:`\hat{CZ} = \hat{H} \cdot \hat{CNOT} \cdot \hat{H}`. - so we can modify the base operation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def controlled_resource_decomp(cls, num_ctrl_wires, num_ctrl_values, num_work_wires): - if num_ctrl_wires == 1 and num_ctrl_values == 0: - return { - re.ResourceHadamard.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 1, - } - raise re.ResourcesNotDefined - - >>> re.ResourceControlled.resources(CustomZ, {}, num_ctrl_wires=1, num_ctrl_values=0, num_work_wires=0) - {Hadamard: 2, CNOT: 1} - - """ - - @classmethod - def _resource_decomp( - cls, base_class, base_params, num_ctrl_wires, num_ctrl_values, num_work_wires, **kwargs - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (Type[~.ResourceOperator]): the class type of the base operator to be controlled - base_params (dict): the resource parameters required to extract the cost of the base operator - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are determined as follows. If the base operation class :code:`base_class` - implements the :code:`.controlled_resource_decomp()` method, then the resources are obtained - directly from this. - - Otherwise, the controlled resources are given in two steps. Firstly, any control qubits which - should be triggered when in the :math:`|0\rangle` state, are flipped. This corresponds to an additional - cost of two :class:`~.ResourceX` gates per :code:`num_ctrl_values`. Secondly, the base operation - resources are extracted (via :code:`.resources()`) and we add to the cost the controlled - variant of each operation in the resources. - - .. seealso:: :class:`~.ops.op_math.controlled.ControlledOp` - - **Example** - - The controlled operation can be constructed like this: - - >>> qft = re.ResourceQFT(wires=range(3)) - >>> controlled_qft = re.ResourceControlled( - ... qft, control_wires=['c0', 'c1', 'c2'], control_values=[1, 1, 1], work_wires=['w1', 'w2'], - ... ) - >>> controlled_qft.resources(**controlled_qft.resource_params) - defaultdict(, {C(Hadamard,3,0,2): 3, C(SWAP,3,0,2): 1, C(ControlledPhaseShift,3,0,2): 3}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourceControlled.resources( - ... base_class = re.ResourceQFT, - ... base_params = {"num_wires": 3}, - ... num_ctrl_wires = 3, - ... num_ctrl_values = 0, - ... num_work_wires = 2, - ... ) - defaultdict(, {C(Hadamard,3,0,2): 3, C(SWAP,3,0,2): 1, C(ControlledPhaseShift,3,0,2): 3}) - - .. details:: - :title: Usage Details - - We can configure the resources for the controlled of a base operation by modifying - its :code:`.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires, - **resource_params)` method. Consider for example this custom PauliZ class, where the - controlled resources are not defined (this is the default for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def controlled_resource_decomp(cls, num_ctrl_wires, num_ctrl_values, num_work_wires): - raise re.ResourcesNotDefined - - When this method is not defined, the controlled resources are computed by taking the - controlled of each operation in the resources of the base operation. - - >>> CustomZ.resources() - {S: 2} - >>> re.ResourceControlled.resources(CustomZ, {}, num_ctrl_wires=1, num_ctrl_values=0, num_work_wires=0) - defaultdict(, {C(S,2,0,3): 2}) - - We can update the controlled resources with the observation that the PauliZ gate when controlled - on a single wire is equivalent to :math:`\hat{CZ} = \hat{H} \cdot \hat{CNOT} \cdot \hat{H}`. - so we can modify the base operation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def controlled_resource_decomp(cls, num_ctrl_wires, num_ctrl_values, num_work_wires): - if num_ctrl_wires == 1 and num_ctrl_values == 0: - return { - re.ResourceHadamard.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 1, - } - raise re.ResourcesNotDefined - - >>> re.ResourceControlled.resources(CustomZ, {}, num_ctrl_wires=1, num_ctrl_values=0, num_work_wires=0) - {Hadamard: 2, CNOT: 1} - - """ - try: - return base_class.controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires, **base_params - ) - except re.ResourcesNotDefined: - pass - - gate_types = defaultdict(int) - - if num_ctrl_values == 0: - decomp = base_class.resources(**base_params, **kwargs) - for gate, count in decomp.items(): - rep = cls.resource_rep(gate.op_type, gate.params, num_ctrl_wires, 0, num_work_wires) - gate_types[rep] = count - - return gate_types - - no_control = cls.resource_rep(base_class, base_params, num_ctrl_wires, 0, num_work_wires) - x = re.ResourceX.resource_rep() - gate_types[no_control] = 1 - gate_types[x] = 2 * num_ctrl_values - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (Type[~.ResourceOperator]): the class type of the base operator to be controlled - * base_params (dict): the resource parameters required to extract the cost of the base operator - * num_ctrl_wires (int): the number of qubits the operation is controlled on - * num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - * num_work_wires (int): the number of additional qubits that can be used for decomposition - """ - return { - "base_class": type(self.base), - "base_params": self.base.resource_params, - "num_ctrl_wires": len(self.control_wires), - "num_ctrl_values": len([val for val in self.control_values if not val]), - "num_work_wires": len(self.work_wires), - } - - @classmethod - def resource_rep( - cls, base_class, base_params, num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (Type[~.ResourceOperator]): the class type of the base operator to be controlled - base_params (dict): the resource parameters required to extract the cost of the base operator - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp( - cls, - { - "base_class": base_class, - "base_params": base_params, - "num_ctrl_wires": num_ctrl_wires, - "num_ctrl_values": num_ctrl_values, - "num_work_wires": num_work_wires, - }, - ) - - @classmethod - def controlled_resource_decomp( - cls, - outer_num_ctrl_wires, - outer_num_ctrl_values, - outer_num_work_wires, - base_class, - base_params, - num_ctrl_wires, - num_ctrl_values, - num_work_wires, - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - outer_num_ctrl_wires (int): The number of control qubits to further control the base - controlled operation upon. - outer_num_ctrl_values (int): The subset of those control qubits, which further control - the base controlled operation, which are controlled when in the :math:`|0\rangle` state. - outer_num_work_wires (int): the number of additional qubits that can be used in the - decomposition for the further controlled, base control oepration. - base_class (Type[~.ResourceOperator]): the class type of the base operator to be controlled - base_params (dict): the resource parameters required to extract the cost of the base operator - num_ctrl_wires (int): the number of control qubits of the operation - num_ctrl_values (int): The subset of control qubits of the operation, that are controlled - when in the :math:`|0\rangle` state. - num_work_wires (int): The number of additional qubits that can be used for the - decomposition of the operation. - - Resources: - The resources are derived by simply combining the control qubits, control-values and - work qubits into a single instance of :class:`~.ResourceControlled` gate, controlled - on the whole set of control-qubits. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return { - cls.resource_rep( - base_class, - base_params, - outer_num_ctrl_wires + num_ctrl_wires, - outer_num_ctrl_values + num_ctrl_values, - outer_num_work_wires + num_work_wires, - ): 1 - } - - @staticmethod - def tracking_name(base_class, base_params, num_ctrl_wires, num_ctrl_values, num_work_wires): - r"""Returns the tracking name built with the operator's parameters.""" - base_name = base_class.tracking_name(**base_params) - return f"C({base_name},{num_ctrl_wires},{num_ctrl_values},{num_work_wires})" - - -class ResourcePow(PowOperation, re.ResourceOperator): - r"""Resource class for the symbolic Pow operation. - - A symbolic class used to represent some base operation raised to a power. - - Args: - base (~.operation.Operator): the operator to be raised to a power - z (float): the exponent (default value is 1) - - Resource Parameters: - * base_class (Type[~.ResourceOperator]): The class type of the base operator to be raised to some power. - * base_params (dict): the resource parameters required to extract the cost of the base operator - * z (int): the power that the operator is being raised to - - Resources: - The resources are determined as follows. If the power :math:`z = 0`, then we have the identitiy - gate and we have no resources. If the base operation class :code:`base_class` implements the - :code:`.pow_resource_decomp()` method, then the resources are obtained from this. Otherwise, - the resources of the operation raised to the power :math:`z` are given by extracting the base - operation's resources (via :code:`.resources()`) and raising each operation to the same power. - - .. seealso:: :class:`~.ops.op_math.pow.PowOperation` - - **Example** - - The operation raised to a power :math:`z` can be constructed like this: - - >>> qft = re.ResourceQFT(wires=range(3)) - >>> pow_qft = re.ResourcePow(qft, 2) - >>> pow_qft.resources(**pow_qft.resource_params) - defaultdict(, {Pow(Hadamard, 2): 3, Pow(SWAP, 2): 1, Pow(ControlledPhaseShift, 2): 3}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourcePow.resources( - ... base_class = re.ResourceQFT, - ... base_params = {"num_wires": 3}, - ... z = 2, - ... ) - defaultdict(, {Pow(Hadamard, 2): 3, Pow(SWAP, 2): 1, Pow(ControlledPhaseShift, 2): 3}) - - .. details:: - :title: Usage Details - - We can configure the resources for the power of a base operation by modifying - its :code:`.pow_resource_decomp(**resource_params, z)` method. Consider for example this - custom PauliZ class, where the pow-resources are not defined (this is the default - for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def pow_resource_decomp(cls, z): - raise re.ResourcesNotDefined - - When this method is not defined, the resources are computed by taking the power of - each operation in the resources of the base operation. - - >>> CustomZ.resources() - {S: 2} - >>> re.ResourcePow.resources(CustomZ, {}, z=2) - defaultdict(, {Pow(S, 2): 2}) - - We can update the resources with the observation that the PauliZ gate is self-inverse, - so the resources should when :math:`z mod 2 = 0` should just be the identity operation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def pow_resource_decomp(cls, z): - if z%2 == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} - - >>> re.ResourcePow.resources(CustomZ, {}, z=2) - {Identity: 1} - >>> re.ResourcePow.resources(CustomZ, {}, z=3) - {CustomZ: 1} - - """ - - @classmethod - def _resource_decomp( - cls, base_class, base_params, z, **kwargs - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (Type[~.ResourceOperator]): The class type of the base operator to be raised to some power. - base_params (dict): the resource parameters required to extract the cost of the base operator - z (int): the power that the operator is being raised to - - Resources: - The resources are determined as follows. If the power :math:`z = 0`, then we have the identitiy - gate and we have no resources. If the base operation class :code:`base_class` implements the - :code:`.pow_resource_decomp()` method, then the resources are obtained from this. Otherwise, - the resources of the operation raised to the power :math:`z` are given by extracting the base - operation's resources (via :code:`.resources()`) and raising each operation to the same power. - - **Example** - - The operation raised to a power :math:`z` can be constructed like this: - - >>> qft = re.ResourceQFT(wires=range(3)) - >>> pow_qft = re.ResourcePow(qft, 2) - >>> pow_qft.resources(**pow_qft.resource_params) - defaultdict(, {Pow(Hadamard, 2): 3, Pow(SWAP, 2): 1, Pow(ControlledPhaseShift, 2): 3}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourcePow.resources( - ... base_class = re.ResourceQFT, - ... base_params = {"num_wires": 3}, - ... z = 2, - ... ) - defaultdict(, {Pow(Hadamard, 2): 3, Pow(SWAP, 2): 1, Pow(ControlledPhaseShift, 2): 3}) - - .. details:: - :title: Usage Details - - We can configure the resources for the power of a base operation by modifying - its :code:`.pow_resource_decomp(**resource_params, z)` method. Consider for example this - custom PauliZ class, where the pow-resources are not defined (this is the default - for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def pow_resource_decomp(cls, z): - raise re.ResourcesNotDefined - - When this method is not defined, the resources are computed by taking the power of - each operation in the resources of the base operation. - - >>> CustomZ.resources() - {S: 2} - >>> re.ResourcePow.resources(CustomZ, {}, z=2) - defaultdict(, {Pow(S, 2): 2}) - - We can update the resources with the observation that the PauliZ gate is self-inverse, - so the resources should when :math:`z mod 2 = 0` should just be the identity operation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def pow_resource_decomp(cls, z): - if z%2 == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} - - >>> re.ResourcePow.resources(CustomZ, {}, z=2) - {Identity: 1} - >>> re.ResourcePow.resources(CustomZ, {}, z=3) - {CustomZ: 1} - - """ - if z == 0: - return {re.ResourceIdentity.resource_rep(): 1} - - if z == 1: - return {base_class.resource_rep(**base_params): 1} - - try: - return base_class.pow_resource_decomp(z, **base_params) - except re.ResourcesNotDefined: - pass - - try: - gate_types = defaultdict(int) - decomp = base_class.resources(**base_params, **kwargs) - for gate, count in decomp.items(): - rep = cls.resource_rep(gate.op_type, gate.params, z) - gate_types[rep] = count - - return gate_types - except re.ResourcesNotDefined: - pass - - return {base_class.resource_rep(**base_params): z} - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (Type[~.ResourceOperator]): The class type of the base operator to be raised to some power. - * base_params (dict): the resource parameters required to extract the cost of the base operator - * z (int): the power that the operator is being raised to - """ - return { - "base_class": type(self.base), - "base_params": self.base.resource_params, - "z": self.z, - } - - @classmethod - def resource_rep(cls, base_class, base_params, z) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (Type[~.ResourceOperator]): The class type of the base operator to be raised to some power. - base_params (dict): the resource parameters required to extract the cost of the base operator - z (int): the power that the operator is being raised to - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp( - cls, {"base_class": base_class, "base_params": base_params, "z": z} - ) - - @classmethod - def pow_resource_decomp( - cls, z0, base_class, base_params, z - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z0 (int): the power that the power-operator is being raised to - base_class (Type[~.ResourceOperator]): The class type of the base operator to be raised to some power. - base_params (dict): The resource parameters required to extract the cost of the base operator. - z (int): the power that the base operator is being raised to - - Resources: - The resources are derived by simply adding together the :math:`z` exponent and the - :math:`z_{0}` exponent into a single instance of :class:`~.ResourcePow` gate, raising - the base operator to the power :math:`z + z_{0}`. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(base_class, base_params, z0 * z): 1} - - @staticmethod - def tracking_name(base_class, base_params, z) -> str: - r"""Returns the tracking name built with the operator's parameters.""" - base_name = base_class.tracking_name(**base_params) - return f"Pow({base_name}, {z})" - - -class ResourceExp(Exp, re.ResourceOperator): - r"""Resource class for the symbolic Exp operation. - - A symbolic class used to represent the exponential of some base operation. - - Args: - base (~.operation.Operator): The operator to be exponentiated - coeff=1 (Number): A scalar coefficient of the operator. - num_steps (int): The number of steps used in the decomposition of the exponential operator, - also known as the Trotter number. If this value is `None` and the Suzuki-Trotter - decomposition is needed, an error will be raised. - id (str): id for the Exp operator. Default is None. - - Resource Parameters: - * base_class (Type[~.ResourceOperator]): The class type of the base operator that is exponentiated. - * base_params (dict): the resource parameters required to extract the cost of the base operator - * base_pauli_rep (Union[PauliSentence, None]): The base operator represented as a linear combination of Pauli words. If such a representation is not applicable, then :code:`None`. - * coeff (complex): a scalar value which multiplies the base operator in the exponent - * num_steps (int): the number of trotter steps to use in approximating the exponential - - Resources: - This symbolic operation represents the exponential of some base operation. The resources - are determined as follows. If the base operation class :code:`base_class` implements the - :code:`.exp_resource_decomp()` method, then the resources are obtained from this. - - Otherwise, the exponetiated operator's resources are computed using the linear combination - of Pauli words representation (:code:`base_pauli_rep`). The exponential is approximated by - the product of the exponential of each Pauli word in the sum. This product is repeated - :code:`num_steps` many times. Specifically, the cost for the exponential of each Pauli word - is given by an associated :class:`~.ResourcePauliRot`. - - .. seealso:: :class:`~.ops.op_math.exp.Exp` - - **Example** - - The exponentiated operation can be constructed like this: - - >>> hamiltonian = qml.dot([0.1, -2.3], [qml.X(0)@qml.Y(1), qml.Z(0)]) - >>> hamiltonian - 0.1 * (X(0) @ Y(1)) + -2.3 * Z(0) - >>> exp_hamiltonian = re.ResourceExp(hamiltonian, 0.1*1j, num_steps=2) - >>> exp_hamiltonian.resources(**exp_hamiltonian.resource_params) - defaultdict(, {PauliRot: 2, PauliRot: 2}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourceExp.resources( - ... base_class = qml.ops.Sum, - ... base_params = {}, - ... base_pauli_rep = hamiltonian.pauli_rep, - ... coeff = 0.1*1j, - ... num_steps = 2, - ... ) - defaultdict(, {PauliRot: 2, PauliRot: 2}) - - .. details:: - :title: Usage Details - - We can configure the resources for the exponential of a base operation by modifying - its :code:`.exp_resource_decomp(scalar, num_steps, **resource_params)` method. Consider - for example this custom PauliZ class, where the exponentiated resources are not defined - (this is the default for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def exp_resource_decomp(cls, scalar, num_steps): - raise re.ResourcesNotDefined - - When this method is not defined, the resources are computed from the linear combination - of Pauli words representation. - - >>> pauli_rep = CustomZ(wires=0).pauli_rep - >>> pauli_rep - 1.0 * Z(0) - >>> re.ResourceExp.resources(CustomZ, {}, base_pauli_rep=pauli_rep, coeff=0.1*1j, num_steps=3) - defaultdict(, {PauliRot: 3}) - - We can update the exponential resources with the observation that the PauliZ gate, when - exponentiated, produces an RZ rotation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def exp_resource_decomp(cls, scalar, num_steps): - return {re.ResourceRZ.resource_rep(): num_steps} - - >>> re.ResourceExp.resources(CustomZ, {}, base_pauli_rep=pauli_rep, coeff=0.1*1j, num_steps=3) - {RZ: 3} - - """ - - @staticmethod - def _resource_decomp( - base_class: Operation, - base_params: Dict, - base_pauli_rep: PauliSentence, - coeff: complex, - num_steps: int, - **kwargs, - ): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (Type[~.ResourceOperator]): The class type of the base operator that is - exponentiated. - base_params (dict): the resource parameters required to extract the cost of the base operator - base_pauli_rep (Union[PauliSentence, None]): The base operator represented as a linear - combination of Pauli words. If such a representation is not applicable, then :code:`None`. - coeff (complex): a scalar value which multiplies the base operator in the exponent - num_steps (int): the number of trotter steps to use in approximating the exponential - - Resources: - This symbolic operation represents the exponential of some base operation. The resources - are determined as follows. If the base operation class :code:`base_class` implements the - :code:`.exp_resource_decomp()` method, then the resources are obtained from this. - - Otherwise, the exponetiated operator's resources are computed using the linear combination - of Pauli words representation (:code:`base_pauli_rep`). The exponential is approximated by - the product of the exponential of each Pauli word in the sum. This product is repeated - :code:`num_steps` many times. Specifically, the cost for the exponential of each Pauli word - is given by an associated :class:`~.ResourcePauliRot`. - - **Example** - - The exponentiated operation can be constructed like this: - - >>> hamiltonian = qml.dot([0.1, -2.3], [qml.X(0)@qml.Y(1), qml.Z(0)]) - >>> hamiltonian - 0.1 * (X(0) @ Y(1)) + -2.3 * Z(0) - >>> exp_hamiltonian = re.ResourceExp(hamiltonian, 0.1*1j, num_steps=2) - >>> exp_hamiltonian.resources(**exp_hamiltonian.resource_params) - defaultdict(, {PauliRot: 2, PauliRot: 2}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourceExp.resources( - ... base_class = qml.ops.Sum, - ... base_params = {}, - ... base_pauli_rep = hamiltonian.pauli_rep, - ... coeff = 0.1*1j, - ... num_steps = 2, - ... ) - defaultdict(, {PauliRot: 2, PauliRot: 2}) - - .. details:: - :title: Usage Details - - We can configure the resources for the exponential of a base operation by modifying - its :code:`.exp_resource_decomp(scalar, num_steps, **resource_params)` method. Consider - for example this custom PauliZ class, where the exponentiated resources are not defined - (this is the default for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def exp_resource_decomp(cls, scalar, num_steps): - raise re.ResourcesNotDefined - - When this method is not defined, the resources are computed from the linear combination - of Pauli words representation. - - >>> pauli_rep = CustomZ(wires=0).pauli_rep - >>> pauli_rep - 1.0 * Z(0) - >>> re.ResourceExp.resources(CustomZ, {}, base_pauli_rep=pauli_rep, coeff=0.1*1j, num_steps=3) - defaultdict(, {PauliRot: 3}) - - We can update the exponential resources with the observation that the PauliZ gate, when - exponentiated, produces an RZ rotation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def exp_resource_decomp(cls, scalar, num_steps): - return {re.ResourceRZ.resource_rep(): num_steps} - - >>> re.ResourceExp.resources(CustomZ, {}, base_pauli_rep=pauli_rep, coeff=0.1*1j, num_steps=3) - {RZ: 3} - - """ - # Custom exponential operator resources: - if issubclass(base_class, re.ResourceOperator): - try: - return base_class.exp_resource_decomp(coeff, num_steps, **base_params) - except re.ResourcesNotDefined: - pass - - if base_pauli_rep and math.real(coeff) == 0: - scalar = num_steps or 1 # 1st-order Trotter-Suzuki with 'num_steps' trotter steps: - return _scale_dict( - _resources_from_pauli_sentence(base_pauli_rep), scalar=scalar, in_place=True - ) - - raise re.ResourcesNotDefined - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (Type[ResourceOperator]): The class type of the base operator that is exponentiated. - * base_params (dict): the resource parameters required to extract the cost of the base operator - * base_pauli_rep (Union[PauliSentence, None]): The base operator represented as a linear combination of Pauli words. If such a representation is not applicable, then :code:`None`. - * coeff (complex): a scalar value which multiplies the base operator in the exponent - * num_steps (int): the number of trotter steps to use in approximating the exponential - """ - return _extract_exp_params(self.base, self.scalar, self.num_steps) - - @classmethod - def resource_rep(cls, base_class, base_params, base_pauli_rep, coeff, num_steps): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (Type[~.ResourceOperator]): The class type of the base operator that is - exponentiated. - base_params (dict): the resource parameters required to extract the cost of the base operator - base_pauli_rep (Union[PauliSentence, None]): The base operator represented as a linear - combination of Pauli words. If such a representation is not applicable, then :code:`None`. - coeff (complex): a scalar value which multiplies the base operator in the exponent - num_steps (int): the number of trotter steps to use in approximating the exponential - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - name = cls.tracking_name(base_class, base_params, base_pauli_rep, coeff, num_steps) - return re.CompressedResourceOp( - cls, - { - "base_class": base_class, - "base_params": base_params, - "base_pauli_rep": base_pauli_rep, - "coeff": coeff, - "num_steps": num_steps, - }, - name=name, - ) - - @classmethod - def pow_resource_decomp( - cls, z0, base_class, base_params, base_pauli_rep, coeff, num_steps - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z0 (int): the power that the operator is being raised to - base_class (Type[~.ResourceOperator]): The class type of the base operator that is - exponentiated. - base_params (dict): the resource parameters required to extract the cost of the base operator - base_pauli_rep (Union[PauliSentence, None]): The base operator represented as a linear - combination of Pauli words. If such a representation is not applicable, then :code:`None`. - coeff (complex): a scalar value which multiplies the base operator in the exponent - num_steps (int): the number of trotter steps to use in approximating the exponential - - Resources: - The resources are derived by simply multiplying together the :math:`z0` exponent and the - :code:`coeff` coefficient into a single instance of :class:`~.ResourceExp` gate with - coefficient :code:`z0 * coeff`. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(base_class, base_params, base_pauli_rep, z0 * coeff, num_steps): 1} - - @staticmethod - def tracking_name( - base_class: Operation, - base_params: Dict, - base_pauli_rep: PauliSentence, - coeff: complex, - num_steps: int, - ): # pylint: disable=unused-argument - r"""Returns the tracking name built with the operator's parameters.""" - base_name = ( - base_class.tracking_name(**base_params) - if issubclass(base_class, re.ResourceOperator) - else base_class.__name__ - ) - - return f"Exp({base_name}, {coeff}, num_steps={num_steps})".replace("Resource", "") - - -class ResourceProd(Prod, re.ResourceOperator): - r"""Resource class for the symbolic Prod operation. - - A symbolic class used to represent a product of some base operations. - - Args: - *factors (tuple[~.operation.Operator]): a tuple of operators which will be multiplied together. - - Resource Parameters: - * cmpr_factors (list[CompressedResourceOp]): A list of operations, in the compressed representation, corresponding to the factors in the product. - - Resources: - This symbolic class represents a product of operations. The resources are defined trivially as the counts for each operation in the product. - - .. seealso:: :class:`~.ops.op_math.prod.Prod` - - **Example** - - The product of operations can be constructed as follows. Note, each operation in the - product must be a valid :class:`~.ResourceOperator` - - >>> prod_op = re.ResourceProd( - ... re.ResourceQFT(range(3)), - ... re.ResourceZ(0), - ... re.ResourceGlobalPhase(1.23, wires=[1]) - ... ) - >>> prod_op - ResourceQFT(wires=[0, 1, 2]) @ Z(0) @ ResourceGlobalPhase(1.23, wires=[1]) - >>> prod_op.resources(**prod_op.resource_params) - defaultdict(, {QFT(3): 1, Z: 1, GlobalPhase: 1}) - - """ - - @staticmethod - def _resource_decomp(cmpr_factors, **kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - cmpr_factors (list[CompressedResourceOp]): A list of operations, in the compressed - representation, corresponding to the factors in the product. - - Resources: - This symbolic class represents a product of operations. The resources are defined - trivially as the counts for each operation in the product. - - .. seealso:: :class:`~.ops.op_math.prod.Prod` - - **Example** - - The product of operations can be constructed as follows. Note, each operation in the - product must be a valid :class:`~.ResourceOperator` - - >>> prod_op = re.ResourceProd( - ... re.ResourceQFT(range(3)), - ... re.ResourceZ(0), - ... re.ResourceGlobalPhase(1.23, wires=[1]) - ... ) - >>> prod_op - ResourceQFT(wires=[0, 1, 2]) @ Z(0) @ ResourceGlobalPhase(1.23, wires=[1]) - >>> prod_op.resources(**prod_op.resource_params) - defaultdict(, {QFT(3): 1, Z: 1, GlobalPhase: 1}) - - """ - res = defaultdict(int) - for factor in cmpr_factors: - res[factor] += 1 - return res - - @property - def resource_params(self) -> Dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * cmpr_factors (list[CompressedResourceOp]): A list of operations, in the compressed representation, corresponding to the factors in the product. - """ - try: - cmpr_factors = tuple(factor.resource_rep_from_op() for factor in self.operands) - except AttributeError as error: - raise ValueError( - "All factors of the Product must be instances of `ResourceOperator` in order to obtain resources." - ) from error - - return {"cmpr_factors": cmpr_factors} - - @classmethod - def resource_rep(cls, cmpr_factors) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - cmpr_factors (list[CompressedResourceOp]): A list of operations, in the compressed - representation, corresponding to the factors in the product. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp(cls, {"cmpr_factors": cmpr_factors}) - - -def _extract_exp_params(base_op, scalar, num_steps): - pauli_rep = base_op.pauli_rep - isinstance_resource_op = isinstance(base_op, re.ResourceOperator) - - if (not isinstance_resource_op) and (pauli_rep is None): - raise ValueError( - f"Cannot obtain resources for the exponential of {base_op}, if it is not a ResourceOperator and it doesn't have a Pauli decomposition." - ) - - base_class = type(base_op) - base_params = base_op.resource_params if isinstance_resource_op else {} - - return { - "base_class": base_class, - "base_params": base_params, - "base_pauli_rep": pauli_rep, - "coeff": scalar, - "num_steps": num_steps, - } - - -def _resources_from_pauli_sentence(pauli_sentence): - gate_types = defaultdict(int) - - for pauli_word in iter(pauli_sentence.keys()): - pauli_string = "".join((str(v) for v in pauli_word.values())) - pauli_rot_gate = re.ResourcePauliRot.resource_rep(pauli_string) - gate_types[pauli_rot_gate] = 1 - - return gate_types diff --git a/pennylane/labs/resource_estimation/ops/qubit/__init__.py b/pennylane/labs/resource_estimation/ops/qubit/__init__.py deleted file mode 100644 index 7c012a8ff6d..00000000000 --- a/pennylane/labs/resource_estimation/ops/qubit/__init__.py +++ /dev/null @@ -1,19 +0,0 @@ -# Copyright 2024 Xanadu Quantum Technologies Inc. - -# 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. -r"""This module contains experimental resource estimation functionality.""" - -from .non_parametric_ops import * -from .parametric_ops_multi_qubit import * -from .parametric_ops_single_qubit import * -from .qchem_ops import * diff --git a/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py b/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py deleted file mode 100644 index 6fcfea57750..00000000000 --- a/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py +++ /dev/null @@ -1,1024 +0,0 @@ -# Copyright 2024 Xanadu Quantum Technologies Inc. - -# 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. -r"""Resource operators for non parametric single qubit operations.""" -from typing import Dict - -import pennylane as qml -import pennylane.labs.resource_estimation as re - -# pylint: disable=arguments-differ - - -class ResourceHadamard(qml.Hadamard, re.ResourceOperator): - r"""Resource class for the Hadamard gate. - - Args: - wires (Sequence[int] or int): the wire the operation acts on - - Resources: - The Hadamard gate is treated as a fundamental gate and thus it cannot be decomposed - further. Requesting the resources of this gate raises a :code:`ResourcesNotDefined` error. - - .. seealso:: :class:`~.Hadamard` - - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The Hadamard gate is treated as a fundamental gate and thus it cannot be decomposed - further. Requesting the resources of this gate raises a :code:`ResourcesNotDefined` error. - - Raises: - ResourcesNotDefined: This gate is fundamental, no further decomposition defined. - """ - raise re.ResourcesNotDefined - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - For a single control wire, the cost is a single instance of :class:`~.ResourceCH`. - Two additional :class:`~.ResourceX` gates are used to flip the control qubit if - it is zero-controlled. - - In the case where multiple controlled wires are provided, the resources are derived from - the following identities (as presented in this `blog post `_): - - .. math:: - - \begin{align} - \hat{H} &= \hat{R}_{y}(\frac{\pi}{4}) \cdot \hat{Z} \cdot \hat{R}_{y}(\frac{-\pi}{4}), \\ - \hat{Z} &= \hat{H} \cdot \hat{X} \cdot \hat{H}. - \end{align} - - Specifically, the resources are given by two :class:`~.ResourceRY` gates, two - :class:`~.ResourceHadamard` gates and a :class:`~.ResourceX` gate. By replacing the - :class:`~.ResourceX` gate with :class:`~.ResourceMultiControlledX` gate, we obtain a - controlled-version of this identity. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires == 1: - gate_types = {re.ResourceCH.resource_rep(): 1} - - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 - - return gate_types - - gate_types = {} - - ry = re.ResourceRY.resource_rep() - h = re.ResourceHadamard.resource_rep() - mcx = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - gate_types[h] = 2 - gate_types[ry] = 2 - gate_types[mcx] = 1 - return gate_types - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - The Hadamard gate raised to even powers produces identity and raised - to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if z % 2 == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} - - -class ResourceS(qml.S, re.ResourceOperator): - r"""Resource class for the S-gate. - - Args: - wires (Sequence[int] or int): the wire the operation acts on - - Resources: - The S-gate decomposes into two T-gates. - - .. seealso:: :class:`~.S` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceS.resources() - {T: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The S-gate decomposes into two T-gates. - """ - gate_types = {} - t = ResourceT.resource_rep() - gate_types[t] = 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of the S-gate is equivalent to the S-gate raised to the third power. - The resources are defined as three instances of the S-gate. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 3} - - @staticmethod - def controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires): - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The S-gate is equivalent to the PhaseShift gate for some fixed phase. Given a single - control wire, the cost is therefore a single instance of - :class:`~.ResourceControlledPhaseShift`. Two additional :class:`~.ResourceX` gates are - used to flip the control qubit if it is zero-controlled. - - In the case where multiple controlled wires are provided, we can collapse the control - wires by introducing one 'clean' auxilliary qubit (which gets reset at the end). - In this case the cost increases by two additional :class:`~.ResourceMultiControlledX` gates, - as described in (Lemma 7.11) `Barenco et al. `_. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires == 1: - gate_types = {re.ResourceControlledPhaseShift.resource_rep(): 1} - - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 - - return gate_types - - cs = re.ResourceControlledPhaseShift.resource_rep() - mcx = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - return {cs: 1, mcx: 2} - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - The S-gate, when raised to a power which is a multiple of four, produces identity. - The cost of raising to an arbitrary integer power :math:`z` is given by - :math:`z \mod 4` instances of the S-gate. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if (mod_4 := z % 4) == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): mod_4} - - -class ResourceSWAP(qml.SWAP, re.ResourceOperator): - r"""Resource class for the SWAP gate. - - Args: - wires (Sequence[int]): the wires the operation acts on - - Resources: - The resources come from the following identity expressing SWAP as the product of - three :class:`~.CNOT` gates: - - .. math:: - - SWAP = \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 0 & 1 & 0\\ - 0 & 1 & 0 & 0\\ - 0 & 0 & 0 & 1 - \end{bmatrix} - = \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 1 & 0 & 0\\ - 0 & 0 & 0 & 1\\ - 0 & 0 & 1 & 0 - \end{bmatrix} - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 0 & 0 & 1\\ - 0 & 0 & 1 & 0\\ - 0 & 1 & 0 & 0 - \end{bmatrix} - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 1 & 0 & 0\\ - 0 & 0 & 0 & 1\\ - 0 & 0 & 1 & 0 - \end{bmatrix}. - - .. seealso:: :class:`~.SWAP` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceSWAP.resources() - {CNOT: 3} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources come from the following identity expressing SWAP as the product of - three CNOT gates: - - .. math:: - - SWAP = \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 0 & 1 & 0\\ - 0 & 1 & 0 & 0\\ - 0 & 0 & 0 & 1 - \end{bmatrix} - = \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 1 & 0 & 0\\ - 0 & 0 & 0 & 1\\ - 0 & 0 & 1 & 0 - \end{bmatrix} - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 0 & 0 & 1\\ - 0 & 0 & 1 & 0\\ - 0 & 1 & 0 & 0 - \end{bmatrix} - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 1 & 0 & 0\\ - 0 & 0 & 0 & 1\\ - 0 & 0 & 1 & 0 - \end{bmatrix}. - """ - gate_types = {} - cnot = re.ResourceCNOT.resource_rep() - gate_types[cnot] = 3 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - For a single control wire, the cost is a single instance of :class:`~.ResourceCSWAP`. - Two additional :class:`~.ResourceX` gates are used to flip the control qubit if - it is zero-controlled. - - In the case where multiple controlled wires are provided, the resources are given by - two :class:`~.ResourceCNOT` gates and one :class:`~.ResourceMultiControlledX` gate. This - is because of the symmetric resource decomposition of the SWAP gate. By controlling on - the middle CNOT gate, we obtain the required controlled operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires == 1: - gate_types = {re.ResourceCSWAP.resource_rep(): 1} - - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 - - return gate_types - - cnot = re.ResourceCNOT.resource_rep() - mcx = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - return {cnot: 2, mcx: 1} - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - The SWAP gate raised to even powers produces identity and raised - to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if z % 2 == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} - - -class ResourceT(qml.T, re.ResourceOperator): - r"""Resource class for the T-gate. - - Args: - wires (Sequence[int] or int): the wire the operation acts on - - Resources: - The T-gate is treated as a fundamental gate and thus it cannot be decomposed - further. Requesting the resources of this gate raises a :code:`ResourcesNotDefined` error. - - .. seealso:: :class:`~.T` - - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The T-gate is treated as a fundamental gate and thus it cannot be decomposed - further. Requesting the resources of this gate raises a :code:`ResourcesNotDefined` error. - - Raises: - ResourcesNotDefined: This gate is fundamental, no further decomposition defined. - """ - raise re.ResourcesNotDefined - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of the T-gate is equivalent to the T-gate raised to the 7th power. - The resources are defined as seven instances of the T-gate. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - - return {cls.resource_rep(): 7} - - @staticmethod - def controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires): - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The T-gate is equivalent to the PhaseShift gate for some fixed phase. Given a single - control wire, the cost is therefore a single instance of - :class:`~.ResourceControlledPhaseShift`. Two additional :class:`~.ResourceX` gates are - used to flip the control qubit if it is zero-controlled. - - In the case where multiple controlled wires are provided, we can collapse the control - wires by introducing one 'clean' auxilliary qubit (which gets reset at the end). - In this case the cost increases by two additional :class:`~.ResourceMultiControlledX` gates, - as described in (Lemma 7.11) `Barenco et al. `_. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires == 1: - gate_types = {re.ResourceControlledPhaseShift.resource_rep(): 1} - - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 - - return gate_types - - ct = re.ResourceControlledPhaseShift.resource_rep() - mcx = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - return {ct: 1, mcx: 2} - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - The T-gate, when raised to a power which is a multiple of eight, produces identity. - The cost of raising to an arbitrary integer power :math:`z` is given by - :math:`z \mod 8` instances of the T-gate. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if (mod_8 := z % 8) == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): mod_8} - - -class ResourceX(qml.X, re.ResourceOperator): - r"""Resource class for the X-gate. - - Args: - wires (Sequence[int] or int): the wire the operation acts on - - Resources: - The X-gate can be decomposed according to the following identities: - - .. math:: - - \begin{align} - \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ - \hat{Z} &= \hat{S}^{2}. - \end{align} - - Thus the resources for an X-gate are two :class:`~.ResourceS` gates and - two :class:`~.ResourceHadamard` gates. - - .. seealso:: :class:`~.X` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceX.resources() - {S: 2, Hadamard: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The X-gate can be decomposed according to the following identities: - - .. math:: - - \begin{align} - \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ - \hat{Z} &= \hat{S}^{2}. - \end{align} - - Thus the resources for an X-gate are two :class:`~.ResourceS` gates and - two :class:`~.ResourceHadamard` gates. - """ - s = re.ResourceS.resource_rep() - h = re.ResourceHadamard.resource_rep() - - gate_types = {} - gate_types[s] = 2 - gate_types[h] = 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires): - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - For one or two control wires, the cost is one of :class:`~.ResourceCNOT` - or :class:`~.ResourceToffoli` respectively. Two additional :class:`~.ResourceX` gates - per control qubit are used to flip the control qubits if they are zero-controlled. - - In the case where multiple controlled wires are provided, the cost is one general - :class:`~.ResourceMultiControlledX` gate. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires > 2: - return { - re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires, num_ctrl_values, num_work_wires - ): 1 - } - - gate_types = {} - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 * num_ctrl_values - - if num_ctrl_wires == 1: - gate_types[re.ResourceCNOT.resource_rep()] = 1 - - if num_ctrl_wires == 2: - gate_types[re.ResourceToffoli.resource_rep()] = 1 - - return gate_types - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - The X-gate raised to even powers produces identity and raised - to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if z % 2 == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} - - -class ResourceY(qml.Y, re.ResourceOperator): - r"""Resource class for the Y-gate. - - Args: - wires (Sequence[int] or int): the wire the operation acts on - - Resources: - The Y-gate can be decomposed according to the following identities: - - .. math:: - - \begin{align} - \hat{Y} &= \hat{S} \cdot \hat{X} \cdot \hat{S}^{\dagger}, \\ - \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ - \hat{Z} &= \hat{S}^{2}, \\ - \hat{S}^{\dagger} &= 3 \hat{S}. - \end{align} - - Thus the resources for a Y-gate are six :class:`~.ResourceS` gates and - two :class:`~.ResourceHadamard` gates. - - .. seealso:: :class:`~.Y` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceY.resources() - {S: 6, Hadamard: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The Y-gate can be decomposed according to the following identities: - - .. math:: - - \begin{align} - \hat{Y} &= \hat{S} \cdot \hat{X} \cdot \hat{S}^{\dagger}, \\ - \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ - \hat{Z} &= \hat{S}^{2}, \\ - \hat{S}^{\dagger} &= 3 \hat{S}. - \end{align} - - Thus the resources for a Y-gate are six :class:`~.ResourceS` gates and - two :class:`~.ResourceHadamard` gates. - """ - s = re.ResourceS.resource_rep() - h = re.ResourceHadamard.resource_rep() - - gate_types = {} - gate_types[s] = 6 - gate_types[h] = 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - For a single control wire, the cost is a single instance of :class:`~.ResourceCY`. - Two additional :class:`~.ResourceX` gates are used to flip the control qubit if - it is zero-controlled. - - In the case where multiple controlled wires are provided, the resources are derived from - the following identity: - - .. math:: \hat{Y} = \hat{S} \cdot \hat{X} \cdot \hat{S}^{\dagger}. - - Specifically, the resources are given by a :class:`~.ResourceX` gate conjugated with - a pair of :class:`~.ResourceS` gates. By replacing the :class:`~.ResourceX` gate with a - :class:`~.ResourceMultiControlledX` gate, we obtain a controlled-version of this identity. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires == 1: - gate_types = {re.ResourceCY.resource_rep(): 1} - - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 - - return gate_types - - gate_types = {} - - s = re.ResourceS.resource_rep() - s_dagg = re.ResourceAdjoint.resource_rep(re.ResourceS, {}) - mcx = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - gate_types[s] = 1 - gate_types[s_dagg] = 1 - gate_types[mcx] = 1 - return gate_types - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - The Y-gate raised to even powers produces identity and raised - to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if z % 2 == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} - - -class ResourceZ(qml.Z, re.ResourceOperator): - r"""Resource class for the Z-gate. - - Args: - wires (Sequence[int] or int): the wire the operation acts on - - Resources: - The Z-gate can be decomposed according to the following identities: - - .. math:: \hat{Z} = \hat{S}^{2}, - - thus the resources for a Z-gate are two :class:`~.ResourceS` gates. - - .. seealso:: :class:`~.Z` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceZ.resources() - {S: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The Z-gate can be decomposed according to the following identities: - - .. math:: \hat{Z} = \hat{S}^{2}, - - thus the resources for a Z-gate are two :class:`~.ResourceS` gates. - """ - s = re.ResourceS.resource_rep() - - gate_types = {} - gate_types[s] = 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - This operation is self-adjoint, so the resources of the adjoint operation results - in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - For one or two control wires, the cost is one of :class:`~.ResourceCZ` - or :class:`~.ResourceCCZ` respectively. Two additional :class:`~.ResourceX` gates - per control qubit are used to flip the control qubits if they are zero-controlled. - - In the case where multiple controlled wires are provided, the resources are derived from - the following identity: - - .. math:: \hat{Z} = \hat{H} \cdot \hat{X} \cdot \hat{H}. - - Specifically, the resources are given by a :class:`~.ResourceX` gate conjugated with - a pair of :class:`~.ResourceHadamard` gates. By replacing the :class:`~.ResourceX` gate - with a :class:`~.ResourceMultiControlledX` gate, we obtain a controlled-version of this - identity. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires > 2: - h = re.ResourceHadamard.resource_rep() - mcx = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - return {h: 2, mcx: 1} - - gate_types = {} - if num_ctrl_wires == 1: - gate_types[re.ResourceCZ.resource_rep()] = 1 - - if num_ctrl_wires == 2: - gate_types[re.ResourceCCZ.resource_rep()] = 1 - - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 * num_ctrl_values - - return gate_types - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - The Z-gate raised to even powers produces identity and raised - to odd powers it produces itself. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if z % 2 == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} diff --git a/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_multi_qubit.py b/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_multi_qubit.py deleted file mode 100644 index b93681aa8f4..00000000000 --- a/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_multi_qubit.py +++ /dev/null @@ -1,1223 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""Resource operators for parametric multi qubit operations.""" -from typing import Dict - -import pennylane as qml -import pennylane.labs.resource_estimation as re - -# pylint: disable=arguments-differ - - -class ResourceMultiRZ(qml.MultiRZ, re.ResourceOperator): - r"""Resource class for the MultiRZ gate. - - Args: - theta (tensor_like or float): rotation angle :math:`\theta` - wires (Sequence[int] or int): the wires the operation acts on - id (str or None): String representing the operation (optional) - - Resource Parameters: - * num_wires (int): the number of qubits the operation acts upon - - Resources: - The resources come from Section VIII (Figure 3) of `The Bravyi-Kitaev transformation for - quantum computation of electronic structure `_ paper. - - Specifically, the resources are given by one :class:`~.ResourceRZ` gate and a cascade of - :math:`2 * (n - 1)` :class:`~.ResourceCNOT` gates where :math:`n` is the number of qubits - the gate acts on. - - .. seealso:: :class:`~.MultiRZ` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceMultiRZ.resources(num_wires=3) - {CNOT: 4, RZ: 1} - """ - - @staticmethod - def _resource_decomp(num_wires, **kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_wires (int): the number of qubits the operation acts upon - - Resources: - The resources come from Section VIII (Figure 3) of `The Bravyi-Kitaev transformation for - quantum computation of electronic structure `_ paper. - - Specifically, the resources are given by one :class:`~.ResourceRZ` gate and a cascade of - :math:`2 * (n - 1)` :class:`~.ResourceCNOT` gates where :math:`n` is the number of qubits - the gate acts on. - """ - cnot = re.ResourceCNOT.resource_rep() - rz = re.ResourceRZ.resource_rep() - - gate_types = {} - gate_types[cnot] = 2 * (num_wires - 1) - gate_types[rz] = 1 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_wires (int): the number of qubits the operation acts upon - """ - return {"num_wires": len(self.wires)} - - @classmethod - def resource_rep(cls, num_wires): - """Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_wires (int): the number of qubits the operation acts upon - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp(cls, {"num_wires": num_wires}) - - @classmethod - def adjoint_resource_decomp(cls, num_wires) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Args: - num_wires (int): the number of qubits the operation acts upon - - Resources: - The adjoint of this operator just changes the sign of the phase, thus - the resources of the adjoint operation results in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(num_wires=num_wires): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, - num_ctrl_values, - num_work_wires, - num_wires, - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - num_wires (int): the number of qubits the base operation acts upon - - Resources: - The resources are derived from the following identity. If an operation :math:`\hat{A}` - can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` - then the controlled operation :math:`C\hat{A}` can be expressed as: - - .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} - - Specifically, the resources are one multi-controlled RZ-gate and a cascade of - :math:`2 * (n - 1)` :class:`~.ResourceCNOT` gates where :math:`n` is the number of qubits - the gate acts on. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_values == 0: - cnot = re.ResourceCNOT.resource_rep() - ctrl_rz = re.ResourceControlled.resource_rep( - base_class=re.ResourceRZ, - base_params={}, - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - gate_types = {} - gate_types[cnot] = 2 * (num_wires - 1) - gate_types[ctrl_rz] = 1 - - return gate_types - - raise re.ResourcesNotDefined - - @classmethod - def pow_resource_decomp(cls, z, num_wires) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - num_wires (int): the number of qubits the base operation acts upon - - Resources: - Taking arbitrary powers of a general rotation produces a sum of rotations. - The resources simplify to just one total multi-RZ rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(num_wires=num_wires): 1} - - -class ResourcePauliRot(qml.PauliRot, re.ResourceOperator): - r"""Resource class for the PauliRot gate. - - Args: - theta (float): rotation angle :math:`\theta` - pauli_word (string): the Pauli word defining the rotation - wires (Sequence[int] or int): the wire the operation acts on - id (str or None): String representing the operation (optional) - - Resource Parameters: - * pauli_string (str): a string describing the pauli operators that define the rotation - - Resources: - When the :code:`pauli_string` is a single Pauli operator (:code:`X, Y, Z, Identity`) - the cost is the associated single qubit rotation (:code:`RX, RY, RZ, GlobalPhase`). - - The resources come from Section VIII (Figures 3 & 4) of `The Bravyi-Kitaev transformation - for quantum computation of electronic structure `_ paper, - in combination with the following identity: - - .. math:: - - \begin{align} - \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ - \hat{Y} &= \hat{S} \cdot \hat{H} \cdot \hat{Z} \cdot \hat{H} \cdot \hat{S}^{\dagger}. - \end{align} - - Specifically, the resources are given by one :class:`~.ResourceRZ` gate and a cascade of - :math:`2 * (n - 1)` :class:`~.ResourceCNOT` gates where :math:`n` is the number of qubits - the gate acts on. Additionally, for each :code:`X` gate in the Pauli word we conjugate by - a pair of :class:`~.ResourceHadamard` gates, and for each :code:`Y` gate in the Pauli word we - conjugate by a pair of :class:`~.ResourceHadamard` and a pair of :class:`~.ResourceS` gates. - - .. seealso:: :class:`~.PauliRot` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourcePauliRot.resources(pauli_string="XYZ") - {Hadamard: 4, S: 1, Adjoint(S): 1, RZ: 1, CNOT: 4} - """ - - @staticmethod - def _resource_decomp(pauli_string, **kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - pauli_string (str): a string describing the pauli operators that define the rotation - - Resources: - When the :code:`pauli_string` is a single Pauli operator (:code:`X, Y, Z, Identity`) - the cost is the associated single qubit rotation (:code:`RX, RY, RZ, GlobalPhase`). - - The resources come from Section VIII (Figures 3 & 4) of `The Bravyi-Kitaev transformation - for quantum computation of electronic structure `_ paper, - in combination with the following identity: - - .. math:: - - \begin{align} - \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ - \hat{Y} &= \hat{S} \cdot \hat{H} \cdot \hat{Z} \cdot \hat{H} \cdot \hat{S}^{\dagger}. - \end{align} - - Specifically, the resources are given by one :class:`~.ResourceRZ` gate and a cascade of - :math:`2 * (n - 1)` :class:`~.ResourceCNOT` gates where :math:`n` is the number of qubits - the gate acts on. Additionally, for each :code:`X` gate in the Pauli word we conjugate by - a pair of :class:`~.ResourceHadamard` gates, and for each :code:`Y` gate in the Pauli word we - conjugate by a pair of :class:`~.ResourceHadamard` and a pair of :class:`~.ResourceS` gates. - """ - if (set(pauli_string) == {"I"}) or (len(pauli_string) == 0): - gp = re.ResourceGlobalPhase.resource_rep() - return {gp: 1} - - if pauli_string == "X": - return {re.ResourceRX.resource_rep(): 1} - if pauli_string == "Y": - return {re.ResourceRY.resource_rep(): 1} - if pauli_string == "Z": - return {re.ResourceRZ.resource_rep(): 1} - - active_wires = len(pauli_string.replace("I", "")) - - h = re.ResourceHadamard.resource_rep() - s = re.ResourceS.resource_rep() - rz = re.ResourceRZ.resource_rep() - s_dagg = re.ResourceAdjoint.resource_rep(re.ResourceS, {}) - cnot = re.ResourceCNOT.resource_rep() - - h_count = 0 - s_count = 0 - - for gate in pauli_string: - if gate == "X": - h_count += 1 - if gate == "Y": - h_count += 1 - s_count += 1 - - gate_types = {} - if h_count: - gate_types[h] = 2 * h_count - - if s_count: - gate_types[s] = s_count - gate_types[s_dagg] = s_count - - gate_types[rz] = 1 - gate_types[cnot] = 2 * (active_wires - 1) - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * pauli_string (str): a string describing the pauli operators that define the rotation - """ - return { - "pauli_string": self.hyperparameters["pauli_word"], - } - - @classmethod - def resource_rep(cls, pauli_string): - """Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - pauli_string (str): a string describing the pauli operators that define the rotation - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp(cls, {"pauli_string": pauli_string}) - - @classmethod - def adjoint_resource_decomp(cls, pauli_string) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Args: - pauli_string (str): a string describing the pauli operators that define the rotation - - Resources: - The adjoint of this operator just changes the sign of the phase, thus - the resources of the adjoint operation results in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(pauli_string=pauli_string): 1} - - @classmethod - def controlled_resource_decomp( - cls, - num_ctrl_wires, - num_ctrl_values, - num_work_wires, - pauli_string, - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - pauli_string (str): a string describing the pauli operators that define the rotation - - Resources: - When the :code:`pauli_string` is a single Pauli operator (:code:`X, Y, Z, Identity`) - the cost is the associated controlled single qubit rotation gate: (:class:`~.ResourceCRX`, - :class:`~.ResourceCRY`, :class:`~.ResourceCRZ`, controlled-:class:`~.ResourceGlobalPhase`). - - The resources are derived from the following identity. If an operation :math:`\hat{A}` - can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` - then the controlled operation :math:`C\hat{A}` can be expressed as: - - .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} - - Specifically, the resources are one multi-controlled RZ-gate and a cascade of - :math:`2 * (n - 1)` :class:`~.ResourceCNOT` gates where :math:`n` is the number of qubits - the gate acts on. Additionally, for each :code:`X` gate in the Pauli word we conjugate by - a pair of :class:`~.ResourceHadamard` gates, and for each :code:`Y` gate in the Pauli word - we conjugate by a pair of :class:`~.ResourceHadamard` and a pair of :class:`~.ResourceS` gates. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - base_gate_types = cls.resources(pauli_string) - - pivotal_gates = ( - re.ResourceRX.resource_rep(), - re.ResourceRY.resource_rep(), - re.ResourceRZ.resource_rep(), - re.ResourceGlobalPhase.resource_rep(), - ) - - for gate in pivotal_gates: - if gate in base_gate_types: - counts = base_gate_types.pop(gate) - ctrl_gate = re.ResourceControlled.resource_rep( - gate.op_type, - gate.params, - num_ctrl_wires, - num_ctrl_values, - num_work_wires, - ) - - base_gate_types[ctrl_gate] = counts - - return base_gate_types - - @classmethod - def pow_resource_decomp(cls, z, pauli_string) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - pauli_string (str): a string describing the pauli operators that define the rotation - - Resources: - Taking arbitrary powers of a general rotation produces a sum of rotations. - The resources simplify to just one total pauli rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(pauli_string=pauli_string): 1} - - -class ResourceIsingXX(qml.IsingXX, re.ResourceOperator): - r"""Resource class for the IsingXX gate. - - Args: - phi (float): the phase angle - wires (int): the subsystem the gate acts on - id (str or None): String representing the operation (optional) - - Resources: - Ising XX coupling gate - - .. math:: XX(\phi) = \exp\left(-i \frac{\phi}{2} (X \otimes X)\right) = - \begin{bmatrix} = - \cos(\phi / 2) & 0 & 0 & -i \sin(\phi / 2) \\ - 0 & \cos(\phi / 2) & -i \sin(\phi / 2) & 0 \\ - 0 & -i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ - -i \sin(\phi / 2) & 0 & 0 & \cos(\phi / 2) - \end{bmatrix}. - - The circuit implementing this transformation is given by: - - .. code-block:: bash - - 0: ─╭●─────RX────╭●─┤ - 1: ─╰X───────────╰X─┤ - - .. seealso:: :class:`~.IsingXX` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceIsingXX.resources() - {CNOT: 2, RX: 1} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - Ising XX coupling gate - - .. math:: XX(\phi) = \exp\left(-i \frac{\phi}{2} (X \otimes X)\right) = - \begin{bmatrix} = - \cos(\phi / 2) & 0 & 0 & -i \sin(\phi / 2) \\ - 0 & \cos(\phi / 2) & -i \sin(\phi / 2) & 0 \\ - 0 & -i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ - -i \sin(\phi / 2) & 0 & 0 & \cos(\phi / 2) - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ─╭●─────RX────╭●─┤ - 1: ─╰X───────────╰X─┤ - - """ - cnot = re.ResourceCNOT.resource_rep() - rx = re.ResourceRX.resource_rep() - - gate_types = {} - gate_types[cnot] = 2 - gate_types[rx] = 1 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of this operator just changes the sign of the phase angle, thus - the resources of the adjoint operation results in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, - num_ctrl_values, - num_work_wires, - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are derived from the following identity. If an operation :math:`\hat{A}` - can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` - then the controlled operation :math:`C\hat{A}` can be expressed as: - - .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} - - Specifically, the resources are one multi-controlled RX-gate and a pair of - :class:`~.ResourceCNOT` gates. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_values == 0: - cnot = re.ResourceCNOT.resource_rep() - ctrl_rx = re.ResourceControlled.resource_rep( - base_class=re.ResourceRX, - base_params={}, - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - gate_types = {} - gate_types[cnot] = 2 - gate_types[ctrl_rx] = 1 - - return gate_types - raise re.ResourcesNotDefined - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a rotation produces a sum of rotations. - The resources simplify to just one total Ising rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - -class ResourceIsingYY(qml.IsingYY, re.ResourceOperator): - r"""Resource class for the IsingYY gate. - - Args: - phi (float): the phase angle - wires (int): the subsystem the gate acts on - id (str or None): String representing the operation (optional) - - Resources: - Ising YY coupling gate - - .. math:: \mathtt{YY}(\phi) = \exp\left(-i \frac{\phi}{2} (Y \otimes Y)\right) = - \begin{bmatrix} - \cos(\phi / 2) & 0 & 0 & i \sin(\phi / 2) \\ - 0 & \cos(\phi / 2) & -i \sin(\phi / 2) & 0 \\ - 0 & -i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ - i \sin(\phi / 2) & 0 & 0 & \cos(\phi / 2) - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ─╭●─────RY────╭●─┤ - 1: ─╰Y───────────╰Y─┤ - - .. seealso:: :class:`~.IsingYY` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceIsingYY.resources() - {CY: 2, RY: 1} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - Ising YY coupling gate - - .. math:: \mathtt{YY}(\phi) = \exp\left(-i \frac{\phi}{2} (Y \otimes Y)\right) = - \begin{bmatrix} - \cos(\phi / 2) & 0 & 0 & i \sin(\phi / 2) \\ - 0 & \cos(\phi / 2) & -i \sin(\phi / 2) & 0 \\ - 0 & -i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ - i \sin(\phi / 2) & 0 & 0 & \cos(\phi / 2) - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ─╭●─────RY────╭●─┤ - 1: ─╰Y───────────╰Y─┤ - - """ - - cy = re.ops.ResourceCY.resource_rep() - ry = re.ops.ResourceRY.resource_rep() - - gate_types = {} - gate_types[cy] = 2 - gate_types[ry] = 1 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of this operator just changes the sign of the phase angle, thus - the resources of the adjoint operation results in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, - num_ctrl_values, - num_work_wires, - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are derived from the following identity. If an operation :math:`\hat{A}` - can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` - then the controlled operation :math:`C\hat{A}` can be expressed as: - - .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} - - Specifically, the resources are one multi-controlled RY-gate and a pair of - :class:`~.ResourceCY` gates. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_values == 0: - cy = re.ops.ResourceCY.resource_rep() - ctrl_ry = re.ResourceControlled.resource_rep( - base_class=re.ResourceRY, - base_params={}, - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - gate_types = {} - gate_types[cy] = 2 - gate_types[ctrl_ry] = 1 - - return gate_types - raise re.ResourcesNotDefined - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a rotation produces a sum of rotations. - The resources simplify to just one total Ising rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - -class ResourceIsingXY(qml.IsingXY, re.ResourceOperator): - r"""Resource class for the IsingXY gate. - - Args: - phi (float): the phase angle - wires (int): the subsystem the gate acts on - id (str or None): String representing the operation (optional) - - Resources: - Ising (XX + YY) coupling gate - - .. math:: \mathtt{XY}(\phi) = \exp\left(i \frac{\theta}{4} (X \otimes X + Y \otimes Y)\right) = - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & \cos(\phi / 2) & i \sin(\phi / 2) & 0 \\ - 0 & i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ - 0 & 0 & 0 & 1 - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ──H─╭●─────RY────╭●──H─┤ - 1: ────╰Y─────RX────╰Y────┤ - - .. seealso:: :class:`~.IsingXY` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceIsingXY.resources() - {Hadamard: 2, CY: 2, RY: 1, RX: 1} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - IsingXY coupling gate - - .. math:: \mathtt{XY}(\phi) = \exp\left(i \frac{\theta}{4} (X \otimes X + Y \otimes Y)\right) = - \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & \cos(\phi / 2) & i \sin(\phi / 2) & 0 \\ - 0 & i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ - 0 & 0 & 0 & 1 - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ──H─╭●─────RY────╭●──H─┤ - 1: ────╰Y─────RX────╰Y────┤ - - """ - h = re.ResourceHadamard.resource_rep() - cy = re.ResourceCY.resource_rep() - ry = re.ResourceRY.resource_rep() - rx = re.ResourceRX.resource_rep() - - gate_types = {} - gate_types[h] = 2 - gate_types[cy] = 2 - gate_types[ry] = 1 - gate_types[rx] = 1 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of this operator just changes the sign of the phase angle, thus - the resources of the adjoint operation results in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, - num_ctrl_values, - num_work_wires, - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are derived from the following identity. If an operation :math:`\hat{A}` - can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` - then the controlled operation :math:`C\hat{A}` can be expressed as: - - .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} - - Specifically, the resources are one multi-controlled RY-gate, one multi-controlled RX-gate, - a pair of :class:`~.ResourceCY` gates and a pair of :class:`~.ResourceHadamard` gates. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_values == 0: - h = re.ResourceHadamard.resource_rep() - cy = re.ResourceCY.resource_rep() - ctrl_rx = re.ResourceControlled.resource_rep( - base_class=re.ResourceRX, - base_params={}, - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - ctrl_ry = re.ResourceControlled.resource_rep( - base_class=re.ResourceRY, - base_params={}, - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - gate_types = {} - gate_types[h] = 2 - gate_types[cy] = 2 - gate_types[ctrl_ry] = 1 - gate_types[ctrl_rx] = 1 - - return gate_types - raise re.ResourcesNotDefined - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a rotation produces a sum of rotations. - The resources simplify to just one total Ising rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - -class ResourceIsingZZ(qml.IsingZZ, re.ResourceOperator): - r"""Resource class for the IsingZZ gate. - - Args: - phi (float): the phase angle - wires (int): the subsystem the gate acts on - id (str or None): String representing the operation (optional) - - Resources: - Ising ZZ coupling gate - - .. math:: ZZ(\phi) = \exp\left(-i \frac{\phi}{2} (Z \otimes Z)\right) = - \begin{bmatrix} - e^{-i \phi / 2} & 0 & 0 & 0 \\ - 0 & e^{i \phi / 2} & 0 & 0 \\ - 0 & 0 & e^{i \phi / 2} & 0 \\ - 0 & 0 & 0 & e^{-i \phi / 2} - \end{bmatrix}. - - The cost for implmenting this transformation is given by: - - .. code-block:: bash - - 0: ─╭●───────────╭●─┤ - 1: ─╰X─────RZ────╰X─┤ - - .. seealso:: :class:`~.IsingZZ` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceIsingZZ.resources() - {CNOT: 2, RZ: 1} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - Ising ZZ coupling gate - - .. math:: ZZ(\phi) = \exp\left(-i \frac{\phi}{2} (Z \otimes Z)\right) = - \begin{bmatrix} - e^{-i \phi / 2} & 0 & 0 & 0 \\ - 0 & e^{i \phi / 2} & 0 & 0 \\ - 0 & 0 & e^{i \phi / 2} & 0 \\ - 0 & 0 & 0 & e^{-i \phi / 2} - \end{bmatrix}. - - The cost for implmenting this transformation is given by: - - .. code-block:: bash - - 0: ─╭●───────────╭●─┤ - 1: ─╰X─────RZ────╰X─┤ - - """ - cnot = re.ResourceCNOT.resource_rep() - rz = re.ResourceRZ.resource_rep() - - gate_types = {} - gate_types[cnot] = 2 - gate_types[rz] = 1 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of this operator just changes the sign of the phase angle, thus - the resources of the adjoint operation results in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, - num_ctrl_values, - num_work_wires, - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are derived from the following identity. If an operation :math:`\hat{A}` - can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` - then the controlled operation :math:`C\hat{A}` can be expressed as: - - .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} - - Specifically, the resources are one multi-controlled RZ-gate and a pair of - :class:`~.ResourceCNOT` gates. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_values == 0: - cnot = re.ResourceCNOT.resource_rep() - ctrl_rz = re.ResourceControlled.resource_rep( - base_class=re.ResourceRZ, - base_params={}, - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - gate_types = {} - gate_types[cnot] = 2 - gate_types[ctrl_rz] = 1 - - return gate_types - raise re.ResourcesNotDefined - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a rotation produces a sum of rotations. - The resources simplify to just one total Ising rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - -class ResourcePSWAP(qml.PSWAP, re.ResourceOperator): - r"""Resource class for the PSWAP gate. - - Args: - phi (float): the phase angle - wires (int): the subsystem the gate acts on - id (str or None): String representing the operation (optional) - - Resources: - The :code:`PSWAP` gate is defined as: - - .. math:: PSWAP(\phi) = \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 0 & e^{i \phi} & 0 \\ - 0 & e^{i \phi} & 0 & 0 \\ - 0 & 0 & 0 & 1 - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ─╭SWAP─╭●───────────╭●─┤ - 1: ─╰SWAP─╰X─────Rϕ────╰X─┤ - - .. seealso:: :class:`~.PSWAP` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourcePSWAP.resources() - {SWAP: 1, CNOT: 2, PhaseShift: 1} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The :code:`PSWAP` gate is defined as: - - .. math:: PSWAP(\phi) = \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & 0 & e^{i \phi} & 0 \\ - 0 & e^{i \phi} & 0 & 0 \\ - 0 & 0 & 0 & 1 - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ─╭SWAP─╭●───────────╭●─┤ - 1: ─╰SWAP─╰X─────Rϕ────╰X─┤ - - """ - swap = re.ResourceSWAP.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - phase = re.ResourcePhaseShift.resource_rep() - - gate_types = {} - gate_types[swap] = 1 - gate_types[cnot] = 2 - gate_types[phase] = 1 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of this operator just changes the sign of the phase angle, thus - the resources of the adjoint operation results in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, - num_ctrl_values, - num_work_wires, - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are derived from the following identity. If an operation :math:`\hat{A}` - can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` - then the controlled operation :math:`C\hat{A}` can be expressed as: - - .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} - - Specifically, the resources are one multi-controlled phase shift gate, one multi-controlled - SWAP gate and a pair of :class:`~.ResourceCNOT` gates. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_values == 0: - cnot = re.ResourceCNOT.resource_rep() - ctrl_swap = re.ResourceControlled.resource_rep( - base_class=re.ResourceSWAP, - base_params={}, - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - ctrl_ps = re.ResourceControlled.resource_rep( - base_class=re.ResourcePhaseShift, - base_params={}, - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - gate_types = {} - gate_types[ctrl_swap] = 1 - gate_types[cnot] = 2 - gate_types[ctrl_ps] = 1 - return gate_types - - raise re.ResourcesNotDefined diff --git a/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py b/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py deleted file mode 100644 index 9c8c503942d..00000000000 --- a/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py +++ /dev/null @@ -1,795 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""Resource operators for parametric single qubit operations.""" -from typing import Dict - -import numpy as np - -import pennylane as qml -import pennylane.labs.resource_estimation as re - -# pylint: disable=arguments-differ - - -def _rotation_resources(epsilon=10e-3): - r"""An estimate on the number of T gates needed to implement a Pauli rotation. - - The expected T-count is taken from (the 'Simulation Results' section) `Efficient - Synthesis of Universal Repeat-Until-Success Circuits `_. - The cost is given as: - - .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil - - Args: - epsilon (float): the acceptable error threshold for the approximation - - Returns: - dict: the T-gate counts - - """ - gate_types = {} - - num_gates = round(1.149 * np.log2(1 / epsilon) + 9.2) - t = re.ResourceT.resource_rep() - gate_types[t] = num_gates - - return gate_types - - -class ResourcePhaseShift(qml.PhaseShift, re.ResourceOperator): - r"""Resource class for the PhaseShift gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int] or int): the wire the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The phase shift gate is equivalent to a Z-rotation upto some global phase, - as defined from the following identity: - - .. math:: R_\phi(\phi) = e^{i\phi/2}R_z(\phi) = \begin{bmatrix} - 1 & 0 \\ - 0 & e^{i\phi} - \end{bmatrix}. - - .. seealso:: :class:`~.PhaseShift` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourcePhaseShift.resources() - {RZ: 1, GlobalPhase: 1} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The phase shift gate is equivalent to a Z-rotation upto some global phase, - as defined from the following identity: - - .. math:: R_\phi(\phi) = e^{i\phi/2}R_z(\phi) = \begin{bmatrix} - 1 & 0 \\ - 0 & e^{i\phi} - \end{bmatrix}. - """ - gate_types = {} - rz = re.ResourceRZ.resource_rep() - global_phase = re.ResourceGlobalPhase.resource_rep() - gate_types[rz] = 1 - gate_types[global_phase] = 1 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of a phase shift operator just changes the sign of the phase, thus - the resources of the adjoint operation results in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - For a single control wire, the cost is a single instance of - :class:`~.ResourceControlledPhaseShift`. Two additional :class:`~.ResourceX` gates are used - to flip the control qubit if it is zero-controlled. - - In the case where multiple controlled wires are provided, we can collapse the control - wires by introducing one 'clean' auxilliary qubit (which gets reset at the end). - In this case the cost increases by two additional :class:`~.ResourceMultiControlledX` gates, - as described in (lemma 7.11) `Elementary gates for quantum computation `_. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires == 1: - gate_types = {re.ResourceControlledPhaseShift.resource_rep(): 1} - - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 - - return gate_types - - c_ps = re.ResourceControlledPhaseShift.resource_rep() - mcx = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - return {c_ps: 1, mcx: 2} - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a phase shift produces a sum of shifts. - The resources simplify to just one total phase shift operator. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if z == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} - - -class ResourceRX(qml.RX, re.ResourceOperator): - r"""Resource class for the RX gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int] or int): the wire the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The - resources are approximating the gate with a series of T gates. The expected T-count is taken - from (the 'Simulation Results' section) `Efficient Synthesis of Universal Repeat-Until-Success - Circuits `_. The cost is given as: - - .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil - - .. seealso:: :class:`~.RX` - - **Example** - - The resources for this operation are computed using: - - >>> re.get_resources(re.ResourceRX(1.23, 0)) - {'gate_types': defaultdict(, {'T': 17}), 'num_gates': 17, 'num_wires': 1} - - The operation does not require any parameters directly, however, it will depend on the single - qubit error threshold, which can be set using a config dictionary. - - >>> config = {"error_rx": 1e-3} - >>> re.get_resources(re.ResourceRX(1.23, 0), config=config) - {'gate_types': defaultdict(, {'T': 21}), 'num_gates': 21, 'num_wires': 1} - """ - - @staticmethod - def _resource_decomp(config, **kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - config (dict): a dictionary containing the error threshold - - Resources: - A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The - resources are approximating the gate with a series of T gates. The expected T-count is taken - from (the 'Simulation Results' section) `Efficient Synthesis of Universal Repeat-Until-Success - Circuits `_. The cost is given as: - - .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil - - """ - return _rotation_resources(epsilon=config["error_rx"]) - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of a single qubit rotation changes the sign of the rotation angle, - thus the resources of the adjoint operation result in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - For a single control wire, the cost is a single instance of :class:`~.ResourceCRX`. - Two additional :class:`~.ResourceX` gates are used to flip the control qubit if - it is zero-controlled. - - In the case where multiple controlled wires are provided, the resources are taken - from Figure 1b of the paper `T-count and T-depth of any multi-qubit unitary - `_. In combination with the following identity: - - .. math:: \hat{RX} = \hat{H} \cdot \hat{RZ} \cdot \hat{H}, - - we can express the :code:`CRX` gate as a :code:`CRZ` gate conjugated by :code:`Hadamard` - gates. The expression for controlled-RZ gates is used as defined in the reference above. - By replacing the :code:`X` gates with multi-controlled :code:`X` gates, we obtain a - controlled-version of that identity. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires == 1: - gate_types = {re.ResourceCRX.resource_rep(): 1} - - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 - - return gate_types - - gate_types = {} - - h = re.ResourceHadamard.resource_rep() - rz = re.ResourceRZ.resource_rep() - mcx = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - gate_types[mcx] = 2 - gate_types[rz] = 2 - gate_types[h] = 2 - - return gate_types - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a single qubit rotation produces a sum of rotations. - The resources simplify to just one total single qubit rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if z == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} - - -class ResourceRY(qml.RY, re.ResourceOperator): - r"""Resource class for the RY gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int] or int): the wire the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The - resources are approximating the gate with a series of T gates. The expected T-count is taken - from (the 'Simulation Results' section) `Efficient Synthesis of Universal Repeat-Until-Success - Circuits `_. The cost is given as: - - .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil - - .. seealso:: :class:`~.RY` - - **Example** - - The resources for this operation are computed using: - - >>> re.get_resources(re.ResourceRY(1.23, 0)) - {'gate_types': defaultdict(, {'T': 17}), 'num_gates': 17, 'num_wires': 1} - - The operation does not require any parameters directly, however, it will depend on the single - qubit error threshold, which can be set using a config dictionary. - - >>> config = {"error_ry": 1e-3} - >>> re.get_resources(re.ResourceRY(1.23, 0), config=config) - {'gate_types': defaultdict(, {'T': 21}), 'num_gates': 21, 'num_wires': 1} - """ - - @staticmethod - def _resource_decomp(config, **kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The - resources are approximating the gate with a series of T gates. The expected T-count is taken - from (the 'Simulation Results' section) `Efficient Synthesis of Universal Repeat-Until-Success - Circuits `_. The cost is given as: - - .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil - - Args: - config (dict): a dictionary containing the error threshold - """ - return _rotation_resources(epsilon=config["error_ry"]) - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of a single qubit rotation changes the sign of the rotation angle, - thus the resources of the adjoint operation result in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - For a single control wire, the cost is a single instance of :class:`~.ResourceCRY`. - Two additional :class:`~.ResourceX` gates are used to flip the control qubit if - it is zero-controlled. - - In the case where multiple controlled wires are provided, the resources are taken - from Figure 1b of the paper `T-count and T-depth of any multi-qubit - unitary `_. The resources are derived with the - following identity: - - .. math:: \hat{RY}(\theta) = \hat{X} \cdot \hat{RY}(- \theta) \cdot \hat{X}. - - By replacing the :code:`X` gates with multi-controlled :code:`X` gates, we obtain a - controlled-version of this identity. Thus we are able to constructively or destructively - interfere the gates based on the value of the control qubits. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires == 1: - gate_types = {re.ResourceCRY.resource_rep(): 1} - - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 - - return gate_types - - ry = re.ResourceRY.resource_rep() - mcx = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - return {ry: 2, mcx: 2} - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a single qubit rotation produces a sum of rotations. - The resources simplify to just one total single qubit rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if z == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} - - -class ResourceRZ(qml.RZ, re.ResourceOperator): - r"""Resource class for the RZ gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int] or int): the wire the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The - resources are approximating the gate with a series of T gates. The expected T-count is taken - from (the 'Simulation Results' section) `Efficient Synthesis of Universal Repeat-Until-Success - Circuits `_. The cost is given as: - - .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil - - .. seealso:: :class:`~.RZ` - - **Example** - - The resources for this operation are computed using: - - >>> re.get_resources(re.ResourceRZ(1.23, 0)) - {'gate_types': defaultdict(, {'T': 17}), 'num_gates': 17, 'num_wires': 1} - - The operation does not require any parameters directly, however, it will depend on the single - qubit error threshold, which can be set using a config dictionary. - - >>> config = {"error_rz": 1e-3} - >>> re.get_resources(re.ResourceRZ(1.23, 0), config=config) - {'gate_types': defaultdict(, {'T': 21}), 'num_gates': 21, 'num_wires': 1}s - """ - - @staticmethod - def _resource_decomp(config, **kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The - resources are approximating the gate with a series of T gates. The expected T-count is taken - from (the 'Simulation Results' section) `Efficient Synthesis of Universal Repeat-Until-Success - Circuits `_. The cost is given as: - - .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil - - Args: - config (dict): a dictionary containing the error threshold - """ - return _rotation_resources(epsilon=config["error_rz"]) - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of a single qubit rotation changes the sign of the rotation angle, - thus the resources of the adjoint operation result in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - For a single control wire, the cost is a single instance of :class:`~.ResourceCRY`. - Two additional :class:`~.ResourceX` gates are used to flip the control qubit if - it is zero-controlled. - - In the case where multiple controlled wires are provided, the resources are obtained - from Figure 1b of the paper `T-count and T-depth of any multi-qubit unitary - `_. They are derived from the following identity: - - .. math:: \hat{RZ}(\theta) = \hat{X} \cdot \hat{RZ}(- \theta) \cdot \hat{X}. - - By replacing the :code:`X` gates with multi-controlled :code:`X` gates, we obtain a - controlled-version of this identity. Thus we are able to constructively or destructively - interfere the gates based on the value of the control qubits. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires == 1: - gate_types = {re.ResourceCRZ.resource_rep(): 1} - - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 - - return gate_types - - rz = re.ResourceRZ.resource_rep() - mcx = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - return {rz: 2, mcx: 2} - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a single qubit rotation produces a sum of rotations. - The resources simplify to just one total single qubit rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if z == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} - - -class ResourceRot(qml.Rot, re.ResourceOperator): - r"""Resource class for the Rot-gate. - - Args: - phi (float): rotation angle :math:`\phi` - theta (float): rotation angle :math:`\theta` - omega (float): rotation angle :math:`\omega` - wires (Any, Wires): the wire the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are obtained according to the definition of the :class:`Rot` gate: - - .. math:: \hat{R}(\omega, \theta, \phi) = \hat{RZ}(\omega) \cdot \hat{RY}(\theta) \cdot \hat{RZ}(\phi). - - .. seealso:: :class:`~.Rot` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceRot.resources() - {RY: 1, RZ: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are obtained according to the definition of the :class:`Rot` gate: - - .. math:: \hat{R}(\omega, \theta, \phi) = \hat{RZ}(\omega) \cdot \hat{RY}(\theta) \cdot \hat{RZ}(\phi). - - """ - ry = ResourceRY.resource_rep() - rz = ResourceRZ.resource_rep() - - gate_types = {ry: 1, rz: 2} - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - @classmethod - def adjoint_resource_decomp(cls) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Resources: - The adjoint of a general single qubit rotation changes the sign of the rotation angles, - thus the resources of the adjoint operation results in the original operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(): 1} - - @staticmethod - def controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - For a single control wire, the cost is a single instance of :class:`~.ResourceCRot`. - Two additional :class:`~.ResourceX` gates are used to flip the control qubit if - it is zero-controlled. - - In the case where multiple controlled wires are provided, the resources are derived - from Figure 1b of the paper `T-count and T-depth of any multi-qubit unitary - `_. The resources are derived with the following - identities: - - .. math:: - - \begin{align} - \hat{RZ}(\theta) = \hat{X} \cdot \hat{RZ}(- \theta) \cdot \hat{X}, \\ - \hat{RY}(\theta) = \hat{X} \cdot \hat{RY}(- \theta) \cdot \hat{X}. - \end{align} - - This identity is applied along with some clever choices for the angle values to combine - rotations; the final circuit takes the form: - - .. code-block:: bash - - ctrl: ─────╭●─────────╭●─────────┤ - trgt: ──RZ─╰X──RZ──RY─╰X──RY──RZ─┤ - - The :code:`CNOT` gates are replaced with multi-controlled X-gates to generalize to the - multi-controlled case. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if num_ctrl_wires == 1: - gate_types = {re.ResourceCRot.resource_rep(): 1} - - if num_ctrl_values: - gate_types[re.ResourceX.resource_rep()] = 2 - - return gate_types - - gate_types = {} - - rz = re.ResourceRZ.resource_rep() - ry = re.ResourceRY.resource_rep() - mcx = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=num_ctrl_wires, - num_ctrl_values=num_ctrl_values, - num_work_wires=num_work_wires, - ) - - gate_types[mcx] = 2 - gate_types[rz] = 3 - gate_types[ry] = 2 - - return gate_types - - @classmethod - def pow_resource_decomp(cls, z) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Resources: - Taking arbitrary powers of a general single qubit rotation produces a sum of rotations. - The resources simplify to just one total single qubit rotation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - if z == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} diff --git a/pennylane/labs/resource_estimation/ops/qubit/qchem_ops.py b/pennylane/labs/resource_estimation/ops/qubit/qchem_ops.py deleted file mode 100644 index 58605336caa..00000000000 --- a/pennylane/labs/resource_estimation/ops/qubit/qchem_ops.py +++ /dev/null @@ -1,689 +0,0 @@ -# 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. -r"""Resource operators for qchem operations.""" -import pennylane as qml -import pennylane.labs.resource_estimation as re - -# pylint: disable=arguments-differ - - -class ResourceSingleExcitation(qml.SingleExcitation, re.ResourceOperator): - r"""Resource class for the SingleExcitation gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int]): the wires the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are obtained by decomposing the following matrix into fundamental gates. - - .. math:: U(\phi) = \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & \cos(\phi/2) & -\sin(\phi/2) & 0 \\ - 0 & \sin(\phi/2) & \cos(\phi/2) & 0 \\ - 0 & 0 & 0 & 1 - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ──T†──H───S─╭X──RZ-─╭X──S†──H──T─┤ - 1: ──T†──S†──H─╰●──RY──╰●──H───S──T─┤ - - .. seealso:: :class:`~.SingleExcitation` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceSingleExcitation.resources() - {Adjoint(T): 2, Hadamard: 4, S: 2, Adjoint(S): 2, CNOT: 2, RZ: 1, RY: 1, T: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are obtained by decomposing the following matrix into fundamental gates. - - .. math:: U(\phi) = \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & \cos(\phi/2) & -\sin(\phi/2) & 0 \\ - 0 & \sin(\phi/2) & \cos(\phi/2) & 0 \\ - 0 & 0 & 0 & 1 - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ──T†──H───S─╭X──RZ-─╭X──S†──H──T─┤ - 1: ──T†──S†──H─╰●──RY──╰●──H───S──T─┤ - - """ - t_dag = re.ResourceAdjoint.resource_rep(re.ResourceT, {}) - h = re.ResourceHadamard.resource_rep() - s = re.ResourceS.resource_rep() - s_dag = re.ResourceAdjoint.resource_rep(re.ResourceS, {}) - cnot = re.ResourceCNOT.resource_rep() - rz = re.ResourceRZ.resource_rep() - ry = re.ResourceRY.resource_rep() - t = re.ResourceT.resource_rep() - - gate_types = {} - gate_types[t_dag] = 2 - gate_types[h] = 4 - gate_types[s] = 2 - gate_types[s_dag] = 2 - gate_types[cnot] = 2 - gate_types[rz] = 1 - gate_types[ry] = 1 - gate_types[t] = 2 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - -class ResourceSingleExcitationMinus(qml.SingleExcitationMinus, re.ResourceOperator): - r"""Resource class for the SingleExcitationMinus gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int] or int): the wires the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are obtained by decomposing the following matrix into fundamental gates. - - .. math:: U_-(\phi) = \begin{bmatrix} - e^{-i\phi/2} & 0 & 0 & 0 \\ - 0 & \cos(\phi/2) & -\sin(\phi/2) & 0 \\ - 0 & \sin(\phi/2) & \cos(\phi/2) & 0 \\ - 0 & 0 & 0 & e^{-i\phi/2} - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ──X─╭Rϕ────X─╭●────╭●─╭RY───╭●─┤ - 1: ──X─╰●─────X─╰Rϕ───╰X─╰●────╰X─┤ - - .. seealso:: :class:`~.SingleExcitationMinus` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceSingleExcitationMinus.resources() - {X: 4, ControlledPhaseShift: 2, CNOT: 2, CRY: 1} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are obtained by decomposing the following matrix into fundamental gates. - - .. math:: U_-(\phi) = \begin{bmatrix} - e^{-i\phi/2} & 0 & 0 & 0 \\ - 0 & \cos(\phi/2) & -\sin(\phi/2) & 0 \\ - 0 & \sin(\phi/2) & \cos(\phi/2) & 0 \\ - 0 & 0 & 0 & e^{-i\phi/2} - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ──X─╭Rϕ────X─╭●────╭●─╭RY───╭●─┤ - 1: ──X─╰●─────X─╰Rϕ───╰X─╰●────╰X─┤ - - """ - x = re.ResourceX.resource_rep() - ctrl_phase_shift = re.ResourceControlledPhaseShift.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - cry = re.ResourceCRY.resource_rep() - - gate_types = {} - gate_types[x] = 4 - gate_types[ctrl_phase_shift] = 2 - gate_types[cnot] = 2 - gate_types[cry] = 1 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - -class ResourceSingleExcitationPlus(qml.SingleExcitationPlus, re.ResourceOperator): - r"""Resource class for the SingleExcitationPlus gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int] or int): the wires the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are obtained by decomposing the following matrix into fundamental gates. - - .. math:: U_+(\phi) = \begin{bmatrix} - e^{i\phi/2} & 0 & 0 & 0 \\ - 0 & \cos(\phi/2) & -\sin(\phi/2) & 0 \\ - 0 & \sin(\phi/2) & \cos(\phi/2) & 0 \\ - 0 & 0 & 0 & e^{i\phi/2} - \end{bmatrix}. - - The cost for implmementing this transformation is given by: - - .. code-block:: bash - - 0: ──X─╭Rϕ──X─╭●───╭●─╭RY──╭●─┤ - 1: ──X─╰●───X─╰Rϕ──╰X─╰●───╰X─┤ - - .. seealso:: :class:`~.SingleExcitationPlus` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceSingleExcitationPlus.resources() - {X: 4, ControlledPhaseShift: 2, CNOT: 2, CRY: 1} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are obtained by decomposing the following matrix into fundamental gates. - - .. math:: U_+(\phi) = \begin{bmatrix} - e^{i\phi/2} & 0 & 0 & 0 \\ - 0 & \cos(\phi/2) & -\sin(\phi/2) & 0 \\ - 0 & \sin(\phi/2) & \cos(\phi/2) & 0 \\ - 0 & 0 & 0 & e^{i\phi/2} - \end{bmatrix}. - - The cost for implmementing this transformation is given by: - - .. code-block:: bash - - 0: ──X─╭Rϕ──X─╭●───╭●─╭RY──╭●─┤ - 1: ──X─╰●───X─╰Rϕ──╰X─╰●───╰X─┤ - - """ - x = re.ResourceX.resource_rep() - ctrl_phase_shift = re.ResourceControlledPhaseShift.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - cry = re.ResourceCRY.resource_rep() - - gate_types = {} - gate_types[x] = 4 - gate_types[ctrl_phase_shift] = 2 - gate_types[cnot] = 2 - gate_types[cry] = 1 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - -class ResourceDoubleExcitation(qml.DoubleExcitation, re.ResourceOperator): - r"""Resource class for the DoubleExcitation gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int]): the wires the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are obtained by decomposing the following mapping into fundamental gates. - - .. math:: - - &|0011\rangle \rightarrow \cos(\phi/2) |0011\rangle + \sin(\phi/2) |1100\rangle\\ - &|1100\rangle \rightarrow \cos(\phi/2) |1100\rangle - \sin(\phi/2) |0011\rangle, - - For the source of this decomposition, see page 17 of `"Local, Expressive, - Quantum-Number-Preserving VQE Ansatze for Fermionic Systems" `_ . - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ────╭●──H─╭●──RY───╭●──RY─────────────╭X──RY──────────╭●──RY───╭●─╭X──H──╭●────┤ - 1: ────│─────╰X──RY───│───────╭X──RY──╭X─│───RY────╭X────│───RY───╰X─│──────│─────┤ - 2: ─╭●─╰X─╭●──────────│───────│───────╰●─╰●────────│─────│───────────╰●─────╰X─╭●─┤ - 3: ─╰X──H─╰X──────────╰X──H───╰●───────────────────╰●──H─╰X──H─────────────────╰X─┤ - - .. seealso:: :class:`~.DoubleExcitation` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceDoubleExcitation.resources() - {Hadamard: 6, RY: 8, CNOT: 14} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are obtained by decomposing the following mapping into fundamental gates. - - .. math:: - - &|0011\rangle \rightarrow \cos(\phi/2) |0011\rangle + \sin(\phi/2) |1100\rangle\\ - &|1100\rangle \rightarrow \cos(\phi/2) |1100\rangle - \sin(\phi/2) |0011\rangle, - - For the source of this decomposition, see page 17 of `"Local, Expressive, - Quantum-Number-Preserving VQE Ansatze for Fermionic Systems" `_ . - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ────╭●──H─╭●──RY───╭●──RY─────────────╭X──RY──────────╭●──RY───╭●─╭X──H──╭●────┤ - 1: ────│─────╰X──RY───│───────╭X──RY──╭X─│───RY────╭X────│───RY───╰X─│──────│─────┤ - 2: ─╭●─╰X─╭●──────────│───────│───────╰●─╰●────────│─────│───────────╰●─────╰X─╭●─┤ - 3: ─╰X──H─╰X──────────╰X──H───╰●───────────────────╰●──H─╰X──H─────────────────╰X─┤ - - """ - h = re.ResourceHadamard.resource_rep() - ry = re.ResourceRY.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - - gate_types = {} - gate_types[h] = 6 - gate_types[ry] = 8 - gate_types[cnot] = 14 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - -class ResourceDoubleExcitationMinus(qml.DoubleExcitationMinus, re.ResourceOperator): - r"""Resource class for the DoubleExcitationMinus gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int]): the wires the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are obtained by decomposing the following mapping into fundamental gates. - - .. math:: - - &|0011\rangle \rightarrow \cos(\phi/2) |0011\rangle - \sin(\phi/2) |1100\rangle\\ - &|1100\rangle \rightarrow \cos(\phi/2) |1100\rangle + \sin(\phi/2) |0011\rangle\\ - &|x\rangle \rightarrow e^{-i\phi/2} |x\rangle, - - Specifically, the resources are given by one :class:`~.ResourceDoubleExcitation`, one - :class:`~.ResourcePhaseShift` gate, two multi-controlled Z-gates controlled on 3 qubits, - and two multi-controlled phase shift gates controlled on 3 qubits. - - .. seealso:: :class:`~.DoubleExcitationMinus` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceDoubleExcitationMinus.resources() - {GlobalPhase: 1, DoubleExcitation: 1, C(Z,3,1,0): 2, C(PhaseShift,3,1,0): 2} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are obtained by decomposing the following mapping into fundamental gates. - - .. math:: - - &|0011\rangle \rightarrow \cos(\phi/2) |0011\rangle - \sin(\phi/2) |1100\rangle\\ - &|1100\rangle \rightarrow \cos(\phi/2) |1100\rangle + \sin(\phi/2) |0011\rangle\\ - &|x\rangle \rightarrow e^{-i\phi/2} |x\rangle, - - Specifically, the resources are given by one :class:`~.ResourceDoubleExcitation`, one - :class:`~.ResourcePhaseShift` gate, two multi-controlled Z-gates controlled on 3 qubits, - and two multi-controlled phase shift gates controlled on 3 qubits. - """ - phase = re.ResourceGlobalPhase.resource_rep() - double = re.ResourceDoubleExcitation.resource_rep() - ctrl_z = re.ResourceControlled.resource_rep(re.ResourceZ, {}, 3, 1, 0) - ctrl_phase = re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 3, 1, 0) - - gate_types = {} - gate_types[phase] = 1 - gate_types[double] = 1 - gate_types[ctrl_z] = 2 - gate_types[ctrl_phase] = 2 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - -class ResourceDoubleExcitationPlus(qml.DoubleExcitationPlus, re.ResourceOperator): - r"""Resource class for the DoubleExcitationPlus gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int]): the wires the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are obtained by decomposing the following mapping into fundamental gates. - - .. math:: - - &|0011\rangle \rightarrow \cos(\phi/2) |0011\rangle - \sin(\phi/2) |1100\rangle\\ - &|1100\rangle \rightarrow \cos(\phi/2) |1100\rangle + \sin(\phi/2) |0011\rangle\\ - &|x\rangle \rightarrow e^{-i\phi/2} |x\rangle, - - Specifically, the resources are given by one :class:`~.ResourceDoubleExcitation`, one - :class:`~.ResourcePhaseShift` gate, two multi-controlled Z-gates controlled on 3 qubits, - and two multi-controlled phase shift gates controlled on 3 qubits. - - .. seealso:: :class:`~.DoubleExcitationPlus` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceDoubleExcitationPlus.resources() - {GlobalPhase: 1, DoubleExcitation: 1, C(Z,3,1,0): 2, C(PhaseShift,3,1,0): 2} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are obtained by decomposing the following mapping into fundamental gates. - - .. math:: - - &|0011\rangle \rightarrow \cos(\phi/2) |0011\rangle - \sin(\phi/2) |1100\rangle\\ - &|1100\rangle \rightarrow \cos(\phi/2) |1100\rangle + \sin(\phi/2) |0011\rangle\\ - &|x\rangle \rightarrow e^{-i\phi/2} |x\rangle, - - Specifically, the resources are given by one :class:`~.ResourceDoubleExcitation`, one - :class:`~.ResourcePhaseShift` gate, two multi-controlled Z-gates controlled on 3 qubits, - and two multi-controlled phase shift gates controlled on 3 qubits. - """ - phase = re.ResourceGlobalPhase.resource_rep() - double = re.ResourceDoubleExcitation.resource_rep() - ctrl_z = re.ResourceControlled.resource_rep(re.ResourceZ, {}, 3, 1, 0) - ctrl_phase = re.ResourceControlled.resource_rep(re.ResourcePhaseShift, {}, 3, 1, 0) - - gate_types = {} - gate_types[phase] = 1 - gate_types[double] = 1 - gate_types[ctrl_z] = 2 - gate_types[ctrl_phase] = 2 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - -class ResourceOrbitalRotation(qml.OrbitalRotation, re.ResourceOperator): - r"""Resource class for the OrbitalRotation gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int]): the wires the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are obtained by decomposing the following mapping into fundamental gates. - - .. math:: - &|\Phi_{0}\rangle = \cos(\phi/2)|\Phi_{0}\rangle - \sin(\phi/2)|\Phi_{1}\rangle\\ - &|\Phi_{1}\rangle = \cos(\phi/2)|\Phi_{0}\rangle + \sin(\phi/2)|\Phi_{1}\rangle, - - Specifically, the resources are given by two :class:`~.ResourceSingleExcitation` gates and - two :class:`~.ResourceFermionicSWAP` gates. - - .. seealso:: :class:`~.OrbitalRotation` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceOrbitalRotation.resources() - {FermionicSWAP: 2, SingleExcitation: 2} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are obtained by decomposing the following mapping into fundamental gates. - - .. math:: - &|\Phi_{0}\rangle = \cos(\phi/2)|\Phi_{0}\rangle - \sin(\phi/2)|\Phi_{1}\rangle\\ - &|\Phi_{1}\rangle = \cos(\phi/2)|\Phi_{0}\rangle + \sin(\phi/2)|\Phi_{1}\rangle, - - Specifically, the resources are given by two :class:`~.ResourceSingleExcitation` gates and - two :class:`~.ResourceFermionicSWAP` gates. - """ - fermionic_swap = re.ResourceFermionicSWAP.resource_rep() - single_excitation = re.ResourceSingleExcitation.resource_rep() - - gate_types = {} - gate_types[fermionic_swap] = 2 - gate_types[single_excitation] = 2 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) - - -class ResourceFermionicSWAP(qml.FermionicSWAP, re.ResourceOperator): - r"""Resource class for the FermionicSWAP gate. - - Args: - phi (float): rotation angle :math:`\phi` - wires (Sequence[int]): the wires the operation acts on - id (str or None): String representing the operation (optional) - - Resources: - The resources are obtained by decomposing the following matrix into fundamental gates. - - .. math:: U(\phi) = \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & e^{i \phi/2} \cos(\phi/2) & -ie^{i \phi/2} \sin(\phi/2) & 0 \\ - 0 & -ie^{i \phi/2} \sin(\phi/2) & e^{i \phi/2} \cos(\phi/2) & 0 \\ - 0 & 0 & 0 & e^{i \phi} - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ──H─╭MultiRZ──H──RX─╭MultiRZ──RX──RZ─╭GlobalPhase─┤ - 1: ──H─╰MultiRZ──H──RX─╰MultiRZ──RX──RZ─╰GlobalPhase─┤ - - .. seealso:: :class:`~.FermionicSWAP` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceFermionicSWAP.resources() - {Hadamard: 4, MultiRZ: 2, RX: 4, RZ: 2, GlobalPhase: 1} - """ - - @staticmethod - def _resource_decomp(**kwargs): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Resources: - The resources are obtained by decomposing the following matrix into fundamental gates. - - .. math:: U(\phi) = \begin{bmatrix} - 1 & 0 & 0 & 0 \\ - 0 & e^{i \phi/2} \cos(\phi/2) & -ie^{i \phi/2} \sin(\phi/2) & 0 \\ - 0 & -ie^{i \phi/2} \sin(\phi/2) & e^{i \phi/2} \cos(\phi/2) & 0 \\ - 0 & 0 & 0 & e^{i \phi} - \end{bmatrix}. - - The cost for implementing this transformation is given by: - - .. code-block:: bash - - 0: ──H─╭MultiRZ──H──RX─╭MultiRZ──RX──RZ─╭GlobalPhase─┤ - 1: ──H─╰MultiRZ──H──RX─╰MultiRZ──RX──RZ─╰GlobalPhase─┤ - - """ - h = re.ResourceHadamard.resource_rep() - multi_rz = re.ResourceMultiRZ.resource_rep(num_wires=2) - rx = re.ResourceRX.resource_rep() - rz = re.ResourceRZ.resource_rep() - phase = re.ResourceGlobalPhase.resource_rep() - - gate_types = {} - gate_types[h] = 4 - gate_types[multi_rz] = 2 - gate_types[rx] = 4 - gate_types[rz] = 2 - gate_types[phase] = 1 - - return gate_types - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. - """ - return {} - - @classmethod - def resource_rep(cls): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - return re.CompressedResourceOp(cls, {}) diff --git a/pennylane/labs/resource_estimation/resource_container.py b/pennylane/labs/resource_estimation/resource_container.py deleted file mode 100644 index 24d2bc86cff..00000000000 --- a/pennylane/labs/resource_estimation/resource_container.py +++ /dev/null @@ -1,347 +0,0 @@ -# Copyright 2024 Xanadu Quantum Technologies Inc. - -# 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. -r"""Base classes for resource estimation.""" -import copy -from collections import defaultdict - -from pennylane.decomposition.resources import CompressedResourceOp as _CompressedResourceOp -from pennylane.labs.resource_estimation import ResourceOperator - - -class CompressedResourceOp(_CompressedResourceOp): # pylint: disable=too-few-public-methods - r"""Instantiate the light weight class corresponding to the operator type and parameters. - - Args: - op_type (Type): the class object of an operation which inherits from :class:`~.ResourceOperator` - params (dict): a dictionary containing the minimal pairs of parameter names and values - required to compute the resources for the given operator - - .. details:: - - This representation is the minimal amount of information required to estimate resources for the operator. - - **Example** - - >>> op_tp = CompressedResourceOp(ResourceHadamard, {"num_wires":1}) - >>> print(op_tp) - Hadamard(num_wires=1) - """ - - def __init__(self, op_type, params: dict, name=None) -> None: - r"""Instantiate the light weight class corresponding to the operator type and parameters. - - Args: - op_type (Type): the class object for an operation which inherits from :class:`~.ResourceOperator` - params (dict): a dictionary containing the minimal pairs of parameter names and values - required to compute the resources for the given operator - - .. details:: - - This representation is the minimal amount of information required to estimate resources for the operator. - - **Example** - - >>> op_tp = CompressedResourceOp(ResourceHadamard, {"num_wires":1}) - >>> print(op_tp) - Hadamard(num_wires=1) - """ - if not issubclass(op_type, ResourceOperator): - raise TypeError(f"op_type must be a subclass of ResourceOperator. Got {op_type}.") - - super().__init__(op_type, params) - self._name = name or op_type.tracking_name(**params) - - def __repr__(self) -> str: - return self._name - - -# @dataclass -class Resources: - r"""Contains attributes which store key resources such as number of gates, number of wires, and gate types. - - Args: - num_wires (int): number of qubits - num_gates (int): number of gates - gate_types (dict): dictionary storing operation names (str) as keys - and the number of times they are used in the circuit (int) as values - - .. details:: - - The resources being tracked can be accessed as class attributes. - Additionally, the :class:`~.Resources` instance can be nicely displayed in the console. - - **Example** - - >>> r = Resources( - ... num_wires=2, - ... num_gates=2, - ... gate_types={"Hadamard": 1, "CNOT": 1} - ... ) - >>> print(r) - wires: 2 - gates: 2 - gate_types: - {'Hadamard': 1, 'CNOT': 1} - """ - - def __init__(self, num_wires: int = 0, num_gates: int = 0, gate_types: dict = None): - gate_types = gate_types or {} - - self.num_wires = num_wires - self.num_gates = num_gates - self.gate_types = ( - gate_types - if (isinstance(gate_types, defaultdict) and isinstance(gate_types.default_factory, int)) - else defaultdict(int, gate_types) - ) - - def __add__(self, other: "Resources") -> "Resources": - """Add two resources objects in series""" - return add_in_series(self, other) - - def __eq__(self, other: "Resources") -> bool: - """Test if two resource objects are equal""" - if self.num_wires != other.num_wires: - return False - if self.num_gates != other.num_gates: - return False - - return self.gate_types == other.gate_types - - def __mul__(self, scalar: int) -> "Resources": - """Scale a resources object in series""" - return mul_in_series(self, scalar) - - __rmul__ = __mul__ # same implementation - - def __iadd__(self, other: "Resources") -> "Resources": - """Add two resources objects in series""" - return add_in_series(self, other, in_place=True) - - def __imull__(self, scalar: int) -> "Resources": - """Scale a resources object in series""" - return mul_in_series(self, scalar, in_place=True) - - def __str__(self): - """String representation of the Resources object.""" - keys = ["wires", "gates"] - vals = [self.num_wires, self.num_gates] - items = "\n".join([str(i) for i in zip(keys, vals)]) - items = items.replace("('", "") - items = items.replace("',", ":") - items = items.replace(")", "") - - gate_type_str = ", ".join( - [f"'{gate_name}': {count}" for gate_name, count in self.gate_types.items()] - ) - items += "\ngate_types:\n{" + gate_type_str + "}" - return items - - def __repr__(self): - """Compact string representation of the Resources object""" - return { - "gate_types": self.gate_types, - "num_gates": self.num_gates, - "num_wires": self.num_wires, - }.__repr__() - - def _ipython_display_(self): - """Displays __str__ in ipython instead of __repr__""" - print(str(self)) - - -def add_in_series(first: Resources, other: Resources, in_place=False) -> Resources: - r"""Add two resources assuming the circuits are executed in series. - - Args: - first (Resources): first resource object to combine - other (Resources): other resource object to combine with - in_place (bool): determines if the first Resources are modified in place (default False) - - Returns: - Resources: combined resources - """ - new_wires = max(first.num_wires, other.num_wires) - new_gates = first.num_gates + other.num_gates - new_gate_types = _combine_dict(first.gate_types, other.gate_types, in_place=in_place) - - if in_place: - first.num_wires = new_wires - first.num_gates = new_gates - return first - - return Resources(new_wires, new_gates, new_gate_types) - - -def add_in_parallel(first: Resources, other: Resources, in_place=False) -> Resources: - r"""Add two resources assuming the circuits are executed in parallel. - - Args: - first (Resources): first resource object to combine - other (Resources): other resource object to combine with - in_place (bool): determines if the first Resources are modified in place (default False) - - Returns: - Resources: combined resources - """ - new_wires = first.num_wires + other.num_wires - new_gates = first.num_gates + other.num_gates - new_gate_types = _combine_dict(first.gate_types, other.gate_types, in_place=in_place) - - if in_place: - first.num_wires = new_wires - first.num_gates = new_gates - return first - - return Resources(new_wires, new_gates, new_gate_types) - - -def mul_in_series(first: Resources, scalar: int, in_place=False) -> Resources: - r"""Multiply the resources by a scalar assuming the circuits are executed in series. - - Args: - first (Resources): first resource object to combine - scalar (int): integer value to scale the resources by - in_place (bool): determines if the first Resources are modified in place (default False) - - Returns: - Resources: combined resources - """ - new_gates = scalar * first.num_gates - new_gate_types = _scale_dict(first.gate_types, scalar, in_place=in_place) - - if in_place: - first.num_gates = new_gates - return first - - return Resources(first.num_wires, new_gates, new_gate_types) - - -def mul_in_parallel(first: Resources, scalar: int, in_place=False) -> Resources: - r"""Multiply the resources by a scalar assuming the circuits are executed in parallel. - - Args: - first (Resources): first resource object to combine - scalar (int): integer value to scale the resources by - in_place (bool): determines if the first Resources are modified in place (default False) - - Returns: - Resources: combined resources - """ - new_wires = scalar * first.num_wires - new_gates = scalar * first.num_gates - new_gate_types = _scale_dict(first.gate_types, scalar, in_place=in_place) - - if in_place: - first.num_wires = new_wires - first.num_gates = new_gates - return first - - return Resources(new_wires, new_gates, new_gate_types) - - -def substitute( - initial_resources: Resources, gate_name: str, replacement_resources: Resources, in_place=False -) -> Resources: - """Replaces a specified gate in a :class:`~.resource.Resources` object with the contents of - another :class:`~.resource.Resources` object. - - Args: - initial_resources (Resources): the resources to be modified - gate_name (str): the name of the operation to be replaced - replacement (Resources): the resources to be substituted instead of the gate - in_place (bool): determines if the initial resources are modified in place or if a new copy is - created - - Returns: - Resources: the updated :class:`~.Resources` after substitution - - .. details:: - - **Example** - - In this example we replace the resources for the :code:`RX` gate: - - .. code-block:: python3 - - from pennylane.labs.resource_estimation import Resources - - replace_gate_name = "RX" - - initial_resources = Resources( - num_wires = 2, - num_gates = 3, - gate_types = {"RX": 2, "CNOT": 1}, - ) - - replacement_rx_resources = Resources( - num_wires = 1, - num_gates = 7, - gate_types = {"Hadamard": 3, "S": 4}, - ) - - Executing the substitution produces: - - >>> from pennylane.labs.resource_estimation import substitute - >>> res = substitute( - ... initial_resources, replace_gate_name, replacement_rx_resources, - ... ) - >>> print(res) - wires: 2 - gates: 15 - gate_types: - {'CNOT': 1, 'Hadamard': 6, 'S': 8} - """ - - count = initial_resources.gate_types.get(gate_name, 0) - - if count > 0: - new_gates = initial_resources.num_gates - count + (count * replacement_resources.num_gates) - - replacement_gate_types = _scale_dict( - replacement_resources.gate_types, count, in_place=in_place - ) - new_gate_types = _combine_dict( - initial_resources.gate_types, replacement_gate_types, in_place=in_place - ) - new_gate_types.pop(gate_name) - - if in_place: - initial_resources.num_gates = new_gates - return initial_resources - - return Resources(initial_resources.num_wires, new_gates, new_gate_types) - - return initial_resources - - -def _combine_dict(dict1: defaultdict, dict2: defaultdict, in_place=False): - r"""Private function which combines two dictionaries together.""" - combined_dict = dict1 if in_place else copy.copy(dict1) - - for k, v in dict2.items(): - combined_dict[k] += v - - return combined_dict - - -def _scale_dict(dict1: defaultdict, scalar: int, in_place=False): - r"""Private function which scales the values in a dictionary.""" - - combined_dict = dict1 if in_place else copy.copy(dict1) - - for k in combined_dict: - combined_dict[k] *= scalar - - return combined_dict diff --git a/pennylane/labs/resource_estimation/resource_operator.py b/pennylane/labs/resource_estimation/resource_operator.py deleted file mode 100644 index b5205c51d66..00000000000 --- a/pennylane/labs/resource_estimation/resource_operator.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""Abstract base class for resource operators.""" -from __future__ import annotations - -from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Callable, Dict - -if TYPE_CHECKING: - from pennylane.labs.resource_estimation import CompressedResourceOp - -# pylint: disable=unused-argument - - -class ResourceOperator(ABC): - r"""Abstract class that defines the methods a PennyLane Operator - must implement in order to be used for resource estimation. - - .. details:: - - **Example** - - A PennyLane Operator can be extended for resource estimation by creating a new class that - inherits from both the ``Operator`` and ``ResourceOperator``. Here is an example showing how to - extend ``qml.QFT`` for resource estimation. - - .. code-block:: python - - import pennylane as qml - from pennylane.labs.resource_estimation import CompressedResourceOp, ResourceOperator - - class ResourceQFT(qml.QFT, ResourceOperator): - - @staticmethod - def _resource_decomp(num_wires) -> Dict[CompressedResourceOp, int]: - gate_types = {} - - hadamard = ResourceHadamard.resource_rep() - swap = ResourceSWAP.resource_rep() - ctrl_phase_shift = ResourceControlledPhaseShift.resource_rep() - - gate_types[hadamard] = num_wires - gate_types[swap] = num_wires // 2 - gate_types[ctrl_phase_shift] = num_wires*(num_wires - 1) // 2 - - return gate_types - - @property - def resource_params(self, num_wires) -> dict: - return {"num_wires": num_wires} - - @classmethod - def resource_rep(cls, num_wires) -> CompressedResourceOp: - params = {"num_wires": num_wires} - return CompressedResourceOp(cls, params) - - Which can be instantiated as a normal operation, but now contains the resources: - - .. code-block:: bash - - >>> op = ResourceQFT(range(3)) - >>> op.resources(**op.resource_params) - {Hadamard: 3, SWAP: 1, ControlledPhaseShift: 3} - - """ - - @staticmethod - @abstractmethod - def _resource_decomp(*args, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts.""" - - @classmethod - def resources(cls, *args, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts.""" - return cls._resource_decomp(*args, **kwargs) - - @classmethod - def set_resources(cls, new_func: Callable) -> None: - """Set a custom resource method.""" - cls.resources = new_func - - @property - @abstractmethod - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to - compute a compressed representation""" - - @classmethod - @abstractmethod - def resource_rep(cls, *args, **kwargs) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation.""" - - def resource_rep_from_op(self) -> CompressedResourceOp: - r"""Returns a compressed representation directly from the operator""" - return self.__class__.resource_rep(**self.resource_params) - - @classmethod - def adjoint_resource_decomp(cls, *args, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Raises: - ResourcesNotDefined: no resources implemented by default - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated values are the counts. - """ - raise ResourcesNotDefined - - @classmethod - def controlled_resource_decomp( - cls, num_ctrl_wires: int, num_ctrl_values: int, num_work_wires: int, *args, **kwargs - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Raises: - ResourcesNotDefined: no resources implemented by default - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated values are the counts. - """ - - raise ResourcesNotDefined - - @classmethod - def pow_resource_decomp(cls, z: int, *args, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - - Raises: - ResourcesNotDefined: no resources implemented by default - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated values are the counts. - """ - raise ResourcesNotDefined - - @classmethod - def exp_resource_decomp( - cls, scalar: complex, num_steps: int, *args, **kwargs - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the exponentiated operator. - - Args: - scalar (complex): complex coefficient of the operator in the exponent - num_steps (int): number of trotter steps to use when decomposing the expoentiated operator - - Raises: - ResourcesNotDefined: no resources implemented by default - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated values are the counts. - """ - raise ResourcesNotDefined - - @classmethod - def tracking_name(cls, *args, **kwargs) -> str: - r"""Returns a name used to track the operator during resource estimation.""" - return cls.__name__.replace("Resource", "") - - def tracking_name_from_op(self) -> str: - r"""Returns the tracking name built with the operator's parameters.""" - return self.__class__.tracking_name(**self.resource_params) - - -class ResourcesNotDefined(Exception): - r"""Exception to be raised when a ``ResourceOperator`` does not implement _resource_decomp""" diff --git a/pennylane/labs/resource_estimation/resource_tracking.py b/pennylane/labs/resource_estimation/resource_tracking.py deleted file mode 100644 index a2b04921a0a..00000000000 --- a/pennylane/labs/resource_estimation/resource_tracking.py +++ /dev/null @@ -1,319 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""Core resource tracking logic.""" -from collections import defaultdict -from collections.abc import Callable -from functools import singledispatch, wraps -from typing import Dict, Iterable, List, Set, Union - -import pennylane as qml -from pennylane.operation import Operation -from pennylane.queuing import AnnotatedQueue -from pennylane.tape import QuantumScript -from pennylane.wires import Wires - -from .resource_container import CompressedResourceOp, Resources -from .resource_operator import ResourceOperator - -# pylint: disable=dangerous-default-value,protected-access - -# user-friendly gateset for visual checks and initial compilation -_StandardGateSet = { - "X", - "Y", - "Z", - "Hadamard", - "SWAP", - "CNOT", - "S", - "T", - "Toffoli", - "RX", - "RY", - "RZ", - "PhaseShift", -} - -# practical/realistic gateset for useful compilation of circuits -DefaultGateSet = { - "Hadamard", - "CNOT", - "S", - "T", - "Toffoli", -} - -# parameters for further configuration of the decompositions -resource_config = { - "error_rx": 10e-3, - "error_ry": 10e-3, - "error_rz": 10e-3, -} - - -@singledispatch -def get_resources( - obj, gate_set: Set = DefaultGateSet, config: Dict = resource_config -) -> Union[Resources, Callable]: - r"""Obtain the resources from a quantum circuit or operation in terms of the gates provided - in the gate_set. - - Args: - obj (Union[Operation, Callable, QuantumScript]): the quantum circuit or operation to obtain resources from - gate_set (Set, optional): python set of strings specifying the names of operations to track - config (Dict, optional): dictionary of additiona; configurations that specify how resources are computed - - Returns: - Resources: the total resources of the quantum circuit - - Raises: - TypeError: could not obtain resources for obj of type `type(obj)` - - **Example** - - We can track the resources of a quantum workflow by passing the quantum function defining our workflow directly - into this function. - - .. code-block:: python - - import copy - import pennylane.labs.resource_estimation as re - - def my_circuit(): - for w in range(2): - re.ResourceHadamard(w) - - re.ResourceCNOT([0, 1]) - re.ResourceRX(1.23, 0) - re.ResourceRY(-4.56, 1) - - re.ResourceQFT(wires=[0, 1, 2]) - return qml.expval(re.ResourceHadamard(2)) - - Note that we are passing a python function NOT a :class:`~.QNode`. The resources for this workflow are then obtained by: - - >>> res = re.get_resources(my_circuit)() - >>> print(res) - wires: 3 - gates: 202 - gate_types: - {'Hadamard': 5, 'CNOT': 10, 'T': 187} - - .. details:: - :title: Usage Details - - Users can provide custom gatesets to track resources with. Consider :code:`my_circuit()` from above: - - >>> my_gateset = {"Hadamard", "RX", "RY", "QFT(3)", "CNOT"} - >>> print(re.get_resources(my_circuit, gate_set = my_gateset)()) - wires: 3 - gates: 6 - gate_types: - {'Hadamard': 2, 'CNOT': 1, 'RX': 1, 'RY': 1, 'QFT(3)': 1} - - We can also obtain resources for individual operations and quantum tapes in a similar manner: - - >>> op = re.ResourceRX(1.23, 0) - >>> print(re.get_resources(op)) - wires: 1 - gates: 17 - gate_types: - {'T': 17} - - Finally, we can modify the config values listed in the global :code:`labs.resource_estimation.resource_config` - dictionary to have finegrain control of how the resources are computed. - - >>> re.resource_config - {'error_rx': 0.01, 'error_ry': 0.01, 'error_rz': 0.01} - >>> - >>> my_config = copy.copy(re.resource_config) - >>> my_config["error_rx"] = 0.001 - >>> - >>> print(re.get_resources(op, config=my_config)) - wires: 1 - gates: 21 - gate_types: - {'T': 21} - - """ - - raise TypeError( - f"Could not obtain resources for obj of type {type(obj)}. obj must be one of Operation, Callable or QuantumScript" - ) - - -@get_resources.register -def resources_from_operation( - obj: Operation, gate_set: Set = DefaultGateSet, config: Dict = resource_config -) -> Resources: - """Get resources from an operation""" - - if isinstance(obj, ResourceOperator): - cp_rep = obj.resource_rep_from_op() - - gate_counts_dict = defaultdict(int) - _counts_from_compressed_res_op(cp_rep, gate_counts_dict, gate_set=gate_set, config=config) - gate_types = _clean_gate_counts(gate_counts_dict) - - n_gates = sum(gate_types.values()) - return Resources(num_wires=len(obj.wires), num_gates=n_gates, gate_types=gate_types) - - res = Resources() # TODO: Add implementation here! - return res - - -@get_resources.register -def resources_from_qfunc( - obj: Callable, gate_set: Set = DefaultGateSet, config: Dict = resource_config -) -> Callable: - """Get resources from a quantum function which queues operations""" - - @wraps(obj) - def wrapper(*args, **kwargs): - with AnnotatedQueue() as q: - obj(*args, **kwargs) - - operations = tuple(op for op in q.queue if isinstance(op, Operation)) - compressed_res_ops_lst = _operations_to_compressed_reps(operations) - - initial_gate_set = set.union(gate_set, _StandardGateSet) - - gate_counts_dict = defaultdict(int) - for cmp_rep_op in compressed_res_ops_lst: - _counts_from_compressed_res_op( - cmp_rep_op, gate_counts_dict, gate_set=initial_gate_set, config=config - ) - - # Validation: - condensed_gate_counts = defaultdict(int) - for sub_cmp_rep, counts in gate_counts_dict.items(): - _counts_from_compressed_res_op( - sub_cmp_rep, condensed_gate_counts, scalar=counts, gate_set=gate_set, config=config - ) - - clean_gate_counts = _clean_gate_counts(condensed_gate_counts) - num_gates = sum(clean_gate_counts.values()) - num_wires = len(Wires.all_wires(tuple(op.wires for op in operations))) - return Resources(num_wires=num_wires, num_gates=num_gates, gate_types=clean_gate_counts) - - return wrapper - - -@get_resources.register -def resources_from_tape( - obj: QuantumScript, gate_set: Set = DefaultGateSet, config: Dict = resource_config -) -> Resources: - """Get resources from a quantum tape""" - num_wires = obj.num_wires - operations = obj.operations - compressed_res_ops_lst = _operations_to_compressed_reps(operations) - - gate_counts_dict = defaultdict(int) - for cmp_rep_op in compressed_res_ops_lst: - _counts_from_compressed_res_op( - cmp_rep_op, gate_counts_dict, gate_set=_StandardGateSet, config=config - ) - - # Validation: - condensed_gate_counts = defaultdict(int) - for sub_cmp_rep, counts in gate_counts_dict.items(): - _counts_from_compressed_res_op( - sub_cmp_rep, condensed_gate_counts, scalar=counts, gate_set=gate_set, config=config - ) - - clean_gate_counts = _clean_gate_counts(condensed_gate_counts) - num_gates = sum(clean_gate_counts.values()) - - return Resources(num_wires=num_wires, num_gates=num_gates, gate_types=clean_gate_counts) - - -def _counts_from_compressed_res_op( - cp_rep: CompressedResourceOp, - gate_counts_dict, - gate_set: Set, - scalar: int = 1, - config: Dict = resource_config, -) -> None: - """Modifies the `gate_counts_dict` argument by adding the (scaled) resources of the operation provided. - - Args: - cp_rep (CompressedResourceOp): operation in compressed representation to extract resources from - gate_counts_dict (Dict): base dictionary to modify with the resource counts - gate_set (Set): the set of operations to track resources with respect to - scalar (int, optional): optional scalar to multiply the counts. Defaults to 1. - config (Dict, optional): additional parameters to specify the resources from an operator. Defaults to resource_config. - """ - ## If op in gate_set add to resources - if cp_rep._name in gate_set: - gate_counts_dict[cp_rep] += scalar - return - - ## Else decompose cp_rep using its resource decomp [cp_rep --> dict[cp_rep: counts]] and extract resources - resource_decomp = cp_rep.op_type.resources(config=config, **cp_rep.params) - - for sub_cp_rep, counts in resource_decomp.items(): - _counts_from_compressed_res_op( - sub_cp_rep, gate_counts_dict, scalar=scalar * counts, gate_set=gate_set, config=config - ) - - return - - -def _temp_map_func(op: Operation) -> ResourceOperator: - """Temp map function""" - raise NotImplementedError - - -def _clean_gate_counts(gate_counts: Dict[CompressedResourceOp, int]) -> Dict[str, int]: - """Map resources with gate_types made from CompressedResourceOps - into one which tracks just strings of operations. - - Args: - gate_counts (Dict[CompressedResourceOp, int]): gate counts in terms of compressed resource ops - - Returns: - Dict[str, int]: gate counts in terms of names of operations - """ - clean_gate_counts = defaultdict(int) - - for cmp_res_op, counts in gate_counts.items(): - clean_gate_counts[cmp_res_op._name] += counts - - return clean_gate_counts - - -@qml.QueuingManager.stop_recording() -def _operations_to_compressed_reps(ops: Iterable[Operation]) -> List[CompressedResourceOp]: - """Convert the sequence of operations to a list of compressed resource ops. - - Args: - ops (Iterable[Operation]): set of operations to convert - - Returns: - List[CompressedResourceOp]: set of converted compressed resource ops - """ - cmp_rep_ops = [] - for op in ops: - if isinstance(op, ResourceOperator): - cmp_rep_ops.append(op.resource_rep_from_op()) - - else: - try: - cmp_rep_ops.append(_temp_map_func(op).resource_rep_from_op()) - - except NotImplementedError: - decomp = op.decomposition() - cmp_rep_ops.extend(_operations_to_compressed_reps(decomp)) - - return cmp_rep_ops diff --git a/pennylane/labs/resource_estimation/templates/__init__.py b/pennylane/labs/resource_estimation/templates/__init__.py deleted file mode 100644 index 58bbf37c40b..00000000000 --- a/pennylane/labs/resource_estimation/templates/__init__.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""This module contains resource operators for PennyLane templates.""" - -from .subroutines import ( - ResourceQFT, - ResourceQuantumPhaseEstimation, - ResourceQPE, - ResourceBasisRotation, - ResourcePrepSelPrep, - ResourceQubitization, - ResourceQROM, - ResourceReflection, - ResourceSelect, - ResourceControlledSequence, - ResourceModExp, - ResourceMultiplier, - ResourcePhaseAdder, - ResourceAmplitudeAmplification, -) - -from .trotter import ResourceTrotterProduct, ResourceTrotterizedQfunc, resource_trotterize - -from .stateprep import ( - ResourceSuperposition, - ResourceStatePrep, - ResourceBasisState, - ResourceMottonenStatePreparation, -) diff --git a/pennylane/labs/resource_estimation/templates/stateprep.py b/pennylane/labs/resource_estimation/templates/stateprep.py deleted file mode 100644 index 291ac605189..00000000000 --- a/pennylane/labs/resource_estimation/templates/stateprep.py +++ /dev/null @@ -1,393 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""Resource operators for PennyLane state preparation templates.""" -import math -from typing import Dict - -import pennylane as qml -from pennylane.labs import resource_estimation as re -from pennylane.labs.resource_estimation import CompressedResourceOp, ResourceOperator - -# pylint: disable=arguments-differ, protected-access - - -class ResourceStatePrep(qml.StatePrep, ResourceOperator): - """Resource class for StatePrep. - - Args: - state (array[complex] or csr_matrix): the state vector to prepare - wires (Sequence[int] or int): the wire(s) the operation acts on - pad_with (float or complex): if not ``None``, ``state`` is padded with this constant to be of size :math:`2^n`, where - :math:`n` is the number of wires. - normalize (bool): whether to normalize the state vector. To represent a valid quantum state vector, the L2-norm - of ``state`` must be one. The argument ``normalize`` can be set to ``True`` to normalize the state automatically. - id (str): custom label given to an operator instance, - can be useful for some applications where the instance has to be identified - validate_norm (bool): whether to validate the norm of the input state - - Resource Parameters: - * num_wires (int): the number of wires that the operation acts on - - Resources: - Uses the resources as defined in the :class:`~.ResourceMottonenStatePreperation` template. - - .. seealso:: :class:`~.StatePrep` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceStatePrep.resources(num_wires=3) - {MottonenStatePrep(3): 1} - """ - - @staticmethod - def _resource_decomp(num_wires, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_wires (int): the number of wires that the operation acts on - - Resources: - Uses the resources as defined in the :class:`~.ResourceMottonenStatePreperation` template. - """ - return {re.ResourceMottonenStatePreparation.resource_rep(num_wires): 1} - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_wires (int): the number of wires that the operation acts on - """ - return {"num_wires": len(self.wires)} - - @classmethod - def resource_rep(cls, num_wires) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_wires (int): the number of wires that the operation acts on - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"num_wires": num_wires} - return CompressedResourceOp(cls, params) - - @classmethod - def tracking_name(cls, num_wires) -> str: - return f"StatePrep({num_wires})" - - -class ResourceMottonenStatePreparation(qml.MottonenStatePreparation, ResourceOperator): - """Resource class for the MottonenStatePreparation template. - - Args: - state_vector (tensor_like): Input array of shape ``(2^n,)``, where ``n`` is the number of wires - the state preparation acts on. The input array must be normalized. - wires (Iterable): wires that the template acts on - - Resource Parameters: - * num_wires(int): the number of wires that the operation acts on - - Resources: - Using the resources as described in `Mottonen et al. (2008) `_. - The resources are defined as :math:`2^{N+2} - 5` :class:`~.ResourceRZ` gates and - :math:`2^{N+2} - 4N - 4` :class:`~.ResourceCNOT` gates. - - .. seealso:: :class:`~.MottonenStatePreperation` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceMottonenStatePreparation.resources(num_wires=3) - {RZ: 27, CNOT: 16} - """ - - @staticmethod - def _resource_decomp(num_wires, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_wires(int): the number of wires that the operation acts on - - Resources: - Using the resources as described in `Mottonen et al. (2008) `_. - The resources are defined as :math:`2^{N+2} - 5` :class:`~.ResourceRZ` gates and - :math:`2^{N+2} - 4N - 4` :class:`~.ResourceCNOT` gates. - """ - gate_types = {} - rz = re.ResourceRZ.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - - r_count = 2 ** (num_wires + 2) - 5 - cnot_count = 2 ** (num_wires + 2) - 4 * num_wires - 4 - - if r_count: - gate_types[rz] = r_count - - if cnot_count: - gate_types[cnot] = cnot_count - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_wires(int): the number of wires that the operation acts on - """ - return {"num_wires": len(self.wires)} - - @classmethod - def resource_rep(cls, num_wires) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_wires(int): the number of wires that the operation acts on - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"num_wires": num_wires} - return CompressedResourceOp(cls, params) - - @classmethod - def tracking_name(cls, num_wires) -> str: - return f"MottonenStatePrep({num_wires})" - - -class ResourceSuperposition(qml.Superposition, ResourceOperator): - """Resource class for the Superposition template. - - Args: - coeffs (tensor-like[float]): normalized coefficients of the superposition - bases (tensor-like[int]): basis states of the superposition - wires (Sequence[int]): wires that the operator acts on - work_wire (Union[Wires, int, str]): the auxiliary wire used for the permutation - - Resource Parameters: - * num_stateprep_wires (int): the number of wires used for the operation - * num_basis_states (int): the number of basis states of the superposition - * size_basis_state (int): the size of each basis state - - Resources: - The resources are computed following the PennyLane decomposition of - the class :class:`~.Superposition`. - - We use the following (somewhat naive) assumptions to approximate the - resources: - - - The MottonenStatePreparation routine is assumed for the state prep - component. - - The permutation block requires 2 multi-controlled X gates and a - series of CNOT gates. On average we will be controlling on and flipping - half the number of bits in :code:`size_basis`. (i.e for any given basis - state, half will be ones and half will be zeros). - - If the number of basis states provided spans the set of all basis states, - then we don't need to permute. In general, there is a probability associated - with not needing to permute wires if the basis states happen to match, we - estimate this quantity aswell. - - .. seealso:: :class:`~.Superposition` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceSuperposition.resources(num_stateprep_wires=3, num_basis_states=3, size_basis_state=3) - {MottonenStatePrep(3): 1, CNOT: 2, MultiControlledX: 4} - """ - - @staticmethod - def _resource_decomp( - num_stateprep_wires, num_basis_states, size_basis_state, **kwargs - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_stateprep_wires (int): the number of wires used for the operation - num_basis_states (int): the number of basis states of the superposition - size_basis_state (int): the size of each basis state - - Resources: - The resources are computed following the PennyLane decomposition of - the class :class:`~.Superposition`. - - We use the following (somewhat naive) assumptions to approximate the - resources: - - - The MottonenStatePreparation routine is assumed for the state prep - component. - - The permutation block requires 2 multi-controlled X gates and a - series of CNOT gates. On average we will be controlling on and flipping - half the number of bits in :code:`size_basis`. (i.e for any given basis - state, half will be ones and half will be zeros). - - If the number of basis states provided spans the set of all basis states, - then we don't need to permute. In general, there is a probability associated - with not needing to permute wires if the basis states happen to match, we - estimate this quantity aswell. - - """ - gate_types = {} - msp = re.ResourceMottonenStatePreparation.resource_rep(num_stateprep_wires) - gate_types[msp] = 1 - - cnot = re.ResourceCNOT.resource_rep() - num_zero_ctrls = size_basis_state // 2 - multi_x = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=size_basis_state, - num_ctrl_values=num_zero_ctrls, - num_work_wires=0, - ) - - basis_size = 2**size_basis_state - prob_matching_basis_states = num_basis_states / basis_size - num_permutes = round(num_basis_states * (1 - prob_matching_basis_states)) - - if num_permutes: - gate_types[cnot] = num_permutes * ( - size_basis_state // 2 - ) # average number of bits to flip - gate_types[multi_x] = 2 * num_permutes # for compute and uncompute - - return gate_types - - @property - def resource_params(self) -> Dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_stateprep_wires (int): the number of wires used for the operation - * num_basis_states (int): the number of basis states of the superposition - * size_basis_state (int): the size of each basis state - """ - bases = self.hyperparameters["bases"] - num_basis_states = len(bases) - size_basis_state = len(bases[0]) # assuming they are all the same size - num_stateprep_wires = math.ceil(math.log2(len(self.coeffs))) - - return { - "num_stateprep_wires": num_stateprep_wires, - "num_basis_states": num_basis_states, - "size_basis_state": size_basis_state, - } - - @classmethod - def resource_rep( - cls, num_stateprep_wires, num_basis_states, size_basis_state - ) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_stateprep_wires (int): the number of wires used for the operation - num_basis_states (int): the number of basis states of the superposition - size_basis_state (int): the size of each basis state - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "num_stateprep_wires": num_stateprep_wires, - "num_basis_states": num_basis_states, - "size_basis_state": size_basis_state, - } - return CompressedResourceOp(cls, params) - - -class ResourceBasisState(qml.BasisState, ResourceOperator): - r"""Resource class for the BasisState template. - - Args: - state (tensor_like): Binary input of shape ``(len(wires), )``. For example, if ``state=np.array([0, 1, 0])`` or ``state=2`` (equivalent to 010 in binary), the quantum system will be prepared in the state :math:`|010 \rangle`. - - wires (Sequence[int] or int): the wire(s) the operation acts on - id (str): Custom label given to an operator instance. Can be useful for some applications where the instance has to be identified. - - Resource Parameters: - * num_bit_flips (int): number of qubits in the :math:`|1\rangle` state - - Resources: - The resources for BasisState are according to the decomposition found in qml.BasisState. - - .. seealso:: :class:`~.BasisState` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceBasisState.resources(num_bit_flips = 6) - {X: 6} - """ - - @staticmethod - def _resource_decomp( - num_bit_flips, - **kwargs, - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_bit_flips (int): number of qubits in the :math:`|1\rangle` state - - Resources: - The resources for BasisState are according to the decomposition found in qml.BasisState. - """ - gate_types = {} - x = re.ResourceX.resource_rep() - gate_types[x] = num_bit_flips - - return gate_types - - @property - def resource_params(self) -> Dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_bit_flips (int): number of qubits in the :math:`|1\rangle` state - """ - num_bit_flips = sum(self.parameters[0]) - return {"num_bit_flips": num_bit_flips} - - @classmethod - def resource_rep(cls, num_bit_flips) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_bit_flips (int): number of qubits in the :math:`|1\rangle` state - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - - params = {"num_bit_flips": num_bit_flips} - return CompressedResourceOp(cls, params) - - @classmethod - def tracking_name(cls, num_bit_flips) -> str: - return f"BasisState({num_bit_flips})" diff --git a/pennylane/labs/resource_estimation/templates/subroutines.py b/pennylane/labs/resource_estimation/templates/subroutines.py deleted file mode 100644 index d2f7e8578aa..00000000000 --- a/pennylane/labs/resource_estimation/templates/subroutines.py +++ /dev/null @@ -1,1635 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""Resource operators for PennyLane subroutine templates.""" -import math -from collections import defaultdict -from typing import Dict - -import pennylane as qml -from pennylane import numpy as qnp -from pennylane.labs import resource_estimation as re -from pennylane.labs.resource_estimation import CompressedResourceOp, ResourceOperator - -# pylint: disable=arguments-differ, protected-access - - -class ResourceQFT(qml.QFT, ResourceOperator): - r"""Resource class for QFT. - - Args: - wires (int or Iterable[Number, str]]): the wire(s) the operation acts on - - Resource Parameters: - * num_wires (int): the number of qubits the operation acts upon - - Resources: - The resources are obtained from the standard decomposition of QFT as presented - in (chapter 5) `Nielsen, M.A. and Chuang, I.L. (2011) Quantum Computation and Quantum Information - `_. - - .. seealso:: :class:`~.QFT` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceQFT.resources(num_wires=3) - {Hadamard: 3, SWAP: 1, ControlledPhaseShift: 3} - """ - - @staticmethod - def _resource_decomp(num_wires, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_wires (int): the number of qubits the operation acts upon - - Resources: - The resources are obtained from the standard decomposition of QFT as presented - in (Chapter 5) `Nielsen, M.A. and Chuang, I.L. (2011) Quantum Computation and Quantum Information - `_. - - """ - gate_types = {} - - hadamard = re.ResourceHadamard.resource_rep() - swap = re.ResourceSWAP.resource_rep() - ctrl_phase_shift = re.ResourceControlledPhaseShift.resource_rep() - - gate_types[hadamard] = num_wires - gate_types[swap] = num_wires // 2 - gate_types[ctrl_phase_shift] = num_wires * (num_wires - 1) // 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_wires (int): the number of qubits the operation acts upon - """ - return {"num_wires": len(self.wires)} - - @classmethod - def resource_rep(cls, num_wires) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_wires (int): the number of qubits the operation acts upon - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"num_wires": num_wires} - return CompressedResourceOp(cls, params) - - @staticmethod - def tracking_name(num_wires) -> str: - r"""Returns the tracking name built with the operator's parameters.""" - return f"QFT({num_wires})" - - -class ResourceControlledSequence(qml.ControlledSequence, re.ResourceOperator): - """Resource class for the ControlledSequence template. - - Args: - base (Operator): the phase estimation unitary, specified as an :class:`~.Operator` - control (Union[Wires, Sequence[int], or int]): the wires to be used for control - - Resource Parameters: - * base_class (ResourceOperator): The type of the operation corresponding to the operator. - * base_params (dict): A dictionary of parameters required to obtain the resources for the operator. - * num_ctrl_wires (int): the number of control wires - - Resources: - The resources are obtained from the standard decomposition of :class:`~.ControlledSequence`. - - .. seealso:: :class:`~.ControlledSequence` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceControlledSequence.resources( - ... base_class=re.ResourceHadamard, - ... base_params={}, - ... num_ctrl_wires=2 - ... ) - {C(Hadamard,1,0,0): 3} - """ - - @staticmethod - def _resource_decomp( - base_class, base_params, num_ctrl_wires, **kwargs - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (ResourceOperator): The type of the operation corresponding to the - operator. - base_params (dict): A dictionary of parameters required to obtain the resources for - the operator. - num_ctrl_wires (int): the number of control wires - - Resources: - The resources are obtained from the standard decomposition of :class:`~.ControlledSequence`. - - """ - return { - re.ResourceControlled.resource_rep(base_class, base_params, 1, 0, 0): 2**num_ctrl_wires - - 1 - } - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (ResourceOperator): The type of the operation corresponding to the operator. - * base_params (dict): A dictionary of parameters required to obtain the resources for the operator. - * num_ctrl_wires (int): the number of control wires - """ - return { - "base_class": type(self.base), - "base_params": self.base.resource_params, - "num_ctrl_wires": len(self.control_wires), - } - - @classmethod - def resource_rep(cls, base_class, base_params, num_ctrl_wires) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (ResourceOperator): The type of the operation corresponding to the - operator. - base_params (dict): A dictionary of parameters required to obtain the resources for - the operator. - num_ctrl_wires (int): the number of control wires - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp( - cls, - { - "base_class": base_class, - "base_params": base_params, - "num_ctrl_wires": num_ctrl_wires, - }, - ) - - @staticmethod - def tracking_name(base_class, base_params, num_ctrl_wires) -> str: - base_name = base_class.tracking_name(**base_params) - return f"ControlledSequence({base_name}, {num_ctrl_wires})" - - -class ResourcePhaseAdder(qml.PhaseAdder, re.ResourceOperator): - r"""Resource class for the PhaseAdder template. - - Args: - k (int): the number that needs to be added - x_wires (Sequence[int]): the wires the operation acts on. The number of wires must be enough - for a binary representation of the value being targeted, :math:`x`. In some cases an additional - wire is needed, see usage details below. The number of wires also limits the maximum - value for `mod`. - mod (int): the modulo for performing the addition. If not provided, it will be set to its maximum value, :math:`2^{\text{len(x_wires)}}`. - work_wire (Sequence[int] or int): the auxiliary wire to use for the addition. Optional - when `mod` is :math:`2^{len(x\_wires)}`. Defaults to empty tuple. - - Resource Parameters: - * mod (int): the module for performing the addition - * num_x_wires (int): the number of wires the operation acts on - - Resources: - The resources are obtained from the standard decomposition of :class:`~.PhaseAdder`. - - .. seealso:: :class:`~.PhaseAdder` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourcePhaseAdder.resources( - ... mod=3, - ... num_x_wires=5 - ... ) - {QFT(5): 2, - Adjoint(QFT(5)): 2, - PhaseShift: 10, - Adjoint(PhaseShift): 10, - C(PhaseShift,1,0,0): 5, - CNOT: 1, - MultiControlledX: 1} - """ - - @staticmethod - def _resource_decomp(mod, num_x_wires, **kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - mod (int): the module for performing the addition - num_x_wires (int): the number of wires the operation acts on - - Resources: - The resources are obtained from the standard decomposition of :class:`~.PhaseAdder`. - - """ - if mod == 2**num_x_wires: - return {re.ResourcePhaseShift.resource_rep(): num_x_wires} - - qft = ResourceQFT.resource_rep(num_x_wires) - qft_dag = re.ResourceAdjoint.resource_rep( - ResourceQFT, - {"num_wires": num_x_wires}, - ) - - phase_shift = re.ResourcePhaseShift.resource_rep() - phase_shift_dag = re.ResourceAdjoint.resource_rep( - re.ResourcePhaseShift, - {}, - ) - ctrl_phase_shift = re.ResourceControlledPhaseShift.resource_rep() - - cnot = re.ResourceCNOT.resource_rep() - multix = re.ResourceMultiControlledX.resource_rep(1, 0, 1) - - gate_types = {} - gate_types[qft] = 2 - gate_types[qft_dag] = 2 - gate_types[phase_shift] = 2 * num_x_wires - gate_types[phase_shift_dag] = 2 * num_x_wires - gate_types[ctrl_phase_shift] = num_x_wires - gate_types[cnot] = 1 - gate_types[multix] = 1 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Resource Parameters: - mod (int): the module for performing the addition - num_x_wires (int): the number of wires the operation acts on - - Returns: - dict: A dictionary containing the resource parameters: - * mod (int): the module for performing the addition - * num_x_wires (int): the number of wires the operation acts on - """ - return { - "mod": self.hyperparameters["mod"], - "num_x_wires": len(self.hyperparameters["x_wires"]), - } - - @classmethod - def resource_rep(cls, mod, num_x_wires) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - mod (int): the module for performing the addition - num_x_wires (int): the number of wires the operation acts on - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - - return re.CompressedResourceOp(cls, {"mod": mod, "num_x_wires": num_x_wires}) - - -class ResourceMultiplier(qml.Multiplier, re.ResourceOperator): - """Resource class for the Multiplier template. - - Args: - k (int): the number that needs to be multiplied - x_wires (Sequence[int]): the wires the operation acts on. The number of wires must be enough for encoding `x` in the computational basis. The number of wires also limits the maximum value for `mod`. - mod (int): the modulo for performing the multiplication. If not provided, it will be set to its maximum value, :math:`2^{\text{len(x_wires)}}`. - work_wires (Sequence[int]): the auxiliary wires to use for the multiplication. If :math:`mod=2^{\text{len(x_wires)}}`, the number of auxiliary wires must be ``len(x_wires)``. Otherwise ``len(x_wires) + 2`` auxiliary wires are needed. - - Resource Parameters: - * mod (int): the module for performing the multiplication - * num_work_wires (int): the number of work wires used for the multiplication. - * num_x_wires (int): the number of wires the operation acts on. - - Resources: - The resources are obtained from the standard decomposition of :class:`~.Multiplier`. - - .. seealso:: :class:`~.Multiplier` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceMultiplier.resources( - ... mod=3, - ... num_work_wires=5, - ... num_x_wires=5 - ... ) - {QFT(4): 2, - Adjoint(QFT(4)): 2, - ControlledSequence(PhaseAdder, 5): 1, - Adjoint(ControlledSequence(PhaseAdder, 5)): 1, - CNOT: 3} - """ - - @staticmethod - def _resource_decomp( - mod, num_work_wires, num_x_wires, **kwargs - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - mod (int): the module for performing the multiplication - num_work_wires (int): the number of work wires used for the multiplication. - num_x_wires (int): the number of wires the operation acts on. - - Resources: - The resources are obtained from the standard decomposition of :class:`~.Multiplier`. - - """ - if mod == 2**num_x_wires: - num_aux_wires = num_x_wires - num_aux_swap = num_x_wires - else: - num_aux_wires = num_work_wires - 1 - num_aux_swap = num_aux_wires - 1 - - qft = ResourceQFT.resource_rep(num_aux_wires) - qft_dag = re.ResourceAdjoint.resource_rep( - ResourceQFT, - {"num_wires": num_aux_wires}, - ) - - sequence = ResourceControlledSequence.resource_rep( - ResourcePhaseAdder, - {}, - num_x_wires, - ) - - sequence_dag = re.ResourceAdjoint.resource_rep( - ResourceControlledSequence, - { - "base_class": ResourcePhaseAdder, - "base_params": {}, - "num_ctrl_wires": num_x_wires, - }, - ) - - cnot = re.ResourceCNOT.resource_rep() - - gate_types = {} - gate_types[qft] = 2 - gate_types[qft_dag] = 2 - gate_types[sequence] = 1 - gate_types[sequence_dag] = 1 - gate_types[cnot] = min(num_x_wires, num_aux_swap) - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * mod (int): The modulus for performing the multiplication. - * num_work_wires (int): The number of work wires used. - * num_x_wires (int): The number of wires the operation acts on. - """ - return { - "mod": self.hyperparameters["mod"], - "num_work_wires": len(self.hyperparameters["work_wires"]), - "num_x_wires": len(self.hyperparameters["x_wires"]), - } - - @classmethod - def resource_rep(cls, mod, num_work_wires, num_x_wires) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - mod (int): the module for performing the multiplication - num_work_wires (int): the number of work wires used for the multiplication. - num_x_wires (int): the number of wires the operation acts on. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp( - cls, {"mod": mod, "num_work_wires": num_work_wires, "num_x_wires": num_x_wires} - ) - - -class ResourceModExp(qml.ModExp, re.ResourceOperator): - r"""Resource class for the :class:`~.ModExp` template. - - Args: - x_wires (Sequence[int]): the wires that store the integer :math:`x` - output_wires (Sequence[int]): the wires that store the operator result. These wires also encode :math:`b`. - base (int): integer that needs to be exponentiated - mod (int): the modulo for performing the exponentiation. If not provided, it will be set to its maximum value, :math:`2^{\text{len(output_wires)}}` - work_wires (Sequence[int]): the auxiliary wires to use for the exponentiation. If - :math:`mod=2^{\text{len(output_wires)}}`, the number of auxiliary wires must be ``len(output_wires)``. Otherwise - ``len(output_wires) + 2`` auxiliary wires are needed. Defaults to empty tuple. - - Resource Parameters: - * mod (int): the modulo for performing the modular exponentiation - * num_output_wires (int): the number of output wires used to encode the integer :math:`b \cdot base^x \; \text{mod} \; mod` in the computational basis - * num_work_wires (int): the number of work wires used to perform the modular exponentiation operation - * num_x_wires (int): the number of wires used to encode the integer :math:`x < mod` in the computational basis - - Resources: - The resources are obtained from the standard decomposition of :class:`~.ModExp`. - - .. seealso:: :class:`~.ModExp` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceModExp.resources( - ... mod=3, - ... num_output_wires=5, - ... num_work_wires=5, - ... num_x_wires=5 - ... ) - {C(QFT(4),1,0,0): 62, - C(Adjoint(QFT(4)),1,0,0): 62, - C(ControlledSequence(PhaseAdder, 5),1,0,0): 31, - C(Adjoint(ControlledSequence(PhaseAdder, 5)),1,0,0): 31, - C(CNOT,1,0,0): 93} - """ - - @staticmethod - def _resource_decomp( - mod, num_output_wires, num_work_wires, num_x_wires, **kwargs - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - mod (int): the module for performing the exponentiation - num_output_wires (int): the number of output wires used to encode the integer :math:`b \cdot base^x \; \text{mod} \; mod` in the computational basis - num_work_wires (int): the number of work wires used to perform the modular exponentiation operation - num_x_wires (int): the number of wires used to encode the integer :math:`x < mod` in the computational basis - - Resources: - The resources are obtained from the standard decomposition of :class:`~.ModExp`. - - """ - mult_resources = ResourceMultiplier._resource_decomp(mod, num_work_wires, num_output_wires) - gate_types = {} - - for comp_rep, _ in mult_resources.items(): - new_rep = re.ResourceControlled.resource_rep(comp_rep.op_type, comp_rep.params, 1, 0, 0) - - # cancel out QFTs from consecutive Multipliers - if comp_rep._name in ("QFT", "Adjoint(QFT)"): - gate_types[new_rep] = 1 - else: - gate_types[new_rep] = mult_resources[comp_rep] * ((2**num_x_wires) - 1) - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * mod (int): the module for performing the exponentiation - * num_output_wires (int): the number of output wires used to encode the integer :math:`b \cdot base^x \; \text{mod} \; mod` in the computational basis - * num_work_wires (int): the number of work wires used to perform the modular exponentiation operation - * num_x_wires (int): the number of wires used to encode the integer :math:`x < mod` in the computational basis - """ - return { - "mod": self.hyperparameters["mod"], - "num_output_wires": len(self.hyperparameters["output_wires"]), - "num_work_wires": len(self.hyperparameters["work_wires"]), - "num_x_wires": len(self.hyperparameters["x_wires"]), - } - - @classmethod - def resource_rep( - cls, mod, num_output_wires, num_work_wires, num_x_wires - ) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - mod (int): the module for performing the exponentiation - num_output_wires (int): the number of output wires used to encode the integer :math:`b \cdot base^x \; \text{mod} \; mod` - in the computational basis - num_work_wires (int): the number of work wires used to perform the modular exponentiation - operation - num_x_wires (int): the number of wires used to encode the integer :math:`x < mod` in the - computational basis - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp( - cls, - { - "mod": mod, - "num_output_wires": num_output_wires, - "num_work_wires": num_work_wires, - "num_x_wires": num_x_wires, - }, - ) - - -class ResourceQuantumPhaseEstimation(qml.QuantumPhaseEstimation, ResourceOperator): - r"""Resource class for QuantumPhaseEstimation (QPE). - - Args: - unitary (array or Operator): the phase estimation unitary, specified as a matrix or an - :class:`~.Operator` - target_wires (Union[Wires, Sequence[int], or int]): the target wires to apply the unitary. - If the unitary is specified as an operator, the target wires should already have been - defined as part of the operator. In this case, target_wires should not be specified. - estimation_wires (Union[Wires, Sequence[int], or int]): the wires to be used for phase - estimation - - Resource Parameters: - * base_class (ResourceOperator): The type of the operation corresponding to the phase estimation unitary. - * base_params (dict): A dictionary of parameters required to obtain the resources for the phase estimation unitary. - * num_estimation_wires (int): the number of wires used for measuring out the phase - - Resources: - The resources are obtained from the standard decomposition of QPE as presented - in (Section 5.2) `Nielsen, M.A. and Chuang, I.L. (2011) Quantum Computation and Quantum - Information `_. - - .. seealso:: :class:`~.QuantumPhaseEstimation` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceQuantumPhaseEstimation.resources( - ... base_class=re.ResourceQFT, - ... base_params={"num_wires": 3}, - ... num_estimation_wires=3, - ... ) - {Hadamard: 3, Adjoint(QFT(3)): 1, C(QFT(3),1,0,0): 7} - """ - - @staticmethod - def _resource_decomp( - base_class, base_params, num_estimation_wires, **kwargs - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (Type(ResourceOperator)): The type of the operation corresponding to the - phase estimation unitary. - base_params (dict): A dictionary of parameters required to obtain the resources for - the phase estimation unitary. - num_estimation_wires (int): the number of wires used for measuring out the phase - - Resources: - The resources are obtained from the standard decomposition of QPE as presented - in (section 5.2) `Nielsen, M.A. and Chuang, I.L. (2011) Quantum Computation and Quantum - Information `_. - """ - gate_types = {} - - hadamard = re.ResourceHadamard.resource_rep() - adj_qft = re.ResourceAdjoint.resource_rep(ResourceQFT, {"num_wires": num_estimation_wires}) - ctrl_op = re.ResourceControlled.resource_rep(base_class, base_params, 1, 0, 0) - - gate_types[hadamard] = num_estimation_wires - gate_types[adj_qft] = 1 - gate_types[ctrl_op] = (2**num_estimation_wires) - 1 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (Type(ResourceOperator)): The type of the operation corresponding to the phase estimation unitary. - * base_params (dict): A dictionary of parameters required to obtain the resources for the phase estimation unitary. - * num_estimation_wires (int): the number of wires used for measuring out the phase - """ - op = self.hyperparameters["unitary"] - num_estimation_wires = len(self.hyperparameters["estimation_wires"]) - - if not isinstance(op, re.ResourceOperator): - raise TypeError( - f"Can't obtain QPE resources when the base unitary {op} isn't an instance" - " of ResourceOperator" - ) - - return { - "base_class": type(op), - "base_params": op.resource_params, - "num_estimation_wires": num_estimation_wires, - } - - @classmethod - def resource_rep( - cls, - base_class, - base_params, - num_estimation_wires, - ) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (Type(ResourceOperator)): The type of the operation corresponding to the - phase estimation unitary. - base_params (dict): A dictionary of parameters required to obtain the resources for - the phase estimation unitary. - num_estimation_wires (int): the number of wires used for measuring out the phase - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "base_class": base_class, - "base_params": base_params, - "num_estimation_wires": num_estimation_wires, - } - return CompressedResourceOp(cls, params) - - @staticmethod - def tracking_name(base_class, base_params, num_estimation_wires) -> str: - r"""Returns the tracking name built with the operator's parameters.""" - base_name = base_class.tracking_name(**base_params) - return f"QPE({base_name}, {num_estimation_wires})" - - -ResourceQPE = ResourceQuantumPhaseEstimation # Alias for ease of typing -r"""Resource class for QuantumPhaseEstimation (QPE). - -Args: - unitary (array or Operator): the phase estimation unitary, specified as a matrix or an - :class:`~.Operator` - target_wires (Union[Wires, Sequence[int], or int]): the target wires to apply the unitary. - If the unitary is specified as an operator, the target wires should already have been - defined as part of the operator. In this case, target_wires should not be specified. - estimation_wires (Union[Wires, Sequence[int], or int]): the wires to be used for phase - estimation - -Resource Parameters: - * base_class (ResourceOperator): The type of the operation corresponding to the phase estimation unitary. - * base_params (dict): A dictionary of parameters required to obtain the resources for the phase estimation unitary. - * num_estimation_wires (int): the number of wires used for measuring out the phase - -Resources: - The resources are obtained from the standard decomposition of QPE as presented - in (Section 5.2) `Nielsen, M.A. and Chuang, I.L. (2011) Quantum Computation and Quantum - Information `_. - -.. seealso:: :class:`~.QuantumPhaseEstimation` - -**Example** - -The resources for this operation are computed using: - ->>> re.ResourceQuantumPhaseEstimation.resources( -... base_class=re.ResourceQFT, -... base_params={"num_wires": 3}, -... num_estimation_wires=3, -... ) -{Hadamard: 3, Adjoint(QFT(3)): 1, C(QFT(3),1,0,0): 7} -""" - - -class ResourceBasisRotation(qml.BasisRotation, ResourceOperator): - r"""Resource class for the BasisRotation gate. - - Args: - wires (Iterable[Any]): wires that the operator acts on - unitary_matrix (array): matrix specifying the basis transformation - check (bool): test unitarity of the provided `unitary_matrix` - - Resource Parameters: - * dim_N (int): The dimensions of the input :code:`unitary_matrix`. This is computed as the number of columns of the matrix. - - Resources: - The resources are obtained from the construction scheme given in `Optica, 3, 1460 (2016) - `_. Specifically, - the resources are given as :math:`dim_N * (dim_N - 1) / 2` instances of the - :class:`~.ResourceSingleExcitation` gate, and :math:`dim_N * (1 + (dim_N - 1) / 2)` instances - of the :class:`~.ResourcePhaseShift` gate. - - .. seealso:: :class:`~.BasisRotation` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceBasisRotation.resources(dim_N=3) - {PhaseShift: 6.0, SingleExcitation: 3.0} - """ - - @staticmethod - def _resource_decomp(dim_N, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - dim_N (int): The dimensions of the input :code:`unitary_matrix`. This is computed - as the number of columns of the matrix. - - Resources: - The resources are obtained from the construction scheme given in `Optica, 3, 1460 (2016) - `_. Specifically, - the resources are given as :math:`dim_N * (dim_N - 1) / 2` instances of the - :class:`~.ResourceSingleExcitation` gate, and :math:`dim_N * (1 + (dim_N - 1) / 2)` instances - of the :class:`~.ResourcePhaseShift` gate. - """ - gate_types = {} - phase_shift = re.ResourcePhaseShift.resource_rep() - single_excitation = re.ResourceSingleExcitation.resource_rep() - - se_count = dim_N * (dim_N - 1) / 2 - ps_count = dim_N + se_count - - gate_types[phase_shift] = ps_count - gate_types[single_excitation] = se_count - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * dim_N (int): The dimensions of the input :code:`unitary_matrix`. This is computed as the number of columns of the matrix. - """ - unitary_matrix = self.parameters[0] - return {"dim_N": qml.math.shape(unitary_matrix)[0]} - - @classmethod - def resource_rep(cls, dim_N) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - dim_N (int): The dimensions of the input :code:`unitary_matrix`. This is computed - as the number of columns of the matrix. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"dim_N": dim_N} - return CompressedResourceOp(cls, params) - - @classmethod - def tracking_name(cls, dim_N) -> str: - r"""Returns the tracking name built with the operator's parameters.""" - return f"BasisRotation({dim_N})" - - -class ResourceSelect(qml.Select, ResourceOperator): - r"""Resource class for the Select gate. - - Args: - ops (list[Operator]): operations to apply - control (Sequence[int]): the wires controlling which operation is applied - id (str or None): String representing the operation (optional) - - Resource Parameters: - * cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, to be applied according to the selected qubits. - - Resources: - The resources correspond directly to the definition of the operation. Specifically, - for each operator in :code:`cmpr_ops`, the cost is given as a controlled version of the operator - controlled on the associated bitstring. - - .. seealso:: :class:`~.Select` - - **Example** - - The resources for this operation are computed using: - - >>> ops_lst = [re.ResourceX.resource_rep(), re.ResourceQFT.resource_rep(num_wires=3)] - >>> re.ResourceSelect.resources(cmpr_ops=ops_lst) - defaultdict(, {X: 2, C(X,1,0,0): 1, C(QFT(3),1,0,0): 1}) - """ - - @staticmethod - def _resource_decomp(cmpr_ops, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed - representation, to be applied according to the selected qubits. - - Resources: - The resources correspond directly to the definition of the operation. Specifically, - for each operator in :code:`cmpr_ops`, the cost is given as a controlled version of the operator - controlled on the associated bitstring. - """ - gate_types = defaultdict(int) - x = re.ResourceX.resource_rep() - - num_ops = len(cmpr_ops) - num_ctrl_wires = int(qnp.ceil(qnp.log2(num_ops))) - num_total_ctrl_possibilities = 2**num_ctrl_wires # 2^n - - num_zero_controls = num_total_ctrl_possibilities // 2 - gate_types[x] = num_zero_controls * 2 # conjugate 0 controls - - for cmp_rep in cmpr_ops: - ctrl_op = re.ResourceControlled.resource_rep( - cmp_rep.op_type, cmp_rep.params, num_ctrl_wires, 0, 0 - ) - gate_types[ctrl_op] += 1 - - return gate_types - - @staticmethod - def resources_for_ui(cmpr_ops, **kwargs): # pylint: disable=unused-argument - r"""The resources for a select implementation taking advantage of the unary iterator trick. - - The resources are based on the analysis in `Babbush et al. (2018) `_ section III.A, - 'Unary Iteration and Indexed Operations'. See Figures 4, 6, and 7. - - Note: This implementation assumes we have access to :math:`S + 1` additional work qubits, - where :math:`S = \ceil{log_{2}(N)}` and :math:`N` is the number of batches of unitaries - to select. - """ - gate_types = defaultdict(int) - x = re.ResourceX.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - toffoli = re.ResourceToffoli.resource_rep() - - num_ops = len(cmpr_ops) - - for cmp_rep in cmpr_ops: - ctrl_op = re.ResourceControlled.resource_rep(cmp_rep.op_type, cmp_rep.params, 1, 0, 0) - gate_types[ctrl_op] += 1 - - gate_types[x] = 2 * (num_ops - 1) # conjugate 0 controlled toffolis - gate_types[cnot] = num_ops - 1 - gate_types[toffoli] = 2 * (num_ops - 1) - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, to be applied according to the selected qubits. - """ - ops = self.hyperparameters["ops"] - cmpr_ops = tuple(op.resource_rep_from_op() for op in ops) - return {"cmpr_ops": cmpr_ops} - - @classmethod - def resource_rep(cls, cmpr_ops) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed - representation, to be applied according to the selected qubits. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"cmpr_ops": cmpr_ops} - return CompressedResourceOp(cls, params) - - -class ResourcePrepSelPrep(qml.PrepSelPrep, ResourceOperator): - r"""Resource class for PrepSelPrep gate. - - Args: - lcu (Union[.Hamiltonian, .Sum, .Prod, .SProd, .LinearCombination]): The operator - written as a linear combination of unitaries. - control (Iterable[Any], Wires): The control qubits for the PrepSelPrep operator. - - Resource Parameters: - * cmpr_ops (tuple[CompressedResourceOp]): The list of operators, in the compressed representation, which correspond to the unitaries in the LCU to be blockencoded. - - Resources: - The resources correspond directly to the definition of the operation. Specifically, - the resources are given as one instance of :class:`~.ResourceSelect`, which is conjugated by - a pair of :class:`~.ResourceStatePrep` operations. - - .. seealso:: :class:`~.PrepSelPrep` - - **Example** - - The resources for this operation are computed using: - - >>> ops_tup = (re.ResourceX.resource_rep(), re.ResourceQFT.resource_rep(num_wires=3)) - >>> re.ResourcePrepSelPrep.resources(cmpr_ops=ops_tup) - {StatePrep(1): 1, Select: 1, Adjoint(StatePrep(1)): 1} - - """ - - @staticmethod - def _resource_decomp(cmpr_ops, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - cmpr_ops (tuple[CompressedResourceOp]): The list of operators, in the compressed - representation, which correspond to the unitaries in the LCU to be blockencoded. - - Resources: - The resources correspond directly to the definition of the operation. Specifically, - the resources are given as one instance of :class:`~.ResourceSelect`, which is conjugated by - a pair of :class:`~.ResourceStatePrep` operations. - """ - gate_types = {} - - num_ops = len(cmpr_ops) - num_wires = int(math.ceil(math.log2(num_ops))) - - prep = re.ResourceStatePrep.resource_rep(num_wires) - sel = ResourceSelect.resource_rep(cmpr_ops) - prep_dag = re.ResourceAdjoint.resource_rep(re.ResourceStatePrep, {"num_wires": num_wires}) - - gate_types[prep] = 1 - gate_types[sel] = 1 - gate_types[prep_dag] = 1 - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * cmpr_ops (tuple[CompressedResourceOp]): The list of operators, in the compressed representation, which correspond to the unitaries in the LCU to be blockencoded. - """ - ops = self.hyperparameters["ops"] - cmpr_ops = tuple(op.resource_rep_from_op() for op in ops) - return {"cmpr_ops": cmpr_ops} - - @classmethod - def resource_rep(cls, cmpr_ops) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - cmpr_ops (tuple[CompressedResourceOp]): The list of operators, in the compressed - representation, to be applied according to the selected qubits. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"cmpr_ops": cmpr_ops} - return CompressedResourceOp(cls, params) - - @classmethod - def pow_resource_decomp(cls, z, cmpr_ops) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - cmpr_ops (tuple[CompressedResourceOp]): The list of operators, in the compressed - representation, to be applied according to the selected qubits. - - Resources: - The resources are derived from the following identity. If an operation :math:`\hat{A}` - can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` - then the operation squared can be expressed as: - - .. math:: - - \begin{align} - \hat{A}^{2} \ &= \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger} \cdot \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger} \\ - \hat{A}^{2} \ &= \ \hat{U} \cdot \hat{B} \cdot \hat{B} \cdot \hat{U}^{\dagger} \\ - \hat{A}^{2} \ &= \ \hat{U} \cdot \hat{B}^{2} \cdot \hat{U}^{\dagger}, - \end{align} - - this holds for any integer power :math:`z`. In general, the resources are given by :math:`z` - instances of :class:`~.ResourceSelect` conjugated by a pair of :class:`~.ResourceStatePrep` - operations. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - gate_types = {} - - num_ops = len(cmpr_ops) - num_wires = int(math.ceil(math.log2(num_ops))) - - prep = re.ResourceStatePrep.resource_rep(num_wires) - pow_sel = re.ResourcePow.resource_rep(ResourceSelect, {"cmpr_ops": cmpr_ops}, z) - prep_dag = re.ResourceAdjoint.resource_rep(re.ResourceStatePrep, {"num_wires": num_wires}) - - gate_types[prep] = 1 - gate_types[pow_sel] = 1 - gate_types[prep_dag] = 1 - return gate_types - - -class ResourceReflection(qml.Reflection, ResourceOperator): - r"""Resource class for the Reflection gate. - - Args: - U (Operator): the operator that prepares the state :math:`|\Psi\rangle` - alpha (float): the angle of the operator, default is :math:`\pi` - reflection_wires (Any or Iterable[Any]): subsystem of wires on which to reflect, the - default is ``None`` and the reflection will be applied on the ``U`` wires. - - Resource Parameters: - * base_class (Type(ResourceOperator)): The type of the operation used to prepare the state we will be reflecting over. - * base_params (dict): A dictionary of parameters required to obtain the resources for the state preparation operator. - * num_ref_wires (int): The number of qubits for the subsystem on which the reflection is applied. - - Resources: - The resources correspond directly to the definition of the operation. The operator is - built as follows: - - .. math:: - - \text{R}(U, \alpha) = -I + (1 - e^{i\alpha}) |\Psi\rangle \langle \Psi| = U(-I + (1 - e^{i\alpha}) |0\rangle \langle 0|)U^{\dagger}. - - The central block is obtained through a controlled :class:`~.ResourcePhaseShift` operator and - a :class:`~.ResourceGlobalPhase` which are conjugated with a pair of :class:`~.ResourceX` gates. - Finally, the block is conjugated with the state preparation unitary :math:`U`. - - .. seealso:: :class:`~.Reflection` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceReflection.resources( - ... base_class=re.ResourceQFT, - ... base_params={"num_wires": 3}, - ... num_ref_wires=3, - ... ) - {X: 2, GlobalPhase: 1, QFT(3): 1, Adjoint(QFT(3)): 1, C(PhaseShift,2,2,0): 1} - """ - - @staticmethod - def _resource_decomp( - base_class, base_params, num_ref_wires, **kwargs - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (Type(ResourceOperator)): The type of the operation used to prepare the - state we will be reflecting over. - base_params (dict): A dictionary of parameters required to obtain the resources for - the state preparation operator. - num_ref_wires (int): The number of qubits for the subsystem on which the reflection is - applied. - - Resources: - The resources correspond directly to the definition of the operation. The operator is - built as follows: - - .. math:: - - \text{R}(U, \alpha) = -I + (1 - e^{i\alpha}) |\Psi\rangle \langle \Psi| = U(-I + (1 - e^{i\alpha}) |0\rangle \langle 0|)U^{\dagger}. - - The central block is obtained through a controlled :class:`~.ResourcePhaseShift` operator and - a :class:`~.ResourceGlobalPhase` which are conjugated with a pair of :class:`~.ResourceX` gates. - Finally, the block is conjugated with the state preparation unitary :math:`U`. - """ - gate_types = {} - base = base_class.resource_rep(**base_params) - - x = re.ResourceX.resource_rep() - gp = re.ResourceGlobalPhase.resource_rep() - adj_base = re.ResourceAdjoint.resource_rep(base_class, base_params) - ps = ( - re.ResourceControlled.resource_rep( - re.ResourcePhaseShift, {}, num_ref_wires - 1, num_ref_wires - 1, 0 - ) - if num_ref_wires > 1 - else re.ResourcePhaseShift.resource_rep() - ) - - gate_types[x] = 2 - gate_types[gp] = 1 - gate_types[base] = 1 - gate_types[adj_base] = 1 - gate_types[ps] = 1 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (Type(ResourceOperator)): The type of the operation used to prepare the state we will be reflecting over. - * base_params (dict): A dictionary of parameters required to obtain the resources for the state preparation operator. - * num_ref_wires (int): The number of qubits for the subsystem on which the reflection is applied. - """ - base_cmpr_rep = self.hyperparameters["base"].resource_rep_from_op() - num_ref_wires = len(self.hyperparameters["reflection_wires"]) - - return { - "base_class": base_cmpr_rep.op_type, - "base_params": base_cmpr_rep.params, - "num_ref_wires": num_ref_wires, - } - - @classmethod - def resource_rep(cls, base_class, base_params, num_ref_wires) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (Type(ResourceOperator)): The type of the operation used to prepare the - state we will be reflecting over. - base_params (dict): A dictionary of parameters required to obtain the resources for - the state preparation operator. - num_ref_wires (int): The number of qubits for the subsystem on which the reflection is - applied. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "base_class": base_class, - "base_params": base_params, - "num_ref_wires": num_ref_wires, - } - return CompressedResourceOp(cls, params) - - -class ResourceQubitization(qml.Qubitization, ResourceOperator): - r"""Resource class for the Qubitization gate. - - Args: - hamiltonian (Union[.Hamiltonian, .Sum, .Prod, .SProd, .LinearCombination]): The Hamiltonian written as a linear combination of unitaries. - control (Iterable[Any], Wires): The control qubits for the Qubitization operator. - - Resource Parameters: - * cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, corresponding to the unitaries of the LCU representation of the hamiltonian being qubitized. - * num_ctrl_wires (int): The number of qubits used to prepare the coefficients vector of the LCU. - - Resources: - The resources are obtained from the definition of the operation as described in (section III. C) - `Simulating key properties of lithium-ion batteries with a fault-tolerant quantum computer - `_: - - .. math:: - - Q = \text{Prep}_{\mathcal{H}}^{\dagger} \text{Sel}_{\mathcal{H}} \text{Prep}_{\mathcal{H}}(2|0\rangle\langle 0| - I). - - Specifically, the resources are given by one :class:`~.ResourcePrepSelPrep` gate and one - :class:`~.ResourceReflection` gate. - - .. seealso:: :class:`~.Qubitization` - - **Example** - - The resources for this operation are computed using: - - >>> ops_tup = (re.ResourceX.resource_rep(), re.ResourceQFT.resource_rep(num_wires=3)) - >>> re.ResourceQubitization.resources( - ... cmpr_ops=ops_tup, - ... num_ctrl_wires=2, - ... ) - {Reflection: 1, PrepSelPrep: 1} - """ - - @staticmethod - def _resource_decomp(cmpr_ops, num_ctrl_wires, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, - corresponding to the unitaries of the LCU representation of the hamiltonian being qubitized. - num_ctrl_wires (int): The number of qubits used to prepare the coefficients vector of the LCU. - - Resources: - The resources are obtained from the definition of the operation as described in (section III. C) - `Simulating key properties of lithium-ion batteries with a fault-tolerant quantum computer - `_: - - .. math:: - - Q = \text{Prep}_{\mathcal{H}}^{\dagger} \text{Sel}_{\mathcal{H}} \text{Prep}_{\mathcal{H}}(2|0\rangle\langle 0| - I). - - Specifically, the resources are given by one :class:`~.ResourcePrepSelPrep` gate and one - :class:`~.ResourceReflection` gate. - """ - gate_types = {} - ref = ResourceReflection.resource_rep(re.ResourceIdentity, {}, num_ctrl_wires) - psp = ResourcePrepSelPrep.resource_rep(cmpr_ops) - - gate_types[ref] = 1 - gate_types[psp] = 1 - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, corresponding to the unitaries of the LCU representation of the hamiltonian being qubitized. - * num_ctrl_wires (int): The number of qubits used to prepare the coefficients vector of the LCU. - """ - lcu = self.hyperparameters["hamiltonian"] - _, ops = lcu.terms() - - cmpr_ops = tuple(op.resource_rep_from_op() for op in ops) - num_ctrl_wires = len(self.hyperparameters["control"]) - return {"cmpr_ops": cmpr_ops, "num_ctrl_wires": num_ctrl_wires} - - @classmethod - def resource_rep(cls, cmpr_ops, num_ctrl_wires) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, corresponding to the unitaries of the LCU representation of the hamiltonian being qubitized. - num_ctrl_wires (int): The number of qubits used to prepare the coefficients vector of the LCU. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"cmpr_ops": cmpr_ops, "num_ctrl_wires": num_ctrl_wires} - return CompressedResourceOp(cls, params) - - -class ResourceQROM(qml.QROM, ResourceOperator): - """Resource class for the QROM template. - - Args: - bitstrings (list[str]): the bitstrings to be encoded - control_wires (Sequence[int]): the wires where the indexes are specified - target_wires (Sequence[int]): the wires where the bitstring is loaded - work_wires (Sequence[int]): the auxiliary wires used for the computation - clean (bool): if True, the work wires are not altered by operator, default is ``True`` - - Resource Parameters: - * num_bitstrings (int): the number of bitstrings that are to be encoded - * num_bit_flips (int): the number of bit flips needed for the list of bitstrings - * num_control_wires (int): the number of control wires where in the indexes are specified - * num_work_wires (int): the number of auxiliary wires used for QROM computation - * size_bitstring (int): the length of each bitstring - * clean (bool): if True, the work wires are not altered by the QROM operator - - Resources: - The resources for QROM are taken from the following two papers: - `Low et al. (2024) `_ (Figure 1.C) and - `Berry et al. (2019) `_ (Figure 4) - - We use the one-auxillary qubit version of select, instead of the built-in select - resources. - - .. seealso:: :class:`~.QROM` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceQROM.resources( - ... num_bitstrings=3, - ... num_bit_flips=7, - ... num_control_wires=5, - ... num_work_wires=5, - ... size_bitstring=3, - ... clean=True - ... ) - {Hadamard: 6, CNOT: 7, MultiControlledX: 8, X: 8, CSWAP: 12} - """ - - # pylint: disable=too-many-arguments - @staticmethod - def _resource_decomp( - num_bitstrings, - num_bit_flips, - num_control_wires, - num_work_wires, - size_bitstring, - clean, - **kwargs, - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_bitstrings (int): the number of bitstrings that are to be encoded - num_bit_flips (int): the number of bit flips needed for the list of bitstrings - num_control_wires (int): the number of control wires where in the indexes are specified - num_work_wires (int): the number of auxiliary wires used for QROM computation - size_bitstring (int): the length of each bitstring - clean (bool): if True, the work wires are not altered by the QROM operator - - Resources: - The resources for QROM are taken from the following two papers: - `Low et al. (2024) `_ (Figure 1.C) and - `Berry et al. (2019) `_ (Figure 4) - - We use the one-auxillary qubit version of select, instead of the built-in select - resources. - """ - gate_types = {} - x = re.ResourceX.resource_rep() - - if num_control_wires == 0: - gate_types[x] = num_bit_flips - return gate_types - - cnot = re.ResourceCNOT.resource_rep() - hadamard = re.ResourceHadamard.resource_rep() - - num_parallel_computations = (num_work_wires + size_bitstring) // size_bitstring - num_parallel_computations = min(num_parallel_computations, num_bitstrings) - - num_swap_wires = math.floor(math.log2(num_parallel_computations)) - num_select_wires = math.ceil(math.log2(math.ceil(num_bitstrings / (2**num_swap_wires)))) - - swap_clean_prefactor = 1 - select_clean_prefactor = 1 - - if clean: - gate_types[hadamard] = 2 * size_bitstring - swap_clean_prefactor = 4 - select_clean_prefactor = 2 - - # SELECT cost: - gate_types[cnot] = num_bit_flips # each unitary in the select is just a CNOT - - multi_x = re.ResourceMultiControlledX.resource_rep(num_select_wires, 0, 0) - num_total_ctrl_possibilities = 2**num_select_wires - gate_types[multi_x] = select_clean_prefactor * ( - 2 * num_total_ctrl_possibilities # two applications targetting the aux qubit - ) - num_zero_controls = (2 * num_total_ctrl_possibilities * num_select_wires) // 2 - gate_types[x] = select_clean_prefactor * ( - num_zero_controls * 2 # conjugate 0 controls on the multi-qubit x gates from above - ) - # SWAP cost: - ctrl_swap = re.ResourceCSWAP.resource_rep() - gate_types[ctrl_swap] = swap_clean_prefactor * ((2**num_swap_wires) - 1) * size_bitstring - - return gate_types - - @property - def resource_params(self) -> Dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_bitstrings (int): the number of bitstrings that are to be encoded - * num_bit_flips (int): the number of bit flips needed for the list of bitstrings - * num_control_wires (int): the number of control wires where in the indexes are specified - * num_work_wires (int): the number of auxiliary wires used for QROM computation - * size_bitstring (int): the length of each bitstring - * clean (bool): if True, the work wires are not altered by the QROM operator - """ - bitstrings = self.hyperparameters["bitstrings"] - num_bitstrings = len(bitstrings) - - num_bit_flips = 0 - for bit_string in bitstrings: - num_bit_flips += bit_string.count("1") - - num_work_wires = len(self.hyperparameters["work_wires"]) - size_bitstring = len(self.hyperparameters["target_wires"]) - num_control_wires = len(self.hyperparameters["control_wires"]) - clean = self.hyperparameters["clean"] - - return { - "num_bitstrings": num_bitstrings, - "num_bit_flips": num_bit_flips, - "num_control_wires": num_control_wires, - "num_work_wires": num_work_wires, - "size_bitstring": size_bitstring, - "clean": clean, - } - - @classmethod - def resource_rep( - cls, num_bitstrings, num_bit_flips, num_control_wires, num_work_wires, size_bitstring, clean - ) -> CompressedResourceOp: # pylint: disable=too-many-arguments - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_bitstrings (int): the number of bitstrings that are to be encoded - num_bit_flips (int): the number of bit flips needed for the list of bitstrings - num_control_wires (int): the number of control wires where in the indexes are specified - num_work_wires (int): the number of auxiliary wires used for QROM computation - size_bitstring (int): the length of each bitstring - clean (bool): if True, the work wires are not altered by the QROM operator - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "num_bitstrings": num_bitstrings, - "num_bit_flips": num_bit_flips, - "num_control_wires": num_control_wires, - "num_work_wires": num_work_wires, - "size_bitstring": size_bitstring, - "clean": clean, - } - return CompressedResourceOp(cls, params) - - -class ResourceAmplitudeAmplification(qml.AmplitudeAmplification, ResourceOperator): - r"""Resource class for the AmplitudeAmplification template. - - Args: - U (Operator): the operator that prepares the state :math:`|\Psi\rangle` - O (Operator): the oracle that flips the sign of the state :math:`|\phi\rangle` and does nothing to the state :math:`|\phi^{\perp}\rangle` - iters (int): the number of iterations of the amplitude amplification subroutine, default is ``1`` - fixed_point (bool): whether to use the fixed-point amplitude amplification algorithm, default is ``False`` - work_wire (int): the auxiliary wire to use for the fixed-point amplitude amplification algorithm, default is ``None`` - reflection_wires (Wires): the wires to reflect on, default is the wires of ``U`` - p_min (int): the lower bound for the probability of success in fixed-point amplitude amplification, default is ``0.9`` - - Resource Parameters: - * U_op (Type[~.ResourceOperator]): the class of the operator that prepares the state :math:`|\Psi\rangle` - * U_params (dict): the parameters for the U operator - * O_op (Type[~.ResourceOperator]): the class of the oracle that flips the sign of the state :math:`|\phi\rangle` and does nothing to the state :math:`|\phi^{\perp}\rangle` - * O_params (dict): the parameters for the O operator - * iters (int): the number of iterations of the amplitude amplification subroutine - * num_ref_wires (int): the number of wires used for the reflection - * fixed_point (bool): whether to use the fixed-point amplitude amplification algorithm - - Resources: - The resources are taken from the decomposition of ``qml.AmplitudeAmplification`` class. - - .. seealso:: :class:`~.AmplitudeAmplification` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceAmplitudeAmplification.resources( - ... U_op=re.ResourceHadamard, - ... U_params={}, - ... O_op=re.ResourceX, - ... O_params={}, - ... iters=5, - ... num_ref_wires=10, - ... fixed_point=True - ... ) - {C(X,1,0,0): 4, PhaseShift: 2, Hadamard: 8, Reflection: 2} - """ - - # pylint: disable=too-many-arguments - @staticmethod - def _resource_decomp( - U_op, - U_params, - O_op, - O_params, - iters, - num_ref_wires, - fixed_point, - **kwargs, - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - U_op (Operator): the operator that prepares the state :math:`|\Psi\rangle` - U_params (dict): the parameters for the U operator - O_op (Operator): the oracle that flips the sign of the state :math:`|\phi\rangle` and does nothing to the state :math:`|\phi^{\perp}\rangle` - O_params (dict): the parameters for the O operator - iters (int): the number of iterations of the amplitude amplification subroutine - num_ref_wires (int): the number of wires used for the reflection - fixed_point (bool): whether to use the fixed-point amplitude amplification algorithm - - - Resources: - The resources are taken from the decomposition of :class:`qml.AmplitudeAmplification` class. - """ - gate_types = {} - ctrl = re.ResourceControlled.resource_rep( - base_class=O_op, - base_params=O_params, - num_ctrl_wires=1, - num_ctrl_values=0, - num_work_wires=0, - ) - phase_shift = re.ResourcePhaseShift.resource_rep() - hadamard = re.ResourceHadamard.resource_rep() - reflection = re.ResourceReflection.resource_rep( - base_class=U_op, base_params=U_params, num_ref_wires=num_ref_wires - ) - - if not fixed_point: - oracles = re.CompressedResourceOp(O_op, params=O_params) - gate_types[oracles] = iters - gate_types[reflection] = iters - - return gate_types - - iters = iters // 2 - - gate_types[ctrl] = iters * 2 - gate_types[phase_shift] = iters - gate_types[hadamard] = iters * 4 - gate_types[reflection] = iters - - return gate_types - - @property - def resource_params(self) -> Dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * U_op (Operator): the operator that prepares the state :math:`|\Psi\rangle` - * U_params (dict): the parameters for the U operator - * O_op (Operator): the oracle that flips the sign of the state :math:`|\phi\rangle` and does nothing to the state :math:`|\phi^{\perp}\rangle` - * O_params (dict): the parameters for the O operator - * iters (int): the number of iterations of the amplitude amplification subroutine - * num_ref_wires (int): the number of wires used for the reflection - * fixed_point (bool): whether to use the fixed-point amplitude amplification algorithm - """ - U_op = self.hyperparameters["U"] - O_op = self.hyperparameters["O"] - try: - U_params = U_op.resource_params - except (NotImplementedError, AttributeError): - U_params = {} - - try: - O_params = O_op.resource_params - except (NotImplementedError, AttributeError): - O_params = {} - - iters = self.hyperparameters["iters"] - fixed_point = self.hyperparameters["fixed_point"] - num_ref_wires = len(self.hyperparameters["reflection_wires"]) - - return { - "U_op": type(U_op), - "U_params": U_params, - "O_op": type(O_op), - "O_params": O_params, - "iters": iters, - "num_ref_wires": num_ref_wires, - "fixed_point": fixed_point, - } - - # pylint: disable=too-many-arguments - @classmethod - def resource_rep( - cls, U_op, U_params, O_op, O_params, iters, num_ref_wires, fixed_point - ) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - U_op (Operator): the operator that prepares the state :math:`|\Psi\rangle` - U_params (dict): the parameters for the U operator - O_op (Operator): the oracle that flips the sign of the state :math:`|\phi\rangle` and does nothing to the state :math:`|\phi^{\perp}\rangle` - O_params (dict): the parameters for the O operator - iters (int): the number of iterations of the amplitude amplification subroutine - num_ref_wires (int): the number of wires used for the reflection - fixed_point (bool): whether to use the fixed-point amplitude amplification algorithm - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "U_op": U_op, - "U_params": U_params, - "O_op": O_op, - "O_params": O_params, - "iters": iters, - "num_ref_wires": num_ref_wires, - "fixed_point": fixed_point, - } - return CompressedResourceOp(cls, params) diff --git a/pennylane/labs/resource_estimation/templates/trotter.py b/pennylane/labs/resource_estimation/templates/trotter.py deleted file mode 100644 index c8e7b3f0c12..00000000000 --- a/pennylane/labs/resource_estimation/templates/trotter.py +++ /dev/null @@ -1,494 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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 templates for Suzuki-Trotter approximation based subroutines. -""" -from collections import defaultdict -from functools import wraps -from typing import Dict - -import pennylane as qml -from pennylane.labs import resource_estimation as re -from pennylane.labs.resource_estimation import ( - CompressedResourceOp, - ResourceExp, - ResourceOperator, - ResourcesNotDefined, -) -from pennylane.templates import TrotterProduct -from pennylane.templates.subroutines.trotter import TrotterizedQfunc - -# pylint: disable=arguments-differ - - -class ResourceTrotterProduct( - TrotterProduct, ResourceOperator -): # pylint: disable=too-many-ancestors - r"""An operation representing the Suzuki-Trotter product approximation for the complex matrix - exponential of a given Hamiltonian. - - The Suzuki-Trotter product formula provides a method to approximate the matrix exponential of - Hamiltonian expressed as a linear combination of terms which in general do not commute. Consider - the Hamiltonian :math:`H = \Sigma^{N}_{j=0} O_{j}`, the product formula is constructed using - symmetrized products of the terms in the Hamiltonian. The symmetrized products of order - :math:`m \in [1, 2, 4, ..., 2k]` with :math:`k \in \mathbb{N}` are given by: - - .. math:: - - \begin{align} - S_{1}(t) &= \Pi_{j=0}^{N} \ e^{i t O_{j}} \\ - S_{2}(t) &= \Pi_{j=0}^{N} \ e^{i \frac{t}{2} O_{j}} \cdot \Pi_{j=N}^{0} \ e^{i \frac{t}{2} O_{j}} \\ - &\vdots \\ - S_{m}(t) &= S_{m-2}(p_{m}t)^{2} \cdot S_{m-2}((1-4p_{m})t) \cdot S_{m-2}(p_{m}t)^{2}, - \end{align} - - where the coefficient is :math:`p_{m} = 1 / (4 - \sqrt[m - 1]{4})`. The :math:`m`th order, - :math:`n`-step Suzuki-Trotter approximation is then defined as: - - .. math:: e^{iHt} \approx \left [S_{m}(t / n) \right ]^{n}. - - For more details see `J. Math. Phys. 32, 400 (1991) `_. - - Args: - hamiltonian (Union[.Hamiltonian, .Sum, .SProd]): The Hamiltonian written as a linear combination - of operators with known matrix exponentials. - time (float): The time of evolution, namely the parameter :math:`t` in :math:`e^{iHt}` - n (int): An integer representing the number of Trotter steps to perform - order (int): An integer (:math:`m`) representing the order of the approximation (must be 1 or even) - check_hermitian (bool): A flag to enable the validation check to ensure this is a valid unitary operator - - Resource Parameters: - * n (int): an integer representing the number of Trotter steps to perform - * order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - * first_order_expansion (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - - Resources: - The resources are defined according to the recursive formula presented above. Specifically, each - operator in the :code:`first_order_expansion` is called a number of times given by the formula: - - .. math:: C_{O_{j}} = 2n \cdot 5^{\frac{m}{2} - 1} - - Furthermore, the first and last terms of the Hamiltonian appear in pairs due to the symmetric form - of the recursive formula. Those counts are further simplified by grouping like terms as: - - .. math:: - - \begin{align} - C_{O_{0}} &= n \cdot 5^{\frac{m}{2} - 1} + 1, \\ - C_{O_{N}} &= n \cdot 5^{\frac{m}{2} - 1}. - \end{align} - - .. seealso:: :class:`~.TrotterProduct` - - **Example** - - The arguments can be provided directly to the :code:`resources()` function to extract the cost: - - >>> n, order = (1, 2) - >>> first_order_expansion = [re.ResourceRX.resource_rep(), re.ResourceRZ.resource_rep()] - >>> re.ResourceTrotterProduct.resources(n, order, first_order_expansion) - defaultdict(, {RX: 2, RZ: 1}) - - """ - - @staticmethod - def _resource_decomp( - n, order, first_order_expansion, **kwargs - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - The Suzuki-Trotter product formula provides a method to approximate the matrix exponential of - Hamiltonian expressed as a linear combination of terms which in general do not commute. Consider - the Hamiltonian :math:`H = \Sigma^{N}_{j=0} O_{j}`, the product formula is constructed using - symmetrized products of the terms in the Hamiltonian. The symmetrized products of order - :math:`m \in [1, 2, 4, ..., 2k]` with :math:`k \in \mathbb{N}` are given by: - - .. math:: - - \begin{align} - S_{1}(t) &= \Pi_{j=0}^{N} \ e^{i t O_{j}} \\ - S_{2}(t) &= \Pi_{j=0}^{N} \ e^{i \frac{t}{2} O_{j}} \cdot \Pi_{j=N}^{0} \ e^{i \frac{t}{2} O_{j}} \\ - &\vdots \\ - S_{m}(t) &= S_{m-2}(p_{m}t)^{2} \cdot S_{m-2}((1-4p_{m})t) \cdot S_{m-2}(p_{m}t)^{2}, - \end{align} - - where the coefficient is :math:`p_{m} = 1 / (4 - \sqrt[m - 1]{4})`. The :math:`m`th order, - :math:`n`-step Suzuki-Trotter approximation is then defined as: - - .. math:: e^{iHt} \approx \left [S_{m}(t / n) \right ]^{n}. - - For more details see `J. Math. Phys. 32, 400 (1991) `_. - - Args: - n (int): an integer representing the number of Trotter steps to perform - order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - first_order_expansion (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - - Resources: - The resources are defined according to the recurrsive formula presented above. Specifically, each - operator in the :code:`first_order_expansion` is called a number of times given by the formula: - - .. math:: C_{O_{j}} = 2n \cdot 5^{\frac{m}{2} - 1} - - Furthermore, the first and last terms of the hamiltonian appear in pairs due to the symmetric form - of the recurrsive formula. Those counts are further simplified by grouping like terms as: - - .. math:: - - \begin{align} - C_{O_{0}} &= n \cdot 5^{\frac{m}{2} - 1} + 1, \\ - C_{O_{N}} &= n \cdot 5^{\frac{m}{2} - 1}. - \end{align} - - **Example** - - The arguments can be provided directly to the :code:`resources()` function to extract the cost: - - >>> n, order = (1, 2) - >>> first_order_expansion = [re.ResourceRX.resource_rep(), re.ResourceRZ.resource_rep()] - >>> re.ResourceTrotterProduct.resources(n, order, first_order_expansion) - defaultdict(, {RX: 2, RZ: 1}) - - """ - k = order // 2 - gate_types = defaultdict(int, {}) - - if order == 1: - for cp_rep in first_order_expansion: - gate_types[cp_rep] += n - return gate_types - - cp_rep_first = first_order_expansion[0] - cp_rep_last = first_order_expansion[-1] - cp_rep_rest = first_order_expansion[1:-1] - - for cp_rep in cp_rep_rest: - gate_types[cp_rep] += 2 * n * (5 ** (k - 1)) - - gate_types[cp_rep_first] += n * (5 ** (k - 1)) + 1 - gate_types[cp_rep_last] += n * (5 ** (k - 1)) - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * n (int): an integer representing the number of Trotter steps to perform - * order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - * first_order_expansion (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - """ - n = self.hyperparameters["n"] - base = self.hyperparameters["base"] - order = self.hyperparameters["order"] - - first_order_expansion = [ - ResourceExp.resource_rep( - **re.ops.op_math.symbolic._extract_exp_params( # pylint: disable=protected-access - op, scalar=1j, num_steps=1 - ) - ) - for op in base.operands - ] - - return { - "n": n, - "order": order, - "first_order_expansion": first_order_expansion, - } - - @classmethod - def resource_rep(cls, n, order, first_order_expansion) -> CompressedResourceOp: - """Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - n (int): an integer representing the number of Trotter steps to perform - order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - first_order_expansion (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "n": n, - "order": order, - "first_order_expansion": first_order_expansion, - } - return CompressedResourceOp(cls, params) - - @classmethod - def resources(cls, *args, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts.""" - return cls._resource_decomp(*args, **kwargs) - - -class ResourceTrotterizedQfunc(TrotterizedQfunc, ResourceOperator): - r"""Generates higher order Suzuki-Trotter product formulas from a set of - operations defined in a function. - - The Suzuki-Trotter product formula provides a method to approximate the matrix exponential of - Hamiltonian expressed as a linear combination of terms which in general do not commute. Consider - the Hamiltonian :math:`H = \Sigma^{N}_{j=0} O_{j}`, the product formula is constructed using - symmetrized products of the terms in the Hamiltonian. The symmetrized products of order - :math:`m \in [1, 2, 4, ..., 2k]` with :math:`k \in \mathbb{N}` are given by: - - .. math:: - - \begin{align} - S_{1}(t) &= \Pi_{j=0}^{N} \ e^{i t O_{j}} \\ - S_{2}(t) &= \Pi_{j=0}^{N} \ e^{i \frac{t}{2} O_{j}} \cdot \Pi_{j=N}^{0} \ e^{i \frac{t}{2} O_{j}} \\ - &\vdots \\ - S_{m}(t) &= S_{m-2}(p_{m}t)^{2} \cdot S_{m-2}((1-4p_{m})t) \cdot S_{m-2}(p_{m}t)^{2}, - \end{align} - - where the coefficient is :math:`p_{m} = 1 / (4 - \sqrt[m - 1]{4})`. The :math:`m`th order, - :math:`n`-step Suzuki-Trotter approximation is then defined as: - - .. math:: e^{iHt} \approx \left [S_{m}(t / n) \right ]^{n}. - - For more details see `J. Math. Phys. 32, 400 (1991) `_. - - Args: - time (float): the time of evolution, namely the parameter :math:`t` in :math:`e^{iHt}` - *trainable_args (tuple): the trainable arguments of the first-order expansion function - qfunc (Callable): the first-order expansion given as a callable function which queues operations - wires (Iterable): the set of wires the operation will act upon (should be identical to qfunc wires) - n (int): an integer representing the number of Trotter steps to perform - order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - reverse (bool): if true, reverse the order of the operations queued by :code:`qfunc` - **non_trainable_kwargs (dict): non-trainable keyword arguments of the first-order expansion function - - Resource Parameters: - * n (int): an integer representing the number of Trotter steps to perform - * order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - * qfunc_compressed_reps (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - - Resources: - The resources are defined according to the recurrsive formula presented above. Specifically, each - operator in the :code:`first_order_expansion` is called a number of times given by the formula: - - .. math:: C_{O_{j}} = 2n \cdot 5^{\frac{m}{2} - 1} - - .. seealso:: :class:`~.TrotterizedQfunc` - - **Example** - - The arguments can be provided directly to the :code:`resources()` function to extract the cost: - - >>> n, order = (1, 2) - >>> first_order_expansion = [re.ResourceRX.resource_rep(), re.ResourceRZ.resource_rep()] - >>> re.ResourceTrotterizedQfunc.resources(n, order, first_order_expansion) - defaultdict(, {RX: 2, RZ: 2}) - - """ - - @staticmethod - def _resource_decomp( - n, order, qfunc_compressed_reps, **kwargs - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - The Suzuki-Trotter product formula provides a method to approximate the matrix exponential of - Hamiltonian expressed as a linear combination of terms which in general do not commute. Consider - the Hamiltonian :math:`H = \Sigma^{N}_{j=0} O_{j}`, the product formula is constructed using - symmetrized products of the terms in the Hamiltonian. The symmetrized products of order - :math:`m \in [1, 2, 4, ..., 2k]` with :math:`k \in \mathbb{N}` are given by: - - .. math:: - - \begin{align} - S_{1}(t) &= \Pi_{j=0}^{N} \ e^{i t O_{j}} \\ - S_{2}(t) &= \Pi_{j=0}^{N} \ e^{i \frac{t}{2} O_{j}} \cdot \Pi_{j=N}^{0} \ e^{i \frac{t}{2} O_{j}} \\ - &\vdots \\ - S_{m}(t) &= S_{m-2}(p_{m}t)^{2} \cdot S_{m-2}((1-4p_{m})t) \cdot S_{m-2}(p_{m}t)^{2}, - \end{align} - - where the coefficient is :math:`p_{m} = 1 / (4 - \sqrt[m - 1]{4})`. The :math:`m`th order, - :math:`n`-step Suzuki-Trotter approximation is then defined as: - - .. math:: e^{iHt} \approx \left [S_{m}(t / n) \right ]^{n}. - - For more details see `J. Math. Phys. 32, 400 (1991) `_. - - Args: - n (int): an integer representing the number of Trotter steps to perform - order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - qfunc_compressed_reps (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - - Resources: - The resources are defined according to the recurrsive formula presented above. Specifically, each - operator in the :code:`first_order_expansion` is called a number of times given by the formula: - - .. math:: C_{O_{j}} = 2n \cdot 5^{\frac{m}{2} - 1} - - **Example** - - The arguments can be provided directly to the :code:`resources()` function to extract the cost: - - >>> n, order = (1, 2) - >>> first_order_expansion = [re.ResourceRX.resource_rep(), re.ResourceRZ.resource_rep()] - >>> re.ResourceTrotterizedQfunc.resources(n, order, first_order_expansion) - defaultdict(, {RX: 2, RZ: 2}) - - """ - k = order // 2 - gate_types = defaultdict(int, {}) - - if order == 1: - for cp_rep in qfunc_compressed_reps: - gate_types[cp_rep] += n - return gate_types - - for cp_rep in qfunc_compressed_reps: - gate_types[cp_rep] += 2 * n * (5 ** (k - 1)) - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: dictionary containing the resource parameters - * n (int): an integer representing the number of Trotter steps to perform - * order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - * qfunc_compressed_reps (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - """ - with qml.QueuingManager.stop_recording(): - with qml.queuing.AnnotatedQueue() as q: - base_hyper_params = ("n", "order", "qfunc", "reverse") - - qfunc_args = self.parameters - qfunc_kwargs = { - k: v for k, v in self.hyperparameters.items() if not k in base_hyper_params - } - - qfunc = self.hyperparameters["qfunc"] - qfunc(*qfunc_args, wires=self.wires, **qfunc_kwargs) - - try: - qfunc_compressed_reps = tuple(op.resource_rep_from_op() for op in q.queue) - - except AttributeError as error: - raise ResourcesNotDefined( - "Every operation in the TrotterizedQfunc should be a ResourceOperator" - ) from error - - return { - "n": self.hyperparameters["n"], - "order": self.hyperparameters["order"], - "qfunc_compressed_reps": qfunc_compressed_reps, - } - - @classmethod - def resource_rep(cls, n, order, qfunc_compressed_reps, name=None) -> CompressedResourceOp: - """Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - n (int): an integer representing the number of Trotter steps to perform - order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - qfunc_compressed_reps (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "n": n, - "order": order, - "qfunc_compressed_reps": qfunc_compressed_reps, - } - return CompressedResourceOp(cls, params, name=name) - - def resource_rep_from_op(self) -> CompressedResourceOp: - r"""Returns a compressed representation directly from the operator""" - return self.__class__.resource_rep(**self.resource_params, name=self._name) - - -def resource_trotterize(qfunc, n=1, order=2, reverse=False): - r"""Generates higher order Suzuki-Trotter product formulas from a set of - operations defined in a function. - - The Suzuki-Trotter product formula provides a method to approximate the matrix exponential of - Hamiltonian expressed as a linear combination of terms which in general do not commute. Consider - the Hamiltonian :math:`H = \Sigma^{N}_{j=0} O_{j}`, the product formula is constructed using - symmetrized products of the terms in the Hamiltonian. The symmetrized products of order - :math:`m \in [1, 2, 4, ..., 2k]` with :math:`k \in \mathbb{N}` are given by: - - .. math:: - - \begin{align} - S_{1}(t) &= \Pi_{j=0}^{N} \ e^{i t O_{j}} \\ - S_{2}(t) &= \Pi_{j=0}^{N} \ e^{i \frac{t}{2} O_{j}} \cdot \Pi_{j=N}^{0} \ e^{i \frac{t}{2} O_{j}} \\ - &\vdots \\ - S_{m}(t) &= S_{m-2}(p_{m}t)^{2} \cdot S_{m-2}((1-4p_{m})t) \cdot S_{m-2}(p_{m}t)^{2}, - \end{align} - - where the coefficient is :math:`p_{m} = 1 / (4 - \sqrt[m - 1]{4})`. The :math:`m`th order, - :math:`n`-step Suzuki-Trotter approximation is then defined as: - - .. math:: e^{iHt} \approx \left [S_{m}(t / n) \right ]^{n}. - - For more details see `J. Math. Phys. 32, 400 (1991) `_. - - Args: - qfunc (Callable): A function which queues the operations corresponding to the exponentiated - terms of the hamiltonian (:math:`e^{i t O_{j}}`). The operations should be queued according - to the first order expression. - n (int): an integer representing the number of Trotter steps to perform - order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - - Resources: - The resources are defined according to the recurrsive formula presented above. Specifically, each - operator in the :code:`first_order_expansion` is called a number of times given by the formula: - - .. math:: C_{O_{j}} = 2n \cdot 5^{\frac{m}{2} - 1} - - .. seealso:: :class:`~.trotterize` - - **Example** - - First we define a function which queues the first-order expansion: - - .. code-block:: python3 - - def first_order_expansion(time, theta, phi, wires): - "This is the first order expansion (U_1)." - re.ResourceRX(time*theta, wires[0]) - re.ResourceRY(time*phi, wires[1]) - - The arguments can be provided directly to the :code:`resources()` function to extract the cost: - - >>> n, order = (1, 2) - >>> time, theta, phi = (0.1, 0.2, 0.3) - >>> resource_op = re.resource_trotterize(first_order_expansion, n, order)(time, theta, phi, wires=['a', 'b']) - >>> resource_op.resources(**resource_op.resource_params) - defaultdict(, {RX: 2, RY: 2}) - - """ - - @wraps(qfunc) - def wrapper(*args, **kwargs): - time = args[0] - other_args = args[1:] - return ResourceTrotterizedQfunc( - time, *other_args, qfunc=qfunc, n=n, order=order, reverse=reverse, **kwargs - ) - - return wrapper From 4d7b994da2bf04c5b2a12a3e148c5f49628fef19 Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Thu, 8 May 2025 11:57:39 -0400 Subject: [PATCH 04/18] clean repo --- pennylane/labs/resource_estimation/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 7d70e810181..fc4f3a7f1d9 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -22,14 +22,4 @@ .. currentmodule:: pennylane.labs.resource_estimation -Resource Estimation Base Classes: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. autosummary:: - :toctree: api - - ~ResourceOperator - """ - -from .resource_operator import ResourceOperator, ResourcesNotDefined From 96600c9895044b2123029a48999fc301548c75af Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Thu, 22 May 2025 10:32:22 -0400 Subject: [PATCH 05/18] remove files that were added in the merge --- .../ops/op_math/symbolic.py | 1251 ------------- .../templates/stateprep.py | 393 ---- .../templates/subroutines.py | 1635 ----------------- .../resource_estimation/templates/trotter.py | 494 ----- 4 files changed, 3773 deletions(-) delete mode 100644 pennylane/labs/resource_estimation/ops/op_math/symbolic.py delete mode 100644 pennylane/labs/resource_estimation/templates/stateprep.py delete mode 100644 pennylane/labs/resource_estimation/templates/subroutines.py delete mode 100644 pennylane/labs/resource_estimation/templates/trotter.py diff --git a/pennylane/labs/resource_estimation/ops/op_math/symbolic.py b/pennylane/labs/resource_estimation/ops/op_math/symbolic.py deleted file mode 100644 index e34fc9c83f7..00000000000 --- a/pennylane/labs/resource_estimation/ops/op_math/symbolic.py +++ /dev/null @@ -1,1251 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""Resource operators for symbolic operations.""" -from collections import defaultdict -from typing import Dict - -import pennylane.labs.resource_estimation as re -from pennylane import math -from pennylane.labs.resource_estimation.resource_container import _scale_dict -from pennylane.operation import Operation -from pennylane.ops.op_math.adjoint import AdjointOperation -from pennylane.ops.op_math.controlled import ControlledOp -from pennylane.ops.op_math.exp import Exp -from pennylane.ops.op_math.pow import PowOperation -from pennylane.ops.op_math.prod import Prod -from pennylane.pauli import PauliSentence - -# pylint: disable=too-many-ancestors,arguments-differ,too-many-arguments,too-many-positional-arguments - - -class ResourceAdjoint(AdjointOperation, re.ResourceOperator): - r"""Resource class for the symbolic AdjointOperation. - - A symbolic class used to represent the adjoint of some base operation. - - Args: - base (~.operation.Operator): The operator that we want the adjoint of. - - Resource Parameters: - * base_class (Type[~.ResourceOperator]): the class type of the base operator that we want the adjoint of - * base_params (dict): the resource parameters required to extract the cost of the base operator - - Resources: - This symbolic operation represents the adjoint of some base operation. The resources are - determined as follows. If the base operation class :code:`base_class` implements the - :code:`.adjoint_resource_decomp()` method, then the resources are obtained from this. - - Otherwise, the adjoint resources are given as the adjoint of each operation in the - base operation's resources (via :code:`.resources()`). - - .. seealso:: :class:`~.ops.op_math.adjoint.AdjointOperation` - - **Example** - - The adjoint operation can be constructed like this: - - >>> qft = re.ResourceQFT(wires=range(3)) - >>> adjoint_qft = re.ResourceAdjoint(qft) - >>> adjoint_qft.resources(**adjoint_qft.resource_params) - defaultdict(, {Adjoint(Hadamard): 3, Adjoint(SWAP): 1, - Adjoint(ControlledPhaseShift): 3}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourceAdjoint.resources( - ... base_class = re.ResourceQFT, - ... base_params = {"num_wires": 3}, - ... ) - defaultdict(, {Adjoint(Hadamard): 3, Adjoint(SWAP): 1, - Adjoint(ControlledPhaseShift): 3}) - - .. details:: - :title: Usage Details - - We can configure the resources for the adjoint of a base operation by modifying - its :code:`.adjoint_resource_decomp(**resource_params)` method. Consider for example this - custom PauliZ class, where the adjoint resources are not defined (this is the default - for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def adjoint_resource_decomp(cls): - raise re.ResourcesNotDefined - - When this method is not defined, the adjoint resources are computed by taking the - adjoint of the resources of the operation. - - >>> CustomZ.resources() - {S: 2} - >>> re.ResourceAdjoint.resources(CustomZ, {}) - defaultdict(, {Adjoint(S): 2}) - - We can update the adjoint resources with the observation that the PauliZ gate is self-adjoint, - so the resources should just be the same as the base operation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def adjoint_resource_decomp(cls): - return {cls.resource_rep(): 1} - - >>> re.ResourceAdjoint.resources(CustomZ, {}) - {CustomZ: 1} - - """ - - @classmethod - def _resource_decomp( - cls, base_class, base_params, **kwargs - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (Type[~.ResourceOperator]): the class type of the base operator that we want the adjoint of - base_params (dict): the resource parameters required to extract the cost of the base operator - - Resources: - This symbolic operation represents the adjoint of some base operation. The resources are - determined as follows. If the base operation class :code:`base_class` implements the - :code:`.adjoint_resource_decomp()` method, then the resources are obtained from this. - - Otherwise, the adjoint resources are given as the adjoint of each operation in the - base operation's resources (via :code:`.resources()`). - - **Example** - - The adjoint operation can be constructed like this: - - >>> qft = re.ResourceQFT(wires=range(3)) - >>> adjoint_qft = re.ResourceAdjoint(qft) - >>> adjoint_qft.resources(**adjoint_qft.resource_params) - defaultdict(, {Adjoint(Hadamard): 3, Adjoint(SWAP): 1, - Adjoint(ControlledPhaseShift): 3}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourceAdjoint.resources( - ... base_class = re.ResourceQFT, - ... base_params = {"num_wires": 3}, - ... ) - defaultdict(, {Adjoint(Hadamard): 3, Adjoint(SWAP): 1, - Adjoint(ControlledPhaseShift): 3}) - - .. details:: - :title: Usage Details - - We can configure the resources for the adjoint of a base operation by modifying - its :code:`.adjoint_resource_decomp(**resource_params)` method. Consider for example this - custom PauliZ class, where the adjoint resources are not defined (this is the default - for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def adjoint_resource_decomp(cls): - raise re.ResourcesNotDefined - - When this method is not defined, the adjoint resources are computed by taking the - adjoint of the resources of the operation. - - >>> CustomZ.resources() - {S: 2} - >>> re.ResourceAdjoint.resources(CustomZ, {}) - defaultdict(, {Adjoint(S): 2}) - - We can update the adjoint resources with the observation that the PauliZ gate is self-adjoint, - so the resources should just be the same as the base operation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def adjoint_resource_decomp(cls): - return {cls.resource_rep(): 1} - - >>> re.ResourceAdjoint.resources(CustomZ, {}) - {CustomZ: 1} - """ - try: - return base_class.adjoint_resource_decomp(**base_params) - except re.ResourcesNotDefined: - gate_types = defaultdict(int) - decomp = base_class.resources(**base_params, **kwargs) - for gate, count in decomp.items(): - rep = cls.resource_rep(gate.op_type, gate.params) - gate_types[rep] = count - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (Type[~.ResourceOperator]): the class type of the base operator that we want the adjoint of - * base_params (dict): the resource parameters required to extract the cost of the base operator - - """ - return {"base_class": type(self.base), "base_params": self.base.resource_params} - - @classmethod - def resource_rep(cls, base_class, base_params) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (Type[~.ResourceOperator]): the class type of the base operator that we want the adjoint of - base_params (dict): the resource parameters required to extract the cost of the base operator - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp(cls, {"base_class": base_class, "base_params": base_params}) - - @staticmethod - def adjoint_resource_decomp(base_class, base_params) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. - - Args: - base_class (Type[~.ResourceOperator]): the class type of the base operator that we want the adjoint of - base_params (dict): the resource parameters required to extract the cost of the base operator - - Resources: - The adjoint of an adjointed operation is just the original operation. The resources - are given as one instance of the base operation. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {base_class.resource_rep(**base_params): 1} - - @staticmethod - def tracking_name(base_class, base_params) -> str: - r"""Returns the tracking name built with the operator's parameters.""" - base_name = base_class.tracking_name(**base_params) - return f"Adjoint({base_name})" - - -class ResourceControlled(ControlledOp, re.ResourceOperator): - r"""Resource class for the symbolic ControlledOp. - - A symbolic class used to represent the application of some base operation controlled on the state - of some control qubits. - - Args: - base (~.operation.Operator): the operator that is controlled - control_wires (Any): The wires to control on. - control_values (Iterable[Bool]): The values to control on. Must be the same - length as ``control_wires``. Defaults to ``True`` for all control wires. - Provided values are converted to `Bool` internally. - work_wires (Any): Any auxiliary wires that can be used in the decomposition - - Resource Parameters: - * base_class (Type[~.ResourceOperator]): the class type of the base operator to be controlled - * base_params (dict): the resource parameters required to extract the cost of the base operator - * num_ctrl_wires (int): the number of qubits the operation is controlled on - * num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - * num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are determined as follows. If the base operation class :code:`base_class` - implements the :code:`.controlled_resource_decomp()` method, then the resources are obtained - directly from this. - - Otherwise, the controlled resources are given in two steps. Firstly, any control qubits which - should be triggered when in the :math:`|0\rangle` state, are flipped. This corresponds to an additional - cost of two :class:`~.ResourceX` gates per :code:`num_ctrl_values`. Secondly, the base operation - resources are extracted (via :code:`.resources()`) and we add to the cost the controlled - variant of each operation in the resources. - - .. seealso:: :class:`~.ops.op_math.controlled.ControlledOp` - - **Example** - - The controlled operation can be constructed like this: - - >>> qft = re.ResourceQFT(wires=range(3)) - >>> controlled_qft = re.ResourceControlled( - ... qft, control_wires=['c0', 'c1', 'c2'], control_values=[1, 1, 1], work_wires=['w1', 'w2'], - ... ) - >>> controlled_qft.resources(**controlled_qft.resource_params) - defaultdict(, {C(Hadamard,3,0,2): 3, C(SWAP,3,0,2): 1, C(ControlledPhaseShift,3,0,2): 3}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourceControlled.resources( - ... base_class = re.ResourceQFT, - ... base_params = {"num_wires": 3}, - ... num_ctrl_wires = 3, - ... num_ctrl_values = 0, - ... num_work_wires = 2, - ... ) - defaultdict(, {C(Hadamard,3,0,2): 3, C(SWAP,3,0,2): 1, C(ControlledPhaseShift,3,0,2): 3}) - - .. details:: - :title: Usage Details - - We can configure the resources for the controlled of a base operation by modifying - its :code:`.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires, - **resource_params)` method. Consider for example this custom PauliZ class, where the - controlled resources are not defined (this is the default for a general :class:`~.ResourceOperator`). - - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def controlled_resource_decomp(cls, num_ctrl_wires, num_ctrl_values, num_work_wires): - raise re.ResourcesNotDefined - - When this method is not defined, the controlled resources are computed by taking the - controlled of each operation in the resources of the base operation. - - >>> CustomZ.resources() - {S: 2} - >>> re.ResourceControlled.resources(CustomZ, {}, num_ctrl_wires=1, num_ctrl_values=0, num_work_wires=0) - defaultdict(, {C(S,2,0,3): 2}) - - We can update the controlled resources with the observation that the PauliZ gate when controlled - on a single wire is equivalent to :math:`\hat{CZ} = \hat{H} \cdot \hat{CNOT} \cdot \hat{H}`. - so we can modify the base operation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def controlled_resource_decomp(cls, num_ctrl_wires, num_ctrl_values, num_work_wires): - if num_ctrl_wires == 1 and num_ctrl_values == 0: - return { - re.ResourceHadamard.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 1, - } - raise re.ResourcesNotDefined - - >>> re.ResourceControlled.resources(CustomZ, {}, num_ctrl_wires=1, num_ctrl_values=0, num_work_wires=0) - {Hadamard: 2, CNOT: 1} - - """ - - @classmethod - def _resource_decomp( - cls, base_class, base_params, num_ctrl_wires, num_ctrl_values, num_work_wires, **kwargs - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (Type[~.ResourceOperator]): the class type of the base operator to be controlled - base_params (dict): the resource parameters required to extract the cost of the base operator - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Resources: - The resources are determined as follows. If the base operation class :code:`base_class` - implements the :code:`.controlled_resource_decomp()` method, then the resources are obtained - directly from this. - - Otherwise, the controlled resources are given in two steps. Firstly, any control qubits which - should be triggered when in the :math:`|0\rangle` state, are flipped. This corresponds to an additional - cost of two :class:`~.ResourceX` gates per :code:`num_ctrl_values`. Secondly, the base operation - resources are extracted (via :code:`.resources()`) and we add to the cost the controlled - variant of each operation in the resources. - - .. seealso:: :class:`~.ops.op_math.controlled.ControlledOp` - - **Example** - - The controlled operation can be constructed like this: - - >>> qft = re.ResourceQFT(wires=range(3)) - >>> controlled_qft = re.ResourceControlled( - ... qft, control_wires=['c0', 'c1', 'c2'], control_values=[1, 1, 1], work_wires=['w1', 'w2'], - ... ) - >>> controlled_qft.resources(**controlled_qft.resource_params) - defaultdict(, {C(Hadamard,3,0,2): 3, C(SWAP,3,0,2): 1, C(ControlledPhaseShift,3,0,2): 3}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourceControlled.resources( - ... base_class = re.ResourceQFT, - ... base_params = {"num_wires": 3}, - ... num_ctrl_wires = 3, - ... num_ctrl_values = 0, - ... num_work_wires = 2, - ... ) - defaultdict(, {C(Hadamard,3,0,2): 3, C(SWAP,3,0,2): 1, C(ControlledPhaseShift,3,0,2): 3}) - - .. details:: - :title: Usage Details - - We can configure the resources for the controlled of a base operation by modifying - its :code:`.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, num_work_wires, - **resource_params)` method. Consider for example this custom PauliZ class, where the - controlled resources are not defined (this is the default for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def controlled_resource_decomp(cls, num_ctrl_wires, num_ctrl_values, num_work_wires): - raise re.ResourcesNotDefined - - When this method is not defined, the controlled resources are computed by taking the - controlled of each operation in the resources of the base operation. - - >>> CustomZ.resources() - {S: 2} - >>> re.ResourceControlled.resources(CustomZ, {}, num_ctrl_wires=1, num_ctrl_values=0, num_work_wires=0) - defaultdict(, {C(S,2,0,3): 2}) - - We can update the controlled resources with the observation that the PauliZ gate when controlled - on a single wire is equivalent to :math:`\hat{CZ} = \hat{H} \cdot \hat{CNOT} \cdot \hat{H}`. - so we can modify the base operation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def controlled_resource_decomp(cls, num_ctrl_wires, num_ctrl_values, num_work_wires): - if num_ctrl_wires == 1 and num_ctrl_values == 0: - return { - re.ResourceHadamard.resource_rep(): 2, - re.ResourceCNOT.resource_rep(): 1, - } - raise re.ResourcesNotDefined - - >>> re.ResourceControlled.resources(CustomZ, {}, num_ctrl_wires=1, num_ctrl_values=0, num_work_wires=0) - {Hadamard: 2, CNOT: 1} - - """ - try: - return base_class.controlled_resource_decomp( - num_ctrl_wires, num_ctrl_values, num_work_wires, **base_params - ) - except re.ResourcesNotDefined: - pass - - gate_types = defaultdict(int) - - if num_ctrl_values == 0: - decomp = base_class.resources(**base_params, **kwargs) - for gate, count in decomp.items(): - rep = cls.resource_rep(gate.op_type, gate.params, num_ctrl_wires, 0, num_work_wires) - gate_types[rep] = count - - return gate_types - - no_control = cls.resource_rep(base_class, base_params, num_ctrl_wires, 0, num_work_wires) - x = re.ResourceX.resource_rep() - gate_types[no_control] = 1 - gate_types[x] = 2 * num_ctrl_values - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (Type[~.ResourceOperator]): the class type of the base operator to be controlled - * base_params (dict): the resource parameters required to extract the cost of the base operator - * num_ctrl_wires (int): the number of qubits the operation is controlled on - * num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - * num_work_wires (int): the number of additional qubits that can be used for decomposition - """ - return { - "base_class": type(self.base), - "base_params": self.base.resource_params, - "num_ctrl_wires": len(self.control_wires), - "num_ctrl_values": len([val for val in self.control_values if not val]), - "num_work_wires": len(self.work_wires), - } - - @classmethod - def resource_rep( - cls, base_class, base_params, num_ctrl_wires, num_ctrl_values, num_work_wires - ) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (Type[~.ResourceOperator]): the class type of the base operator to be controlled - base_params (dict): the resource parameters required to extract the cost of the base operator - num_ctrl_wires (int): the number of qubits the operation is controlled on - num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state - num_work_wires (int): the number of additional qubits that can be used for decomposition - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp( - cls, - { - "base_class": base_class, - "base_params": base_params, - "num_ctrl_wires": num_ctrl_wires, - "num_ctrl_values": num_ctrl_values, - "num_work_wires": num_work_wires, - }, - ) - - @classmethod - def controlled_resource_decomp( - cls, - outer_num_ctrl_wires, - outer_num_ctrl_values, - outer_num_work_wires, - base_class, - base_params, - num_ctrl_wires, - num_ctrl_values, - num_work_wires, - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. - - Args: - outer_num_ctrl_wires (int): The number of control qubits to further control the base - controlled operation upon. - outer_num_ctrl_values (int): The subset of those control qubits, which further control - the base controlled operation, which are controlled when in the :math:`|0\rangle` state. - outer_num_work_wires (int): the number of additional qubits that can be used in the - decomposition for the further controlled, base control oepration. - base_class (Type[~.ResourceOperator]): the class type of the base operator to be controlled - base_params (dict): the resource parameters required to extract the cost of the base operator - num_ctrl_wires (int): the number of control qubits of the operation - num_ctrl_values (int): The subset of control qubits of the operation, that are controlled - when in the :math:`|0\rangle` state. - num_work_wires (int): The number of additional qubits that can be used for the - decomposition of the operation. - - Resources: - The resources are derived by simply combining the control qubits, control-values and - work qubits into a single instance of :class:`~.ResourceControlled` gate, controlled - on the whole set of control-qubits. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return { - cls.resource_rep( - base_class, - base_params, - outer_num_ctrl_wires + num_ctrl_wires, - outer_num_ctrl_values + num_ctrl_values, - outer_num_work_wires + num_work_wires, - ): 1 - } - - @staticmethod - def tracking_name(base_class, base_params, num_ctrl_wires, num_ctrl_values, num_work_wires): - r"""Returns the tracking name built with the operator's parameters.""" - base_name = base_class.tracking_name(**base_params) - return f"C({base_name},{num_ctrl_wires},{num_ctrl_values},{num_work_wires})" - - -class ResourcePow(PowOperation, re.ResourceOperator): - r"""Resource class for the symbolic Pow operation. - - A symbolic class used to represent some base operation raised to a power. - - Args: - base (~.operation.Operator): the operator to be raised to a power - z (float): the exponent (default value is 1) - - Resource Parameters: - * base_class (Type[~.ResourceOperator]): The class type of the base operator to be raised to some power. - * base_params (dict): the resource parameters required to extract the cost of the base operator - * z (int): the power that the operator is being raised to - - Resources: - The resources are determined as follows. If the power :math:`z = 0`, then we have the identitiy - gate and we have no resources. If the base operation class :code:`base_class` implements the - :code:`.pow_resource_decomp()` method, then the resources are obtained from this. Otherwise, - the resources of the operation raised to the power :math:`z` are given by extracting the base - operation's resources (via :code:`.resources()`) and raising each operation to the same power. - - .. seealso:: :class:`~.ops.op_math.pow.PowOperation` - - **Example** - - The operation raised to a power :math:`z` can be constructed like this: - - >>> qft = re.ResourceQFT(wires=range(3)) - >>> pow_qft = re.ResourcePow(qft, 2) - >>> pow_qft.resources(**pow_qft.resource_params) - defaultdict(, {Pow(Hadamard, 2): 3, Pow(SWAP, 2): 1, Pow(ControlledPhaseShift, 2): 3}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourcePow.resources( - ... base_class = re.ResourceQFT, - ... base_params = {"num_wires": 3}, - ... z = 2, - ... ) - defaultdict(, {Pow(Hadamard, 2): 3, Pow(SWAP, 2): 1, Pow(ControlledPhaseShift, 2): 3}) - - .. details:: - :title: Usage Details - - We can configure the resources for the power of a base operation by modifying - its :code:`.pow_resource_decomp(**resource_params, z)` method. Consider for example this - custom PauliZ class, where the pow-resources are not defined (this is the default - for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def pow_resource_decomp(cls, z): - raise re.ResourcesNotDefined - - When this method is not defined, the resources are computed by taking the power of - each operation in the resources of the base operation. - - >>> CustomZ.resources() - {S: 2} - >>> re.ResourcePow.resources(CustomZ, {}, z=2) - defaultdict(, {Pow(S, 2): 2}) - - We can update the resources with the observation that the PauliZ gate is self-inverse, - so the resources should when :math:`z mod 2 = 0` should just be the identity operation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def pow_resource_decomp(cls, z): - if z%2 == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} - - >>> re.ResourcePow.resources(CustomZ, {}, z=2) - {Identity: 1} - >>> re.ResourcePow.resources(CustomZ, {}, z=3) - {CustomZ: 1} - - """ - - @classmethod - def _resource_decomp( - cls, base_class, base_params, z, **kwargs - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (Type[~.ResourceOperator]): The class type of the base operator to be raised to some power. - base_params (dict): the resource parameters required to extract the cost of the base operator - z (int): the power that the operator is being raised to - - Resources: - The resources are determined as follows. If the power :math:`z = 0`, then we have the identitiy - gate and we have no resources. If the base operation class :code:`base_class` implements the - :code:`.pow_resource_decomp()` method, then the resources are obtained from this. Otherwise, - the resources of the operation raised to the power :math:`z` are given by extracting the base - operation's resources (via :code:`.resources()`) and raising each operation to the same power. - - **Example** - - The operation raised to a power :math:`z` can be constructed like this: - - >>> qft = re.ResourceQFT(wires=range(3)) - >>> pow_qft = re.ResourcePow(qft, 2) - >>> pow_qft.resources(**pow_qft.resource_params) - defaultdict(, {Pow(Hadamard, 2): 3, Pow(SWAP, 2): 1, Pow(ControlledPhaseShift, 2): 3}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourcePow.resources( - ... base_class = re.ResourceQFT, - ... base_params = {"num_wires": 3}, - ... z = 2, - ... ) - defaultdict(, {Pow(Hadamard, 2): 3, Pow(SWAP, 2): 1, Pow(ControlledPhaseShift, 2): 3}) - - .. details:: - :title: Usage Details - - We can configure the resources for the power of a base operation by modifying - its :code:`.pow_resource_decomp(**resource_params, z)` method. Consider for example this - custom PauliZ class, where the pow-resources are not defined (this is the default - for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def pow_resource_decomp(cls, z): - raise re.ResourcesNotDefined - - When this method is not defined, the resources are computed by taking the power of - each operation in the resources of the base operation. - - >>> CustomZ.resources() - {S: 2} - >>> re.ResourcePow.resources(CustomZ, {}, z=2) - defaultdict(, {Pow(S, 2): 2}) - - We can update the resources with the observation that the PauliZ gate is self-inverse, - so the resources should when :math:`z mod 2 = 0` should just be the identity operation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def pow_resource_decomp(cls, z): - if z%2 == 0: - return {re.ResourceIdentity.resource_rep(): 1} - return {cls.resource_rep(): 1} - - >>> re.ResourcePow.resources(CustomZ, {}, z=2) - {Identity: 1} - >>> re.ResourcePow.resources(CustomZ, {}, z=3) - {CustomZ: 1} - - """ - if z == 0: - return {re.ResourceIdentity.resource_rep(): 1} - - if z == 1: - return {base_class.resource_rep(**base_params): 1} - - try: - return base_class.pow_resource_decomp(z, **base_params) - except re.ResourcesNotDefined: - pass - - try: - gate_types = defaultdict(int) - decomp = base_class.resources(**base_params, **kwargs) - for gate, count in decomp.items(): - rep = cls.resource_rep(gate.op_type, gate.params, z) - gate_types[rep] = count - - return gate_types - except re.ResourcesNotDefined: - pass - - return {base_class.resource_rep(**base_params): z} - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (Type[~.ResourceOperator]): The class type of the base operator to be raised to some power. - * base_params (dict): the resource parameters required to extract the cost of the base operator - * z (int): the power that the operator is being raised to - """ - return { - "base_class": type(self.base), - "base_params": self.base.resource_params, - "z": self.z, - } - - @classmethod - def resource_rep(cls, base_class, base_params, z) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (Type[~.ResourceOperator]): The class type of the base operator to be raised to some power. - base_params (dict): the resource parameters required to extract the cost of the base operator - z (int): the power that the operator is being raised to - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp( - cls, {"base_class": base_class, "base_params": base_params, "z": z} - ) - - @classmethod - def pow_resource_decomp( - cls, z0, base_class, base_params, z - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z0 (int): the power that the power-operator is being raised to - base_class (Type[~.ResourceOperator]): The class type of the base operator to be raised to some power. - base_params (dict): The resource parameters required to extract the cost of the base operator. - z (int): the power that the base operator is being raised to - - Resources: - The resources are derived by simply adding together the :math:`z` exponent and the - :math:`z_{0}` exponent into a single instance of :class:`~.ResourcePow` gate, raising - the base operator to the power :math:`z + z_{0}`. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(base_class, base_params, z0 * z): 1} - - @staticmethod - def tracking_name(base_class, base_params, z) -> str: - r"""Returns the tracking name built with the operator's parameters.""" - base_name = base_class.tracking_name(**base_params) - return f"Pow({base_name}, {z})" - - -class ResourceExp(Exp, re.ResourceOperator): - r"""Resource class for the symbolic Exp operation. - - A symbolic class used to represent the exponential of some base operation. - - Args: - base (~.operation.Operator): The operator to be exponentiated - coeff=1 (Number): A scalar coefficient of the operator. - num_steps (int): The number of steps used in the decomposition of the exponential operator, - also known as the Trotter number. If this value is `None` and the Suzuki-Trotter - decomposition is needed, an error will be raised. - id (str): id for the Exp operator. Default is None. - - Resource Parameters: - * base_class (Type[~.ResourceOperator]): The class type of the base operator that is exponentiated. - * base_params (dict): the resource parameters required to extract the cost of the base operator - * base_pauli_rep (Union[PauliSentence, None]): The base operator represented as a linear combination of Pauli words. If such a representation is not applicable, then :code:`None`. - * coeff (complex): a scalar value which multiplies the base operator in the exponent - * num_steps (int): the number of trotter steps to use in approximating the exponential - - Resources: - This symbolic operation represents the exponential of some base operation. The resources - are determined as follows. If the base operation class :code:`base_class` implements the - :code:`.exp_resource_decomp()` method, then the resources are obtained from this. - - Otherwise, the exponetiated operator's resources are computed using the linear combination - of Pauli words representation (:code:`base_pauli_rep`). The exponential is approximated by - the product of the exponential of each Pauli word in the sum. This product is repeated - :code:`num_steps` many times. Specifically, the cost for the exponential of each Pauli word - is given by an associated :class:`~.ResourcePauliRot`. - - .. seealso:: :class:`~.ops.op_math.exp.Exp` - - **Example** - - The exponentiated operation can be constructed like this: - - >>> hamiltonian = qml.dot([0.1, -2.3], [qml.X(0)@qml.Y(1), qml.Z(0)]) - >>> hamiltonian - 0.1 * (X(0) @ Y(1)) + -2.3 * Z(0) - >>> exp_hamiltonian = re.ResourceExp(hamiltonian, 0.1*1j, num_steps=2) - >>> exp_hamiltonian.resources(**exp_hamiltonian.resource_params) - defaultdict(, {PauliRot: 2, PauliRot: 2}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourceExp.resources( - ... base_class = qml.ops.Sum, - ... base_params = {}, - ... base_pauli_rep = hamiltonian.pauli_rep, - ... coeff = 0.1*1j, - ... num_steps = 2, - ... ) - defaultdict(, {PauliRot: 2, PauliRot: 2}) - - .. details:: - :title: Usage Details - - We can configure the resources for the exponential of a base operation by modifying - its :code:`.exp_resource_decomp(scalar, num_steps, **resource_params)` method. Consider - for example this custom PauliZ class, where the exponentiated resources are not defined - (this is the default for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def exp_resource_decomp(cls, scalar, num_steps): - raise re.ResourcesNotDefined - - When this method is not defined, the resources are computed from the linear combination - of Pauli words representation. - - >>> pauli_rep = CustomZ(wires=0).pauli_rep - >>> pauli_rep - 1.0 * Z(0) - >>> re.ResourceExp.resources(CustomZ, {}, base_pauli_rep=pauli_rep, coeff=0.1*1j, num_steps=3) - defaultdict(, {PauliRot: 3}) - - We can update the exponential resources with the observation that the PauliZ gate, when - exponentiated, produces an RZ rotation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def exp_resource_decomp(cls, scalar, num_steps): - return {re.ResourceRZ.resource_rep(): num_steps} - - >>> re.ResourceExp.resources(CustomZ, {}, base_pauli_rep=pauli_rep, coeff=0.1*1j, num_steps=3) - {RZ: 3} - - """ - - @staticmethod - def _resource_decomp( - base_class: Operation, - base_params: Dict, - base_pauli_rep: PauliSentence, - coeff: complex, - num_steps: int, - **kwargs, - ): - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (Type[~.ResourceOperator]): The class type of the base operator that is - exponentiated. - base_params (dict): the resource parameters required to extract the cost of the base operator - base_pauli_rep (Union[PauliSentence, None]): The base operator represented as a linear - combination of Pauli words. If such a representation is not applicable, then :code:`None`. - coeff (complex): a scalar value which multiplies the base operator in the exponent - num_steps (int): the number of trotter steps to use in approximating the exponential - - Resources: - This symbolic operation represents the exponential of some base operation. The resources - are determined as follows. If the base operation class :code:`base_class` implements the - :code:`.exp_resource_decomp()` method, then the resources are obtained from this. - - Otherwise, the exponetiated operator's resources are computed using the linear combination - of Pauli words representation (:code:`base_pauli_rep`). The exponential is approximated by - the product of the exponential of each Pauli word in the sum. This product is repeated - :code:`num_steps` many times. Specifically, the cost for the exponential of each Pauli word - is given by an associated :class:`~.ResourcePauliRot`. - - **Example** - - The exponentiated operation can be constructed like this: - - >>> hamiltonian = qml.dot([0.1, -2.3], [qml.X(0)@qml.Y(1), qml.Z(0)]) - >>> hamiltonian - 0.1 * (X(0) @ Y(1)) + -2.3 * Z(0) - >>> exp_hamiltonian = re.ResourceExp(hamiltonian, 0.1*1j, num_steps=2) - >>> exp_hamiltonian.resources(**exp_hamiltonian.resource_params) - defaultdict(, {PauliRot: 2, PauliRot: 2}) - - Alternatively, we can call the resources method on from the class: - - >>> re.ResourceExp.resources( - ... base_class = qml.ops.Sum, - ... base_params = {}, - ... base_pauli_rep = hamiltonian.pauli_rep, - ... coeff = 0.1*1j, - ... num_steps = 2, - ... ) - defaultdict(, {PauliRot: 2, PauliRot: 2}) - - .. details:: - :title: Usage Details - - We can configure the resources for the exponential of a base operation by modifying - its :code:`.exp_resource_decomp(scalar, num_steps, **resource_params)` method. Consider - for example this custom PauliZ class, where the exponentiated resources are not defined - (this is the default for a general :class:`~.ResourceOperator`). - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def exp_resource_decomp(cls, scalar, num_steps): - raise re.ResourcesNotDefined - - When this method is not defined, the resources are computed from the linear combination - of Pauli words representation. - - >>> pauli_rep = CustomZ(wires=0).pauli_rep - >>> pauli_rep - 1.0 * Z(0) - >>> re.ResourceExp.resources(CustomZ, {}, base_pauli_rep=pauli_rep, coeff=0.1*1j, num_steps=3) - defaultdict(, {PauliRot: 3}) - - We can update the exponential resources with the observation that the PauliZ gate, when - exponentiated, produces an RZ rotation: - - .. code-block:: python - - class CustomZ(re.ResourceZ): - - @classmethod - def exp_resource_decomp(cls, scalar, num_steps): - return {re.ResourceRZ.resource_rep(): num_steps} - - >>> re.ResourceExp.resources(CustomZ, {}, base_pauli_rep=pauli_rep, coeff=0.1*1j, num_steps=3) - {RZ: 3} - - """ - # Custom exponential operator resources: - if issubclass(base_class, re.ResourceOperator): - try: - return base_class.exp_resource_decomp(coeff, num_steps, **base_params) - except re.ResourcesNotDefined: - pass - - if base_pauli_rep and math.real(coeff) == 0: - scalar = num_steps or 1 # 1st-order Trotter-Suzuki with 'num_steps' trotter steps: - return _scale_dict( - _resources_from_pauli_sentence(base_pauli_rep), scalar=scalar, in_place=True - ) - - raise re.ResourcesNotDefined - - @property - def resource_params(self): - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (Type[ResourceOperator]): The class type of the base operator that is exponentiated. - * base_params (dict): the resource parameters required to extract the cost of the base operator - * base_pauli_rep (Union[PauliSentence, None]): The base operator represented as a linear combination of Pauli words. If such a representation is not applicable, then :code:`None`. - * coeff (complex): a scalar value which multiplies the base operator in the exponent - * num_steps (int): the number of trotter steps to use in approximating the exponential - """ - return _extract_exp_params(self.base, self.scalar, self.num_steps) - - @classmethod - def resource_rep(cls, base_class, base_params, base_pauli_rep, coeff, num_steps): - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (Type[~.ResourceOperator]): The class type of the base operator that is - exponentiated. - base_params (dict): the resource parameters required to extract the cost of the base operator - base_pauli_rep (Union[PauliSentence, None]): The base operator represented as a linear - combination of Pauli words. If such a representation is not applicable, then :code:`None`. - coeff (complex): a scalar value which multiplies the base operator in the exponent - num_steps (int): the number of trotter steps to use in approximating the exponential - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - name = cls.tracking_name(base_class, base_params, base_pauli_rep, coeff, num_steps) - return re.CompressedResourceOp( - cls, - { - "base_class": base_class, - "base_params": base_params, - "base_pauli_rep": base_pauli_rep, - "coeff": coeff, - "num_steps": num_steps, - }, - name=name, - ) - - @classmethod - def pow_resource_decomp( - cls, z0, base_class, base_params, base_pauli_rep, coeff, num_steps - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z0 (int): the power that the operator is being raised to - base_class (Type[~.ResourceOperator]): The class type of the base operator that is - exponentiated. - base_params (dict): the resource parameters required to extract the cost of the base operator - base_pauli_rep (Union[PauliSentence, None]): The base operator represented as a linear - combination of Pauli words. If such a representation is not applicable, then :code:`None`. - coeff (complex): a scalar value which multiplies the base operator in the exponent - num_steps (int): the number of trotter steps to use in approximating the exponential - - Resources: - The resources are derived by simply multiplying together the :math:`z0` exponent and the - :code:`coeff` coefficient into a single instance of :class:`~.ResourceExp` gate with - coefficient :code:`z0 * coeff`. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - return {cls.resource_rep(base_class, base_params, base_pauli_rep, z0 * coeff, num_steps): 1} - - @staticmethod - def tracking_name( - base_class: Operation, - base_params: Dict, - base_pauli_rep: PauliSentence, - coeff: complex, - num_steps: int, - ): # pylint: disable=unused-argument - r"""Returns the tracking name built with the operator's parameters.""" - base_name = ( - base_class.tracking_name(**base_params) - if issubclass(base_class, re.ResourceOperator) - else base_class.__name__ - ) - - return f"Exp({base_name}, {coeff}, num_steps={num_steps})".replace("Resource", "") - - -class ResourceProd(Prod, re.ResourceOperator): - r"""Resource class for the symbolic Prod operation. - - A symbolic class used to represent a product of some base operations. - - Args: - *factors (tuple[~.operation.Operator]): a tuple of operators which will be multiplied together. - - Resource Parameters: - * cmpr_factors (list[CompressedResourceOp]): A list of operations, in the compressed representation, corresponding to the factors in the product. - - Resources: - This symbolic class represents a product of operations. The resources are defined trivially as the counts for each operation in the product. - - .. seealso:: :class:`~.ops.op_math.prod.Prod` - - **Example** - - The product of operations can be constructed as follows. Note, each operation in the - product must be a valid :class:`~.ResourceOperator` - - >>> prod_op = re.ResourceProd( - ... re.ResourceQFT(range(3)), - ... re.ResourceZ(0), - ... re.ResourceGlobalPhase(1.23, wires=[1]) - ... ) - >>> prod_op - ResourceQFT(wires=[0, 1, 2]) @ Z(0) @ ResourceGlobalPhase(1.23, wires=[1]) - >>> prod_op.resources(**prod_op.resource_params) - defaultdict(, {QFT(3): 1, Z: 1, GlobalPhase: 1}) - - """ - - @staticmethod - def _resource_decomp(cmpr_factors, **kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - cmpr_factors (list[CompressedResourceOp]): A list of operations, in the compressed - representation, corresponding to the factors in the product. - - Resources: - This symbolic class represents a product of operations. The resources are defined - trivially as the counts for each operation in the product. - - .. seealso:: :class:`~.ops.op_math.prod.Prod` - - **Example** - - The product of operations can be constructed as follows. Note, each operation in the - product must be a valid :class:`~.ResourceOperator` - - >>> prod_op = re.ResourceProd( - ... re.ResourceQFT(range(3)), - ... re.ResourceZ(0), - ... re.ResourceGlobalPhase(1.23, wires=[1]) - ... ) - >>> prod_op - ResourceQFT(wires=[0, 1, 2]) @ Z(0) @ ResourceGlobalPhase(1.23, wires=[1]) - >>> prod_op.resources(**prod_op.resource_params) - defaultdict(, {QFT(3): 1, Z: 1, GlobalPhase: 1}) - - """ - res = defaultdict(int) - for factor in cmpr_factors: - res[factor] += 1 - return res - - @property - def resource_params(self) -> Dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * cmpr_factors (list[CompressedResourceOp]): A list of operations, in the compressed representation, corresponding to the factors in the product. - """ - try: - cmpr_factors = tuple(factor.resource_rep_from_op() for factor in self.operands) - except AttributeError as error: - raise ValueError( - "All factors of the Product must be instances of `ResourceOperator` in order to obtain resources." - ) from error - - return {"cmpr_factors": cmpr_factors} - - @classmethod - def resource_rep(cls, cmpr_factors) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - cmpr_factors (list[CompressedResourceOp]): A list of operations, in the compressed - representation, corresponding to the factors in the product. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp(cls, {"cmpr_factors": cmpr_factors}) - - -def _extract_exp_params(base_op, scalar, num_steps): - pauli_rep = base_op.pauli_rep - isinstance_resource_op = isinstance(base_op, re.ResourceOperator) - - if (not isinstance_resource_op) and (pauli_rep is None): - raise ValueError( - f"Cannot obtain resources for the exponential of {base_op}, if it is not a ResourceOperator and it doesn't have a Pauli decomposition." - ) - - base_class = type(base_op) - base_params = base_op.resource_params if isinstance_resource_op else {} - - return { - "base_class": base_class, - "base_params": base_params, - "base_pauli_rep": pauli_rep, - "coeff": scalar, - "num_steps": num_steps, - } - - -def _resources_from_pauli_sentence(pauli_sentence): - gate_types = defaultdict(int) - - for pauli_word in iter(pauli_sentence.keys()): - pauli_string = "".join((str(v) for v in pauli_word.values())) - pauli_rot_gate = re.ResourcePauliRot.resource_rep(pauli_string) - gate_types[pauli_rot_gate] = 1 - - return gate_types diff --git a/pennylane/labs/resource_estimation/templates/stateprep.py b/pennylane/labs/resource_estimation/templates/stateprep.py deleted file mode 100644 index ca7d52690bb..00000000000 --- a/pennylane/labs/resource_estimation/templates/stateprep.py +++ /dev/null @@ -1,393 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""Resource operators for PennyLane state preparation templates.""" -import math -from typing import Dict - -import pennylane as qml -from pennylane.labs import resource_estimation as re -from pennylane.labs.resource_estimation import CompressedResourceOp, ResourceOperator - -# pylint: disable=arguments-differ - - -class ResourceStatePrep(qml.StatePrep, ResourceOperator): - """Resource class for StatePrep. - - Args: - state (array[complex] or csr_matrix): the state vector to prepare - wires (Sequence[int] or int): the wire(s) the operation acts on - pad_with (float or complex): if not ``None``, ``state`` is padded with this constant to be of size :math:`2^n`, where - :math:`n` is the number of wires. - normalize (bool): whether to normalize the state vector. To represent a valid quantum state vector, the L2-norm - of ``state`` must be one. The argument ``normalize`` can be set to ``True`` to normalize the state automatically. - id (str): custom label given to an operator instance, - can be useful for some applications where the instance has to be identified - validate_norm (bool): whether to validate the norm of the input state - - Resource Parameters: - * num_wires (int): the number of wires that the operation acts on - - Resources: - Uses the resources as defined in the :class:`~.ResourceMottonenStatePreperation` template. - - .. seealso:: :class:`~.StatePrep` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceStatePrep.resources(num_wires=3) - {MottonenStatePrep(3): 1} - """ - - @staticmethod - def _resource_decomp(num_wires, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_wires (int): the number of wires that the operation acts on - - Resources: - Uses the resources as defined in the :class:`~.ResourceMottonenStatePreperation` template. - """ - return {re.ResourceMottonenStatePreparation.resource_rep(num_wires): 1} - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_wires (int): the number of wires that the operation acts on - """ - return {"num_wires": len(self.wires)} - - @classmethod - def resource_rep(cls, num_wires) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_wires (int): the number of wires that the operation acts on - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"num_wires": num_wires} - return CompressedResourceOp(cls, params) - - @classmethod - def tracking_name(cls, num_wires) -> str: - return f"StatePrep({num_wires})" - - -class ResourceMottonenStatePreparation(qml.MottonenStatePreparation, ResourceOperator): - """Resource class for the MottonenStatePreparation template. - - Args: - state_vector (tensor_like): Input array of shape ``(2^n,)``, where ``n`` is the number of wires - the state preparation acts on. The input array must be normalized. - wires (Iterable): wires that the template acts on - - Resource Parameters: - * num_wires(int): the number of wires that the operation acts on - - Resources: - Using the resources as described in `Mottonen et al. (2008) `_. - The resources are defined as :math:`2^{N+2} - 5` :class:`~.ResourceRZ` gates and - :math:`2^{N+2} - 4N - 4` :class:`~.ResourceCNOT` gates. - - .. seealso:: :class:`~.MottonenStatePreperation` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceMottonenStatePreparation.resources(num_wires=3) - {RZ: 27, CNOT: 16} - """ - - @staticmethod - def _resource_decomp(num_wires, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_wires(int): the number of wires that the operation acts on - - Resources: - Using the resources as described in `Mottonen et al. (2008) `_. - The resources are defined as :math:`2^{N+2} - 5` :class:`~.ResourceRZ` gates and - :math:`2^{N+2} - 4N - 4` :class:`~.ResourceCNOT` gates. - """ - gate_types = {} - rz = re.ResourceRZ.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - - r_count = 2 ** (num_wires + 2) - 5 - cnot_count = 2 ** (num_wires + 2) - 4 * num_wires - 4 - - if r_count: - gate_types[rz] = r_count - - if cnot_count: - gate_types[cnot] = cnot_count - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_wires(int): the number of wires that the operation acts on - """ - return {"num_wires": len(self.wires)} - - @classmethod - def resource_rep(cls, num_wires) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_wires(int): the number of wires that the operation acts on - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"num_wires": num_wires} - return CompressedResourceOp(cls, params) - - @classmethod - def tracking_name(cls, num_wires) -> str: - return f"MottonenStatePrep({num_wires})" - - -class ResourceSuperposition(qml.Superposition, ResourceOperator): - """Resource class for the Superposition template. - - Args: - coeffs (tensor-like[float]): normalized coefficients of the superposition - bases (tensor-like[int]): basis states of the superposition - wires (Sequence[int]): wires that the operator acts on - work_wire (Union[Wires, int, str]): the auxiliary wire used for the permutation - - Resource Parameters: - * num_stateprep_wires (int): the number of wires used for the operation - * num_basis_states (int): the number of basis states of the superposition - * size_basis_state (int): the size of each basis state - - Resources: - The resources are computed following the PennyLane decomposition of - the class :class:`~.Superposition`. - - We use the following (somewhat naive) assumptions to approximate the - resources: - - - The MottonenStatePreparation routine is assumed for the state prep - component. - - The permutation block requires 2 multi-controlled X gates and a - series of CNOT gates. On average we will be controlling on and flipping - half the number of bits in :code:`size_basis`. (i.e for any given basis - state, half will be ones and half will be zeros). - - If the number of basis states provided spans the set of all basis states, - then we don't need to permute. In general, there is a probability associated - with not needing to permute wires if the basis states happen to match, we - estimate this quantity aswell. - - .. seealso:: :class:`~.Superposition` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceSuperposition.resources(num_stateprep_wires=3, num_basis_states=3, size_basis_state=3) - {MottonenStatePrep(3): 1, CNOT: 2, MultiControlledX: 4} - """ - - @staticmethod - def _resource_decomp( - num_stateprep_wires, num_basis_states, size_basis_state, **kwargs - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_stateprep_wires (int): the number of wires used for the operation - num_basis_states (int): the number of basis states of the superposition - size_basis_state (int): the size of each basis state - - Resources: - The resources are computed following the PennyLane decomposition of - the class :class:`~.Superposition`. - - We use the following (somewhat naive) assumptions to approximate the - resources: - - - The MottonenStatePreparation routine is assumed for the state prep - component. - - The permutation block requires 2 multi-controlled X gates and a - series of CNOT gates. On average we will be controlling on and flipping - half the number of bits in :code:`size_basis`. (i.e for any given basis - state, half will be ones and half will be zeros). - - If the number of basis states provided spans the set of all basis states, - then we don't need to permute. In general, there is a probability associated - with not needing to permute wires if the basis states happen to match, we - estimate this quantity aswell. - - """ - gate_types = {} - msp = re.ResourceMottonenStatePreparation.resource_rep(num_stateprep_wires) - gate_types[msp] = 1 - - cnot = re.ResourceCNOT.resource_rep() - num_zero_ctrls = size_basis_state // 2 - multi_x = re.ResourceMultiControlledX.resource_rep( - num_ctrl_wires=size_basis_state, - num_ctrl_values=num_zero_ctrls, - num_work_wires=0, - ) - - basis_size = 2**size_basis_state - prob_matching_basis_states = num_basis_states / basis_size - num_permutes = round(num_basis_states * (1 - prob_matching_basis_states)) - - if num_permutes: - gate_types[cnot] = num_permutes * ( - size_basis_state // 2 - ) # average number of bits to flip - gate_types[multi_x] = 2 * num_permutes # for compute and uncompute - - return gate_types - - @property - def resource_params(self) -> Dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_stateprep_wires (int): the number of wires used for the operation - * num_basis_states (int): the number of basis states of the superposition - * size_basis_state (int): the size of each basis state - """ - bases = self.hyperparameters["bases"] - num_basis_states = len(bases) - size_basis_state = len(bases[0]) # assuming they are all the same size - num_stateprep_wires = math.ceil(math.log2(len(self.coeffs))) - - return { - "num_stateprep_wires": num_stateprep_wires, - "num_basis_states": num_basis_states, - "size_basis_state": size_basis_state, - } - - @classmethod - def resource_rep( - cls, num_stateprep_wires, num_basis_states, size_basis_state - ) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_stateprep_wires (int): the number of wires used for the operation - num_basis_states (int): the number of basis states of the superposition - size_basis_state (int): the size of each basis state - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "num_stateprep_wires": num_stateprep_wires, - "num_basis_states": num_basis_states, - "size_basis_state": size_basis_state, - } - return CompressedResourceOp(cls, params) - - -class ResourceBasisState(qml.BasisState, ResourceOperator): - r"""Resource class for the BasisState template. - - Args: - state (tensor_like): Binary input of shape ``(len(wires), )``. For example, if ``state=np.array([0, 1, 0])`` or ``state=2`` (equivalent to 010 in binary), the quantum system will be prepared in the state :math:`|010 \rangle`. - - wires (Sequence[int] or int): the wire(s) the operation acts on - id (str): Custom label given to an operator instance. Can be useful for some applications where the instance has to be identified. - - Resource Parameters: - * num_bit_flips (int): number of qubits in the :math:`|1\rangle` state - - Resources: - The resources for BasisState are according to the decomposition found in qml.BasisState. - - .. seealso:: :class:`~.BasisState` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceBasisState.resources(num_bit_flips = 6) - {X: 6} - """ - - @staticmethod - def _resource_decomp( - num_bit_flips, - **kwargs, - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_bit_flips (int): number of qubits in the :math:`|1\rangle` state - - Resources: - The resources for BasisState are according to the decomposition found in qml.BasisState. - """ - gate_types = {} - x = re.ResourceX.resource_rep() - gate_types[x] = num_bit_flips - - return gate_types - - @property - def resource_params(self) -> Dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_bit_flips (int): number of qubits in the :math:`|1\rangle` state - """ - num_bit_flips = sum(self.parameters[0]) - return {"num_bit_flips": num_bit_flips} - - @classmethod - def resource_rep(cls, num_bit_flips) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_bit_flips (int): number of qubits in the :math:`|1\rangle` state - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - - params = {"num_bit_flips": num_bit_flips} - return CompressedResourceOp(cls, params) - - @classmethod - def tracking_name(cls, num_bit_flips) -> str: - return f"BasisState({num_bit_flips})" diff --git a/pennylane/labs/resource_estimation/templates/subroutines.py b/pennylane/labs/resource_estimation/templates/subroutines.py deleted file mode 100644 index f0822cd3c1f..00000000000 --- a/pennylane/labs/resource_estimation/templates/subroutines.py +++ /dev/null @@ -1,1635 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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. -r"""Resource operators for PennyLane subroutine templates.""" -import math -from collections import defaultdict -from typing import Dict - -import pennylane as qml -from pennylane import numpy as qnp -from pennylane.labs import resource_estimation as re -from pennylane.labs.resource_estimation import CompressedResourceOp, ResourceOperator - -# pylint: disable=arguments-differ, protected-access - - -class ResourceQFT(qml.QFT, ResourceOperator): - r"""Resource class for QFT. - - Args: - wires (int or Iterable[Number, str]]): the wire(s) the operation acts on - - Resource Parameters: - * num_wires (int): the number of qubits the operation acts upon - - Resources: - The resources are obtained from the standard decomposition of QFT as presented - in (chapter 5) `Nielsen, M.A. and Chuang, I.L. (2011) Quantum Computation and Quantum Information - `_. - - .. seealso:: :class:`~.QFT` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceQFT.resources(num_wires=3) - {Hadamard: 3, SWAP: 1, ControlledPhaseShift: 3} - """ - - @staticmethod - def _resource_decomp(num_wires, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_wires (int): the number of qubits the operation acts upon - - Resources: - The resources are obtained from the standard decomposition of QFT as presented - in (Chapter 5) `Nielsen, M.A. and Chuang, I.L. (2011) Quantum Computation and Quantum Information - `_. - - """ - gate_types = {} - - hadamard = re.ResourceHadamard.resource_rep() - swap = re.ResourceSWAP.resource_rep() - ctrl_phase_shift = re.ResourceControlledPhaseShift.resource_rep() - - gate_types[hadamard] = num_wires - gate_types[swap] = num_wires // 2 - gate_types[ctrl_phase_shift] = num_wires * (num_wires - 1) // 2 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_wires (int): the number of qubits the operation acts upon - """ - return {"num_wires": len(self.wires)} - - @classmethod - def resource_rep(cls, num_wires) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_wires (int): the number of qubits the operation acts upon - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"num_wires": num_wires} - return CompressedResourceOp(cls, params) - - @staticmethod - def tracking_name(num_wires) -> str: - r"""Returns the tracking name built with the operator's parameters.""" - return f"QFT({num_wires})" - - -class ResourceControlledSequence(qml.ControlledSequence, re.ResourceOperator): - """Resource class for the ControlledSequence template. - - Args: - base (Operator): the phase estimation unitary, specified as an :class:`~.Operator` - control (Union[Wires, Sequence[int], or int]): the wires to be used for control - - Resource Parameters: - * base_class (ResourceOperator): The type of the operation corresponding to the operator. - * base_params (dict): A dictionary of parameters required to obtain the resources for the operator. - * num_ctrl_wires (int): the number of control wires - - Resources: - The resources are obtained from the standard decomposition of :class:`~.ControlledSequence`. - - .. seealso:: :class:`~.ControlledSequence` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceControlledSequence.resources( - ... base_class=re.ResourceHadamard, - ... base_params={}, - ... num_ctrl_wires=2 - ... ) - {C(Hadamard,1,0,0): 3} - """ - - @staticmethod - def _resource_decomp( - base_class, base_params, num_ctrl_wires, **kwargs - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (ResourceOperator): The type of the operation corresponding to the - operator. - base_params (dict): A dictionary of parameters required to obtain the resources for - the operator. - num_ctrl_wires (int): the number of control wires - - Resources: - The resources are obtained from the standard decomposition of :class:`~.ControlledSequence`. - - """ - return { - re.ResourceControlled.resource_rep(base_class, base_params, 1, 0, 0): 2**num_ctrl_wires - - 1 - } - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (ResourceOperator): The type of the operation corresponding to the operator. - * base_params (dict): A dictionary of parameters required to obtain the resources for the operator. - * num_ctrl_wires (int): the number of control wires - """ - return { - "base_class": type(self.base), - "base_params": self.base.resource_params, - "num_ctrl_wires": len(self.control_wires), - } - - @classmethod - def resource_rep(cls, base_class, base_params, num_ctrl_wires) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (ResourceOperator): The type of the operation corresponding to the - operator. - base_params (dict): A dictionary of parameters required to obtain the resources for - the operator. - num_ctrl_wires (int): the number of control wires - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp( - cls, - { - "base_class": base_class, - "base_params": base_params, - "num_ctrl_wires": num_ctrl_wires, - }, - ) - - @staticmethod - def tracking_name(base_class, base_params, num_ctrl_wires) -> str: - base_name = base_class.tracking_name(**base_params) - return f"ControlledSequence({base_name}, {num_ctrl_wires})" - - -class ResourcePhaseAdder(qml.PhaseAdder, re.ResourceOperator): - r"""Resource class for the PhaseAdder template. - - Args: - k (int): the number that needs to be added - x_wires (Sequence[int]): the wires the operation acts on. The number of wires must be enough - for a binary representation of the value being targeted, :math:`x`. In some cases an additional - wire is needed, see usage details below. The number of wires also limits the maximum - value for `mod`. - mod (int): the modulo for performing the addition. If not provided, it will be set to its maximum value, :math:`2^{\text{len(x_wires)}}`. - work_wire (Sequence[int] or int): the auxiliary wire to use for the addition. Optional - when `mod` is :math:`2^{len(x\_wires)}`. Defaults to empty tuple. - - Resource Parameters: - * mod (int): the module for performing the addition - * num_x_wires (int): the number of wires the operation acts on - - Resources: - The resources are obtained from the standard decomposition of :class:`~.PhaseAdder`. - - .. seealso:: :class:`~.PhaseAdder` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourcePhaseAdder.resources( - ... mod=3, - ... num_x_wires=5 - ... ) - {QFT(5): 2, - Adjoint(QFT(5)): 2, - PhaseShift: 10, - Adjoint(PhaseShift): 10, - C(PhaseShift,1,0,0): 5, - CNOT: 1, - MultiControlledX: 1} - """ - - @staticmethod - def _resource_decomp(mod, num_x_wires, **kwargs) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - mod (int): the module for performing the addition - num_x_wires (int): the number of wires the operation acts on - - Resources: - The resources are obtained from the standard decomposition of :class:`~.PhaseAdder`. - - """ - if mod == 2**num_x_wires: - return {re.ResourcePhaseShift.resource_rep(): num_x_wires} - - qft = ResourceQFT.resource_rep(num_x_wires) - qft_dag = re.ResourceAdjoint.resource_rep( - ResourceQFT, - {"num_wires": num_x_wires}, - ) - - phase_shift = re.ResourcePhaseShift.resource_rep() - phase_shift_dag = re.ResourceAdjoint.resource_rep( - re.ResourcePhaseShift, - {}, - ) - ctrl_phase_shift = re.ResourceControlledPhaseShift.resource_rep() - - cnot = re.ResourceCNOT.resource_rep() - multix = re.ResourceMultiControlledX.resource_rep(1, 0, 1) - - gate_types = {} - gate_types[qft] = 2 - gate_types[qft_dag] = 2 - gate_types[phase_shift] = 2 * num_x_wires - gate_types[phase_shift_dag] = 2 * num_x_wires - gate_types[ctrl_phase_shift] = num_x_wires - gate_types[cnot] = 1 - gate_types[multix] = 1 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Resource Parameters: - mod (int): the module for performing the addition - num_x_wires (int): the number of wires the operation acts on - - Returns: - dict: A dictionary containing the resource parameters: - * mod (int): the module for performing the addition - * num_x_wires (int): the number of wires the operation acts on - """ - return { - "mod": self.hyperparameters["mod"], - "num_x_wires": len(self.hyperparameters["x_wires"]), - } - - @classmethod - def resource_rep(cls, mod, num_x_wires) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - mod (int): the module for performing the addition - num_x_wires (int): the number of wires the operation acts on - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - - return re.CompressedResourceOp(cls, {"mod": mod, "num_x_wires": num_x_wires}) - - -class ResourceMultiplier(qml.Multiplier, re.ResourceOperator): - """Resource class for the Multiplier template. - - Args: - k (int): the number that needs to be multiplied - x_wires (Sequence[int]): the wires the operation acts on. The number of wires must be enough for encoding `x` in the computational basis. The number of wires also limits the maximum value for `mod`. - mod (int): the modulo for performing the multiplication. If not provided, it will be set to its maximum value, :math:`2^{\text{len(x_wires)}}`. - work_wires (Sequence[int]): the auxiliary wires to use for the multiplication. If :math:`mod=2^{\text{len(x_wires)}}`, the number of auxiliary wires must be ``len(x_wires)``. Otherwise ``len(x_wires) + 2`` auxiliary wires are needed. - - Resource Parameters: - * mod (int): the module for performing the multiplication - * num_work_wires (int): the number of work wires used for the multiplication. - * num_x_wires (int): the number of wires the operation acts on. - - Resources: - The resources are obtained from the standard decomposition of :class:`~.Multiplier`. - - .. seealso:: :class:`~.Multiplier` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceMultiplier.resources( - ... mod=3, - ... num_work_wires=5, - ... num_x_wires=5 - ... ) - {QFT(4): 2, - Adjoint(QFT(4)): 2, - ControlledSequence(PhaseAdder, 5): 1, - Adjoint(ControlledSequence(PhaseAdder, 5)): 1, - CNOT: 3} - """ - - @staticmethod - def _resource_decomp( - mod, num_work_wires, num_x_wires, **kwargs - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - mod (int): the module for performing the multiplication - num_work_wires (int): the number of work wires used for the multiplication. - num_x_wires (int): the number of wires the operation acts on. - - Resources: - The resources are obtained from the standard decomposition of :class:`~.Multiplier`. - - """ - if mod == 2**num_x_wires: - num_aux_wires = num_x_wires - num_aux_swap = num_x_wires - else: - num_aux_wires = num_work_wires - 1 - num_aux_swap = num_aux_wires - 1 - - qft = ResourceQFT.resource_rep(num_aux_wires) - qft_dag = re.ResourceAdjoint.resource_rep( - ResourceQFT, - {"num_wires": num_aux_wires}, - ) - - sequence = ResourceControlledSequence.resource_rep( - ResourcePhaseAdder, - {}, - num_x_wires, - ) - - sequence_dag = re.ResourceAdjoint.resource_rep( - ResourceControlledSequence, - { - "base_class": ResourcePhaseAdder, - "base_params": {}, - "num_ctrl_wires": num_x_wires, - }, - ) - - cnot = re.ResourceCNOT.resource_rep() - - gate_types = {} - gate_types[qft] = 2 - gate_types[qft_dag] = 2 - gate_types[sequence] = 1 - gate_types[sequence_dag] = 1 - gate_types[cnot] = min(num_x_wires, num_aux_swap) - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * mod (int): The modulus for performing the multiplication. - * num_work_wires (int): The number of work wires used. - * num_x_wires (int): The number of wires the operation acts on. - """ - return { - "mod": self.hyperparameters["mod"], - "num_work_wires": len(self.hyperparameters["work_wires"]), - "num_x_wires": len(self.hyperparameters["x_wires"]), - } - - @classmethod - def resource_rep(cls, mod, num_work_wires, num_x_wires) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - mod (int): the module for performing the multiplication - num_work_wires (int): the number of work wires used for the multiplication. - num_x_wires (int): the number of wires the operation acts on. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp( - cls, {"mod": mod, "num_work_wires": num_work_wires, "num_x_wires": num_x_wires} - ) - - -class ResourceModExp(qml.ModExp, re.ResourceOperator): - r"""Resource class for the :class:`~.ModExp` template. - - Args: - x_wires (Sequence[int]): the wires that store the integer :math:`x` - output_wires (Sequence[int]): the wires that store the operator result. These wires also encode :math:`b`. - base (int): integer that needs to be exponentiated - mod (int): the modulo for performing the exponentiation. If not provided, it will be set to its maximum value, :math:`2^{\text{len(output_wires)}}` - work_wires (Sequence[int]): the auxiliary wires to use for the exponentiation. If - :math:`mod=2^{\text{len(output_wires)}}`, the number of auxiliary wires must be ``len(output_wires)``. Otherwise - ``len(output_wires) + 2`` auxiliary wires are needed. Defaults to empty tuple. - - Resource Parameters: - * mod (int): the modulo for performing the modular exponentiation - * num_output_wires (int): the number of output wires used to encode the integer :math:`b \cdot base^x \; \text{mod} \; mod` in the computational basis - * num_work_wires (int): the number of work wires used to perform the modular exponentiation operation - * num_x_wires (int): the number of wires used to encode the integer :math:`x < mod` in the computational basis - - Resources: - The resources are obtained from the standard decomposition of :class:`~.ModExp`. - - .. seealso:: :class:`~.ModExp` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceModExp.resources( - ... mod=3, - ... num_output_wires=5, - ... num_work_wires=5, - ... num_x_wires=5 - ... ) - {C(QFT(4),1,0,0): 62, - C(Adjoint(QFT(4)),1,0,0): 62, - C(ControlledSequence(PhaseAdder, 5),1,0,0): 31, - C(Adjoint(ControlledSequence(PhaseAdder, 5)),1,0,0): 31, - C(CNOT,1,0,0): 93} - """ - - @staticmethod - def _resource_decomp( - mod, num_output_wires, num_work_wires, num_x_wires, **kwargs - ) -> Dict[re.CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - mod (int): the module for performing the exponentiation - num_output_wires (int): the number of output wires used to encode the integer :math:`b \cdot base^x \; \text{mod} \; mod` in the computational basis - num_work_wires (int): the number of work wires used to perform the modular exponentiation operation - num_x_wires (int): the number of wires used to encode the integer :math:`x < mod` in the computational basis - - Resources: - The resources are obtained from the standard decomposition of :class:`~.ModExp`. - - """ - mult_resources = ResourceMultiplier._resource_decomp(mod, num_work_wires, num_output_wires) - gate_types = {} - - for comp_rep, _ in mult_resources.items(): - new_rep = re.ResourceControlled.resource_rep(comp_rep.op_type, comp_rep.params, 1, 0, 0) - - # cancel out QFTs from consecutive Multipliers - if comp_rep._name in ("QFT", "Adjoint(QFT)"): - gate_types[new_rep] = 1 - else: - gate_types[new_rep] = mult_resources[comp_rep] * ((2**num_x_wires) - 1) - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * mod (int): the module for performing the exponentiation - * num_output_wires (int): the number of output wires used to encode the integer :math:`b \cdot base^x \; \text{mod} \; mod` in the computational basis - * num_work_wires (int): the number of work wires used to perform the modular exponentiation operation - * num_x_wires (int): the number of wires used to encode the integer :math:`x < mod` in the computational basis - """ - return { - "mod": self.hyperparameters["mod"], - "num_output_wires": len(self.hyperparameters["output_wires"]), - "num_work_wires": len(self.hyperparameters["work_wires"]), - "num_x_wires": len(self.hyperparameters["x_wires"]), - } - - @classmethod - def resource_rep( - cls, mod, num_output_wires, num_work_wires, num_x_wires - ) -> re.CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - mod (int): the module for performing the exponentiation - num_output_wires (int): the number of output wires used to encode the integer :math:`b \cdot base^x \; \text{mod} \; mod` - in the computational basis - num_work_wires (int): the number of work wires used to perform the modular exponentiation - operation - num_x_wires (int): the number of wires used to encode the integer :math:`x < mod` in the - computational basis - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - return re.CompressedResourceOp( - cls, - { - "mod": mod, - "num_output_wires": num_output_wires, - "num_work_wires": num_work_wires, - "num_x_wires": num_x_wires, - }, - ) - - -class ResourceQuantumPhaseEstimation(qml.QuantumPhaseEstimation, ResourceOperator): - r"""Resource class for QuantumPhaseEstimation (QPE). - - Args: - unitary (array or Operator): the phase estimation unitary, specified as a matrix or an - :class:`~.Operator` - target_wires (Union[Wires, Sequence[int], or int]): the target wires to apply the unitary. - If the unitary is specified as an operator, the target wires should already have been - defined as part of the operator. In this case, target_wires should not be specified. - estimation_wires (Union[Wires, Sequence[int], or int]): the wires to be used for phase - estimation - - Resource Parameters: - * base_class (ResourceOperator): The type of the operation corresponding to the phase estimation unitary. - * base_params (dict): A dictionary of parameters required to obtain the resources for the phase estimation unitary. - * num_estimation_wires (int): the number of wires used for measuring out the phase - - Resources: - The resources are obtained from the standard decomposition of QPE as presented - in (Section 5.2) `Nielsen, M.A. and Chuang, I.L. (2011) Quantum Computation and Quantum - Information `_. - - .. seealso:: :class:`~.QuantumPhaseEstimation` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceQuantumPhaseEstimation.resources( - ... base_class=re.ResourceQFT, - ... base_params={"num_wires": 3}, - ... num_estimation_wires=3, - ... ) - {Hadamard: 3, Adjoint(QFT(3)): 1, C(QFT(3),1,0,0): 7} - """ - - @staticmethod - def _resource_decomp( - base_class, base_params, num_estimation_wires, **kwargs - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (Type(ResourceOperator)): The type of the operation corresponding to the - phase estimation unitary. - base_params (dict): A dictionary of parameters required to obtain the resources for - the phase estimation unitary. - num_estimation_wires (int): the number of wires used for measuring out the phase - - Resources: - The resources are obtained from the standard decomposition of QPE as presented - in (section 5.2) `Nielsen, M.A. and Chuang, I.L. (2011) Quantum Computation and Quantum - Information `_. - """ - gate_types = {} - - hadamard = re.ResourceHadamard.resource_rep() - adj_qft = re.ResourceAdjoint.resource_rep(ResourceQFT, {"num_wires": num_estimation_wires}) - ctrl_op = re.ResourceControlled.resource_rep(base_class, base_params, 1, 0, 0) - - gate_types[hadamard] = num_estimation_wires - gate_types[adj_qft] = 1 - gate_types[ctrl_op] = (2**num_estimation_wires) - 1 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (Type(ResourceOperator)): The type of the operation corresponding to the phase estimation unitary. - * base_params (dict): A dictionary of parameters required to obtain the resources for the phase estimation unitary. - * num_estimation_wires (int): the number of wires used for measuring out the phase - """ - op = self.hyperparameters["unitary"] - num_estimation_wires = len(self.hyperparameters["estimation_wires"]) - - if not isinstance(op, re.ResourceOperator): - raise TypeError( - f"Can't obtain QPE resources when the base unitary {op} isn't an instance" - " of ResourceOperator" - ) - - return { - "base_class": type(op), - "base_params": op.resource_params, - "num_estimation_wires": num_estimation_wires, - } - - @classmethod - def resource_rep( - cls, - base_class, - base_params, - num_estimation_wires, - ) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (Type(ResourceOperator)): The type of the operation corresponding to the - phase estimation unitary. - base_params (dict): A dictionary of parameters required to obtain the resources for - the phase estimation unitary. - num_estimation_wires (int): the number of wires used for measuring out the phase - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "base_class": base_class, - "base_params": base_params, - "num_estimation_wires": num_estimation_wires, - } - return CompressedResourceOp(cls, params) - - @staticmethod - def tracking_name(base_class, base_params, num_estimation_wires) -> str: - r"""Returns the tracking name built with the operator's parameters.""" - base_name = base_class.tracking_name(**base_params) - return f"QPE({base_name}, {num_estimation_wires})" - - -ResourceQPE = ResourceQuantumPhaseEstimation # Alias for ease of typing -r"""Resource class for QuantumPhaseEstimation (QPE). - -Args: - unitary (array or Operator): the phase estimation unitary, specified as a matrix or an - :class:`~.Operator` - target_wires (Union[Wires, Sequence[int], or int]): the target wires to apply the unitary. - If the unitary is specified as an operator, the target wires should already have been - defined as part of the operator. In this case, target_wires should not be specified. - estimation_wires (Union[Wires, Sequence[int], or int]): the wires to be used for phase - estimation - -Resource Parameters: - * base_class (ResourceOperator): The type of the operation corresponding to the phase estimation unitary. - * base_params (dict): A dictionary of parameters required to obtain the resources for the phase estimation unitary. - * num_estimation_wires (int): the number of wires used for measuring out the phase - -Resources: - The resources are obtained from the standard decomposition of QPE as presented - in (Section 5.2) `Nielsen, M.A. and Chuang, I.L. (2011) Quantum Computation and Quantum - Information `_. - -.. seealso:: :class:`~.QuantumPhaseEstimation` - -**Example** - -The resources for this operation are computed using: - ->>> re.ResourceQuantumPhaseEstimation.resources( -... base_class=re.ResourceQFT, -... base_params={"num_wires": 3}, -... num_estimation_wires=3, -... ) -{Hadamard: 3, Adjoint(QFT(3)): 1, C(QFT(3),1,0,0): 7} -""" - - -class ResourceBasisRotation(qml.BasisRotation, ResourceOperator): - r"""Resource class for the BasisRotation gate. - - Args: - wires (Iterable[Any]): wires that the operator acts on - unitary_matrix (array): matrix specifying the basis transformation - check (bool): test unitarity of the provided `unitary_matrix` - - Resource Parameters: - * dim_N (int): The dimensions of the input :code:`unitary_matrix`. This is computed as the number of columns of the matrix. - - Resources: - The resources are obtained from the construction scheme given in `Optica, 3, 1460 (2016) - `_. Specifically, - the resources are given as :math:`dim_N * (dim_N - 1) / 2` instances of the - :class:`~.ResourceSingleExcitation` gate, and :math:`dim_N * (1 + (dim_N - 1) / 2)` instances - of the :class:`~.ResourcePhaseShift` gate. - - .. seealso:: :class:`~.BasisRotation` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceBasisRotation.resources(dim_N=3) - {PhaseShift: 6.0, SingleExcitation: 3.0} - """ - - @staticmethod - def _resource_decomp(dim_N, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - dim_N (int): The dimensions of the input :code:`unitary_matrix`. This is computed - as the number of columns of the matrix. - - Resources: - The resources are obtained from the construction scheme given in `Optica, 3, 1460 (2016) - `_. Specifically, - the resources are given as :math:`dim_N * (dim_N - 1) / 2` instances of the - :class:`~.ResourceSingleExcitation` gate, and :math:`dim_N * (1 + (dim_N - 1) / 2)` instances - of the :class:`~.ResourcePhaseShift` gate. - """ - gate_types = {} - phase_shift = re.ResourcePhaseShift.resource_rep() - single_excitation = re.ResourceSingleExcitation.resource_rep() - - se_count = dim_N * (dim_N - 1) / 2 - ps_count = dim_N + se_count - - gate_types[phase_shift] = ps_count - gate_types[single_excitation] = se_count - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * dim_N (int): The dimensions of the input :code:`unitary_matrix`. This is computed as the number of columns of the matrix. - """ - unitary_matrix = self.parameters[0] - return {"dim_N": qml.math.shape(unitary_matrix)[0]} - - @classmethod - def resource_rep(cls, dim_N) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - dim_N (int): The dimensions of the input :code:`unitary_matrix`. This is computed - as the number of columns of the matrix. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"dim_N": dim_N} - return CompressedResourceOp(cls, params) - - @classmethod - def tracking_name(cls, dim_N) -> str: - r"""Returns the tracking name built with the operator's parameters.""" - return f"BasisRotation({dim_N})" - - -class ResourceSelect(qml.Select, ResourceOperator): - r"""Resource class for the Select gate. - - Args: - ops (list[Operator]): operations to apply - control (Sequence[int]): the wires controlling which operation is applied - id (str or None): String representing the operation (optional) - - Resource Parameters: - * cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, to be applied according to the selected qubits. - - Resources: - The resources correspond directly to the definition of the operation. Specifically, - for each operator in :code:`cmpr_ops`, the cost is given as a controlled version of the operator - controlled on the associated bitstring. - - .. seealso:: :class:`~.Select` - - **Example** - - The resources for this operation are computed using: - - >>> ops_lst = [re.ResourceX.resource_rep(), re.ResourceQFT.resource_rep(num_wires=3)] - >>> re.ResourceSelect.resources(cmpr_ops=ops_lst) - defaultdict(, {X: 2, C(X,1,0,0): 1, C(QFT(3),1,0,0): 1}) - """ - - @staticmethod - def _resource_decomp(cmpr_ops, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed - representation, to be applied according to the selected qubits. - - Resources: - The resources correspond directly to the definition of the operation. Specifically, - for each operator in :code:`cmpr_ops`, the cost is given as a controlled version of the operator - controlled on the associated bitstring. - """ - gate_types = defaultdict(int) - x = re.ResourceX.resource_rep() - - num_ops = len(cmpr_ops) - num_ctrl_wires = int(qnp.ceil(qnp.log2(num_ops))) - num_total_ctrl_possibilities = 2**num_ctrl_wires # 2^n - - num_zero_controls = num_total_ctrl_possibilities // 2 - gate_types[x] = num_zero_controls * 2 # conjugate 0 controls - - for cmp_rep in cmpr_ops: - ctrl_op = re.ResourceControlled.resource_rep( - cmp_rep.op_type, cmp_rep.params, num_ctrl_wires, 0, 0 - ) - gate_types[ctrl_op] += 1 - - return gate_types - - @staticmethod - def resources_for_ui(cmpr_ops, **kwargs): # pylint: disable=unused-argument - r"""The resources for a select implementation taking advantage of the unary iterator trick. - - The resources are based on the analysis in `Babbush et al. (2018) `_ section III.A, - 'Unary Iteration and Indexed Operations'. See Figures 4, 6, and 7. - - Note: This implementation assumes we have access to :math:`S + 1` additional work qubits, - where :math:`S = \ceil{log_{2}(N)}` and :math:`N` is the number of batches of unitaries - to select. - """ - gate_types = defaultdict(int) - x = re.ResourceX.resource_rep() - cnot = re.ResourceCNOT.resource_rep() - toffoli = re.ResourceToffoli.resource_rep() - - num_ops = len(cmpr_ops) - - for cmp_rep in cmpr_ops: - ctrl_op = re.ResourceControlled.resource_rep(cmp_rep.op_type, cmp_rep.params, 1, 0, 0) - gate_types[ctrl_op] += 1 - - gate_types[x] = 2 * (num_ops - 1) # conjugate 0 controlled toffolis - gate_types[cnot] = num_ops - 1 - gate_types[toffoli] = 2 * (num_ops - 1) - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, to be applied according to the selected qubits. - """ - ops = self.hyperparameters["ops"] - cmpr_ops = tuple(op.resource_rep_from_op() for op in ops) - return {"cmpr_ops": cmpr_ops} - - @classmethod - def resource_rep(cls, cmpr_ops) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed - representation, to be applied according to the selected qubits. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"cmpr_ops": cmpr_ops} - return CompressedResourceOp(cls, params) - - -class ResourcePrepSelPrep(qml.PrepSelPrep, ResourceOperator): - r"""Resource class for PrepSelPrep gate. - - Args: - lcu (Union[.Hamiltonian, .Sum, .Prod, .SProd, .LinearCombination]): The operator - written as a linear combination of unitaries. - control (Iterable[Any], Wires): The control qubits for the PrepSelPrep operator. - - Resource Parameters: - * cmpr_ops (tuple[CompressedResourceOp]): The list of operators, in the compressed representation, which correspond to the unitaries in the LCU to be blockencoded. - - Resources: - The resources correspond directly to the definition of the operation. Specifically, - the resources are given as one instance of :class:`~.ResourceSelect`, which is conjugated by - a pair of :class:`~.ResourceStatePrep` operations. - - .. seealso:: :class:`~.PrepSelPrep` - - **Example** - - The resources for this operation are computed using: - - >>> ops_tup = (re.ResourceX.resource_rep(), re.ResourceQFT.resource_rep(num_wires=3)) - >>> re.ResourcePrepSelPrep.resources(cmpr_ops=ops_tup) - {StatePrep(1): 1, Select: 1, Adjoint(StatePrep(1)): 1} - - """ - - @staticmethod - def _resource_decomp(cmpr_ops, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - cmpr_ops (tuple[CompressedResourceOp]): The list of operators, in the compressed - representation, which correspond to the unitaries in the LCU to be blockencoded. - - Resources: - The resources correspond directly to the definition of the operation. Specifically, - the resources are given as one instance of :class:`~.ResourceSelect`, which is conjugated by - a pair of :class:`~.ResourceStatePrep` operations. - """ - gate_types = {} - - num_ops = len(cmpr_ops) - num_wires = int(math.ceil(math.log2(num_ops))) - - prep = re.ResourceStatePrep.resource_rep(num_wires) - sel = ResourceSelect.resource_rep(cmpr_ops) - prep_dag = re.ResourceAdjoint.resource_rep(re.ResourceStatePrep, {"num_wires": num_wires}) - - gate_types[prep] = 1 - gate_types[sel] = 1 - gate_types[prep_dag] = 1 - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * cmpr_ops (tuple[CompressedResourceOp]): The list of operators, in the compressed representation, which correspond to the unitaries in the LCU to be blockencoded. - """ - ops = self.hyperparameters["ops"] - cmpr_ops = tuple(op.resource_rep_from_op() for op in ops) - return {"cmpr_ops": cmpr_ops} - - @classmethod - def resource_rep(cls, cmpr_ops) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - cmpr_ops (tuple[CompressedResourceOp]): The list of operators, in the compressed - representation, to be applied according to the selected qubits. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"cmpr_ops": cmpr_ops} - return CompressedResourceOp(cls, params) - - @classmethod - def pow_resource_decomp(cls, z, cmpr_ops) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources for an operator raised to a power. - - Args: - z (int): the power that the operator is being raised to - cmpr_ops (tuple[CompressedResourceOp]): The list of operators, in the compressed - representation, to be applied according to the selected qubits. - - Resources: - The resources are derived from the following identity. If an operation :math:`\hat{A}` - can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` - then the operation squared can be expressed as: - - .. math:: - - \begin{align} - \hat{A}^{2} \ &= \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger} \cdot \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger} \\ - \hat{A}^{2} \ &= \ \hat{U} \cdot \hat{B} \cdot \hat{B} \cdot \hat{U}^{\dagger} \\ - \hat{A}^{2} \ &= \ \hat{U} \cdot \hat{B}^{2} \cdot \hat{U}^{\dagger}, - \end{align} - - this holds for any integer power :math:`z`. In general, the resources are given by :math:`z` - instances of :class:`~.ResourceSelect` conjugated by a pair of :class:`~.ResourceStatePrep` - operations. - - Returns: - Dict[CompressedResourceOp, int]: The keys are the operators and the associated - values are the counts. - """ - gate_types = {} - - num_ops = len(cmpr_ops) - num_wires = int(math.ceil(math.log2(num_ops))) - - prep = re.ResourceStatePrep.resource_rep(num_wires) - pow_sel = re.ResourcePow.resource_rep(ResourceSelect, {"cmpr_ops": cmpr_ops}, z) - prep_dag = re.ResourceAdjoint.resource_rep(re.ResourceStatePrep, {"num_wires": num_wires}) - - gate_types[prep] = 1 - gate_types[pow_sel] = 1 - gate_types[prep_dag] = 1 - return gate_types - - -class ResourceReflection(qml.Reflection, ResourceOperator): - r"""Resource class for the Reflection gate. - - Args: - U (Operator): the operator that prepares the state :math:`|\Psi\rangle` - alpha (float): the angle of the operator, default is :math:`\pi` - reflection_wires (Any or Iterable[Any]): subsystem of wires on which to reflect, the - default is ``None`` and the reflection will be applied on the ``U`` wires. - - Resource Parameters: - * base_class (Type(ResourceOperator)): The type of the operation used to prepare the state we will be reflecting over. - * base_params (dict): A dictionary of parameters required to obtain the resources for the state preparation operator. - * num_ref_wires (int): The number of qubits for the subsystem on which the reflection is applied. - - Resources: - The resources correspond directly to the definition of the operation. The operator is - built as follows: - - .. math:: - - \text{R}(U, \alpha) = -I + (1 - e^{i\alpha}) |\Psi\rangle \langle \Psi| = U(-I + (1 - e^{i\alpha}) |0\rangle \langle 0|)U^{\dagger}. - - The central block is obtained through a controlled :class:`~.ResourcePhaseShift` operator and - a :class:`~.ResourceGlobalPhase` which are conjugated with a pair of :class:`~.ResourceX` gates. - Finally, the block is conjugated with the state preparation unitary :math:`U`. - - .. seealso:: :class:`~.Reflection` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceReflection.resources( - ... base_class=re.ResourceQFT, - ... base_params={"num_wires": 3}, - ... num_ref_wires=3, - ... ) - {X: 2, GlobalPhase: 1, QFT(3): 1, Adjoint(QFT(3)): 1, C(PhaseShift,2,2,0): 1} - """ - - @staticmethod - def _resource_decomp( - base_class, base_params, num_ref_wires, **kwargs - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - base_class (Type(ResourceOperator)): The type of the operation used to prepare the - state we will be reflecting over. - base_params (dict): A dictionary of parameters required to obtain the resources for - the state preparation operator. - num_ref_wires (int): The number of qubits for the subsystem on which the reflection is - applied. - - Resources: - The resources correspond directly to the definition of the operation. The operator is - built as follows: - - .. math:: - - \text{R}(U, \alpha) = -I + (1 - e^{i\alpha}) |\Psi\rangle \langle \Psi| = U(-I + (1 - e^{i\alpha}) |0\rangle \langle 0|)U^{\dagger}. - - The central block is obtained through a controlled :class:`~.ResourcePhaseShift` operator and - a :class:`~.ResourceGlobalPhase` which are conjugated with a pair of :class:`~.ResourceX` gates. - Finally, the block is conjugated with the state preparation unitary :math:`U`. - """ - gate_types = {} - base = base_class.resource_rep(**base_params) - - x = re.ResourceX.resource_rep() - gp = re.ResourceGlobalPhase.resource_rep() - adj_base = re.ResourceAdjoint.resource_rep(base_class, base_params) - ps = ( - re.ResourceControlled.resource_rep( - re.ResourcePhaseShift, {}, num_ref_wires - 1, num_ref_wires - 1, 0 - ) - if num_ref_wires > 1 - else re.ResourcePhaseShift.resource_rep() - ) - - gate_types[x] = 2 - gate_types[gp] = 1 - gate_types[base] = 1 - gate_types[adj_base] = 1 - gate_types[ps] = 1 - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * base_class (Type(ResourceOperator)): The type of the operation used to prepare the state we will be reflecting over. - * base_params (dict): A dictionary of parameters required to obtain the resources for the state preparation operator. - * num_ref_wires (int): The number of qubits for the subsystem on which the reflection is applied. - """ - base_cmpr_rep = self.hyperparameters["base"].resource_rep_from_op() - num_ref_wires = len(self.hyperparameters["reflection_wires"]) - - return { - "base_class": base_cmpr_rep.op_type, - "base_params": base_cmpr_rep.params, - "num_ref_wires": num_ref_wires, - } - - @classmethod - def resource_rep(cls, base_class, base_params, num_ref_wires) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - base_class (Type(ResourceOperator)): The type of the operation used to prepare the - state we will be reflecting over. - base_params (dict): A dictionary of parameters required to obtain the resources for - the state preparation operator. - num_ref_wires (int): The number of qubits for the subsystem on which the reflection is - applied. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "base_class": base_class, - "base_params": base_params, - "num_ref_wires": num_ref_wires, - } - return CompressedResourceOp(cls, params) - - -class ResourceQubitization(qml.Qubitization, ResourceOperator): - r"""Resource class for the Qubitization gate. - - Args: - hamiltonian (Union[.Hamiltonian, .Sum, .Prod, .SProd, .LinearCombination]): The Hamiltonian written as a linear combination of unitaries. - control (Iterable[Any], Wires): The control qubits for the Qubitization operator. - - Resource Parameters: - * cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, corresponding to the unitaries of the LCU representation of the hamiltonian being qubitized. - * num_ctrl_wires (int): The number of qubits used to prepare the coefficients vector of the LCU. - - Resources: - The resources are obtained from the definition of the operation as described in (section III. C) - `Simulating key properties of lithium-ion batteries with a fault-tolerant quantum computer - `_: - - .. math:: - - Q = \text{Prep}_{\mathcal{H}}^{\dagger} \text{Sel}_{\mathcal{H}} \text{Prep}_{\mathcal{H}}(2|0\rangle\langle 0| - I). - - Specifically, the resources are given by one :class:`~.ResourcePrepSelPrep` gate and one - :class:`~.ResourceReflection` gate. - - .. seealso:: :class:`~.Qubitization` - - **Example** - - The resources for this operation are computed using: - - >>> ops_tup = (re.ResourceX.resource_rep(), re.ResourceQFT.resource_rep(num_wires=3)) - >>> re.ResourceQubitization.resources( - ... cmpr_ops=ops_tup, - ... num_ctrl_wires=2, - ... ) - {Reflection: 1, PrepSelPrep: 1} - """ - - @staticmethod - def _resource_decomp(cmpr_ops, num_ctrl_wires, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, - corresponding to the unitaries of the LCU representation of the hamiltonian being qubitized. - num_ctrl_wires (int): The number of qubits used to prepare the coefficients vector of the LCU. - - Resources: - The resources are obtained from the definition of the operation as described in (section III. C) - `Simulating key properties of lithium-ion batteries with a fault-tolerant quantum computer - `_: - - .. math:: - - Q = \text{Prep}_{\mathcal{H}}^{\dagger} \text{Sel}_{\mathcal{H}} \text{Prep}_{\mathcal{H}}(2|0\rangle\langle 0| - I). - - Specifically, the resources are given by one :class:`~.ResourcePrepSelPrep` gate and one - :class:`~.ResourceReflection` gate. - """ - gate_types = {} - ref = ResourceReflection.resource_rep(re.ResourceIdentity, {}, num_ctrl_wires) - psp = ResourcePrepSelPrep.resource_rep(cmpr_ops) - - gate_types[ref] = 1 - gate_types[psp] = 1 - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, corresponding to the unitaries of the LCU representation of the hamiltonian being qubitized. - * num_ctrl_wires (int): The number of qubits used to prepare the coefficients vector of the LCU. - """ - lcu = self.hyperparameters["hamiltonian"] - _, ops = lcu.terms() - - cmpr_ops = tuple(op.resource_rep_from_op() for op in ops) - num_ctrl_wires = len(self.hyperparameters["control"]) - return {"cmpr_ops": cmpr_ops, "num_ctrl_wires": num_ctrl_wires} - - @classmethod - def resource_rep(cls, cmpr_ops, num_ctrl_wires) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, corresponding to the unitaries of the LCU representation of the hamiltonian being qubitized. - num_ctrl_wires (int): The number of qubits used to prepare the coefficients vector of the LCU. - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = {"cmpr_ops": cmpr_ops, "num_ctrl_wires": num_ctrl_wires} - return CompressedResourceOp(cls, params) - - -class ResourceQROM(qml.QROM, ResourceOperator): - """Resource class for the QROM template. - - Args: - bitstrings (list[str]): the bitstrings to be encoded - control_wires (Sequence[int]): the wires where the indexes are specified - target_wires (Sequence[int]): the wires where the bitstring is loaded - work_wires (Sequence[int]): the auxiliary wires used for the computation - clean (bool): if True, the work wires are not altered by operator, default is ``True`` - - Resource Parameters: - * num_bitstrings (int): the number of bitstrings that are to be encoded - * num_bit_flips (int): the number of bit flips needed for the list of bitstrings - * num_control_wires (int): the number of control wires where in the indexes are specified - * num_work_wires (int): the number of auxiliary wires used for QROM computation - * size_bitstring (int): the length of each bitstring - * clean (bool): if True, the work wires are not altered by the QROM operator - - Resources: - The resources for QROM are taken from the following two papers: - `Low et al. (2024) `_ (Figure 1.C) and - `Berry et al. (2019) `_ (Figure 4) - - We use the one-auxillary qubit version of select, instead of the built-in select - resources. - - .. seealso:: :class:`~.QROM` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceQROM.resources( - ... num_bitstrings=3, - ... num_bit_flips=7, - ... num_control_wires=5, - ... num_work_wires=5, - ... size_bitstring=3, - ... clean=True - ... ) - {Hadamard: 6, CNOT: 7, MultiControlledX: 8, X: 8, CSWAP: 12} - """ - - # pylint: disable=too-many-arguments - @staticmethod - def _resource_decomp( - num_bitstrings, - num_bit_flips, - num_control_wires, - num_work_wires, - size_bitstring, - clean, - **kwargs, - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - num_bitstrings (int): the number of bitstrings that are to be encoded - num_bit_flips (int): the number of bit flips needed for the list of bitstrings - num_control_wires (int): the number of control wires where in the indexes are specified - num_work_wires (int): the number of auxiliary wires used for QROM computation - size_bitstring (int): the length of each bitstring - clean (bool): if True, the work wires are not altered by the QROM operator - - Resources: - The resources for QROM are taken from the following two papers: - `Low et al. (2024) `_ (Figure 1.C) and - `Berry et al. (2019) `_ (Figure 4) - - We use the one-auxillary qubit version of select, instead of the built-in select - resources. - """ - gate_types = {} - x = re.ResourceX.resource_rep() - - if num_control_wires == 0: - gate_types[x] = num_bit_flips - return gate_types - - cnot = re.ResourceCNOT.resource_rep() - hadamard = re.ResourceHadamard.resource_rep() - - num_parallel_computations = (num_work_wires + size_bitstring) // size_bitstring - num_parallel_computations = min(num_parallel_computations, num_bitstrings) - - num_swap_wires = math.floor(math.log2(num_parallel_computations)) - num_select_wires = math.ceil(math.log2(math.ceil(num_bitstrings / (2**num_swap_wires)))) - - swap_clean_prefactor = 1 - select_clean_prefactor = 1 - - if clean: - gate_types[hadamard] = 2 * size_bitstring - swap_clean_prefactor = 4 - select_clean_prefactor = 2 - - # SELECT cost: - gate_types[cnot] = num_bit_flips # each unitary in the select is just a CNOT - - multi_x = re.ResourceMultiControlledX.resource_rep(num_select_wires, 0, 0) - num_total_ctrl_possibilities = 2**num_select_wires - gate_types[multi_x] = select_clean_prefactor * ( - 2 * num_total_ctrl_possibilities # two applications targetting the aux qubit - ) - num_zero_controls = (2 * num_total_ctrl_possibilities * num_select_wires) // 2 - gate_types[x] = select_clean_prefactor * ( - num_zero_controls * 2 # conjugate 0 controls on the multi-qubit x gates from above - ) - # SWAP cost: - ctrl_swap = re.ResourceCSWAP.resource_rep() - gate_types[ctrl_swap] = swap_clean_prefactor * ((2**num_swap_wires) - 1) * size_bitstring - - return gate_types - - @property - def resource_params(self) -> Dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * num_bitstrings (int): the number of bitstrings that are to be encoded - * num_bit_flips (int): the number of bit flips needed for the list of bitstrings - * num_control_wires (int): the number of control wires where in the indexes are specified - * num_work_wires (int): the number of auxiliary wires used for QROM computation - * size_bitstring (int): the length of each bitstring - * clean (bool): if True, the work wires are not altered by the QROM operator - """ - bitstrings = self.hyperparameters["bitstrings"] - num_bitstrings = len(bitstrings) - - num_bit_flips = 0 - for bit_string in bitstrings: - num_bit_flips += bit_string.count("1") - - num_work_wires = len(self.hyperparameters["work_wires"]) - size_bitstring = len(self.hyperparameters["target_wires"]) - num_control_wires = len(self.hyperparameters["control_wires"]) - clean = self.hyperparameters["clean"] - - return { - "num_bitstrings": num_bitstrings, - "num_bit_flips": num_bit_flips, - "num_control_wires": num_control_wires, - "num_work_wires": num_work_wires, - "size_bitstring": size_bitstring, - "clean": clean, - } - - @classmethod - def resource_rep( - cls, num_bitstrings, num_bit_flips, num_control_wires, num_work_wires, size_bitstring, clean - ) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - num_bitstrings (int): the number of bitstrings that are to be encoded - num_bit_flips (int): the number of bit flips needed for the list of bitstrings - num_control_wires (int): the number of control wires where in the indexes are specified - num_work_wires (int): the number of auxiliary wires used for QROM computation - size_bitstring (int): the length of each bitstring - clean (bool): if True, the work wires are not altered by the QROM operator - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "num_bitstrings": num_bitstrings, - "num_bit_flips": num_bit_flips, - "num_control_wires": num_control_wires, - "num_work_wires": num_work_wires, - "size_bitstring": size_bitstring, - "clean": clean, - } - return CompressedResourceOp(cls, params) - - -class ResourceAmplitudeAmplification(qml.AmplitudeAmplification, ResourceOperator): - r"""Resource class for the AmplitudeAmplification template. - - Args: - U (Operator): the operator that prepares the state :math:`|\Psi\rangle` - O (Operator): the oracle that flips the sign of the state :math:`|\phi\rangle` and does nothing to the state :math:`|\phi^{\perp}\rangle` - iters (int): the number of iterations of the amplitude amplification subroutine, default is ``1`` - fixed_point (bool): whether to use the fixed-point amplitude amplification algorithm, default is ``False`` - work_wire (int): the auxiliary wire to use for the fixed-point amplitude amplification algorithm, default is ``None`` - reflection_wires (Wires): the wires to reflect on, default is the wires of ``U`` - p_min (int): the lower bound for the probability of success in fixed-point amplitude amplification, default is ``0.9`` - - Resource Parameters: - * U_op (Type[~.ResourceOperator]): the class of the operator that prepares the state :math:`|\Psi\rangle` - * U_params (dict): the parameters for the U operator - * O_op (Type[~.ResourceOperator]): the class of the oracle that flips the sign of the state :math:`|\phi\rangle` and does nothing to the state :math:`|\phi^{\perp}\rangle` - * O_params (dict): the parameters for the O operator - * iters (int): the number of iterations of the amplitude amplification subroutine - * num_ref_wires (int): the number of wires used for the reflection - * fixed_point (bool): whether to use the fixed-point amplitude amplification algorithm - - Resources: - The resources are taken from the decomposition of ``qml.AmplitudeAmplification`` class. - - .. seealso:: :class:`~.AmplitudeAmplification` - - **Example** - - The resources for this operation are computed using: - - >>> re.ResourceAmplitudeAmplification.resources( - ... U_op=re.ResourceHadamard, - ... U_params={}, - ... O_op=re.ResourceX, - ... O_params={}, - ... iters=5, - ... num_ref_wires=10, - ... fixed_point=True - ... ) - {C(X,1,0,0): 4, PhaseShift: 2, Hadamard: 8, Reflection: 2} - """ - - # pylint: disable=too-many-arguments - @staticmethod - def _resource_decomp( - U_op, - U_params, - O_op, - O_params, - iters, - num_ref_wires, - fixed_point, - **kwargs, - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - Args: - U_op (Operator): the operator that prepares the state :math:`|\Psi\rangle` - U_params (dict): the parameters for the U operator - O_op (Operator): the oracle that flips the sign of the state :math:`|\phi\rangle` and does nothing to the state :math:`|\phi^{\perp}\rangle` - O_params (dict): the parameters for the O operator - iters (int): the number of iterations of the amplitude amplification subroutine - num_ref_wires (int): the number of wires used for the reflection - fixed_point (bool): whether to use the fixed-point amplitude amplification algorithm - - - Resources: - The resources are taken from the decomposition of :class:`qml.AmplitudeAmplification` class. - """ - gate_types = {} - ctrl = re.ResourceControlled.resource_rep( - base_class=O_op, - base_params=O_params, - num_ctrl_wires=1, - num_ctrl_values=0, - num_work_wires=0, - ) - phase_shift = re.ResourcePhaseShift.resource_rep() - hadamard = re.ResourceHadamard.resource_rep() - reflection = re.ResourceReflection.resource_rep( - base_class=U_op, base_params=U_params, num_ref_wires=num_ref_wires - ) - - if not fixed_point: - oracles = re.CompressedResourceOp(O_op, params=O_params) - gate_types[oracles] = iters - gate_types[reflection] = iters - - return gate_types - - iters = iters // 2 - - gate_types[ctrl] = iters * 2 - gate_types[phase_shift] = iters - gate_types[hadamard] = iters * 4 - gate_types[reflection] = iters - - return gate_types - - @property - def resource_params(self) -> Dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * U_op (Operator): the operator that prepares the state :math:`|\Psi\rangle` - * U_params (dict): the parameters for the U operator - * O_op (Operator): the oracle that flips the sign of the state :math:`|\phi\rangle` and does nothing to the state :math:`|\phi^{\perp}\rangle` - * O_params (dict): the parameters for the O operator - * iters (int): the number of iterations of the amplitude amplification subroutine - * num_ref_wires (int): the number of wires used for the reflection - * fixed_point (bool): whether to use the fixed-point amplitude amplification algorithm - """ - U_op = self.hyperparameters["U"] - O_op = self.hyperparameters["O"] - try: - U_params = U_op.resource_params - except (NotImplementedError, AttributeError): - U_params = {} - - try: - O_params = O_op.resource_params - except (NotImplementedError, AttributeError): - O_params = {} - - iters = self.hyperparameters["iters"] - fixed_point = self.hyperparameters["fixed_point"] - num_ref_wires = len(self.hyperparameters["reflection_wires"]) - - return { - "U_op": type(U_op), - "U_params": U_params, - "O_op": type(O_op), - "O_params": O_params, - "iters": iters, - "num_ref_wires": num_ref_wires, - "fixed_point": fixed_point, - } - - # pylint: disable=too-many-arguments - @classmethod - def resource_rep( - cls, U_op, U_params, O_op, O_params, iters, num_ref_wires, fixed_point - ) -> CompressedResourceOp: - r"""Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - U_op (Operator): the operator that prepares the state :math:`|\Psi\rangle` - U_params (dict): the parameters for the U operator - O_op (Operator): the oracle that flips the sign of the state :math:`|\phi\rangle` and does nothing to the state :math:`|\phi^{\perp}\rangle` - O_params (dict): the parameters for the O operator - iters (int): the number of iterations of the amplitude amplification subroutine - num_ref_wires (int): the number of wires used for the reflection - fixed_point (bool): whether to use the fixed-point amplitude amplification algorithm - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "U_op": U_op, - "U_params": U_params, - "O_op": O_op, - "O_params": O_params, - "iters": iters, - "num_ref_wires": num_ref_wires, - "fixed_point": fixed_point, - } - return CompressedResourceOp(cls, params) diff --git a/pennylane/labs/resource_estimation/templates/trotter.py b/pennylane/labs/resource_estimation/templates/trotter.py deleted file mode 100644 index 25ef9b15262..00000000000 --- a/pennylane/labs/resource_estimation/templates/trotter.py +++ /dev/null @@ -1,494 +0,0 @@ -# Copyright 2025 Xanadu Quantum Technologies Inc. - -# 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 templates for Suzuki-Trotter approximation based subroutines. -""" -from collections import defaultdict -from functools import wraps -from typing import Dict - -import pennylane as qml -from pennylane.labs import resource_estimation as re -from pennylane.labs.resource_estimation import ( - CompressedResourceOp, - ResourceExp, - ResourceOperator, - ResourcesNotDefined, -) -from pennylane.templates import TrotterProduct -from pennylane.templates.subroutines.trotter import TrotterizedQfunc - -# pylint: disable=arguments-differ - - -# TODO: Remove when PL supports pylint==3.3.6 (it is considered a useless-suppression) [sc-91362] -# pylint: disable=too-many-ancestors -class ResourceTrotterProduct(TrotterProduct, ResourceOperator): - r"""An operation representing the Suzuki-Trotter product approximation for the complex matrix - exponential of a given Hamiltonian. - - The Suzuki-Trotter product formula provides a method to approximate the matrix exponential of - Hamiltonian expressed as a linear combination of terms which in general do not commute. Consider - the Hamiltonian :math:`H = \Sigma^{N}_{j=0} O_{j}`, the product formula is constructed using - symmetrized products of the terms in the Hamiltonian. The symmetrized products of order - :math:`m \in [1, 2, 4, ..., 2k]` with :math:`k \in \mathbb{N}` are given by: - - .. math:: - - \begin{align} - S_{1}(t) &= \Pi_{j=0}^{N} \ e^{i t O_{j}} \\ - S_{2}(t) &= \Pi_{j=0}^{N} \ e^{i \frac{t}{2} O_{j}} \cdot \Pi_{j=N}^{0} \ e^{i \frac{t}{2} O_{j}} \\ - &\vdots \\ - S_{m}(t) &= S_{m-2}(p_{m}t)^{2} \cdot S_{m-2}((1-4p_{m})t) \cdot S_{m-2}(p_{m}t)^{2}, - \end{align} - - where the coefficient is :math:`p_{m} = 1 / (4 - \sqrt[m - 1]{4})`. The :math:`m`th order, - :math:`n`-step Suzuki-Trotter approximation is then defined as: - - .. math:: e^{iHt} \approx \left [S_{m}(t / n) \right ]^{n}. - - For more details see `J. Math. Phys. 32, 400 (1991) `_. - - Args: - hamiltonian (Union[.Hamiltonian, .Sum, .SProd]): The Hamiltonian written as a linear combination - of operators with known matrix exponentials. - time (float): The time of evolution, namely the parameter :math:`t` in :math:`e^{iHt}` - n (int): An integer representing the number of Trotter steps to perform - order (int): An integer (:math:`m`) representing the order of the approximation (must be 1 or even) - check_hermitian (bool): A flag to enable the validation check to ensure this is a valid unitary operator - - Resource Parameters: - * n (int): an integer representing the number of Trotter steps to perform - * order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - * first_order_expansion (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - - Resources: - The resources are defined according to the recursive formula presented above. Specifically, each - operator in the :code:`first_order_expansion` is called a number of times given by the formula: - - .. math:: C_{O_{j}} = 2n \cdot 5^{\frac{m}{2} - 1} - - Furthermore, the first and last terms of the Hamiltonian appear in pairs due to the symmetric form - of the recursive formula. Those counts are further simplified by grouping like terms as: - - .. math:: - - \begin{align} - C_{O_{0}} &= n \cdot 5^{\frac{m}{2} - 1} + 1, \\ - C_{O_{N}} &= n \cdot 5^{\frac{m}{2} - 1}. - \end{align} - - .. seealso:: :class:`~.TrotterProduct` - - **Example** - - The arguments can be provided directly to the :code:`resources()` function to extract the cost: - - >>> n, order = (1, 2) - >>> first_order_expansion = [re.ResourceRX.resource_rep(), re.ResourceRZ.resource_rep()] - >>> re.ResourceTrotterProduct.resources(n, order, first_order_expansion) - defaultdict(, {RX: 2, RZ: 1}) - - """ - - @staticmethod - def _resource_decomp( - n, order, first_order_expansion, **kwargs - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - The Suzuki-Trotter product formula provides a method to approximate the matrix exponential of - Hamiltonian expressed as a linear combination of terms which in general do not commute. Consider - the Hamiltonian :math:`H = \Sigma^{N}_{j=0} O_{j}`, the product formula is constructed using - symmetrized products of the terms in the Hamiltonian. The symmetrized products of order - :math:`m \in [1, 2, 4, ..., 2k]` with :math:`k \in \mathbb{N}` are given by: - - .. math:: - - \begin{align} - S_{1}(t) &= \Pi_{j=0}^{N} \ e^{i t O_{j}} \\ - S_{2}(t) &= \Pi_{j=0}^{N} \ e^{i \frac{t}{2} O_{j}} \cdot \Pi_{j=N}^{0} \ e^{i \frac{t}{2} O_{j}} \\ - &\vdots \\ - S_{m}(t) &= S_{m-2}(p_{m}t)^{2} \cdot S_{m-2}((1-4p_{m})t) \cdot S_{m-2}(p_{m}t)^{2}, - \end{align} - - where the coefficient is :math:`p_{m} = 1 / (4 - \sqrt[m - 1]{4})`. The :math:`m`th order, - :math:`n`-step Suzuki-Trotter approximation is then defined as: - - .. math:: e^{iHt} \approx \left [S_{m}(t / n) \right ]^{n}. - - For more details see `J. Math. Phys. 32, 400 (1991) `_. - - Args: - n (int): an integer representing the number of Trotter steps to perform - order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - first_order_expansion (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - - Resources: - The resources are defined according to the recurrsive formula presented above. Specifically, each - operator in the :code:`first_order_expansion` is called a number of times given by the formula: - - .. math:: C_{O_{j}} = 2n \cdot 5^{\frac{m}{2} - 1} - - Furthermore, the first and last terms of the hamiltonian appear in pairs due to the symmetric form - of the recurrsive formula. Those counts are further simplified by grouping like terms as: - - .. math:: - - \begin{align} - C_{O_{0}} &= n \cdot 5^{\frac{m}{2} - 1} + 1, \\ - C_{O_{N}} &= n \cdot 5^{\frac{m}{2} - 1}. - \end{align} - - **Example** - - The arguments can be provided directly to the :code:`resources()` function to extract the cost: - - >>> n, order = (1, 2) - >>> first_order_expansion = [re.ResourceRX.resource_rep(), re.ResourceRZ.resource_rep()] - >>> re.ResourceTrotterProduct.resources(n, order, first_order_expansion) - defaultdict(, {RX: 2, RZ: 1}) - - """ - k = order // 2 - gate_types = defaultdict(int, {}) - - if order == 1: - for cp_rep in first_order_expansion: - gate_types[cp_rep] += n - return gate_types - - cp_rep_first = first_order_expansion[0] - cp_rep_last = first_order_expansion[-1] - cp_rep_rest = first_order_expansion[1:-1] - - for cp_rep in cp_rep_rest: - gate_types[cp_rep] += 2 * n * (5 ** (k - 1)) - - gate_types[cp_rep_first] += n * (5 ** (k - 1)) + 1 - gate_types[cp_rep_last] += n * (5 ** (k - 1)) - - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: A dictionary containing the resource parameters: - * n (int): an integer representing the number of Trotter steps to perform - * order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - * first_order_expansion (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - """ - n = self.hyperparameters["n"] - base = self.hyperparameters["base"] - order = self.hyperparameters["order"] - - first_order_expansion = [ - ResourceExp.resource_rep( - **re.ops.op_math.symbolic._extract_exp_params( # pylint: disable=protected-access - op, scalar=1j, num_steps=1 - ) - ) - for op in base.operands - ] - - return { - "n": n, - "order": order, - "first_order_expansion": first_order_expansion, - } - - @classmethod - def resource_rep(cls, n, order, first_order_expansion) -> CompressedResourceOp: - """Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - n (int): an integer representing the number of Trotter steps to perform - order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - first_order_expansion (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "n": n, - "order": order, - "first_order_expansion": first_order_expansion, - } - return CompressedResourceOp(cls, params) - - @classmethod - def resources(cls, *args, **kwargs) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts.""" - return cls._resource_decomp(*args, **kwargs) - - -class ResourceTrotterizedQfunc(TrotterizedQfunc, ResourceOperator): - r"""Generates higher order Suzuki-Trotter product formulas from a set of - operations defined in a function. - - The Suzuki-Trotter product formula provides a method to approximate the matrix exponential of - Hamiltonian expressed as a linear combination of terms which in general do not commute. Consider - the Hamiltonian :math:`H = \Sigma^{N}_{j=0} O_{j}`, the product formula is constructed using - symmetrized products of the terms in the Hamiltonian. The symmetrized products of order - :math:`m \in [1, 2, 4, ..., 2k]` with :math:`k \in \mathbb{N}` are given by: - - .. math:: - - \begin{align} - S_{1}(t) &= \Pi_{j=0}^{N} \ e^{i t O_{j}} \\ - S_{2}(t) &= \Pi_{j=0}^{N} \ e^{i \frac{t}{2} O_{j}} \cdot \Pi_{j=N}^{0} \ e^{i \frac{t}{2} O_{j}} \\ - &\vdots \\ - S_{m}(t) &= S_{m-2}(p_{m}t)^{2} \cdot S_{m-2}((1-4p_{m})t) \cdot S_{m-2}(p_{m}t)^{2}, - \end{align} - - where the coefficient is :math:`p_{m} = 1 / (4 - \sqrt[m - 1]{4})`. The :math:`m`th order, - :math:`n`-step Suzuki-Trotter approximation is then defined as: - - .. math:: e^{iHt} \approx \left [S_{m}(t / n) \right ]^{n}. - - For more details see `J. Math. Phys. 32, 400 (1991) `_. - - Args: - time (float): the time of evolution, namely the parameter :math:`t` in :math:`e^{iHt}` - *trainable_args (tuple): the trainable arguments of the first-order expansion function - qfunc (Callable): the first-order expansion given as a callable function which queues operations - wires (Iterable): the set of wires the operation will act upon (should be identical to qfunc wires) - n (int): an integer representing the number of Trotter steps to perform - order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - reverse (bool): if true, reverse the order of the operations queued by :code:`qfunc` - **non_trainable_kwargs (dict): non-trainable keyword arguments of the first-order expansion function - - Resource Parameters: - * n (int): an integer representing the number of Trotter steps to perform - * order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - * qfunc_compressed_reps (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - - Resources: - The resources are defined according to the recurrsive formula presented above. Specifically, each - operator in the :code:`first_order_expansion` is called a number of times given by the formula: - - .. math:: C_{O_{j}} = 2n \cdot 5^{\frac{m}{2} - 1} - - .. seealso:: :class:`~.TrotterizedQfunc` - - **Example** - - The arguments can be provided directly to the :code:`resources()` function to extract the cost: - - >>> n, order = (1, 2) - >>> first_order_expansion = [re.ResourceRX.resource_rep(), re.ResourceRZ.resource_rep()] - >>> re.ResourceTrotterizedQfunc.resources(n, order, first_order_expansion) - defaultdict(, {RX: 2, RZ: 2}) - - """ - - @staticmethod - def _resource_decomp( - n, order, qfunc_compressed_reps, **kwargs - ) -> Dict[CompressedResourceOp, int]: - r"""Returns a dictionary representing the resources of the operator. The - keys are the operators and the associated values are the counts. - - The Suzuki-Trotter product formula provides a method to approximate the matrix exponential of - Hamiltonian expressed as a linear combination of terms which in general do not commute. Consider - the Hamiltonian :math:`H = \Sigma^{N}_{j=0} O_{j}`, the product formula is constructed using - symmetrized products of the terms in the Hamiltonian. The symmetrized products of order - :math:`m \in [1, 2, 4, ..., 2k]` with :math:`k \in \mathbb{N}` are given by: - - .. math:: - - \begin{align} - S_{1}(t) &= \Pi_{j=0}^{N} \ e^{i t O_{j}} \\ - S_{2}(t) &= \Pi_{j=0}^{N} \ e^{i \frac{t}{2} O_{j}} \cdot \Pi_{j=N}^{0} \ e^{i \frac{t}{2} O_{j}} \\ - &\vdots \\ - S_{m}(t) &= S_{m-2}(p_{m}t)^{2} \cdot S_{m-2}((1-4p_{m})t) \cdot S_{m-2}(p_{m}t)^{2}, - \end{align} - - where the coefficient is :math:`p_{m} = 1 / (4 - \sqrt[m - 1]{4})`. The :math:`m`th order, - :math:`n`-step Suzuki-Trotter approximation is then defined as: - - .. math:: e^{iHt} \approx \left [S_{m}(t / n) \right ]^{n}. - - For more details see `J. Math. Phys. 32, 400 (1991) `_. - - Args: - n (int): an integer representing the number of Trotter steps to perform - order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - qfunc_compressed_reps (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - - Resources: - The resources are defined according to the recurrsive formula presented above. Specifically, each - operator in the :code:`first_order_expansion` is called a number of times given by the formula: - - .. math:: C_{O_{j}} = 2n \cdot 5^{\frac{m}{2} - 1} - - **Example** - - The arguments can be provided directly to the :code:`resources()` function to extract the cost: - - >>> n, order = (1, 2) - >>> first_order_expansion = [re.ResourceRX.resource_rep(), re.ResourceRZ.resource_rep()] - >>> re.ResourceTrotterizedQfunc.resources(n, order, first_order_expansion) - defaultdict(, {RX: 2, RZ: 2}) - - """ - k = order // 2 - gate_types = defaultdict(int, {}) - - if order == 1: - for cp_rep in qfunc_compressed_reps: - gate_types[cp_rep] += n - return gate_types - - for cp_rep in qfunc_compressed_reps: - gate_types[cp_rep] += 2 * n * (5 ** (k - 1)) - return gate_types - - @property - def resource_params(self) -> dict: - r"""Returns a dictionary containing the minimal information needed to compute the resources. - - Returns: - dict: dictionary containing the resource parameters - * n (int): an integer representing the number of Trotter steps to perform - * order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - * qfunc_compressed_reps (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - """ - with qml.QueuingManager.stop_recording(): - with qml.queuing.AnnotatedQueue() as q: - base_hyper_params = ("n", "order", "qfunc", "reverse") - - qfunc_args = self.parameters - qfunc_kwargs = { - k: v for k, v in self.hyperparameters.items() if not k in base_hyper_params - } - - qfunc = self.hyperparameters["qfunc"] - qfunc(*qfunc_args, wires=self.wires, **qfunc_kwargs) - - try: - qfunc_compressed_reps = tuple(op.resource_rep_from_op() for op in q.queue) - - except AttributeError as error: - raise ResourcesNotDefined( - "Every operation in the TrotterizedQfunc should be a ResourceOperator" - ) from error - - return { - "n": self.hyperparameters["n"], - "order": self.hyperparameters["order"], - "qfunc_compressed_reps": qfunc_compressed_reps, - } - - @classmethod - def resource_rep(cls, n, order, qfunc_compressed_reps, name=None) -> CompressedResourceOp: - """Returns a compressed representation containing only the parameters of - the Operator that are needed to compute a resource estimation. - - Args: - n (int): an integer representing the number of Trotter steps to perform - order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - qfunc_compressed_reps (list[CompressedResourceOp]): A list of compressed operations corresponding to the exponentiated terms of the hamiltonian (:math:`e^{i t O_{j}}`). - - Returns: - CompressedResourceOp: the operator in a compressed representation - """ - params = { - "n": n, - "order": order, - "qfunc_compressed_reps": qfunc_compressed_reps, - } - return CompressedResourceOp(cls, params, name=name) - - def resource_rep_from_op(self) -> CompressedResourceOp: - r"""Returns a compressed representation directly from the operator""" - return self.__class__.resource_rep(**self.resource_params, name=self._name) - - -def resource_trotterize(qfunc, n=1, order=2, reverse=False): - r"""Generates higher order Suzuki-Trotter product formulas from a set of - operations defined in a function. - - The Suzuki-Trotter product formula provides a method to approximate the matrix exponential of - Hamiltonian expressed as a linear combination of terms which in general do not commute. Consider - the Hamiltonian :math:`H = \Sigma^{N}_{j=0} O_{j}`, the product formula is constructed using - symmetrized products of the terms in the Hamiltonian. The symmetrized products of order - :math:`m \in [1, 2, 4, ..., 2k]` with :math:`k \in \mathbb{N}` are given by: - - .. math:: - - \begin{align} - S_{1}(t) &= \Pi_{j=0}^{N} \ e^{i t O_{j}} \\ - S_{2}(t) &= \Pi_{j=0}^{N} \ e^{i \frac{t}{2} O_{j}} \cdot \Pi_{j=N}^{0} \ e^{i \frac{t}{2} O_{j}} \\ - &\vdots \\ - S_{m}(t) &= S_{m-2}(p_{m}t)^{2} \cdot S_{m-2}((1-4p_{m})t) \cdot S_{m-2}(p_{m}t)^{2}, - \end{align} - - where the coefficient is :math:`p_{m} = 1 / (4 - \sqrt[m - 1]{4})`. The :math:`m`th order, - :math:`n`-step Suzuki-Trotter approximation is then defined as: - - .. math:: e^{iHt} \approx \left [S_{m}(t / n) \right ]^{n}. - - For more details see `J. Math. Phys. 32, 400 (1991) `_. - - Args: - qfunc (Callable): A function which queues the operations corresponding to the exponentiated - terms of the hamiltonian (:math:`e^{i t O_{j}}`). The operations should be queued according - to the first order expression. - n (int): an integer representing the number of Trotter steps to perform - order (int): an integer (:math:`m`) representing the order of the approximation (must be 1 or even) - - Resources: - The resources are defined according to the recurrsive formula presented above. Specifically, each - operator in the :code:`first_order_expansion` is called a number of times given by the formula: - - .. math:: C_{O_{j}} = 2n \cdot 5^{\frac{m}{2} - 1} - - .. seealso:: :class:`~.trotterize` - - **Example** - - First we define a function which queues the first-order expansion: - - .. code-block:: python3 - - def first_order_expansion(time, theta, phi, wires): - "This is the first order expansion (U_1)." - re.ResourceRX(time*theta, wires[0]) - re.ResourceRY(time*phi, wires[1]) - - The arguments can be provided directly to the :code:`resources()` function to extract the cost: - - >>> n, order = (1, 2) - >>> time, theta, phi = (0.1, 0.2, 0.3) - >>> resource_op = re.resource_trotterize(first_order_expansion, n, order)(time, theta, phi, wires=['a', 'b']) - >>> resource_op.resources(**resource_op.resource_params) - defaultdict(, {RX: 2, RY: 2}) - - """ - - @wraps(qfunc) - def wrapper(*args, **kwargs): - time = args[0] - other_args = args[1:] - return ResourceTrotterizedQfunc( - time, *other_args, qfunc=qfunc, n=n, order=order, reverse=reverse, **kwargs - ) - - return wrapper From 71bfde750bde6f6b4bd120df9795344e23552b0a Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Tue, 27 May 2025 21:39:15 -0400 Subject: [PATCH 06/18] Added Qubit Manager class (#7404) **Context:** Added QubitManager class for tracking auxiliary qubits. **Description of the Change:** **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** [sc-90641] --------- Co-authored-by: Utkarsh Co-authored-by: Jay Soni --- doc/releases/changelog-dev.md | 3 + .../labs/resource_estimation/__init__.py | 12 + .../labs/resource_estimation/qubit_manager.py | 196 ++++++++++++++++ .../resource_estimation/test_qubit_manager.py | 217 ++++++++++++++++++ 4 files changed, 428 insertions(+) create mode 100644 pennylane/labs/resource_estimation/qubit_manager.py create mode 100644 pennylane/labs/tests/resource_estimation/test_qubit_manager.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 4ad7ed5f9c3..45b199141a8 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -406,6 +406,8 @@ [(#7229)](https://github.com/PennyLaneAI/pennylane/pull/7229) [(#7333)](https://github.com/PennyLaneAI/pennylane/pull/7333) +* `pennylane.labs.QubitManager`, `pennylane.labs.AllocWires`, and `pennylane.labs.FreeWires` classes have been added to track and manage auxilliary qubits. + [(#7404)](https://github.com/PennyLaneAI/pennylane/pull/7404)

Breaking changes 💔

@@ -627,6 +629,7 @@ This release contains contributions from (in alphabetical order): Guillermo Alonso-Linaje, Astral Cai, Yushao Chen, +Diksha Dhawan, Marcus Edwards, Lillian Frederiksen, Pietropaolo Frisoni, diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index fc4f3a7f1d9..f29b824d5d0 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -22,4 +22,16 @@ .. currentmodule:: pennylane.labs.resource_estimation +Qubit Management Classes: +~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autosummary:: + :toctree: api + + ~QubitManager + ~AllocWires + ~FreeWires + """ + +from .qubit_manager import AllocWires, FreeWires, QubitManager diff --git a/pennylane/labs/resource_estimation/qubit_manager.py b/pennylane/labs/resource_estimation/qubit_manager.py new file mode 100644 index 00000000000..eaa42268987 --- /dev/null +++ b/pennylane/labs/resource_estimation/qubit_manager.py @@ -0,0 +1,196 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""This module contains the base class for qubit management""" + +from typing import Union + +import pennylane as qml + +# pylint: disable= too-few-public-methods + + +class QubitManager: + r"""Manages and tracks the auxiliary and algorithmic qubits used in a quantum circuit. + + Args: + work_wires (int or Dict[str, int]): Number of work wires or a dictionary containing + number of clean and dirty work wires. All ``work_wires`` are assumed to be clean when + `int` is provided. + algo_wires (int): Number of algorithmic wires, default value is ``0``. + tight_budget (bool): Determines whether extra clean qubits can be allocated when they + exceed the available amount. The default is ``False``. + + **Example** + + >>> q = QubitManager( + ... work_wires={"clean": 2, "dirty": 2}, + ... tight_budget=False, + ... ) + >>> print(q) + QubitManager(clean=2, dirty=2, logic=0, tight_budget=False) + + """ + + def __init__(self, work_wires: Union[int, dict], algo_wires=0, tight_budget=False) -> None: + + if isinstance(work_wires, dict): + clean_wires = work_wires["clean"] + dirty_wires = work_wires["dirty"] + else: + clean_wires = work_wires + dirty_wires = 0 + + self.tight_budget = tight_budget + self._logic_qubit_counts = algo_wires + self._clean_qubit_counts = clean_wires + self._dirty_qubit_counts = dirty_wires + + def __str__(self): + return ( + f"QubitManager(clean qubits={self._clean_qubit_counts}, dirty qubits={self._dirty_qubit_counts}, " + f"algorithmic qubits={self._logic_qubit_counts}, tight budget={self.tight_budget})" + ) + + def __repr__(self) -> str: + work_wires_str = repr( + {"clean": self._clean_qubit_counts, "dirty": self._dirty_qubit_counts} + ) + return ( + f"QubitManager(work_wires={work_wires_str}, algo_wires={self._logic_qubit_counts}, " + f"tight_budget={self.tight_budget})" + ) + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, self.__class__) + and (self.clean_qubits == other.clean_qubits) + and (self.dirty_qubits == other.dirty_qubits) + and (self.algo_qubits == other.algo_qubits) + and (self.tight_budget == other.tight_budget) + ) + + @property + def clean_qubits(self): + r"""Returns the number of clean qubits.""" + return self._clean_qubit_counts + + @property + def dirty_qubits(self): + r"""Returns the number of dirty qubits.""" + return self._dirty_qubit_counts + + @property + def algo_qubits(self): + r"""Returns the number of algorithmic qubits.""" + return self._logic_qubit_counts + + @property + def total_qubits(self): + r"""Returns the number of total qubits.""" + return self._clean_qubit_counts + self._dirty_qubit_counts + self.algo_qubits + + @algo_qubits.setter + def algo_qubits(self, count: int): # these get set manually, the rest are dynamically updated + r"""Setter for algorithmic qubits.""" + self._logic_qubit_counts = count + + def allocate_qubits(self, num_qubits: int): + r"""Allocates extra clean qubits. + + Args: + num_qubits(int): number of qubits to be allocated + + """ + self._clean_qubit_counts += num_qubits + + def grab_clean_qubits(self, num_qubits: int): + r"""Grabs clean qubits. + + Args: + num_qubits(int) : number of clean qubits to be grabbed + + Raises: + ValueError: If tight_budget is `True` number of qubits to be grabbed is greater than + available clean qubits. + + """ + available_clean = self.clean_qubits + + if num_qubits > available_clean: + if self.tight_budget: + raise ValueError( + f"Grabbing more qubits than available clean qubits." + f"Number of clean qubits available is {available_clean}, while {num_qubits} are being grabbed." + ) + self._clean_qubit_counts = 0 + else: + self._clean_qubit_counts -= num_qubits + self._dirty_qubit_counts += num_qubits + + def free_qubits(self, num_qubits: int): + r"""Frees dirty qubits and converts them to clean qubits. + + Args: + num_qubits(int) : number of qubits to be freed + + Raises: + ValueError: If number of qubits to be freed is greater than available dirty qubits. + """ + + if num_qubits > self.dirty_qubits: + raise ValueError( + f"Freeing more qubits than available dirty qubits." + f"Number of dirty qubits available is {self.dirty_qubits}, while {num_qubits} qubits are being released." + ) + + self._dirty_qubit_counts -= num_qubits + self._clean_qubit_counts += num_qubits + + +class _WireAction: + """Base class for operations that manage qubit resources.""" + + _queue_category = "_resource_qubit_action" + + def __init__(self, num_wires): + self.num_wires = num_wires + if qml.QueuingManager.recording(): + self.queue() + + def queue(self, context=qml.QueuingManager): + r"""Adds the wire action object to a queue.""" + context.append(self) + return self + + +class AllocWires(_WireAction): + r"""Allows users to allocate clean work wires. + + Args: + num_wires (int): number of work wires to be allocated. + """ + + def __repr__(self) -> str: + return f"AllocWires({self.num_wires})" + + +class FreeWires(_WireAction): + r"""Allows users to free dirty work wires. + + Args: + num_wires (int): number of dirty work wires to be freed. + """ + + def __repr__(self) -> str: + return f"FreeWires({self.num_wires})" diff --git a/pennylane/labs/tests/resource_estimation/test_qubit_manager.py b/pennylane/labs/tests/resource_estimation/test_qubit_manager.py new file mode 100644 index 00000000000..8387b4fa12f --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/test_qubit_manager.py @@ -0,0 +1,217 @@ +# Copyright 2018-2025 Xanadu Quantum Technologies Inc. + +# 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. +""" +This module contains tests for classes needed to track auxilliary qubits. +""" +import copy + +import pytest + +import pennylane as qml +from pennylane.labs.resource_estimation import AllocWires, FreeWires, QubitManager + + +# pylint: disable= no-self-use +class TestQubitManager: + """Test the methods and attributes of the QubitManager class""" + + qm_quantities = ( + QubitManager(work_wires=2), + QubitManager(work_wires={"clean": 4, "dirty": 2}, algo_wires=20), + QubitManager({"clean": 2, "dirty": 2}, algo_wires=10, tight_budget=True), + ) + + qm_parameters = ( + (2, 0, 0, False), + (4, 2, 20, False), + (2, 2, 10, True), + ) + + @pytest.mark.parametrize("qm, attribute_tup", zip(qm_quantities, qm_parameters)) + def test_init(self, qm, attribute_tup): + """Test that the QubitManager class is instantiated as expected.""" + clean_qubits, dirty_qubits, logic_qubits, tight_budget = attribute_tup + + assert qm.clean_qubits == clean_qubits + assert qm.dirty_qubits == dirty_qubits + assert qm.algo_qubits == logic_qubits + assert qm.tight_budget == tight_budget + + @pytest.mark.parametrize("qm, attribute_tup", zip(qm_quantities, qm_parameters)) + def test_equality(self, qm, attribute_tup): + """Test that the equality methods behaves as expected""" + + clean_qubits, dirty_qubits, algo_qubits, tight_budget = attribute_tup + + qm2 = QubitManager( + work_wires={"clean": clean_qubits, "dirty": dirty_qubits}, + algo_wires=algo_qubits, + tight_budget=tight_budget, + ) + assert qm == qm2 + + extra_qubits = (0, 2, 4) + + @pytest.mark.parametrize( + "qm, attribute_tup, alloc_q", zip(copy.deepcopy(qm_quantities), qm_parameters, extra_qubits) + ) + def test_allocate_qubits(self, qm, attribute_tup, alloc_q): + """Test that the extra qubits are allocated correctly.""" + + clean_qubits = attribute_tup[0] + + qm.allocate_qubits(alloc_q) + assert qm.clean_qubits == clean_qubits + alloc_q + + qm_parameters_algo = ( + (2, 0, 0, False), + (4, 2, 2, False), + (2, 2, 4, True), + ) + + @pytest.mark.parametrize( + "qm, attribute_tup, algo_q", + zip(copy.deepcopy(qm_quantities), qm_parameters_algo, extra_qubits), + ) + def test_repr(self, qm, attribute_tup, algo_q): + """Test that the QubitManager representation is correct.""" + + clean_qubits, dirty_qubits, logic_qubits, tight_budget = attribute_tup + + work_wires_str = repr({"clean": clean_qubits, "dirty": dirty_qubits}) + expected_string = ( + f"QubitManager(work_wires={work_wires_str}, algo_wires={logic_qubits}, " + f"tight_budget={tight_budget})" + ) + + qm.algo_qubits = algo_q + assert repr(qm) == expected_string + + @pytest.mark.parametrize( + "qm, attribute_tup, algo_q", + zip(copy.deepcopy(qm_quantities), qm_parameters_algo, extra_qubits), + ) + def test_str(self, qm, attribute_tup, algo_q): + """Test that the QubitManager string is correct.""" + + clean_qubits, dirty_qubits, logic_qubits, tight_budget = attribute_tup + + expected_string = ( + f"QubitManager(clean qubits={clean_qubits}, dirty qubits={dirty_qubits}, " + f"algorithmic qubits={logic_qubits}, tight budget={tight_budget})" + ) + qm.algo_qubits = algo_q + assert str(qm) == expected_string + + @pytest.mark.parametrize( + "qm, attribute_tup, algo_q", + zip(copy.deepcopy(qm_quantities), qm_parameters_algo, extra_qubits), + ) + def test_algo_qubits(self, qm, attribute_tup, algo_q): + """Test that the logic qubits are set correctly.""" + + logic_qubits = attribute_tup[2] + + qm.algo_qubits = algo_q + assert qm.algo_qubits == logic_qubits + + @pytest.mark.parametrize( + "qm, attribute_tup, algo_q", + zip(copy.deepcopy(qm_quantities), qm_parameters_algo, extra_qubits), + ) + def test_total_qubits(self, qm, attribute_tup, algo_q): + """Test that the total qubits returned are correct.""" + + qm.algo_qubits = algo_q + total_qubits = attribute_tup[0] + attribute_tup[1] + attribute_tup[2] + assert qm.total_qubits == total_qubits + + def test_grab_clean_qubits(self): + """Test that the clean qubits are grabbed properly.""" + + qm = QubitManager(work_wires={"clean": 4, "dirty": 2}, tight_budget=False) + qm.grab_clean_qubits(6) + assert qm.clean_qubits == 0 + assert qm.dirty_qubits == 8 + + def test_error_grab_clean_qubits(self): + """Test that an error is raised when the number of clean qubits required is greater + than the available qubits.""" + + qm = QubitManager(work_wires={"clean": 4, "dirty": 2}, tight_budget=True) + with pytest.raises(ValueError, match="Grabbing more qubits than available clean qubits."): + qm.grab_clean_qubits(6) + + def test_free_qubits(self): + """Test that the dirty qubits are freed properly.""" + + qm = QubitManager(work_wires={"clean": 4, "dirty": 2}) + qm.free_qubits(2) + assert qm.clean_qubits == 6 + assert qm.dirty_qubits == 0 + + def test_error_free_qubits(self): + """Test that an error is raised when the number of qubits being freed is greater + than the available dirty qubits.""" + + qm = QubitManager(work_wires={"clean": 4, "dirty": 2}, tight_budget=True) + with pytest.raises(ValueError, match="Freeing more qubits than available dirty qubits."): + qm.free_qubits(6) + + +class TestAllocWires: + """Test the methods and attributes of the AllocWires class""" + + def test_init(self): + """Test that the AllocWires class is instantiated as expected when there is no active recording.""" + + for i in range(3): + assert AllocWires(i).num_wires == i + + def test_repr(self): + """Test that correct representation is returned for AllocWires class""" + + wires = 3 + exp_string = f"AllocWires({wires})" + assert repr(AllocWires(wires)) == exp_string + + def test_init_recording(self): + """Test that the AllocWires class is instantiated as expected when there is active recording.""" + with qml.queuing.AnnotatedQueue() as q: + ops = [AllocWires(2), AllocWires(4)] + assert q.queue == ops + + +class TestFreeWires: + """Test the methods and attributes of the FreeWires class""" + + def test_init(self): + """Test that the FreeWires class is instantiated as expected when there is no recording.""" + + for i in range(3): + assert FreeWires(i).num_wires == i + + def test_repr(self): + """Test that correct representation is returned for FreeWires class""" + + wires = 3 + exp_string = f"FreeWires({wires})" + assert repr(FreeWires(wires)) == exp_string + + def test_init_recording(self): + """Test that the FreeWires class is instantiated as expected when there is active recording.""" + with qml.queuing.AnnotatedQueue() as q: + ops = [FreeWires(2), FreeWires(4), FreeWires(8)] + + assert q.queue == ops From 8ef0446a7198f7cdd22785e412aabc4af1a52092 Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Wed, 28 May 2025 10:48:06 -0400 Subject: [PATCH 07/18] Add `Resources` object to labs (#7406) **Context:** Adding the updated `Resources` object to the labs module **Description of the Change:** --------- Co-authored-by: Diksha Dhawan Co-authored-by: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Co-authored-by: Utkarsh Co-authored-by: Soran Jahangiri <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 5 +- .../labs/resource_estimation/__init__.py | 10 + .../resource_estimation/resources_base.py | 388 ++++++++++++++++++ .../test_resources_base.py | 306 ++++++++++++++ 4 files changed, 708 insertions(+), 1 deletion(-) create mode 100644 pennylane/labs/resource_estimation/resources_base.py create mode 100644 pennylane/labs/tests/resource_estimation/test_resources_base.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 45b199141a8..edef8738831 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -394,7 +394,6 @@

Labs: a place for unified and rapid prototyping of research software 🧪

- * A new module :mod:`pennylane.labs.intermediate_reps ` provides functionality to compute intermediate representations for particular circuits. :func:`parity_matrix ` computes @@ -409,6 +408,9 @@ * `pennylane.labs.QubitManager`, `pennylane.labs.AllocWires`, and `pennylane.labs.FreeWires` classes have been added to track and manage auxilliary qubits. [(#7404)](https://github.com/PennyLaneAI/pennylane/pull/7404) +* Added a `pennylane.labs.Resources` class to store and track the quantum resources from a circuit. + [(#7406)](https://github.com/PennyLaneAI/pennylane/pull/7406) +

Breaking changes 💔

* The `return_type` property of `MeasurementProcess` has been removed. Please use `isinstance` for type checking instead. @@ -641,6 +643,7 @@ Lee J. O'Riordan, Mudit Pandey, Andrija Paurevic, Shuli Shu, +Jay Soni, Kalman Szenes, David Wierichs, Jake Zaia diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index f29b824d5d0..1510eb0e5bc 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -22,6 +22,15 @@ .. currentmodule:: pennylane.labs.resource_estimation + +Resource Estimation Base Classes: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autosummary:: + :toctree: api + + ~Resources + Qubit Management Classes: ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -35,3 +44,4 @@ """ from .qubit_manager import AllocWires, FreeWires, QubitManager +from .resources_base import Resources diff --git a/pennylane/labs/resource_estimation/resources_base.py b/pennylane/labs/resource_estimation/resources_base.py new file mode 100644 index 00000000000..2a567bed8f1 --- /dev/null +++ b/pennylane/labs/resource_estimation/resources_base.py @@ -0,0 +1,388 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""Base class for storing resources.""" +from __future__ import annotations + +import copy +from collections import defaultdict +from decimal import Decimal + +from pennylane.labs.resource_estimation.qubit_manager import QubitManager + + +class Resources: + r"""A container to track and update the resources used throughout a quantum circuit. + The resources tracked include number of gates, number of wires, and gate types. + + Args: + qubit_manager (QubitManager): A qubit tracker class which contains the number of available + work wires, categorized as clean, dirty or algorithmic wires. + gate_types (dict): A dictionary storing operations (ResourceOperator) as keys and the number + of times they are used in the circuit (int) as values. + + **Example** + + The resources can be accessed as class attributes. Additionally, the :class:`~.Resources` + instance can be nicely displayed in the console. + + >>> H = re.resource_rep(re.ResourceHadamard) + >>> X = re.resource_rep(re.ResourceX) + >>> Y = re.resource_rep(re.ResourceY) + >>> qm = re.QubitManager(work_wires=3) + >>> gt = defaultdict(int, {H: 10, X:7, Y:3}) + >>> + >>> res = re.Resources(qubit_manager=qm, gate_types=gt) + >>> print(res) + --- Resources: --- + Total qubits: 3 + Total gates : 20 + Qubit breakdown: + clean qubits: 3, dirty qubits: 0, algorithmic qubits: 0 + Gate breakdown: + {'Hadamard': 10, 'X': 7, 'Y': 3} + + .. details:: + :title: Usage Details + + The :class:`Resources` object supports arithmetic operations which allow for quick addition + and multiplication of resources. When combining resources, we can make a simplifying + assumption about they are applied in a quantum circuit (in series or in parallel). + + When assuming the circuits were executed in series, the number of algorithmic qubits add + together. When assuming the circuits were executed in parallel, the maximum of each set of + algorithmic qubits is used. The clean auxiliary qubits can be reused between the circuits, + and thus we always use the maximum of each set when combining the resources. Finally, the + dirty qubits cannot be reused between circuits, thus we always add them together. + + .. code-block:: + + from collections import defaultdict + + # Resource reps for each operator: + H = re.resource_rep(re.ResourceHadamard) + X = re.resource_rep(re.ResourceX) + Z = re.resource_rep(re.ResourceZ) + CNOT = re.resource_rep(re.ResourceCNOT) + + # state of qubits: + qm1 = re.QubitManager(work_wires={"clean":2, "dirty":1}, algo_wires=3) + + qm2 = re.QubitManager(work_wires={"clean":1, "dirty":2}, algo_wires=4) + + # state of gates: + gt1 = defaultdict(int, {H: 10, X:5, CNOT:2}) + gt2 = defaultdict(int, {H: 15, Z:5, CNOT:4}) + + # resources: + res1 = re.Resources(qubit_manager=qm1, gate_types=gt1) + res2 = re.Resources(qubit_manager=qm2, gate_types=gt2) + + + .. code-block:: pycon + + >>> print(res1) + --- Resources: --- + Total qubits: 6 + Total gates : 17 + Qubit breakdown: + clean qubits: 2, dirty qubits: 1, algorithmic qubits: 3 + Gate breakdown: + {'Hadamard': 10, 'X': 5, 'CNOT': 2} + + >>> print(res2) + --- Resources: --- + Total qubits: 7 + Total gates : 24 + Qubit breakdown: + clean qubits: 1, dirty qubits: 2, algorithmic qubits: 4 + Gate breakdown: + {'Hadamard': 15, 'Z': 5, 'CNOT': 4} + + Specifically, users can add together two instances of resources using the :code:`+` and + :code:`&` operators. These represent combining the resources assuming the circuits were + executed in series or parallel respectively. + + .. code-block:: pycon + + >>> res_in_series = res1 + res2 + >>> print(res_in_series) + --- Resources: --- + Total qubits: 9 + Total gates : 41 + Qubit breakdown: + clean qubits: 2, dirty qubits: 3, algorithmic qubits: 4 + Gate breakdown: + {'Hadamard': 25, 'X': 5, 'CNOT': 6, 'Z': 5} + + >>> res_in_parallel = res1 & res2 + >>> print(res_in_parallel) + --- Resources: --- + Total qubits: 12 + Total gates : 41 + Qubit breakdown: + clean qubits: 2, dirty qubits: 3, algorithmic qubits: 7 + Gate breakdown: + {'Hadamard': 25, 'X': 5, 'CNOT': 6, 'Z': 5} + + Similarly, users can scale up the resources for an operator by some integer factor using + the :code:`*` and :code:`@` operators. These represent scaling the resources assuming the + circuits were executed in series or parallel respectively. + + .. code-block:: pycon + + >>> res_in_series = 5 * res1 + >>> print(res_in_series) + --- Resources: --- + Total qubits: 10 + Total gates : 85 + Qubit breakdown: + clean qubits: 2, dirty qubits: 5, algorithmic qubits: 3 + Gate breakdown: + {'Hadamard': 50, 'X': 25, 'CNOT': 10} + + >>> res_in_parallel = 5 @ res1 + >>> print(res_in_parallel) + --- Resources: --- + Total qubits: 22 + Total gates : 85 + Qubit breakdown: + clean qubits: 2, dirty qubits: 5, algorithmic qubits: 15 + Gate breakdown: + {'Hadamard': 50, 'X': 25, 'CNOT': 10} + + """ + + def __init__(self, qubit_manager: QubitManager, gate_types: dict = None): + """Initialize the Resources class.""" + gate_types = gate_types or {} + + self.qubit_manager = qubit_manager + self.gate_types = ( + gate_types + if (isinstance(gate_types, defaultdict) and isinstance(gate_types.default_factory, int)) + else defaultdict(int, gate_types) + ) + + def __add__(self, other: "Resources") -> "Resources": + """Add two resources objects in series""" + assert isinstance(other, self.__class__) + return add_in_series(self, other) + + def __and__(self, other: "Resources") -> "Resources": + """Add two resources objects in parallel""" + assert isinstance(other, self.__class__) + return add_in_parallel(self, other) + + def __eq__(self, other: "Resources") -> bool: + """Determine if two resources objects are equal""" + return (self.gate_types == other.gate_types) and (self.qubit_manager == other.qubit_manager) + + def __mul__(self, scalar: int) -> "Resources": + """Scale a resources object in series""" + assert isinstance(scalar, int) + return mul_in_series(self, scalar) + + def __matmul__(self, scalar: int) -> "Resources": + """Scale a resources object in parallel""" + assert isinstance(scalar, int) + return mul_in_parallel(self, scalar) + + __rmul__ = __mul__ + __radd__ = __add__ + __rand__ = __and__ + __rmatmul__ = __matmul__ + + @property + def clean_gate_counts(self): + r"""Produce a dictionary which stores the gate counts + using the operator names as keys. + + Returns: + dict: A dictionary with operator names (str) as keys + and the number of occurances in the circuit (int) as values. + """ + clean_gate_counts = defaultdict(int) + + for cmp_res_op, counts in self.gate_types.items(): + clean_gate_counts[cmp_res_op.name] += counts + + return clean_gate_counts + + def __str__(self): + """Generates a string representation of the Resources object.""" + qm = self.qubit_manager + total_qubits = qm.total_qubits + total_gates = sum(self.clean_gate_counts.values()) + + total_gates_str = str(total_gates) if total_gates <= 999 else f"{Decimal(total_gates):.3E}" + total_qubits_str = ( + str(total_qubits) if total_qubits <= 9999 else f"{Decimal(total_qubits):.3E}" + ) + + items = "--- Resources: ---\n" + items += f" Total qubits: {total_qubits_str}\n" + items += f" Total gates : {total_gates_str}\n" + + qubit_breakdown_str = f"clean qubits: {qm.clean_qubits}, dirty qubits: {qm.dirty_qubits}, algorithmic qubits: {qm.algo_qubits}" + items += f" Qubit breakdown:\n {qubit_breakdown_str}\n" + + gate_type_str = ", ".join( + [ + f"'{gate_name}': {Decimal(count):.3E}" if count > 999 else f"'{gate_name}': {count}" + for gate_name, count in self.clean_gate_counts.items() + ] + ) + items += " Gate breakdown:\n {" + gate_type_str + "}" + + return items + + def __repr__(self): + """Compact string representation of the Resources object""" + return { + "qubit_manager": self.qubit_manager, + "gate_types": self.gate_types, + }.__repr__() + + def _ipython_display_(self): # pragma: no cover + """Displays __str__ in ipython instead of __repr__""" + print(str(self)) + + +def add_in_series(first: Resources, other) -> Resources: # + + r"""Add two resources assuming the circuits are executed in series. + + Args: + first (Resources): first resource object to combine + other (Resources): other resource object to combine with + + Returns: + Resources: combined resources + """ + qm1, qm2 = (first.qubit_manager, other.qubit_manager) + + new_clean = max(qm1.clean_qubits, qm2.clean_qubits) + new_dirty = qm1.dirty_qubits + qm2.dirty_qubits + new_budget = qm1.tight_budget or qm2.tight_budget + new_logic = max(qm1.algo_qubits, qm2.algo_qubits) + + new_qubit_manager = QubitManager( + work_wires={"clean": new_clean, "dirty": new_dirty}, tight_budget=new_budget + ) + + new_qubit_manager.algo_qubits = new_logic + new_gate_types = _combine_dict(first.gate_types, other.gate_types) + return Resources(new_qubit_manager, new_gate_types) + + +def add_in_parallel(first: Resources, other) -> Resources: # & + r"""Add two resources assuming the circuits are executed in parallel. + + Args: + first (Resources): first resource object to combine + other (Resources): other resource object to combine with + + Returns: + Resources: combined resources + """ + qm1, qm2 = (first.qubit_manager, other.qubit_manager) + + new_clean = max(qm1.clean_qubits, qm2.clean_qubits) + new_dirty = qm1.dirty_qubits + qm2.dirty_qubits + new_budget = qm1.tight_budget or qm2.tight_budget + new_logic = qm1.algo_qubits + qm2.algo_qubits + + new_qubit_manager = QubitManager( + work_wires={"clean": new_clean, "dirty": new_dirty}, + tight_budget=new_budget, + ) + + new_qubit_manager.algo_qubits = new_logic + new_gate_types = _combine_dict(first.gate_types, other.gate_types) + return Resources(new_qubit_manager, new_gate_types) + + +def mul_in_series(first: Resources, scalar: int) -> Resources: # * + r"""Multiply the resources by a scalar assuming the circuits are executed in series. + + Args: + first (Resources): first resource object to scale + scalar (int): integer value to scale the resources by + + Returns: + Resources: scaled resources + """ + qm = first.qubit_manager + + new_clean = qm.clean_qubits + new_dirty = scalar * qm.dirty_qubits + new_budget = qm.tight_budget + new_logic = qm.algo_qubits + + new_qubit_manager = QubitManager( + work_wires={"clean": new_clean, "dirty": new_dirty}, + tight_budget=new_budget, + ) + + new_qubit_manager.algo_qubits = new_logic + new_gate_types = _scale_dict(first.gate_types, scalar) + + return Resources(new_qubit_manager, new_gate_types) + + +def mul_in_parallel(first: Resources, scalar: int) -> Resources: # @ + r"""Multiply the resources by a scalar assuming the circuits are executed in parallel. + + Args: + first (Resources): first resource object to scale + scalar (int): integer value to scale the resources by + + Returns: + Resources: scaled resources + """ + qm = first.qubit_manager + + new_clean = qm.clean_qubits + new_dirty = scalar * qm.dirty_qubits + new_budget = qm.tight_budget + new_logic = scalar * qm.algo_qubits + + new_qubit_manager = QubitManager( + work_wires={"clean": new_clean, "dirty": new_dirty}, + tight_budget=new_budget, + ) + + new_qubit_manager.algo_qubits = new_logic + new_gate_types = _scale_dict(first.gate_types, scalar) + + return Resources(new_qubit_manager, new_gate_types) + + +def _combine_dict(dict1: defaultdict, dict2: defaultdict) -> defaultdict: + r"""Private function which combines two dictionaries together.""" + combined_dict = copy.copy(dict1) + + for k, v in dict2.items(): + combined_dict[k] += v + + return combined_dict + + +def _scale_dict(dict1: defaultdict, scalar: int) -> defaultdict: + r"""Private function which scales the values in a dictionary.""" + scaled_dict = copy.copy(dict1) + + for k in scaled_dict: + scaled_dict[k] *= scalar + + return scaled_dict diff --git a/pennylane/labs/tests/resource_estimation/test_resources_base.py b/pennylane/labs/tests/resource_estimation/test_resources_base.py new file mode 100644 index 00000000000..272b9ebdc3e --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/test_resources_base.py @@ -0,0 +1,306 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +""" +This module contains tests for the Resources container class. +""" +from collections import defaultdict +from dataclasses import dataclass + +import pytest + +from pennylane.labs.resource_estimation.qubit_manager import QubitManager +from pennylane.labs.resource_estimation.resources_base import ( + Resources, + _combine_dict, + _scale_dict, + add_in_parallel, + add_in_series, + mul_in_parallel, + mul_in_series, +) + +# pylint: disable= no-self-use,too-few-public-methods,comparison-with-itself + + +@dataclass(frozen=True) +class DummyResOp: + """A dummy class to populate the gate types dictionary for testing.""" + + name: str + + +h = DummyResOp("Hadamard") +x = DummyResOp("X") +y = DummyResOp("Y") +z = DummyResOp("Z") +cnot = DummyResOp("CNOT") + +gate_types_data = ( + defaultdict( + int, + {h: 2, x: 1, z: 1}, + ), + defaultdict( + int, + {h: 467, cnot: 791}, + ), + defaultdict( + int, + {x: 100, y: 120, z: 1000, cnot: 4523}, + ), +) + +qm1 = QubitManager(work_wires=5) +qm2 = QubitManager(work_wires={"clean": 8753, "dirty": 2347}, algo_wires=22) +qm3 = QubitManager(work_wires={"clean": 400, "dirty": 222}, algo_wires=108) + +qubit_manager_data = (qm1, qm2, qm3) + + +class TestResources: + """Test the Resources class""" + + @pytest.mark.parametrize("gt", gate_types_data + (None,)) + @pytest.mark.parametrize("qm", qubit_manager_data) + def test_init(self, qm, gt): + """Test that the class is correctly initialized""" + resources = Resources(qubit_manager=qm, gate_types=gt) + + expected_qm = qm + expected_gt = defaultdict(int, {}) if gt is None else gt + + assert resources.qubit_manager == expected_qm + assert resources.gate_types == expected_gt + + str_data = ( + ( + "--- Resources: ---\n" + + " Total qubits: 5\n" + + " Total gates : 4\n" + + " Qubit breakdown:\n" + + " clean qubits: 5, dirty qubits: 0, algorithmic qubits: 0\n" + + " Gate breakdown:\n" + + " {'Hadamard': 2, 'X': 1, 'Z': 1}" + ), + ( + "--- Resources: ---\n" + + " Total qubits: 1.112E+4\n" + + " Total gates : 1.258E+3\n" + + " Qubit breakdown:\n" + + " clean qubits: 8753, dirty qubits: 2347, algorithmic qubits: 22\n" + + " Gate breakdown:\n" + + " {'Hadamard': 467, 'CNOT': 791}" + ), + ( + "--- Resources: ---\n" + + " Total qubits: 730\n" + + " Total gates : 5.743E+3\n" + + " Qubit breakdown:\n" + + " clean qubits: 400, dirty qubits: 222, algorithmic qubits: 108\n" + + " Gate breakdown:\n" + + " {'X': 100, 'Y': 120, 'Z': 1.000E+3, 'CNOT': 4.523E+3}" + ), + ) + + @pytest.mark.parametrize( + "resources, expected_str", + zip( + tuple(Resources(qm, gt) for qm, gt in zip(qubit_manager_data, gate_types_data)), + str_data, + ), + ) + def test_str_method(self, resources, expected_str): + """Test that the str method correctly displays the information.""" + assert str(resources) == expected_str + + @pytest.mark.parametrize("gt", gate_types_data + (None,)) + @pytest.mark.parametrize("qm", qubit_manager_data) + def test_repr_method(self, gt, qm): + """Test that the repr method correctly represents the class.""" + resources = Resources(qubit_manager=qm, gate_types=gt) + + expected_qm = qm + expected_gt = defaultdict(int, {}) if gt is None else gt + assert repr(resources) == repr({"qubit_manager": expected_qm, "gate_types": expected_gt}) + + def test_clean_gate_counts(self): + """Test that this function correctly simplifies the gate types + dictionary by grouping together gates with the same name.""" + + class DummyResOp2: + """A dummy class to populate the gate types dictionary for testing.""" + + def __init__(self, name, parameter=None): + """Initialize dummy class.""" + self.name = name + self.parameter = parameter + + def __hash__(self): + """Custom hash which only depends on instance name.""" + return hash((self.name, self.parameter)) + + rx1 = DummyResOp2("RX", parameter=3.14) + rx2 = DummyResOp2("RX", parameter=3.14 / 2) + cnots = DummyResOp2("CNOT") + ry1 = DummyResOp2("RY", parameter=3.14) + ry2 = DummyResOp2("RY", parameter=3.14 / 4) + + gate_types = {rx1: 1, ry1: 2, cnots: 3, rx2: 4, ry2: 5} + res = Resources(qubit_manager=qm1, gate_types=gate_types) + + expected_clean_gate_counts = {"RX": 5, "RY": 7, "CNOT": 3} + assert res.clean_gate_counts == expected_clean_gate_counts + + def test_equality(self): + """Test that the equality method works as expected.""" + gt1, gt2 = (gate_types_data[0], gate_types_data[1]) + + res1 = Resources(qubit_manager=qm1, gate_types=gt1) + res1_copy = Resources(qubit_manager=qm1, gate_types=gt1) + res2 = Resources(qubit_manager=qm2, gate_types=gt2) + + assert res1 == res1 + assert res1 == res1_copy + assert res1 != res2 + + def test_arithmetic_raises_error(self): + """Test that an assertion error is raised when arithmetic methods are used""" + res = Resources(qubit_manager=qm1, gate_types=gate_types_data[0]) + + with pytest.raises(AssertionError): + _ = res + 2 # Can only add two Resources instances + + with pytest.raises(AssertionError): + _ = res & 2 # Can only add two Resources instances + + with pytest.raises(AssertionError): + _ = res * res # Can only multiply a Resources instance with an int + + with pytest.raises(AssertionError): + _ = res @ res # Can only multiply a Resources instance with an int + + def test_add_in_series(self): + """Test that we can add two resources assuming the gates occur in series""" + res1 = Resources(qubit_manager=qm3, gate_types=gate_types_data[2]) + res2 = Resources(qubit_manager=qm2, gate_types=gate_types_data[1]) + + expected_qm_add = QubitManager( + work_wires={ + "clean": 8753, # max(clean1, clean2) + "dirty": 2569, # dirty1 + dirty2 + } + ) + expected_qm_add.algo_qubits = 108 # max(algo1, algo2) + expected_gt_add = defaultdict( + int, + {h: 467, x: 100, y: 120, z: 1000, cnot: 5314}, # add gate counts + ) + + expected_add = Resources(expected_qm_add, expected_gt_add) + assert (res1 + res2) == expected_add + assert add_in_series(res1, res2) == expected_add + + def test_add_in_parallel(self): + """Test that we can add two resources assuming the gates occur in parallel""" + res1 = Resources(qubit_manager=qm3, gate_types=gate_types_data[2]) + res2 = Resources(qubit_manager=qm2, gate_types=gate_types_data[1]) + + expected_qm_and = QubitManager( + work_wires={ + "clean": 8753, # max(clean1, clean2) + "dirty": 2569, # dirty1 + dirty2 + } + ) + expected_qm_and.algo_qubits = 130 # algo1 + algo2 + expected_gt_and = defaultdict( + int, + {h: 467, x: 100, y: 120, z: 1000, cnot: 5314}, # add gate counts + ) + + expected_and = Resources(expected_qm_and, expected_gt_and) + assert (res1 & res2) == expected_and + assert add_in_parallel(res1, res2) == expected_and + + def test_mul_in_series(self): + """Test that we can scale resources by an integer assuming the gates occur in series""" + k = 3 + res = Resources(qubit_manager=qm3, gate_types=gate_types_data[2]) + + expected_qm_mul = QubitManager( + work_wires={ + "clean": 400, # clean + "dirty": 222 * k, # k * dirty1 + } + ) + expected_qm_mul.algo_qubits = 108 # algo + expected_gt_mul = defaultdict( + int, + {x: 100 * k, y: 120 * k, z: 1000 * k, cnot: 4523 * k}, # multiply gate counts + ) + + expected_mul = Resources(expected_qm_mul, expected_gt_mul) + + assert (k * res) == expected_mul + assert (res * k) == expected_mul + assert mul_in_series(res, k) == expected_mul + + def test_mul_in_parallel(self): + """Test that we can scale resources by an integer assuming the gates occur in parallel""" + k = 3 + res = Resources(qubit_manager=qm3, gate_types=gate_types_data[2]) + + expected_qm_matmul = QubitManager( + work_wires={ + "clean": 400, # clean + "dirty": 222 * k, # k * dirty1 + } + ) + expected_qm_matmul.algo_qubits = 108 * k # k * algo + expected_gt_matmul = defaultdict( + int, + {x: 100 * k, y: 120 * k, z: 1000 * k, cnot: 4523 * k}, # multiply gate counts + ) + + expected_matmul = Resources(expected_qm_matmul, expected_gt_matmul) + assert (k @ res) == expected_matmul + assert (res @ k) == expected_matmul + assert mul_in_parallel(res, k) == expected_matmul + + +def test_combine_dict(): + """Test the private _combine_dict function works as expected""" + g0 = defaultdict(int, {}) + g1 = defaultdict(int, {"a": 1, "b": 2, "c": 3}) + g2 = defaultdict(int, {"b": -1, "c": 0, "d": 1}) + + g_res = defaultdict(int, {"a": 1, "b": 1, "c": 3, "d": 1}) + assert _combine_dict(g0, g1) == g1 + assert _combine_dict(g1, g2) == g_res + + +@pytest.mark.parametrize( + "k, expected_dict", + ( + (0, defaultdict(int, {"a": 0, "b": 0, "c": 0})), + (1, defaultdict(int, {"a": 1, "b": 2, "c": 3})), + (3, defaultdict(int, {"a": 3, "b": 6, "c": 9})), + ), +) +def test_scale_dict(k, expected_dict): + """Test the private _scale_dict function works as expected""" + g0 = defaultdict(int, {}) + g1 = defaultdict(int, {"a": 1, "b": 2, "c": 3}) + + assert _scale_dict(g0, k) == g0 + assert _scale_dict(g1, k) == expected_dict From 5c7440c0a6378421f295b59cb51a1b68eaa3a9a6 Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Fri, 6 Jun 2025 16:18:46 -0400 Subject: [PATCH 08/18] Update the `ResourceOperator` class in labs (#7399) **Context:** Upgrading the `ResourceOperator` class according to the new ADR. **Description of the Change:** - Add `ResourceOperator` class --------- Co-authored-by: Diksha Dhawan Co-authored-by: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Co-authored-by: Utkarsh Co-authored-by: Soran Jahangiri <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 4 + .../labs/resource_estimation/__init__.py | 20 + .../resource_estimation/resource_operator.py | 530 ++++++++++++++++++ .../resource_estimation/resources_base.py | 3 +- .../test_resource_operator.py | 387 +++++++++++++ 5 files changed, 942 insertions(+), 2 deletions(-) create mode 100644 pennylane/labs/resource_estimation/resource_operator.py create mode 100644 pennylane/labs/tests/resource_estimation/test_resource_operator.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index abddcf09b09..2c5de441ece 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -505,6 +505,10 @@ * Added a `pennylane.labs.Resources` class to store and track the quantum resources from a circuit. [(#7406)](https://github.com/PennyLaneAI/pennylane/pull/7406) +* Added the base `pennylane.labs.ResourceOperator` class which will be used to implement all quantum + operators for resource estimation. + [(#7399)](https://github.com/PennyLaneAI/pennylane/pull/7399) +

Breaking changes 💔

* A new decomposition for two-qubit unitaries was implemented in `two_qubit_decomposition`. diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 1510eb0e5bc..5372e222f09 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -30,6 +30,18 @@ :toctree: api ~Resources + ~ResourceOperator + +Resource Estimation Functions: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autosummary:: + :toctree: api + + ~set_decomp + ~set_adj_decomp + ~set_ctrl_decomp + ~set_pow_decomp Qubit Management Classes: ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -45,3 +57,11 @@ from .qubit_manager import AllocWires, FreeWires, QubitManager from .resources_base import Resources +from .resource_operator import ( + ResourceOperator, + ResourcesNotDefined, + set_adj_decomp, + set_ctrl_decomp, + set_decomp, + set_pow_decomp, +) diff --git a/pennylane/labs/resource_estimation/resource_operator.py b/pennylane/labs/resource_estimation/resource_operator.py new file mode 100644 index 00000000000..e090c00ec1d --- /dev/null +++ b/pennylane/labs/resource_estimation/resource_operator.py @@ -0,0 +1,530 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""Abstract base class for resource operators.""" +from __future__ import annotations + +from abc import ABC, abstractmethod +from collections import defaultdict +from inspect import signature +from typing import Callable, List, Type + +from pennylane.labs.resource_estimation.qubit_manager import QubitManager +from pennylane.labs.resource_estimation.resources_base import Resources +from pennylane.operation import classproperty +from pennylane.queuing import QueuingManager +from pennylane.wires import Wires + +# pylint: disable=unused-argument, no-member + + +class ResourceOperator(ABC): + r"""Base class to represent quantum operators according to the set of information + required for resource estimation. + + A :class:`~.pennylane.labs.resource_estimation.ResourceOperator` is uniquely defined by its + name (the class type) and its resource parameters (:code:`op.resource_params`). + + **Example** + + This example shows how to create a custom :class:`~.pennylane.labs.resource_estimation.ResourceOperator` + class for resource estimation. We use :class:`~.pennylane.QFT` as a well known gate for + simplicity. + + .. code-block:: python + + from pennylane.labs import resource_estimation as plre + + class ResourceQFT(plre.ResourceOperator): + + resource_keys = {"num_wires"} # the only parameter that its resources depend upon. + + def __init__(self, num_wires, wires=None): # wire labels are optional + self.num_wires = num_wires + super().__init__(wires=wires) + + @property + def resource_params(self) -> dict: # The keys must match the `resource_keys` + return {"num_wires": self.num_wires} # and values obtained from the operator. + + @classmethod + def resource_rep(cls, num_wires): # Takes the `resource_keys` as input + params = {"num_wires": num_wires} # and produces a compressed + return plre.CompressedResourceOp(cls, params) # representation of the operator + + @classmethod + def default_resource_decomp(cls, num_wires, **kwargs): # `resource_keys` are input + + # Get compressed reps for each gate in the decomposition: + + swap = plre.resource_rep(plre.ResourceSWAP) + hadamard = plre.resource_rep(plre.ResourceHadamard) + ctrl_phase_shift = plre.resource_rep(plre.ResourceControlledPhaseShift) + + # Figure out the associated counts for each type of gate: + + swap_counts = num_wires // 2 + hadamard_counts = num_wires + ctrl_phase_shift_counts = num_wires*(num_wires - 1) // 2 + + return [ # Return the decomposition + plre.GateCount(swap, swap_counts), + plre.GateCount(hadamard, hadamard_counts), + plre.GateCount(ctrl_phase_shift, ctrl_phase_shift_counts), + ] + + Which can be instantiated as a normal operation, but now contains the resources: + + .. code-block:: pycon + + >>> op = ResourceQFT(num_wires=3) + >>> print(plre.estimate_resources(op, gate_set={'Hadamard', 'SWAP', 'ControlledPhaseShift'})) + --- Resources: --- + Total qubits: 3 + Total gates : 7 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3 + Gate breakdown: + {'SWAP': 1, 'Hadamard': 3, 'ControlledPhaseShift': 3} + + """ + + num_wires = 0 + _queue_category = "_resource_op" + + def __init__(self, *args, wires=None, **kwargs) -> None: + self.wires = None + if wires: + self.wires = Wires(wires) + self.num_wires = len(self.wires) + + self.queue() + super().__init__() + + def queue(self, context: QueuingManager = QueuingManager): + """Append the operator to the Operator queue.""" + context.append(self) + return self + + @classproperty + @classmethod + def resource_keys(cls) -> set: # pylint: disable=no-self-use + """The set of parameters that affects the resource requirement of the operator. + + All resource decomposition functions for this operator class are expected to accept the + keyword arguments that match these keys exactly. The :func:`~pennylane.resource_rep` + function will also expect keyword arguments that match these keys when called with this + operator type. + + The default implementation is an empty set, which is suitable for most operators. + """ + return set() + + @property + @abstractmethod + def resource_params(self) -> dict: + """A dictionary containing the minimal information needed to compute a resource estimate + of the operator's decomposition. The keys of this dictionary should match the + ``resource_keys`` attribute of the operator class. + """ + + @classmethod + @abstractmethod + def resource_rep(cls, *args, **kwargs): + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to estimate the resources.""" + + def resource_rep_from_op(self): + r"""Returns a compressed representation directly from the operator""" + return self.__class__.resource_rep(**self.resource_params) + + @classmethod + @abstractmethod + def default_resource_decomp(cls, *args, **kwargs) -> List: + r"""Returns a list of actions that define the resources of the operator.""" + + @classmethod + def resource_decomp(cls, *args, **kwargs) -> List: + r"""Returns a list of actions that define the resources of the operator.""" + return cls.default_resource_decomp(*args, **kwargs) + + @classmethod + def default_adjoint_resource_decomp(cls, *args, **kwargs) -> List: + r"""Returns a list representing the resources for the adjoint of the operator.""" + raise ResourcesNotDefined + + @classmethod + def adjoint_resource_decomp(cls, *args, **kwargs) -> List: + r"""Returns a list of actions that define the resources of the operator.""" + return cls.default_adjoint_resource_decomp(*args, **kwargs) + + @classmethod + def default_controlled_resource_decomp( + cls, ctrl_num_ctrl_wires: int, ctrl_num_ctrl_values: int, *args, **kwargs + ) -> List: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the + operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are + controlled when in the :math:`|0\rangle` state + """ + raise ResourcesNotDefined + + @classmethod + def controlled_resource_decomp( + cls, ctrl_num_ctrl_wires: int, ctrl_num_ctrl_values: int, *args, **kwargs + ) -> List: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the + operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are + controlled when in the :math:`|0\rangle` state + """ + return cls.default_controlled_resource_decomp( + ctrl_num_ctrl_wires, ctrl_num_ctrl_values, *args, **kwargs + ) + + @classmethod + def default_pow_resource_decomp(cls, pow_z: int, *args, **kwargs) -> List: + r"""Returns a list representing the resources for an operator + raised to a power. + + Args: + pow_z (int): exponent that the operator is being raised to + """ + raise ResourcesNotDefined + + @classmethod + def pow_resource_decomp(cls, pow_z, *args, **kwargs) -> List: + r"""Returns a list representing the resources for an operator + raised to a power. + + Args: + pow_z (int): exponent that the operator is being raised to + """ + return cls.default_pow_resource_decomp(pow_z, *args, **kwargs) + + @classmethod + def set_resources(cls, new_func: Callable, override_type: str = "base"): + """Set a custom function to override the default resource decomposition. + + This method allows users to replace any of the `resource_decomp`, `adjoint_resource_decomp`, + `ctrl_resource_decomp`, or `pow_resource_decomp` methods globally for every instance of + the class. + + """ + if override_type == "base": + keys = cls.resource_keys.union({"kwargs"}) + _validate_signature(new_func, keys) + cls.resource_decomp = new_func + if override_type == "pow": + keys = cls.resource_keys.union({"pow_z", "kwargs"}) + _validate_signature(new_func, keys) + cls.pow_resource_decomp = new_func + if override_type == "adj": + keys = cls.resource_keys.union({"kwargs"}) + _validate_signature(new_func, keys) + cls.adjoint_resource_decomp = new_func + if override_type == "ctrl": + keys = cls.resource_keys.union( + {"ctrl_num_ctrl_wires", "ctrl_num_ctrl_values", "kwargs"} + ) + _validate_signature(new_func, keys) + cls.controlled_resource_decomp = new_func + + def __repr__(self) -> str: + str_rep = self.__class__.__name__ + "(" + str(self.resource_params) + ")" + return str_rep + + def __mul__(self, scalar: int): + assert isinstance(scalar, int) + gate_types = defaultdict(int, {self.resource_rep_from_op(): scalar}) + qubit_manager = QubitManager(0, algo_wires=self.num_wires) + + return Resources(qubit_manager, gate_types) + + def __matmul__(self, scalar: int): + assert isinstance(scalar, int) + gate_types = defaultdict(int, {self.resource_rep_from_op(): scalar}) + qubit_manager = QubitManager(0, algo_wires=scalar * self.num_wires) + + return Resources(qubit_manager, gate_types) + + def __add__(self, other): + if isinstance(other, ResourceOperator): + return (1 * self) + (1 * other) + if isinstance(other, Resources): + return (1 * self) + other + + raise TypeError(f"Cannot add resource operator {self} with type {type(other)}.") + + def __and__(self, other): + if isinstance(other, ResourceOperator): + return (1 * self) & (1 * other) + if isinstance(other, Resources): + return (1 * self) & other + + raise TypeError(f"Cannot add resource operator {self} with type {type(other)}.") + + __radd__ = __add__ + __rand__ = __and__ + __rmul__ = __mul__ + __rmatmul__ = __matmul__ + + @classmethod + def tracking_name(cls, *args, **kwargs) -> str: + r"""Returns a name used to track the operator during resource estimation.""" + return cls.__name__.replace("Resource", "") + + def tracking_name_from_op(self) -> str: + r"""Returns the tracking name built with the operator's parameters.""" + return self.__class__.tracking_name(**self.resource_params) + + +def _validate_signature(func: Callable, expected_args: set): + """Raise an error if the provided function doesn't match expected signature + + Args: + func (Callable): function to match signature with + expected_args (set): expected signature + """ + + sig = signature(func) + actual_args = set(sig.parameters) + + if extra_args := actual_args - expected_args: + raise ValueError( + f"The function provided specifies additional arguments ({extra_args}) from" + + f" the expected arguments ({expected_args}). Please update the function signature or" + + " modify the base class' `resource_keys` argument." + ) + + if missing_args := expected_args - actual_args: + raise ValueError( + f"The function is missing arguments ({missing_args}) which are expected. Please" + + " update the function signature or modify the base class' `resource_keys` argument." + ) + + +class ResourcesNotDefined(Exception): + r"""Exception to be raised when a ``ResourceOperator`` does not implement _resource_decomp""" + + +def set_decomp(cls: Type[ResourceOperator], decomp_func: Callable) -> None: + """Set a custom function to override the default resource decomposition. This + function will be set globally for every instance of the class. + + Args: + cls (Type[ResourceOperator]): the operator class whose decomposition is being overriden. + decomp_func (Callable): the new resource decomposition function to be set as default. + + .. note:: + + The new decomposition function should have the same signature as the one it replaces. + Specifically, the signature should match the :code:`resource_keys` of the base resource + operator class being overriden. + + **Example** + + .. code-block:: python + + from pennylane.labs import resource_estimation as plre + + def custom_res_decomp(**kwargs): + h = plre.resource_rep(plre.ResourceHadamard) + s = plre.resource_rep(plre.ResourceS) + return [plre.GateCount(h, 2), plre.GateCount(s, 2)] + + .. code-block:: pycon + + >>> print(plre.estimate_resources(plre.ResourceX(), gate_set={"Hadamard", "Z", "S"})) + --- Resources: --- + Total qubits: 1 + Total gates : 3 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'Z': 1, 'Hadamard': 2} + >>> plre.set_decomp(plre.ResourceX, custom_res_decomp) + >>> print(plre.estimate_resources(plre.ResourceX(), gate_set={"Hadamard", "Z", "S"})) + --- Resources: --- + Total qubits: 1 + Total gates : 4 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'S': 2, 'Hadamard': 2} + + """ + cls.set_resources(decomp_func, override_type="base") + + +def set_ctrl_decomp(cls: Type[ResourceOperator], decomp_func: Callable) -> None: + """Set a custom function to override the default controlled-resource decomposition. This + function will be set globally for every instance of the class. + + Args: + cls (Type[ResourceOperator]): the operator class whose decomposition is being overriden. + decomp_func (Callable): the new resource decomposition function to be set as default. + + .. note:: + + The new decomposition function should have the same signature as the one it replaces. + Specifically, the signature should match the `resource_keys` of the base resource operator + class being overriden. Addtionally, the controlled decomposition requires two additional + arguments: :code:`ctrl_num_ctrl_wires` and :code:`ctrl_num_ctrl_values`. + + **Example** + + .. code-block:: python + + from pennylane.labs import resource_estimation as plre + + def custom_ctrl_decomp(ctrl_num_ctrl_wires, ctrl_num_ctrl_values, **kwargs): + h = plre.resource_rep(plre.ResourceHadamard) + cz = plre.resource_rep(plre.ResourceCZ) + return [plre.GateCount(h, 2), plre.GateCount(cz, 1)] + + .. code-block:: pycon + + >>> cx = plre.ResourceControlled(plre.ResourceX(), 1, 0) + >>> print(plre.estimate_resources(cx, gate_set={"CNOT", "Hadamard", "CZ"})) + --- Resources: --- + Total qubits: 2 + Total gates : 1 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 2 + Gate breakdown: + {'CNOT': 1} + >>> plre.set_ctrl_decomp(plre.ResourceX, custom_ctrl_decomp) + >>> print(plre.estimate_resources(cx, gate_set={"CNOT", "Hadamard", "CZ"})) + --- Resources: --- + Total qubits: 2 + Total gates : 3 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 2 + Gate breakdown: + {'Hadamard': 2, 'CZ': 1} + + """ + cls.set_resources(decomp_func, override_type="ctrl") + + +def set_adj_decomp(cls: Type[ResourceOperator], decomp_func: Callable) -> None: + """Set a custom function to override the default adjoint-resource decomposition. This + function will be set globally for every instance of the class. + + Args: + cls (Type[ResourceOperator]): the operator class whose decomposition is being overriden. + decomp_func (Callable): the new resource decomposition function to be set as default. + + .. note:: + + The new decomposition function should have the same signature as the one it replaces. + Specifically, the signature should match the `resource_keys` of the base resource operator + class being overriden. + + **Example** + + .. code-block:: python + + from pennylane.labs import resource_estimation as plre + + def custom_adj_decomp(**kwargs): + h = plre.resource_rep(plre.ResourceHadamard) + s = plre.resource_rep(plre.ResourceS) + return [plre.GateCount(h, 2), plre.GateCount(s, 2)] + + .. code-block:: pycon + + >>> adj_x = plre.ResourceAdjoint(plre.ResourceX()) + >>> print(plre.estimate_resources(adj_x, gate_set={"X", "Hadamard", "S"})) + --- Resources: --- + Total qubits: 1 + Total gates : 1 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'X': 1} + >>> plre.set_adj_decomp(plre.ResourceX, custom_adj_decomp) + >>> print(plre.estimate_resources(adj_x, gate_set={"X", "Hadamard", "S"})) + --- Resources: --- + Total qubits: 1 + Total gates : 4 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'Hadamard': 2, 'S': 2} + + """ + cls.set_resources(decomp_func, override_type="adj") + + +def set_pow_decomp(cls: Type[ResourceOperator], decomp_func: Callable) -> None: + """Set a custom function to override the default pow-resource decomposition. This + function will be set globally for every instance of the class. + + Args: + cls (Type[ResourceOperator]): the operator class whose decomposition is being overriden. + decomp_func (Callable): the new resource decomposition function to be set as default. + + .. note:: + + The new decomposition function should have the same signature as the one it replaces. + Specifically, the signature should match the `resource_keys` of the base resource operator + class being overriden. Addtionally, the pow-decomposition requires an additional argument: + :code:`pow_z`. + + **Example** + + .. code-block:: python + + from pennylane.labs import resource_estimation as plre + + def custom_pow_decomp(pow_z, **kwargs): + h = plre.resource_rep(plre.ResourceHadamard) + s = plre.resource_rep(plre.ResourceS) + id = plre.resource_rep(plre.ResourceIdentity) + + if pow_z % 2 == 0: + return [plre.GateCount(id, 1)] + + return [plre.GateCount(h, 2), plre.GateCount(s, 2)] + + .. code-block:: pycon + + >>> pow_x = plre.ResourcePow(plre.ResourceX(), 3) + >>> print(plre.estimate_resources(pow_x, gate_set={"X", "Hadamard", "S"})) + --- Resources: --- + Total qubits: 1 + Total gates : 1 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'X': 1} + >>> plre.set_pow_decomp(plre.ResourceX, custom_pow_decomp) + >>> print(plre.estimate_resources(pow_x, gate_set={"X", "Hadamard", "S"})) + --- Resources: --- + Total qubits: 1 + Total gates : 4 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'Hadamard': 2, 'S': 2} + + """ + cls.set_resources(decomp_func, override_type="pow") diff --git a/pennylane/labs/resource_estimation/resources_base.py b/pennylane/labs/resource_estimation/resources_base.py index 2a567bed8f1..5ae60082b35 100644 --- a/pennylane/labs/resource_estimation/resources_base.py +++ b/pennylane/labs/resource_estimation/resources_base.py @@ -57,7 +57,7 @@ class Resources: The :class:`Resources` object supports arithmetic operations which allow for quick addition and multiplication of resources. When combining resources, we can make a simplifying - assumption about they are applied in a quantum circuit (in series or in parallel). + assumption about how they are applied in a quantum circuit (in series or in parallel). When assuming the circuits were executed in series, the number of algorithmic qubits add together. When assuming the circuits were executed in parallel, the maximum of each set of @@ -77,7 +77,6 @@ class Resources: # state of qubits: qm1 = re.QubitManager(work_wires={"clean":2, "dirty":1}, algo_wires=3) - qm2 = re.QubitManager(work_wires={"clean":1, "dirty":2}, algo_wires=4) # state of gates: diff --git a/pennylane/labs/tests/resource_estimation/test_resource_operator.py b/pennylane/labs/tests/resource_estimation/test_resource_operator.py new file mode 100644 index 00000000000..b2fe87c69a2 --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/test_resource_operator.py @@ -0,0 +1,387 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +""" +Test the base and abstract Resource class +""" +from collections import defaultdict +from dataclasses import dataclass +from typing import List + +import pytest + +from pennylane.labs.resource_estimation import ( + QubitManager, + ResourceOperator, + Resources, + set_adj_decomp, + set_ctrl_decomp, + set_decomp, + set_pow_decomp, +) +from pennylane.queuing import AnnotatedQueue +from pennylane.wires import Wires + +# pylint: disable=protected-access, too-few-public-methods, no-self-use, unused-argument, arguments-differ + + +@dataclass(frozen=True) +class DummyCmprsRep: + """A dummy class to populate the gate types dictionary for testing.""" + + name: str + param: int = 0 + + +class DummyOp(ResourceOperator): + """A dummy class to test ResourceOperator instantiation.""" + + resource_keys = {"x"} + + def __init__(self, x=None, wires=None): + self.x = x + super().__init__(wires=wires) + + def __eq__(self, other: object) -> bool: + return (self.__class__.__name__ == other.__class__.__name__) and (self.x == other.x) + + @property + def resource_params(self): + return {"x": self.x} + + @classmethod + def resource_rep(cls, x): + return DummyCmprsRep(cls.__name__, param=x) + + @classmethod + def default_resource_decomp(cls, x) -> List: + return [x] + + +class DummyOp_no_resource_rep(ResourceOperator): + """A dummy class to test ResourceOperator instantiation.""" + + def __init__(self, x, wires=None): + self.x = x + super().__init__(wires=wires) + + @property + def resource_params(self): + return DummyCmprsRep({"x": self.x}) + + @classmethod + def default_resource_decomp(cls, x) -> List: + return [x] + + +class DummyOp_no_resource_params(ResourceOperator): + """A dummy class to test ResourceOperator instantiation.""" + + def __init__(self, x, wires=None): + self.x = x + super().__init__(wires=wires) + + @classmethod + def resource_rep(cls, x): + return DummyCmprsRep(cls.__name__, param=x) + + @classmethod + def default_resource_decomp(cls, x) -> List: + return [x] + + +class DummyOp_no_resource_decomp(ResourceOperator): + """A dummy class to test ResourceOperator instatiation.""" + + def __init__(self, x, wires=None): + self.x = x + super().__init__(wires=wires) + + @classmethod + def resource_rep(cls, x): + return DummyCmprsRep(cls.__name__, param=x) + + @property + def resource_params(self): + return DummyCmprsRep({"x": self.x}) + + +class ResourceRX(DummyOp): + """Dummy op representing RX""" + + num_wires = 1 + + +class ResourceHadamard(DummyOp): + """Dummy op representing Hadamard""" + + num_wires = 1 + + +class ResourceCNOT(DummyOp): + """Dummy op representing CNOT""" + + num_wires = 2 + + +class TestResourceOperator: + """Tests for the ResourceOperator class""" + + res_op_error_lst = [ + DummyOp_no_resource_rep, + DummyOp_no_resource_params, + DummyOp_no_resource_decomp, + ] + + @pytest.mark.parametrize("res_op", res_op_error_lst) + def test_init_error_abstract_methods(self, res_op): + """Test that errors are raised when the resource operator + is implemented without specifying the abstract methods.""" + + with pytest.raises(TypeError, match="Can't instantiate abstract class"): + res_op(x=1) + + def test_init_queuing(self): + """Test that instantiating a resource operator correctly sets its arguments + and queues it.""" + + with AnnotatedQueue() as q: + ResourceHadamard(wires=[0]) + ResourceHadamard(wires=[1]) + ResourceCNOT(wires=[0, 1]) + ResourceRX(x=1.23, wires=[0]) + + expected_queue = [ + ResourceHadamard(wires=[0]), + ResourceHadamard(wires=[1]), + ResourceCNOT(wires=[0, 1]), + ResourceRX(x=1.23, wires=[0]), + ] + + assert q.queue == expected_queue + + def test_init_wire_override(self): + """Test that setting the wires correctly overrides the num_wires argument.""" + dummy_op1 = DummyOp() + assert dummy_op1.wires is None + assert dummy_op1.num_wires == 0 + + dummy_op2 = DummyOp(wires=[0, 1, 2]) + assert dummy_op2.wires == Wires([0, 1, 2]) + assert dummy_op2.num_wires == 3 + + @pytest.mark.parametrize("s", [1, 2, 3]) + def test_mul(self, s): + """Test multiply dunder method""" + op = ResourceRX(1.23) + resources = s * op + + gt = defaultdict(int, {DummyCmprsRep("ResourceRX", 1.23): s}) + qm = QubitManager(work_wires=0, algo_wires=1) + expected_resources = Resources(qubit_manager=qm, gate_types=gt) + assert resources == expected_resources + + @pytest.mark.parametrize("s", [1, 2, 3]) + def test_mat_mul(self, s): + """Test matrix-multiply dunder method""" + op = ResourceCNOT() + resources = s @ op + + gt = defaultdict(int, {DummyCmprsRep("ResourceCNOT", None): s}) + qm = QubitManager(work_wires=0, algo_wires=s * 2) + expected_resources = Resources(qubit_manager=qm, gate_types=gt) + assert resources == expected_resources + + def test_add(self): + """Test addition dunder method between two ResourceOperator classes""" + op1 = ResourceRX(1.23) + op2 = ResourceCNOT() + resources = op1 + op2 + + gt = defaultdict( + int, + { + DummyCmprsRep("ResourceRX", 1.23): 1, + DummyCmprsRep("ResourceCNOT", None): 1, + }, + ) + qm = QubitManager(work_wires=0, algo_wires=2) + expected_resources = Resources(qubit_manager=qm, gate_types=gt) + assert resources == expected_resources + + def test_add_resources(self): + """Test addition dunder method between a ResourceOperator and a Resources object""" + op1 = ResourceRX(1.23) + gt2 = defaultdict(int, {DummyCmprsRep("ResourceCNOT", None): 1}) + qm2 = QubitManager(work_wires=0, algo_wires=2) + res2 = Resources(qubit_manager=qm2, gate_types=gt2) + resources = op1 + res2 + + gt = defaultdict( + int, + { + DummyCmprsRep("ResourceRX", 1.23): 1, + DummyCmprsRep("ResourceCNOT", None): 1, + }, + ) + qm = QubitManager(work_wires=0, algo_wires=2) + expected_resources = Resources(qubit_manager=qm, gate_types=gt) + assert resources == expected_resources + + def test_add_error(self): + """Test addition dunder method raises error when adding with unsupported type""" + with pytest.raises(TypeError, match="Cannot add resource operator"): + op1 = ResourceRX(1.23) + _ = op1 + True + + def test_and(self): + """Test and dunder method between two ResourceOperator classes""" + op1 = ResourceRX(1.23) + op2 = ResourceCNOT() + resources = op1 & op2 + + gt = defaultdict( + int, + { + DummyCmprsRep("ResourceRX", 1.23): 1, + DummyCmprsRep("ResourceCNOT", None): 1, + }, + ) + qm = QubitManager(work_wires=0, algo_wires=3) + expected_resources = Resources(qubit_manager=qm, gate_types=gt) + assert resources == expected_resources + + def test_and_resources(self): + """Test and dunder method between a ResourceOperator and a Resources object""" + op1 = ResourceRX(1.23) + gt2 = defaultdict(int, {DummyCmprsRep("ResourceCNOT", None): 1}) + qm2 = QubitManager(work_wires=0, algo_wires=2) + res2 = Resources(qubit_manager=qm2, gate_types=gt2) + resources = op1 & res2 + + gt = defaultdict( + int, + { + DummyCmprsRep("ResourceRX", 1.23): 1, + DummyCmprsRep("ResourceCNOT", None): 1, + }, + ) + qm = QubitManager(work_wires=0, algo_wires=3) + expected_resources = Resources(qubit_manager=qm, gate_types=gt) + assert resources == expected_resources + + def test_and_error(self): + """Test and dunder method raises error when adding with unsupported type""" + with pytest.raises(TypeError, match="Cannot add resource operator"): + op1 = ResourceRX(1.23) + _ = op1 & True + + +def test_set_decomp(): + """Test that the set_decomp function works as expected.""" + op1 = DummyOp(x=5) + assert DummyOp.resource_decomp(**op1.resource_params) == [5] + + def custom_res_decomp(x, **kwargs): + return [x + 1] + + set_decomp(DummyOp, custom_res_decomp) + + assert DummyOp.resource_decomp(**op1.resource_params) == [6] + + def custom_res_decomp_error(y): # must match signature of default_resource_decomp + return [y + 1] + + with pytest.raises(ValueError): + set_decomp(DummyOp, custom_res_decomp_error) + + +def test_set_adj_decomp(): + """Test that the set_decomp function works as expected.""" + + class DummyAdjOp(DummyOp): + """Dummy Adjoint Op class""" + + @classmethod + def default_adjoint_resource_decomp(cls, x): + return cls.default_resource_decomp(x=x) + + op1 = DummyAdjOp(x=5) + assert DummyAdjOp.adjoint_resource_decomp(**op1.resource_params) == [5] + + def custom_res_decomp(x, **kwargs): + return [x + 1] + + set_adj_decomp(DummyAdjOp, custom_res_decomp) + + assert DummyAdjOp.adjoint_resource_decomp(**op1.resource_params) == [6] + + def custom_res_decomp_error(y): # must match signature of default_adjoint_resource_decomp + return [y + 1] + + with pytest.raises(ValueError): + set_adj_decomp(DummyAdjOp, custom_res_decomp_error) + + +def test_set_ctrl_decomp(): + """Test that the set_decomp function works as expected.""" + + class DummyCtrlOp(DummyOp): + """Dummy Controlled Op class""" + + @classmethod + def default_controlled_resource_decomp(cls, ctrl_num_ctrl_wires, ctrl_num_ctrl_values, x): + return cls.default_resource_decomp(x=x + ctrl_num_ctrl_values) + + op1 = DummyCtrlOp(x=5) + assert DummyCtrlOp.controlled_resource_decomp(1, 0, **op1.resource_params) == [5] + + def custom_res_decomp(ctrl_num_ctrl_wires, ctrl_num_ctrl_values, x, **kwargs): + return [x + ctrl_num_ctrl_wires] + + set_ctrl_decomp(DummyCtrlOp, custom_res_decomp) + + assert DummyCtrlOp.controlled_resource_decomp(1, 0, **op1.resource_params) == [6] + + def custom_res_decomp_error(x): # must match signature of default_controlled_resource_decomp + return [x + 1] + + with pytest.raises(ValueError): + set_ctrl_decomp(DummyCtrlOp, custom_res_decomp_error) + + +def test_set_pow_decomp(): + """Test that the set_decomp function works as expected.""" + + class DummyPowOp(DummyOp): + """Dummy Pow Op class""" + + @classmethod + def default_pow_resource_decomp(cls, pow_z, x): + return cls.default_resource_decomp(x=x) + + op1 = DummyPowOp(x=5) + assert DummyPowOp.pow_resource_decomp(pow_z=3, **op1.resource_params) == [5] + + def custom_res_decomp(pow_z, x, **kwargs): + return [x * pow_z] + + set_pow_decomp(DummyPowOp, custom_res_decomp) + + assert DummyPowOp.pow_resource_decomp(pow_z=3, **op1.resource_params) == [15] + + def custom_res_decomp_error(x): # must match signature of default_pow_resource_decomp + return [x + 1] + + with pytest.raises(ValueError): + set_pow_decomp(DummyPowOp, custom_res_decomp_error) From d05b251f78a7cdbbe040ea9bd6b104e0ee7eb42c Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Fri, 6 Jun 2025 17:12:29 -0400 Subject: [PATCH 09/18] Add CompressedResourceOp class (#7408) **Context:** Add CompressedResourceOp class corresponding to operator type and parameters to labs **Description of the Change:** **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** [sc-90645] --------- Co-authored-by: Jay Soni Co-authored-by: Utkarsh Co-authored-by: Soran Jahangiri <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 7 + .../labs/resource_estimation/__init__.py | 2 + .../resource_estimation/resource_operator.py | 102 ++++++++++- .../test_resource_operator.py | 159 +++++++++++++++++- 4 files changed, 266 insertions(+), 4 deletions(-) diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 2c5de441ece..ded36508e2b 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -504,6 +504,13 @@ * Added a `pennylane.labs.Resources` class to store and track the quantum resources from a circuit. [(#7406)](https://github.com/PennyLaneAI/pennylane/pull/7406) + +* `pennylane.labs.CompressedResourceOp` class has been added to store information about the operator type and parameters. + [(#7408)](https://github.com/PennyLaneAI/pennylane/pull/7408) + +* Added the base `pennylane.labs.ResourceOperator` class which will be used to implement all quantum + operators for resource estimation. + [(#7399)](https://github.com/PennyLaneAI/pennylane/pull/7399) * Added the base `pennylane.labs.ResourceOperator` class which will be used to implement all quantum operators for resource estimation. diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 5372e222f09..4b1802b308b 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -31,6 +31,7 @@ ~Resources ~ResourceOperator + ~CompressedResourceOp Resource Estimation Functions: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -58,6 +59,7 @@ from .qubit_manager import AllocWires, FreeWires, QubitManager from .resources_base import Resources from .resource_operator import ( + CompressedResourceOp, ResourceOperator, ResourcesNotDefined, set_adj_decomp, diff --git a/pennylane/labs/resource_estimation/resource_operator.py b/pennylane/labs/resource_estimation/resource_operator.py index e090c00ec1d..b154032cb61 100644 --- a/pennylane/labs/resource_estimation/resource_operator.py +++ b/pennylane/labs/resource_estimation/resource_operator.py @@ -17,7 +17,9 @@ from abc import ABC, abstractmethod from collections import defaultdict from inspect import signature -from typing import Callable, List, Type +from typing import Callable, Hashable, List, Optional, Type + +import numpy as np from pennylane.labs.resource_estimation.qubit_manager import QubitManager from pennylane.labs.resource_estimation.resources_base import Resources @@ -28,6 +30,100 @@ # pylint: disable=unused-argument, no-member +class CompressedResourceOp: # pylint: disable=too-few-public-methods + r"""Instantiate a light weight class corresponding to the operator type and parameters. + + This class provides a minimal representation of an operation, containing + only the operator type and the necessary parameters to estimate its resources. + It's designed for efficient hashing and comparison, allowing it to be used + effectively in collections where uniqueness and quick lookups are important. + + Args: + op_type (Type): the class object of an operation which inherits from :class:'~.pennylane.labs.resource_estimation.ResourceOperator' + params (dict): a dictionary containing the minimal pairs of parameter names and values + required to compute the resources for the given operator + name (str, optional): A custom name for the compressed operator. If not + provided, a name will be generated using `op_type.tracking_name` + with the given parameters. + + .. details:: + + This representation is the minimal amount of information required to estimate resources for the operator. + + **Example** + + >>> op_tp = plre.CompressedResourceOp(plre.ResourceHadamard, {"num_wires":1}) + >>> print(op_tp) + CompressedResourceOp(ResourceHadamard, params={'num_wires':1}) + """ + + def __init__( + self, op_type: Type[ResourceOperator], params: Optional[dict] = None, name: str = None + ): + + if not issubclass(op_type, ResourceOperator): + raise TypeError(f"op_type must be a subclass of ResourceOperator. Got {op_type}.") + self.op_type = op_type + self.params = params or {} + self._hashable_params = _make_hashable(params) if params else () + self._name = name or op_type.tracking_name(**self.params) + + def __hash__(self) -> int: + return hash((self.op_type, self._hashable_params)) + + def __eq__(self, other: CompressedResourceOp) -> bool: + return ( + isinstance(other, CompressedResourceOp) + and self.op_type == other.op_type + and self.params == other.params + ) + + def __repr__(self) -> str: + + class_name = self.__class__.__qualname__ + op_type_name = self.op_type.__name__ + + params_arg_str = "" + if self.params: + params = sorted(self.params.items()) + params_str = ", ".join(f"{k!r}:{v!r}" for k, v in params) + params_arg_str = f", params={{{params_str}}}" + + return f"{class_name}({op_type_name}{params_arg_str})" + + @property + def name(self) -> str: + r"""Returns the name of operator.""" + return self._name + + +def _make_hashable(d) -> tuple: + r"""Converts a potentially non-hashable object into a hashable tuple. + + Args: + d : The object to potentially convert to a hashable tuple. + This can be a dictionary, list, set, or an array. + + Returns: + A hashable tuple representation of the input. + + """ + + if isinstance(d, Hashable): + return d + + if isinstance(d, dict): + return tuple(sorted((_make_hashable(k), _make_hashable(v)) for k, v in d.items())) + if isinstance(d, (list, tuple)): + return tuple(_make_hashable(elem) for elem in d) + if isinstance(d, set): + return tuple(sorted(_make_hashable(elem) for elem in d)) + if isinstance(d, np.ndarray): + return _make_hashable(d.tolist()) + + raise TypeError(f"Object of type {type(d)} is not hashable and cannot be converted.") + + class ResourceOperator(ABC): r"""Base class to represent quantum operators according to the set of information required for resource estimation. @@ -93,9 +189,9 @@ def default_resource_decomp(cls, num_wires, **kwargs): # `resource_keys` are in Total qubits: 3 Total gates : 7 Qubit breakdown: - clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3 + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3 Gate breakdown: - {'SWAP': 1, 'Hadamard': 3, 'ControlledPhaseShift': 3} + {'SWAP': 1, 'Hadamard': 3, 'ControlledPhaseShift': 3} """ diff --git a/pennylane/labs/tests/resource_estimation/test_resource_operator.py b/pennylane/labs/tests/resource_estimation/test_resource_operator.py index b2fe87c69a2..41581a617be 100644 --- a/pennylane/labs/tests/resource_estimation/test_resource_operator.py +++ b/pennylane/labs/tests/resource_estimation/test_resource_operator.py @@ -16,11 +16,14 @@ """ from collections import defaultdict from dataclasses import dataclass -from typing import List +from typing import Hashable, List +import numpy as np import pytest +import pennylane as qml from pennylane.labs.resource_estimation import ( + CompressedResourceOp, QubitManager, ResourceOperator, Resources, @@ -29,12 +32,132 @@ set_decomp, set_pow_decomp, ) +from pennylane.labs.resource_estimation.resource_operator import _make_hashable from pennylane.queuing import AnnotatedQueue from pennylane.wires import Wires # pylint: disable=protected-access, too-few-public-methods, no-self-use, unused-argument, arguments-differ +class ResourceDummyX(ResourceOperator): + """Dummy testing class representing X gate""" + + +class ResourceDummyQFT(ResourceOperator): + """Dummy testing class representing QFT gate""" + + +class ResourceDummyQSVT(ResourceOperator): + """Dummy testing class representing QSVT gate""" + + +class ResourceDummyTrotterProduct(ResourceOperator): + """Dummy testing class representing TrotterProduct gate""" + + +class ResourceDummyAdjoint(ResourceOperator): + """Dummy testing class representing the Adjoint symbolic operator""" + + +class TestCompressedResourceOp: + """Testing the methods and attributes of the CompressedResourceOp class""" + + test_hamiltonian = qml.dot([1, -1, 0.5], [qml.X(0), qml.Y(1), qml.Z(0) @ qml.Z(1)]) + compressed_ops_and_params_lst = ( + ("DummyX", ResourceDummyX, {"num_wires": 1}, None), + ("DummyQFT", ResourceDummyQFT, {"num_wires": 5}, None), + ("DummyQSVT", ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}, None), + ( + "DummyTrotterProduct", + ResourceDummyTrotterProduct, + {"Hamiltonian": test_hamiltonian, "num_steps": 5, "order": 2}, + None, + ), + ("X", ResourceDummyX, {"num_wires": 1}, "X"), + ) + + compressed_op_names = ( + "DummyX", + "DummyQFT", + "DummyQSVT", + "DummyTrotterProduct", + "X", + ) + + @pytest.mark.parametrize("name, op_type, parameters, name_param", compressed_ops_and_params_lst) + def test_init(self, name, op_type, parameters, name_param): + """Test that we can correctly instantiate CompressedResourceOp""" + cr_op = CompressedResourceOp(op_type, parameters, name=name_param) + + assert cr_op._name == name + assert cr_op.op_type is op_type + assert cr_op.params == parameters + assert sorted(cr_op._hashable_params) == sorted(tuple(parameters.items())) + + def test_hash(self): + """Test that the hash method behaves as expected""" + CmprssedQSVT1 = CompressedResourceOp(ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}) + CmprssedQSVT2 = CompressedResourceOp(ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}) + Other = CompressedResourceOp(ResourceDummyQFT, {"num_wires": 3}) + + assert hash(CmprssedQSVT1) == hash(CmprssedQSVT1) # compare same object + assert hash(CmprssedQSVT1) == hash(CmprssedQSVT2) # compare identical instance + assert hash(CmprssedQSVT1) != hash(Other) + + # test dictionary as parameter + CmprssedAdjoint1 = CompressedResourceOp( + ResourceDummyAdjoint, {"base_class": ResourceDummyQFT, "base_params": {"num_wires": 1}} + ) + CmprssedAdjoint2 = CompressedResourceOp( + ResourceDummyAdjoint, {"base_class": ResourceDummyQFT, "base_params": {"num_wires": 1}} + ) + Other = CompressedResourceOp( + ResourceDummyAdjoint, {"base_class": ResourceDummyQFT, "base_params": {"num_wires": 2}} + ) + + assert hash(CmprssedAdjoint1) == hash(CmprssedAdjoint1) + assert hash(CmprssedAdjoint1) == hash(CmprssedAdjoint2) + assert hash(CmprssedAdjoint1) != hash(Other) + + def test_equality(self): + """Test that the equality methods behaves as expected""" + CmprssedQSVT1 = CompressedResourceOp(ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}) + CmprssedQSVT2 = CompressedResourceOp(ResourceDummyQSVT, {"num_wires": 3, "num_angles": 5}) + CmprssedQSVT3 = CompressedResourceOp(ResourceDummyQSVT, {"num_angles": 5, "num_wires": 3}) + Other = CompressedResourceOp(ResourceDummyQFT, {"num_wires": 3}) + + assert CmprssedQSVT1 == CmprssedQSVT2 # compare identical instance + assert CmprssedQSVT1 == CmprssedQSVT3 # compare swapped parameters + assert CmprssedQSVT1 != Other + + @pytest.mark.parametrize("args, name", zip(compressed_ops_and_params_lst, compressed_op_names)) + def test_name(self, args, name): + """Test that the name method behaves as expected.""" + _, op_type, parameters, name_param = args + cr_op = CompressedResourceOp(op_type, parameters, name=name_param) + + assert cr_op.name == name + + @pytest.mark.parametrize("args", compressed_ops_and_params_lst) + def test_repr(self, args): + """Test that the name method behaves as expected.""" + _, op_type, parameters, name_param = args + + cr_op = CompressedResourceOp(op_type, parameters, name=name_param) + + op_name = op_type.__name__ + expected_params_str_parts = [f"{k!r}:{v!r}" for k, v in sorted(parameters.items())] + expected_params_str = ", ".join(expected_params_str_parts) + + expected_repr_string = f"CompressedResourceOp({op_name}, params={{{expected_params_str}}})" + assert str(cr_op) == expected_repr_string + + def test_type_error(self): + """Test that an error is raised if wrong type is provided for op_type.""" + with pytest.raises(TypeError, match="op_type must be a subclass of ResourceOperator."): + CompressedResourceOp(type(1)) + + @dataclass(frozen=True) class DummyCmprsRep: """A dummy class to populate the gate types dictionary for testing.""" @@ -287,6 +410,40 @@ def test_and_error(self): _ = op1 & True +@pytest.mark.parametrize( + "input_obj, expected_hashable", + [ + (123, 123), + ("hello", "hello"), + (3.14, 3.14), + (None, None), + ((1, 2, 3), (1, 2, 3)), + ([], ()), + ([1, 2, 3], (1, 2, 3)), + ([[1, 2], [3, 4]], ((1, 2), (3, 4))), + (set(), ()), + ({3, 1, 2}, (1, 2, 3)), + ({}, ()), + ({"b": 2, "a": 1}, (("a", 1), ("b", 2))), + ({"key": [1, 2]}, (("key", (1, 2)),)), + ({"nested": {"x": 1, "y": [2, 3]}}, (("nested", (("x", 1), ("y", (2, 3)))),)), + (np.array([1, 2, 3]), (1, 2, 3)), + (np.array([["a", "b"], ["c", "d"]]), (("a", "b"), ("c", "d"))), + ([{"a": 1}, {2, 3}], ((("a", 1),), (2, 3))), + ( + {"list_key": [1, {"nested_dict": "val"}]}, + (("list_key", (1, (("nested_dict", "val"),))),), + ), + ], +) +def test_make_hashable(input_obj, expected_hashable): + """Test that _make_hashable function works as expected""" + result = _make_hashable(input_obj) + assert result == expected_hashable + assert isinstance(result, Hashable) + assert hash(result) is not None + + def test_set_decomp(): """Test that the set_decomp function works as expected.""" op1 = DummyOp(x=5) From 6926173c7a44e93d869ae17a0b37c0f39e442b5f Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Mon, 9 Jun 2025 10:45:21 -0400 Subject: [PATCH 10/18] Add `map_to_resource_op` to integrate with PL Operators (#7434) **Context:** Adding mapping functionality for RE integration [sc-90643] --------- Co-authored-by: Diksha Dhawan Co-authored-by: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Co-authored-by: Utkarsh Co-authored-by: Soran Jahangiri <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 3 ++ .../labs/resource_estimation/__init__.py | 1 + .../resource_estimation/resource_mapping.py | 43 +++++++++++++++++++ .../test_resource_mapping.py | 40 +++++++++++++++++ 4 files changed, 87 insertions(+) create mode 100644 pennylane/labs/resource_estimation/resource_mapping.py create mode 100644 pennylane/labs/tests/resource_estimation/test_resource_mapping.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index b25290f34ab..c5dc27e20ff 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -507,6 +507,9 @@ * `pennylane.labs.QubitManager`, `pennylane.labs.AllocWires`, and `pennylane.labs.FreeWires` classes have been added to track and manage auxilliary qubits. [(#7404)](https://github.com/PennyLaneAI/pennylane/pull/7404) +* `pennylane.labs.map_to_resource_op` function has been added to map PennyLane Operations to their resource equivalents. + [(#7434)](https://github.com/PennyLaneAI/pennylane/pull/7434) + * Added a `pennylane.labs.Resources` class to store and track the quantum resources from a circuit. [(#7406)](https://github.com/PennyLaneAI/pennylane/pull/7406) diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 4b1802b308b..8118767161a 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -58,6 +58,7 @@ from .qubit_manager import AllocWires, FreeWires, QubitManager from .resources_base import Resources +from .resource_mapping import map_to_resource_op from .resource_operator import ( CompressedResourceOp, ResourceOperator, diff --git a/pennylane/labs/resource_estimation/resource_mapping.py b/pennylane/labs/resource_estimation/resource_mapping.py new file mode 100644 index 00000000000..cd30bd2bd91 --- /dev/null +++ b/pennylane/labs/resource_estimation/resource_mapping.py @@ -0,0 +1,43 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""Mapping PL operations to their ResourceOperator.""" +from __future__ import annotations + +from functools import singledispatch + +from pennylane.operation import Operation + + +@singledispatch +def map_to_resource_op(op: Operation): + r"""Maps an instance of :class:`~.Operation` to its associated :class:`~.pennylane.labs.resource_estimation.ResourceOperator`. + + Args: + op (~.Operation): base operation to be mapped + + Return: + (~.pennylane.labs.resource_estimation.ResourceOperator): the resource operator equal of the base operator + + Raises: + TypeError: The op is not a valid operation + NotImplementedError: Operation doesn't have a resource equivalent and doesn't define + a decomposition. + """ + + if not isinstance(op, Operation): + raise TypeError(f"The op {op} is not a valid operation.") + + raise NotImplementedError( + "Operation doesn't have a resource equivalent and doesn't define a decomposition." + ) diff --git a/pennylane/labs/tests/resource_estimation/test_resource_mapping.py b/pennylane/labs/tests/resource_estimation/test_resource_mapping.py new file mode 100644 index 00000000000..d5ac92ebd3b --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/test_resource_mapping.py @@ -0,0 +1,40 @@ +# Copyright 2018-2025 Xanadu Quantum Technologies Inc. + +# 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. +""" +This module contains tests for class needed to map PennyLane operations to their ResourceOperator. +""" +import pytest + +import pennylane as qml +from pennylane.labs.resource_estimation import map_to_resource_op +from pennylane.operation import Operation + +# pylint: disable= no-self-use + + +class Test_map_to_resource_op: + """Test the class for mapping PennyLane operations to their ResourceOperators.""" + + def test_map_to_resource_op_raises_type_error_if_not_operation(self): + """Test that a TypeError is raised if the input is not an Operation.""" + with pytest.raises(TypeError, match="The op oper is not a valid operation"): + map_to_resource_op("oper") + + def test_map_to_resource_op_raises_not_implemented_error(self): + """Test that a NotImplementedError is raised for a valid Operation.""" + operation = Operation(qml.wires.Wires([4])) + with pytest.raises( + NotImplementedError, match="Operation doesn't have a resource equivalent" + ): + map_to_resource_op(operation) From b97533d650f0a82850cd0bfcc1be7da00a23215a Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Thu, 12 Jun 2025 17:21:32 -0400 Subject: [PATCH 11/18] Add `estimate_resources()` (#7407) **Context:** Add the main `estimate_resources()` function to labs **Description of the Change:** --------- Co-authored-by: Diksha Dhawan Co-authored-by: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Co-authored-by: Utkarsh Co-authored-by: Soran Jahangiri <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 6 +- .../labs/resource_estimation/__init__.py | 13 +- .../resource_estimation/resource_mapping.py | 3 +- .../resource_estimation/resource_operator.py | 93 ++++- .../resource_estimation/resource_tracking.py | 387 ++++++++++++++++++ .../test_resource_operator.py | 132 +++++- .../test_resource_tracking.py | 356 ++++++++++++++++ 7 files changed, 981 insertions(+), 9 deletions(-) create mode 100644 pennylane/labs/resource_estimation/resource_tracking.py create mode 100644 pennylane/labs/tests/resource_estimation/test_resource_tracking.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index c5dc27e20ff..2d1da881bf4 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -520,9 +520,9 @@ operators for resource estimation. [(#7399)](https://github.com/PennyLaneAI/pennylane/pull/7399) -* Added the base `pennylane.labs.ResourceOperator` class which will be used to implement all quantum - operators for resource estimation. - [(#7399)](https://github.com/PennyLaneAI/pennylane/pull/7399) +* Added the `pennylane.labs.estimate_resources` function which will be used to perform resource + estimation on circuits, `pennylane.labs.ResourceOperator` and `pennylane.labs.Resources` objects. + [(#7407)](https://github.com/PennyLaneAI/pennylane/pull/7407)

Breaking changes 💔

diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 8118767161a..c788d9efa75 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -32,6 +32,7 @@ ~Resources ~ResourceOperator ~CompressedResourceOp + ~GateCount Resource Estimation Functions: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -39,6 +40,8 @@ .. autosummary:: :toctree: api + ~estimate_resources + ~resource_rep ~set_decomp ~set_adj_decomp ~set_ctrl_decomp @@ -58,13 +61,21 @@ from .qubit_manager import AllocWires, FreeWires, QubitManager from .resources_base import Resources -from .resource_mapping import map_to_resource_op from .resource_operator import ( CompressedResourceOp, ResourceOperator, ResourcesNotDefined, + resource_rep, set_adj_decomp, set_ctrl_decomp, set_decomp, set_pow_decomp, + GateCount, +) +from .resource_mapping import map_to_resource_op +from .resource_tracking import ( + StandardGateSet, + DefaultGateSet, + resource_config, + estimate_resources, ) diff --git a/pennylane/labs/resource_estimation/resource_mapping.py b/pennylane/labs/resource_estimation/resource_mapping.py index cd30bd2bd91..c3a26e58479 100644 --- a/pennylane/labs/resource_estimation/resource_mapping.py +++ b/pennylane/labs/resource_estimation/resource_mapping.py @@ -16,11 +16,12 @@ from functools import singledispatch +from pennylane.labs.resource_estimation import ResourceOperator from pennylane.operation import Operation @singledispatch -def map_to_resource_op(op: Operation): +def map_to_resource_op(op: Operation) -> ResourceOperator: r"""Maps an instance of :class:`~.Operation` to its associated :class:`~.pennylane.labs.resource_estimation.ResourceOperator`. Args: diff --git a/pennylane/labs/resource_estimation/resource_operator.py b/pennylane/labs/resource_estimation/resource_operator.py index b154032cb61..043b0466a49 100644 --- a/pennylane/labs/resource_estimation/resource_operator.py +++ b/pennylane/labs/resource_estimation/resource_operator.py @@ -17,7 +17,7 @@ from abc import ABC, abstractmethod from collections import defaultdict from inspect import signature -from typing import Callable, Hashable, List, Optional, Type +from typing import Callable, Dict, Hashable, List, Optional, Type, Union import numpy as np @@ -200,7 +200,7 @@ def default_resource_decomp(cls, num_wires, **kwargs): # `resource_keys` are in def __init__(self, *args, wires=None, **kwargs) -> None: self.wires = None - if wires: + if wires is not None: self.wires = Wires(wires) self.num_wires = len(self.wires) @@ -624,3 +624,92 @@ def custom_pow_decomp(pow_z, **kwargs): """ cls.set_resources(decomp_func, override_type="pow") + + +class GateCount: + r"""A class to represent a gate and its number of occurrences in a circuit or decomposition. + + Args: + gate (CompressedResourceOp): a compressed resource representation of the gate being counted + counts (int, optional): The number of occurances of the quantum gate in the circuit or + decomposition. Defaults to 1. + + Returns: + GateCount: the container object holding both pieces of information + + **Example** + + In this example we create an object to count 5 instances of :code:`plre.ResourceQFT` acting + on three wires: + + >>> qft = plre.resource_rep(plre.ResourceQFT, {"num_wires": 3}) + >>> counts = plre.GateCount(qft, 5) + >>> counts + (5 x QFT(3)) + + """ + + def __init__(self, gate: CompressedResourceOp, count: int = 1) -> None: + self.gate = gate + self.count = count + + def __mul__(self, other): + if isinstance(other, int): + return self.__class__(self.gate, self.count * other) + raise NotImplementedError + + def __add__(self, other): + if isinstance(other, self.__class__) and (self.gate == other.gate): + return self.__class__(self.gate, self.count + other.count) + raise NotImplementedError + + __rmul__ = __mul__ + + def __eq__(self, other) -> bool: + if not isinstance(other, GateCount): + return False + return self.gate == other.gate and self.count == other.count + + def __repr__(self) -> str: + return f"({self.count} x {self.gate._name})" + + +def resource_rep( + resource_op: Type[ResourceOperator], + resource_params: Union[Dict, None] = None, +) -> CompressedResourceOp: + r"""Produce a compressed representation of the resource operator to be used when + tracking resources. + + Note, the :code:`resource_params` dictionary should specify the required resource + parameters of the operator. The required resource parameters are listed in the + :code:`resource_keys` class property of every :class:`~.pennylane.labs.resource_estimation.ResourceOperator`. + + Args: + resource_op (Type[ResourceOperator]]): The type of operator we wish to compactify + resource_params (Dict): The required set of parameters to specify the operator + + Returns: + CompressedResourceOp: A compressed representation of a resource operator + + **Example** + + In this example we obtain the compressed resource representation for :code:`ResourceQFT`. + We begin by checking what parameters are required for resource estimation, and then providing + them accordingly: + + >>> plre.ResourceQFT.resource_keys + {'num_wires'} + >>> cmpr_qft = plre.resource_rep( + ... plre.ResourceQFT, + ... {"num_wires": 3}, + ... ) + >>> cmpr_qft + QFT(3) + + """ + + if resource_params: + return resource_op.resource_rep(**resource_params) + + return resource_op.resource_rep() diff --git a/pennylane/labs/resource_estimation/resource_tracking.py b/pennylane/labs/resource_estimation/resource_tracking.py new file mode 100644 index 00000000000..992f6a605db --- /dev/null +++ b/pennylane/labs/resource_estimation/resource_tracking.py @@ -0,0 +1,387 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""Core resource tracking logic.""" +import copy +from collections import defaultdict +from collections.abc import Callable +from functools import singledispatch, wraps +from typing import Dict, Iterable, List, Set, Union + +from pennylane.labs.resource_estimation.qubit_manager import AllocWires, FreeWires, QubitManager +from pennylane.labs.resource_estimation.resource_mapping import map_to_resource_op +from pennylane.labs.resource_estimation.resource_operator import ( + CompressedResourceOp, + GateCount, + ResourceOperator, +) +from pennylane.labs.resource_estimation.resources_base import Resources +from pennylane.operation import Operation +from pennylane.queuing import AnnotatedQueue, QueuingManager +from pennylane.wires import Wires + +# pylint: disable=dangerous-default-value,protected-access,too-many-arguments + +# user-friendly gateset for visual checks and initial compilation +StandardGateSet = { + "X", + "Y", + "Z", + "Hadamard", + "SWAP", + "CNOT", + "S", + "T", + "Adjoint(S)", + "Adjoint(T)", + "Toffoli", + "RX", + "RY", + "RZ", + "PhaseShift", +} + +# realistic gateset for useful compilation of circuits +DefaultGateSet = { + "X", + "Y", + "Z", + "Hadamard", + "CNOT", + "S", + "T", + "Toffoli", +} + +# parameters for further configuration of the decompositions +resource_config = { + "error_rx": 1e-5, + "error_ry": 1e-5, + "error_rz": 1e-5, + "precision_multiplexer": 1e-3, + "precision_qrom_state_prep": 1e-3, +} + + +def estimate_resources( + obj: Union[ResourceOperator, Callable, Resources, List], + gate_set: Set = DefaultGateSet, + config: Dict = resource_config, + work_wires: Union[int, Dict] = 0, + tight_budget: bool = False, + single_qubit_rotation_error: Union[float, None] = None, +) -> Union[Resources, Callable]: + r"""Estimate the quantum resources required from a circuit or operation in terms of the gates + provided in the gateset. + + Args: + obj (Union[ResourceOperator, Callable, Resources, List]): The quantum circuit or operation + to obtain resources from. + gate_set (Set, optional): A set of names (strings) of the fundamental operations to track + counts for throughout the quantum workflow. + config (Dict, optional): A dictionary of additional parameters which sets default values + when they are not specified on the operator. + single_qubit_rotation_error (Union[float, None]): The acceptable error when decomposing + single qubit rotations to `T`-gates using a Clifford + T approximation. This value takes + preference over the values set in the :code:`config`. + + Returns: + Resources: the quantum resources required to execute the circuit + + Raises: + TypeError: could not obtain resources for obj of type :code:`type(obj)` + + **Example** + + We can track the resources of a quantum workflow by passing the quantum function defining our + workflow directly into this function. + + .. code-block:: python + + import pennylane.labs.resource_estimation as plre + + def my_circuit(): + for w in range(2): + plre.ResourceHadamard(wires=w) + + plre.ResourceCNOT(wires=[0,1]) + plre.ResourceRX(wires=0) + plre.ResourceRY(wires=1) + + plre.ResourceQFT(num_wires=3, wires=[0, 1, 2]) + return + + Note that we are passing a python function NOT a :class:`~.QNode`. The resources for this + workflow are then obtained by: + + >>> res = plre.estimate_resources( + ... my_circuit, + ... gate_set = plre.DefaultGateSet, + ... single_qubit_rotation_error = 1e-4, + ... )() + ... + >>> print(res) + --- Resources: --- + Total qubits: 3 + Total gates : 279 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3 + Gate breakdown: + {'Hadamard': 5, 'CNOT': 10, 'T': 264} + + """ + + if single_qubit_rotation_error is not None: + config = _update_config_single_qubit_rot_error(config, single_qubit_rotation_error) + + return _estimate_resources(obj, gate_set, config, work_wires, tight_budget) + + +@singledispatch +def _estimate_resources( + obj: Union[ResourceOperator, Callable, Resources, List], + gate_set: Set = DefaultGateSet, + config: Dict = resource_config, + work_wires: Union[int, Dict] = 0, + tight_budget: bool = False, +) -> Union[Resources, Callable]: + r"""Raise error if there is no implementation registered for the object type.""" + + raise TypeError( + f"Could not obtain resources for obj of type {type(obj)}. obj must be one of Resources, Callable or ResourceOperator" + ) + + +@_estimate_resources.register +def resources_from_qfunc( + obj: Callable, + gate_set: Set = DefaultGateSet, + config: Dict = resource_config, + work_wires=0, + tight_budget=False, +) -> Callable: + """Get resources from a quantum function which queues operations""" + + @wraps(obj) + def wrapper(*args, **kwargs): + with AnnotatedQueue() as q: + obj(*args, **kwargs) + + qm = QubitManager(work_wires, tight_budget) + # Get algorithm wires: + num_algo_qubits = 0 + circuit_wires = [] + for op in q.queue: + if op._queue_category in ["_ops", "_resource_op"]: + if op.wires: + circuit_wires.append(op.wires) + else: + num_algo_qubits = max(num_algo_qubits, op.num_wires) + + num_algo_qubits += len(Wires.all_wires(circuit_wires)) + qm.algo_qubits = num_algo_qubits # set the algorithmic qubits in the qubit manager + + # Obtain resources in the gate_set + compressed_res_ops_lst = _ops_to_compressed_reps(q.queue) + + gate_counts = defaultdict(int) + for cmp_rep_op in compressed_res_ops_lst: + _counts_from_compressed_res_op( + cmp_rep_op, gate_counts, qbit_mngr=qm, gate_set=gate_set, config=config + ) + + return Resources(qubit_manager=qm, gate_types=gate_counts) + + return wrapper + + +@_estimate_resources.register +def resources_from_resource( + obj: Resources, + gate_set: Set = DefaultGateSet, + config: Dict = resource_config, + work_wires=None, + tight_budget=None, +) -> Resources: + """Further process resources from a resources object.""" + + existing_qm = obj.qubit_manager + if work_wires is not None: + if isinstance(work_wires, dict): + clean_wires = work_wires["clean"] + dirty_wires = work_wires["dirty"] + else: + clean_wires = work_wires + dirty_wires = 0 + + existing_qm._clean_qubit_counts = max(clean_wires, existing_qm._clean_qubit_counts) + existing_qm._dirty_qubit_counts = max(dirty_wires, existing_qm._dirty_qubit_counts) + + if tight_budget is not None: + existing_qm.tight_budget = tight_budget + + gate_counts = defaultdict(int) + for cmpr_rep_op, count in obj.gate_types.items(): + _counts_from_compressed_res_op( + cmpr_rep_op, + gate_counts, + qbit_mngr=existing_qm, + gate_set=gate_set, + scalar=count, + config=config, + ) + + # Update: + return Resources(qubit_manager=existing_qm, gate_types=gate_counts) + + +@_estimate_resources.register +def resources_from_resource_ops( + obj: ResourceOperator, + gate_set: Set = DefaultGateSet, + config: Dict = resource_config, + work_wires=None, + tight_budget=None, +) -> Resources: + """Extract resources from a resource operator.""" + if isinstance(obj, Operation): + obj = map_to_resource_op(obj) + + return resources_from_resource( + 1 * obj, + gate_set, + config, + work_wires, + tight_budget, + ) + + +@_estimate_resources.register +def resources_from_pl_ops( + obj: Operation, + gate_set: Set = DefaultGateSet, + config: Dict = resource_config, + work_wires=None, + tight_budget=None, +) -> Resources: + """Extract resources from a pl operator.""" + obj = map_to_resource_op(obj) + return resources_from_resource( + 1 * obj, + gate_set, + config, + work_wires, + tight_budget, + ) + + +def _counts_from_compressed_res_op( + cp_rep: CompressedResourceOp, + gate_counts_dict, + qbit_mngr, + gate_set: Set, + scalar: int = 1, + config: Dict = resource_config, +) -> None: + """Modifies the `gate_counts_dict` argument by adding the (scaled) resources of the operation provided. + + Args: + cp_rep (CompressedResourceOp): operation in compressed representation to extract resources from + gate_counts_dict (Dict): base dictionary to modify with the resource counts + gate_set (Set): the set of operations to track resources with respect to + scalar (int, optional): optional scalar to multiply the counts. Defaults to 1. + config (Dict, optional): additional parameters to specify the resources from an operator. Defaults to resource_config. + """ + ## If op in gate_set add to resources + if cp_rep.name in gate_set: + gate_counts_dict[cp_rep] += scalar + return + + ## Else decompose cp_rep using its resource decomp [cp_rep --> list[GateCounts]] and extract resources + resource_decomp = cp_rep.op_type.resource_decomp(config=config, **cp_rep.params) + qubit_alloc_sum = _sum_allocated_wires(resource_decomp) + + for action in resource_decomp: + if isinstance(action, GateCount): + _counts_from_compressed_res_op( + action.gate, + gate_counts_dict, + qbit_mngr=qbit_mngr, + scalar=scalar * action.count, + gate_set=gate_set, + config=config, + ) + continue + + if isinstance(action, AllocWires): + if qubit_alloc_sum != 0 and scalar > 1: + qbit_mngr.grab_clean_qubits(action.num_wires * scalar) + else: + qbit_mngr.grab_clean_qubits(action.num_wires) + if isinstance(action, FreeWires): + if qubit_alloc_sum != 0 and scalar > 1: + qbit_mngr.free_qubits(action.num_wires * scalar) + else: + qbit_mngr.free_qubits(action.num_wires) + + return + + +def _sum_allocated_wires(decomp): + """Sum together the allocated and released wires in a decomposition.""" + s = 0 + for action in decomp: + if isinstance(action, AllocWires): + s += action.num_wires + if isinstance(action, FreeWires): + s -= action.num_wires + return s + + +def _update_config_single_qubit_rot_error(config, error): + r"""Create a new config dictionary with the new single qubit + error threshold. + + Args: + config (Dict): the configuration dictionary to override + error (float): the new error threshold to be set + + """ + new_config = copy.copy(config) + new_config["error_rx"] = error + new_config["error_ry"] = error + new_config["error_rz"] = error + return new_config + + +@QueuingManager.stop_recording() +def _ops_to_compressed_reps( + ops: Iterable[Union[Operation, ResourceOperator]], +) -> List[CompressedResourceOp]: + """Convert the sequence of operations to a list of compressed resource ops. + + Args: + ops (Iterable[Union[Operation, ResourceOperator]]): set of operations to convert + + Returns: + List[CompressedResourceOp]: set of converted compressed resource ops + """ + cmp_rep_ops = [] + for op in ops: # We are skipping measurement processes here. + if op._queue_category == "_resource_op": + cmp_rep_ops.append(op.resource_rep_from_op()) + + elif op._queue_category == "_ops": # map: op --> res_op, then: res_op --> cmprsd_res_op + cmp_rep_ops.append(map_to_resource_op(op).resource_rep_from_op()) + + return cmp_rep_ops diff --git a/pennylane/labs/tests/resource_estimation/test_resource_operator.py b/pennylane/labs/tests/resource_estimation/test_resource_operator.py index 41581a617be..85b8c481035 100644 --- a/pennylane/labs/tests/resource_estimation/test_resource_operator.py +++ b/pennylane/labs/tests/resource_estimation/test_resource_operator.py @@ -32,11 +32,15 @@ set_decomp, set_pow_decomp, ) -from pennylane.labs.resource_estimation.resource_operator import _make_hashable +from pennylane.labs.resource_estimation.resource_operator import ( + GateCount, + _make_hashable, + resource_rep, +) from pennylane.queuing import AnnotatedQueue from pennylane.wires import Wires -# pylint: disable=protected-access, too-few-public-methods, no-self-use, unused-argument, arguments-differ +# pylint: disable=protected-access, too-few-public-methods, no-self-use, unused-argument, arguments-differ, no-member, comparison-with-itself class ResourceDummyX(ResourceOperator): @@ -542,3 +546,127 @@ def custom_res_decomp_error(x): # must match signature of default_pow_resource_ with pytest.raises(ValueError): set_pow_decomp(DummyPowOp, custom_res_decomp_error) + + +class TestGateCount: + """Tests for the GateCount class.""" + + @pytest.mark.parametrize("n", (1, 2, 3, 5)) + def test_init(self, n): + """Test that we can correctly instantiate a GateCount object""" + op = DummyOp(x=5) + gate_counts = GateCount(op) if n == 1 else GateCount(op, n) + + assert gate_counts.gate == op + assert gate_counts.count == n + + def test_equality(self): + """Test that the equality method works as expected""" + op = DummyOp(x=5) + op1 = DummyOp(x=6) + + gc1 = GateCount(op, count=5) + gc2 = GateCount(op, count=5) + gc3 = GateCount(op, count=3) + gc4 = GateCount(op1, count=5) + + assert gc1 == gc1 + assert gc1 == gc2 + assert gc1 != gc3 + assert gc1 != gc4 + + def test_add_method(self): + """Test that the arithmetic methods work as expected""" + op = DummyOp(x=5) + op1 = DummyOp(x=6) + + gc = GateCount(op, count=3) + gc1 = GateCount(op, count=2) + assert gc + gc1 == GateCount(op, count=5) + + with pytest.raises(NotImplementedError): + gc2 = GateCount(op1, count=2) + _ = gc + gc2 + + def test_mul_method(self): + """Test that the arithmetic methods work as expected""" + op = DummyOp(x=5) + gc = GateCount(op, count=3) + + assert gc * 5 == GateCount(op, count=15) + + with pytest.raises(NotImplementedError): + _ = gc * 0.5 + + +def test_resource_rep(): + """Test that the resource_rep method works as expected""" + + class ResourceOpA(ResourceOperator): + """Test resource op class""" + + resource_keys = {"num_wires", "continuous_param", "bool_param"} + + @property + def resource_params(self): + """resource params method""" + return { + "num_wires": self.num_wires, + "continuous_param": self.continuous_param, + "bool_param": self.bool_param, + } + + @classmethod + def resource_rep(cls, num_wires, continuous_param, bool_param): + """resource rep method""" + params = { + "num_wires": num_wires, + "continuous_param": continuous_param, + "bool_param": bool_param, + } + return CompressedResourceOp(cls, params) + + @classmethod + def default_resource_decomp(cls, num_wires, continuous_param, bool_param): + raise NotImplementedError + + class ResourceOpB(ResourceOperator): + """Test resource op class""" + + resource_keys = {} + + @property + def resource_params(self): + """resource params method""" + return {} + + @classmethod + def resource_rep(cls): + """resource rep method""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_resource_decomp(cls, **kwargs): + raise NotImplementedError + + expected_resource_rep_A = CompressedResourceOp( + ResourceOpA, + { + "num_wires": 1, + "continuous_param": 2.34, + "bool_param": False, + }, + ) + actual_resource_rep_A = resource_rep( + ResourceOpA, + { + "num_wires": 1, + "continuous_param": 2.34, + "bool_param": False, + }, + ) + assert expected_resource_rep_A == actual_resource_rep_A + + expected_resource_rep_B = CompressedResourceOp(ResourceOpB, {}) + actual_resource_rep_B = resource_rep(ResourceOpB) + assert expected_resource_rep_B == actual_resource_rep_B diff --git a/pennylane/labs/tests/resource_estimation/test_resource_tracking.py b/pennylane/labs/tests/resource_estimation/test_resource_tracking.py new file mode 100644 index 00000000000..89976999ba5 --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/test_resource_tracking.py @@ -0,0 +1,356 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +""" +Test the core resource tracking functionality. +""" +import copy +from collections import defaultdict + +import pytest + +from pennylane.labs.resource_estimation.qubit_manager import AllocWires, FreeWires, QubitManager +from pennylane.labs.resource_estimation.resource_operator import ( + CompressedResourceOp, + GateCount, + ResourceOperator, + ResourcesNotDefined, + resource_rep, +) +from pennylane.labs.resource_estimation.resource_tracking import estimate_resources, resource_config +from pennylane.labs.resource_estimation.resources_base import Resources + +# pylint: disable= no-self-use, arguments-differ + + +class ResourceTestCNOT(ResourceOperator): + """Dummy class for testing""" + + num_wires = 2 + resource_keys = {} + + @classmethod + def resource_rep(cls): + return CompressedResourceOp(cls, {}) + + @property + def resource_params(self): + return {} + + @classmethod + def default_resource_decomp(cls, **kwargs): + raise ResourcesNotDefined + + +class ResourceTestHadamard(ResourceOperator): + """Dummy class for testing""" + + num_wires = 1 + resource_keys = {} + + @classmethod + def resource_rep(cls): + return CompressedResourceOp(cls, {}) + + @property + def resource_params(self): + return {} + + @classmethod + def default_resource_decomp(cls, **kwargs): + raise ResourcesNotDefined + + +class ResourceTestT(ResourceOperator): + """Dummy class for testing""" + + num_wires = 1 + resource_keys = {} + + @classmethod + def resource_rep(cls): + return CompressedResourceOp(cls, {}) + + @property + def resource_params(self): + return {} + + @classmethod + def default_resource_decomp(cls, **kwargs): + raise ResourcesNotDefined + + +class ResourceTestZ(ResourceOperator): + """Dummy class for testing""" + + num_wires = 1 + resource_keys = {} + + @classmethod + def resource_rep(cls): + return CompressedResourceOp(cls, {}) + + @property + def resource_params(self): + return {} + + @classmethod + def default_resource_decomp(cls, **kwargs): + t = resource_rep(ResourceTestT) + return [GateCount(t, count=4)] + + +class ResourceTestRZ(ResourceOperator): + """Dummy class for testing""" + + num_wires = 1 + resource_keys = {"epsilon"} + + def __init__(self, epsilon=None, wires=None) -> None: + self.epsilon = epsilon + super().__init__(wires=wires) + + @classmethod + def resource_rep(cls, epsilon=None): + return CompressedResourceOp(cls, {"epsilon": epsilon}) + + @property + def resource_params(self): + return {"epsilon": self.epsilon} + + @classmethod + def default_resource_decomp(cls, epsilon, **kwargs): + if epsilon is None: + epsilon = kwargs["config"]["error_rz"] + + t = resource_rep(ResourceTestT) + t_counts = round(1 / epsilon) + return [GateCount(t, count=t_counts)] + + +class ResourceTestAlg1(ResourceOperator): + """Dummy class for testing""" + + num_wires = 2 + resource_keys = {"num_iter"} + + def __init__(self, num_iter, wires=None) -> None: + self.num_iter = num_iter + super().__init__(wires=wires) + + @classmethod + def resource_rep(cls, num_iter): + return CompressedResourceOp(cls, {"num_iter": num_iter}) + + @property + def resource_params(self): + return {"num_iter": self.num_iter} + + @classmethod + def default_resource_decomp(cls, num_iter, **kwargs): + cnot = resource_rep(ResourceTestCNOT) + h = resource_rep(ResourceTestHadamard) + + return [ + AllocWires(num_wires=num_iter), + GateCount(h, num_iter), + GateCount(cnot, num_iter), + FreeWires(num_wires=num_iter - 1), + ] + + +class ResourceTestAlg2(ResourceOperator): + """Dummy class for testing""" + + resource_keys = {"num_wires"} + + def __init__(self, num_wires, wires=None) -> None: + self.num_wires = num_wires + super().__init__(wires=wires) + + @classmethod + def resource_rep(cls, num_wires): + return CompressedResourceOp(cls, {"num_wires": num_wires}) + + @property + def resource_params(self): + return {"num_wires": self.num_wires} + + @classmethod + def default_resource_decomp(cls, num_wires, **kwargs): + rz = resource_rep(ResourceTestRZ, {"epsilon": 1e-2}) + alg1 = resource_rep(ResourceTestAlg1, {"num_iter": 3}) + + return [ + AllocWires(num_wires=num_wires), + GateCount(rz, num_wires), + GateCount(alg1, num_wires // 2), + FreeWires(num_wires=num_wires), + ] + + +class TestEstimateResources: + """Test that core resource estimation functionality""" + + def test_estimate_resources_from_qfunc(self): + """Test that we can accurately obtain resources from qfunc""" + + def my_circuit(): + for w in range(5): + ResourceTestHadamard(wires=[w]) + ResourceTestCNOT(wires=[0, 1]) + ResourceTestRZ(wires=[1]) + ResourceTestRZ(epsilon=1e-2, wires=[2]) + ResourceTestCNOT(wires=[3, 4]) + ResourceTestAlg1(num_iter=5, wires=[5, 6]) + + expected_gates = defaultdict( + int, + { + resource_rep(ResourceTestT): round(1 / 1e-2) + round(1 / 1e-5), + resource_rep(ResourceTestCNOT): 7, + resource_rep(ResourceTestHadamard): 10, + }, + ) + expected_qubits = QubitManager(work_wires={"clean": 4, "dirty": 1}, algo_wires=7) + expected_resources = Resources(qubit_manager=expected_qubits, gate_types=expected_gates) + + gate_set = {"TestCNOT", "TestT", "TestHadamard"} + computed_resources = estimate_resources(my_circuit, gate_set=gate_set)() + assert computed_resources == expected_resources + + def test_estimate_resources_from_resource_operator(self): + """Test that we can accurately obtain resources from qfunc""" + op = ResourceTestAlg2(num_wires=4) + actual_resources = estimate_resources(op, gate_set={"TestRZ", "TestAlg1"}) + + expected_gates = defaultdict( + int, + { + resource_rep(ResourceTestRZ, {"epsilon": 1e-2}): 4, + resource_rep(ResourceTestAlg1, {"num_iter": 3}): 2, + }, + ) + expected_qubits = QubitManager(work_wires=4, algo_wires=4) + expected_resources = Resources(qubit_manager=expected_qubits, gate_types=expected_gates) + + assert actual_resources == expected_resources + + def test_estimate_resources_from_resources_obj(self): + """Test that we can accurately obtain resources from qfunc""" + gates = defaultdict( + int, + { + resource_rep(ResourceTestRZ, {"epsilon": 1e-2}): 4, + resource_rep(ResourceTestAlg1, {"num_iter": 3}): 2, + }, + ) + qubits = QubitManager(work_wires=0, algo_wires=4) + resources = Resources(qubit_manager=qubits, gate_types=gates) + + gate_set = {"TestCNOT", "TestT", "TestHadamard"} + actual_resources = estimate_resources(resources, gate_set=gate_set) + + expected_gates = defaultdict( + int, + { + resource_rep(ResourceTestT): 4 * round(1 / 1e-2), + resource_rep(ResourceTestCNOT): 6, + resource_rep(ResourceTestHadamard): 6, + }, + ) + expected_qubits = QubitManager( + work_wires={"clean": 4, "dirty": 2}, algo_wires=4 + ) # TODO: optimize allocation + expected_resources = Resources(qubit_manager=expected_qubits, gate_types=expected_gates) + + assert actual_resources == expected_resources + + def test_estimate_resources_from_pl_operator(self): + """Test that we can accurately obtain resources from qfunc""" + assert True + + @pytest.mark.parametrize( + "gate_set, expected_resources", + ( + ( + {"TestRZ", "TestAlg1", "TestZ"}, + Resources( + qubit_manager=QubitManager(work_wires=4, algo_wires=4), + gate_types=defaultdict( + int, + { + resource_rep(ResourceTestRZ, {"epsilon": 1e-2}): 4, + resource_rep(ResourceTestAlg1, {"num_iter": 3}): 2, + resource_rep(ResourceTestZ): 4, + }, + ), + ), + ), + ( + {"TestCNOT", "TestT", "TestHadamard"}, + Resources( + qubit_manager=QubitManager(work_wires={"clean": 8, "dirty": 2}, algo_wires=4), + gate_types=defaultdict( + int, + { + resource_rep(ResourceTestT): 416, + resource_rep(ResourceTestCNOT): 6, + resource_rep(ResourceTestHadamard): 6, + }, + ), + ), + ), + ), + ) + def test_varying_gate_sets(self, gate_set, expected_resources): + """Test that changing the gate_set correctly updates the resources""" + + def my_circ(num_wires): + ResourceTestAlg2(num_wires, wires=range(num_wires)) + for w in range(num_wires): + ResourceTestZ(wires=w) + + actual_resources = estimate_resources(my_circ, gate_set=gate_set)(num_wires=4) + assert actual_resources == expected_resources + + @pytest.mark.parametrize("error_val", (0.1, 0.01, 0.001)) + def test_varying_config(self, error_val): + """Test that changing the resource_config correctly updates the resources""" + custom_config = copy.copy(resource_config) + custom_config["error_rz"] = error_val + + op = ResourceTestRZ() # don't specify epsilon + computed_resources = estimate_resources(op, gate_set={"TestT"}, config=custom_config) + + expected_resources = Resources( + qubit_manager=QubitManager(work_wires=0, algo_wires=1), + gate_types=defaultdict(int, {resource_rep(ResourceTestT): round(1 / error_val)}), + ) + + assert computed_resources == expected_resources + + @pytest.mark.parametrize("error_val", (0.1, 0.01, 0.001)) + def test_varying_single_qubit_rotation_error(self, error_val): + """Test that setting the single_qubit_rotation_error correctly updates the resources""" + op = ResourceTestRZ() # don't specify epsilon + computed_resources = estimate_resources( + op, gate_set={"TestT"}, single_qubit_rotation_error=error_val + ) + + expected_resources = Resources( + qubit_manager=QubitManager(work_wires=0, algo_wires=1), + gate_types=defaultdict(int, {resource_rep(ResourceTestT): round(1 / error_val)}), + ) + + assert computed_resources == expected_resources From 705fdc9918c9a456f0a992b12cf564e19ef16555 Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Mon, 16 Jun 2025 12:12:49 -0400 Subject: [PATCH 12/18] Add non-parametric single qubit ResourceOperator classes (#7540) **Context:** Add functionality for decomposition of non-parametric single qubit operators as ResourceOperator classes **Description of the Change:** **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** [sc-91013] --------- Co-authored-by: Jay Soni Co-authored-by: Utkarsh Co-authored-by: Soran Jahangiri <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 4 + .../labs/resource_estimation/__init__.py | 25 + .../labs/resource_estimation/ops/__init__.py | 17 + .../labs/resource_estimation/ops/identity.py | 213 ++++++ .../resource_estimation/ops/qubit/__init__.py | 23 + .../ops/qubit/non_parametric_ops.py | 614 ++++++++++++++++++ .../ops/qubit/test_non_parametric_ops.py | 325 +++++++++ .../resource_estimation/ops/test_identity.py | 132 ++++ 8 files changed, 1353 insertions(+) create mode 100644 pennylane/labs/resource_estimation/ops/__init__.py create mode 100644 pennylane/labs/resource_estimation/ops/identity.py create mode 100644 pennylane/labs/resource_estimation/ops/qubit/__init__.py create mode 100644 pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py create mode 100644 pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py create mode 100644 pennylane/labs/tests/resource_estimation/ops/test_identity.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 24490136d65..3dddbfb3ee6 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -571,6 +571,10 @@ estimation on circuits, `pennylane.labs.ResourceOperator` and `pennylane.labs.Resources` objects. [(#7407)](https://github.com/PennyLaneAI/pennylane/pull/7407) +* Added the `pennylane.labs.ResourceOperator` templates which will be used to perform resource + estimation for non-parametric single qubit gates. + [(#7540)](https://github.com/PennyLaneAI/pennylane/pull/7540) + * A new module :mod:`pennylane.labs.zxopt ` provides access to the basic optimization passes from [pyzx](https://pyzx.readthedocs.io/en/latest/) for PennyLane circuits. diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index c788d9efa75..57d9b03b84b 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -57,6 +57,21 @@ ~AllocWires ~FreeWires +Operators: +~~~~~~~~~~ + +.. autosummary:: + :toctree: api + + ~ResourceGlobalPhase + ~ResourceHadamard + ~ResourceIdentity + ~ResourceS + ~ResourceT + ~ResourceX + ~ResourceY + ~ResourceZ + """ from .qubit_manager import AllocWires, FreeWires, QubitManager @@ -79,3 +94,13 @@ resource_config, estimate_resources, ) +from .ops import ( + ResourceGlobalPhase, + ResourceHadamard, + ResourceIdentity, + ResourceS, + ResourceT, + ResourceX, + ResourceY, + ResourceZ, +) diff --git a/pennylane/labs/resource_estimation/ops/__init__.py b/pennylane/labs/resource_estimation/ops/__init__.py new file mode 100644 index 00000000000..fec35db4dc4 --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""This module contains resource operators for PennyLane Operators""" + +from .identity import ResourceGlobalPhase, ResourceIdentity +from .qubit import ResourceHadamard, ResourceS, ResourceT, ResourceX, ResourceY, ResourceZ diff --git a/pennylane/labs/resource_estimation/ops/identity.py b/pennylane/labs/resource_estimation/ops/identity.py new file mode 100644 index 00000000000..be159e9cc2d --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/identity.py @@ -0,0 +1,213 @@ +# Copyright 2024 Xanadu Quantum Technologies Inc. + +# 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. +r"""Resource operators for identity and global phase operations.""" + +from pennylane.labs.resource_estimation.resource_operator import ( + CompressedResourceOp, + GateCount, + ResourceOperator, +) + +# pylint: disable=arguments-differ,no-self-use,too-many-ancestors + + +class ResourceIdentity(ResourceOperator): + r"""Resource class for the Identity gate. + + Args: + wires (Iterable[Any], optional): wire label(s) that the identity acts on + + Resources: + The Identity gate is treated as a free gate and thus it cannot be decomposed + further. Requesting the resources of this gate returns an empty list. + + .. seealso:: :class:`~.Identity` + + **Example** + + The resources for this operation are computed using: + + >>> plre.ResourceIdentity.resource_decomp() + [] + """ + + num_wires = 1 + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls, **kwargs) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Resources: + The Identity gate is treated as a free gate and thus it cannot be decomposed + further. Requesting the resources of this gate returns an empty list. + + Returns: + list: empty list + """ + return [] + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation is the base operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires: int, + ctrl_num_ctrl_values: int, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + num_ctrl_wires (int): the number of qubits the operation is controlled on + num_ctrl_values (int): The number of control qubits, that are triggered when in the :math:`|0\rangle` state. + + Resources: + The Identity gate acts trivially when controlled. The resources of this operation are + the original (un-controlled) operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + The Identity gate acts trivially when raised to a power. The resources of this + operation are the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + +class ResourceGlobalPhase(ResourceOperator): + r"""Resource class for the GlobalPhase gate. + + Args: + wires (Iterable[Any], optional): the wires the operator acts on + + Resources: + The GlobalPhase gate is treated as a free gate and thus it cannot be decomposed + further. Requesting the resources of this gate returns an empty list. + + .. seealso:: :class:`~.GlobalPhase` + + **Example** + + The resources for this operation are computed using: + + >>> plre.ResourceGlobalPhase.resource_decomp() + [] + + """ + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls, **kwargs) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Resources: + The GlobalPhase gate is treated as a free gate and thus it cannot be decomposed + further. Requesting the resources of this gate returns an empty list. + + Returns: + list: empty list + """ + return [] + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of a global phase operator changes the sign of the phase, thus + the resources of the adjoint operation is the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + Taking arbitrary powers of a global phase produces a sum of global phases. + The resources simplify to just one total global phase operator. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] diff --git a/pennylane/labs/resource_estimation/ops/qubit/__init__.py b/pennylane/labs/resource_estimation/ops/qubit/__init__.py new file mode 100644 index 00000000000..f6a082e8412 --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/qubit/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""This module contains experimental resource estimation functionality.""" + +from .non_parametric_ops import ( + ResourceHadamard, + ResourceS, + ResourceT, + ResourceX, + ResourceY, + ResourceZ, +) diff --git a/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py b/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py new file mode 100644 index 00000000000..3a56f030aeb --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py @@ -0,0 +1,614 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""Resource operators for non parametric single qubit operations.""" +import pennylane.labs.resource_estimation as plre +from pennylane.labs.resource_estimation.resource_operator import ( + CompressedResourceOp, + GateCount, + ResourceOperator, + resource_rep, +) + +# pylint: disable=arguments-differ + + +class ResourceHadamard(ResourceOperator): + r"""Resource class for the Hadamard gate. + + Args: + wires (Sequence[int] or int, optional): the wire the operation acts on + + Resources: + The Hadamard gate is treated as a fundamental gate and thus it cannot be decomposed + further. Requesting the resources of this gate raises a :code:`ResourcesNotDefined` error. + + .. seealso:: :class:`~.Hadamard` + + """ + + num_wires = 1 + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Resources: + The Hadamard gate is treated as a fundamental gate and thus it cannot be decomposed + further. Requesting the resources of this gate raises a :code:`ResourcesNotDefined` error. + + Raises: + ResourcesNotDefined: This gate is fundamental, no further decomposition defined. + """ + raise plre.ResourcesNotDefined + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(), 1)] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + The Hadamard gate raised to even powers produces identity and raised + to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if pow_z % 2 == 0: + return [GateCount(plre.resource_rep(plre.ResourceIdentity))] + return [GateCount(cls.resource_rep())] + + +class ResourceS(ResourceOperator): + r"""Resource class for the S-gate. + + Args: + wires (Sequence[int] or int, optional): the wire the operation acts on + + Resources: + The S-gate decomposes into two T-gates. + + .. seealso:: :class:`~.S` + + **Example** + + The resources for this operation are computed using: + + >>> plre.ResourceS.resource_decomp() + [(2 x T)] + """ + + num_wires = 1 + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Resources: + The S-gate decomposes into two T-gates. + """ + t = resource_rep(ResourceT) + return [GateCount(t, 2)] + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of the S-gate is equivalent to :math:`\hat{Z} \cdot \hat{S}`. + The resources are defined as one instance of Z-gate, and one instance of S-gate. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + z = resource_rep(ResourceZ) + return [GateCount(z, 1), GateCount(cls.resource_rep(), 1)] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + - The S-gate, when raised to a power which is a multiple of four, produces identity. + - The cost of raising to an arbitrary integer power :math:`z`, when :math:`z \mod 4` + is equal to one, means one instance of the S-gate. + - The cost of raising to an arbitrary integer power :math:`z`, when :math:`z \mod 4` + is equal to two, means one instance of the Z-gate. + - The cost of raising to an arbitrary integer power :math:`z`, when :math:`z \mod 4` + is equal to three, means one instance of the Z-gate and one instance of S-gate. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + mod_4 = pow_z % 4 + if mod_4 == 0: + return [GateCount(resource_rep(plre.ResourceIdentity))] + if mod_4 == 1: + return [GateCount(cls.resource_rep())] + if mod_4 == 2: + return [GateCount(resource_rep(ResourceZ))] + + return [GateCount(resource_rep(ResourceZ)), GateCount(cls.resource_rep())] + + +class ResourceT(ResourceOperator): + r"""Resource class for the T-gate. + + Args: + wires (Sequence[int] or int, optional): the wire the operation acts on + + Resources: + The T-gate is treated as a fundamental gate and thus it cannot be decomposed + further. Requesting the resources of this gate raises a :code:`ResourcesNotDefined` error. + + .. seealso:: :class:`~.T` + + """ + + num_wires = 1 + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Resources: + The T-gate is treated as a fundamental gate and thus it cannot be decomposed + further. Requesting the resources of this gate raises a :code:`ResourcesNotDefined` error. + + Raises: + ResourcesNotDefined: This gate is fundamental, no further decomposition defined. + """ + raise plre.ResourcesNotDefined + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of the T-gate is equivalent to the T-gate raised to the 7th power. + The resources are defined as one Z-gate (:math:`Z = T^{4}`), one S-gate (:math:`S = T^{2}`) and one T-gate. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + z = resource_rep(ResourceZ) + s = resource_rep(ResourceS) + return [GateCount(cls.resource_rep()), GateCount(s), GateCount(z)] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + The T-gate, when raised to a power which is a multiple of eight, produces identity. + Consequently, for any integer power `z`, the effective quantum operation :math:`T^{z}` is equivalent + to :math:`T^{z \pmod 8}`. + + + The decomposition for :math:`T^{z}` (where :math:`z \pmod 8` is denoted as `z'`) is as follows: + + - If `z' = 0`: The operation is equivalent to the Identity gate (`I`). + - If `z' = 1`: The operation is equivalent to the T-gate (`T`). + - If `z' = 2`: The operation is equivalent to the S-gate (`S`). + - If `z' = 3`: The operation is equivalent to a composition of an S-gate and a T-gate (:math:`S \cdot T`). + - If `z' = 4` : The operation is equivalent to the Z-gate (`Z`). + - If `z' = 5`: The operation is equivalent to a composition of a Z-gate and a T-gate (:math:`Z \cdot T`). + - If `z' = 6`: The operation is equivalent to a composition of a Z-gate and an S-gate (:math:`Z \cdot S`). + - If `z' = 7`: The operation is equivalent to a composition of a Z-gate, an S-gate and a T-gate. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + + if (mod_8 := pow_z % 8) == 0: + return [GateCount(resource_rep(plre.ResourceIdentity))] + + gate_lst = [] + if mod_8 >= 4: + gate_lst.append(GateCount(resource_rep(ResourceZ))) + mod_8 -= 4 + + if mod_8 >= 2: + gate_lst.append(GateCount(resource_rep(ResourceS))) + mod_8 -= 2 + + if mod_8 >= 1: + gate_lst.append(GateCount(cls.resource_rep())) + + return gate_lst + + +class ResourceX(ResourceOperator): + r"""Resource class for the X-gate. + + Args: + wires (Sequence[int] or int, optional): the wire the operation acts on + + Resources: + The X-gate can be decomposed according to the following identities: + + .. math:: + + \begin{align} + \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ + \hat{Z} &= \hat{S}^{2}. + \end{align} + + Thus the resources for an X-gate are two :class:`~.ResourceS` gates and + two :class:`~.ResourceHadamard` gates. + + .. seealso:: :class:`~.X` + + **Example** + + The resources for this operation are computed using: + + >>> plre.ResourceX.resource_decomp() + [(2 x Hadamard), (2 x S)] + """ + + num_wires = 1 + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Resources: + The X-gate can be decomposed according to the following identities: + + .. math:: + + \begin{align} + \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ + \hat{Z} &= \hat{S}^{2}. + \end{align} + + Thus the resources for an X-gate are two :class:`~.ResourceS` gates and + two :class:`~.ResourceHadamard` gates. + """ + s = resource_rep(ResourceS) + h = resource_rep(ResourceHadamard) + + return [GateCount(h, 2), GateCount(s, 2)] + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + The X-gate raised to even powers produces identity and raised + to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if pow_z % 2 == 0: + return [GateCount(resource_rep(plre.ResourceIdentity))] + return [GateCount(cls.resource_rep())] + + +class ResourceY(ResourceOperator): + r"""Resource class for the Y-gate. + + Args: + wires (Sequence[int] or int, optional): the wire the operation acts on + + Resources: + The Y-gate can be decomposed according to the following identities: + + .. math:: + + \begin{align} + \hat{Y} &= \hat{S} \cdot \hat{X} \cdot \hat{S}^{\dagger}, \\ + \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ + \hat{Z} &= \hat{S}^{2}, \\ + \hat{S}^{\dagger} &= 3 \hat{S}. + \end{align} + + Thus the resources for a Y-gate are six :class:`~.ResourceS` gates and + two :class:`~.ResourceHadamard` gates. + + .. seealso:: :class:`~.Y` + + **Example** + + The resources for this operation are computed using: + + >>> plre.ResourceY.resource_decomp() + [(6 x S), (2 x Hadamard)] + """ + + num_wires = 1 + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Resources: + The Y-gate can be decomposed according to the following identities: + + .. math:: + + \begin{align} + \hat{Y} &= \hat{S} \cdot \hat{X} \cdot \hat{S}^{\dagger}, \\ + \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ + \hat{Z} &= \hat{S}^{2}, \\ + \hat{S}^{\dagger} &= 3 \hat{S}. + \end{align} + + Thus the resources for a Y-gate are six :class:`~.ResourceS` gates and + two :class:`~.ResourceHadamard` gates. + """ + s = resource_rep(ResourceS) + h = resource_rep(ResourceHadamard) + + return [GateCount(s, 6), GateCount(h, 2)] + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + The Y-gate raised to even powers produces identity and raised + to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if pow_z % 2 == 0: + return [GateCount(resource_rep(plre.ResourceIdentity))] + return [GateCount(cls.resource_rep())] + + +class ResourceZ(ResourceOperator): + r"""Resource class for the Z-gate. + + Args: + wires (Sequence[int] or int, optional): the wire the operation acts on + + Resources: + The Z-gate can be decomposed according to the following identities: + + .. math:: \hat{Z} = \hat{S}^{2}, + + thus the resources for a Z-gate are two :class:`~.ResourceS` gates. + + .. seealso:: :class:`~.Z` + + **Example** + + The resources for this operation are computed using: + + >>> plre.ResourceZ.resource_decomp() + [(2 x S)] + """ + + num_wires = 1 + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Resources: + The Z-gate can be decomposed according to the following identities: + + .. math:: \hat{Z} = \hat{S}^{2}, + + thus the resources for a Z-gate are two :class:`~.ResourceS` gates. + """ + s = resource_rep(ResourceS) + return [GateCount(s, 2)] + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + The Z-gate raised to even powers produces identity and raised + to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if pow_z % 2 == 0: + return [GateCount(resource_rep(plre.ResourceIdentity))] + return [GateCount(cls.resource_rep())] diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py new file mode 100644 index 00000000000..3c5d8f71c39 --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py @@ -0,0 +1,325 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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 non parametric resource operators. +""" +import pytest + +import pennylane.labs.resource_estimation as plre + +# pylint: disable=no-self-use,use-implicit-booleaness-not-comparison + + +class TestHadamard: + """Tests for ResourceHadamard""" + + def test_resources(self): + """Test that ResourceHadamard does not implement a decomposition""" + op = plre.ResourceHadamard() + with pytest.raises(plre.ResourcesNotDefined): + op.resource_decomp() + + def test_resource_params(self): + """Test that the resource params are correct""" + op = plre.ResourceHadamard() + assert op.resource_params == {} + + def test_resource_rep(self): + """Test that the compact representation is correct""" + expected = plre.CompressedResourceOp(plre.ResourceHadamard, {}) + assert plre.ResourceHadamard.resource_rep() == expected + + def test_adjoint_decomp(self): + """Test that the adjoint decomposition is correct.""" + h = plre.ResourceHadamard() + h_dag = h.adjoint_resource_decomp() + + expected = [plre.GateCount(plre.ResourceHadamard.resource_rep(), 1)] + assert h_dag == expected + + pow_data = ( + (1, [plre.GateCount(plre.ResourceHadamard.resource_rep(), 1)]), + (2, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + (3, [plre.GateCount(plre.ResourceHadamard.resource_rep(), 1)]), + (4, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_pow_decomp(self, z, expected_res): + """Test that the pow decomposition is correct.""" + op = plre.ResourceHadamard(0) + assert op.pow_resource_decomp(z) == expected_res + + +class TestS: + """Tests for ResourceS""" + + def test_resources(self): + """Test that S decomposes into two Ts""" + op = plre.ResourceS(0) + expected = [plre.GateCount(plre.ResourceT.resource_rep(), 2)] + assert op.resource_decomp() == expected + + def test_resource_params(self): + """Test that the resource params are correct""" + op = plre.ResourceS(0) + assert op.resource_params == {} + + def test_resource_rep(self): + """Test that the compressed representation is correct""" + expected = plre.CompressedResourceOp(plre.ResourceS, {}) + assert plre.ResourceS.resource_rep() == expected + + def test_resources_from_rep(self): + """Test that the resources can be computed from the compressed representation""" + + op = plre.ResourceS(0) + expected = [plre.GateCount(plre.ResourceT.resource_rep(), 2)] + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resource_decomp(**op_resource_params) == expected + + def test_adjoint_decomposition(self): + """Test that the adjoint resources are correct.""" + expected = [ + plre.GateCount(plre.ResourceZ.resource_rep(), 1), + plre.GateCount(plre.ResourceS.resource_rep(), 1), + ] + assert plre.ResourceS.adjoint_resource_decomp() == expected + + pow_data = ( + (1, [plre.GateCount(plre.ResourceS.resource_rep(), 1)]), + (2, [plre.GateCount(plre.ResourceZ.resource_rep(), 1)]), + ( + 3, + [ + plre.GateCount(plre.ResourceZ.resource_rep(), 1), + plre.GateCount(plre.ResourceS.resource_rep(), 1), + ], + ), + (4, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + ( + 7, + [ + plre.GateCount(plre.ResourceZ.resource_rep(), 1), + plre.GateCount(plre.ResourceS.resource_rep(), 1), + ], + ), + (8, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + (14, [plre.GateCount(plre.ResourceZ.resource_rep(), 1)]), + ( + 15, + [ + plre.GateCount(plre.ResourceZ.resource_rep(), 1), + plre.GateCount(plre.ResourceS.resource_rep(), 1), + ], + ), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_pow_decomp(self, z, expected_res): + """Test that the pow decomposition is correct.""" + op = plre.ResourceS(0) + assert op.pow_resource_decomp(z) == expected_res + + +class TestT: + """Tests for ResourceT""" + + def test_resources(self): + """Test that there is no further decomposition of the T gate.""" + op = plre.ResourceT(0) + with pytest.raises(plre.ResourcesNotDefined): + op.resource_decomp() + + def test_resource_params(self): + """Test that the resource params are correct""" + op = plre.ResourceT(0) + assert op.resource_params == {} + + def test_resource_rep(self): + """Test that the compact representation is correct""" + expected = plre.CompressedResourceOp(plre.ResourceT, {}) + assert plre.ResourceT.resource_rep() == expected + + def test_adjoint_decomposition(self): + """Test that the adjoint resources are correct.""" + expected = [ + plre.GateCount(plre.ResourceT.resource_rep(), 1), + plre.GateCount(plre.ResourceS.resource_rep(), 1), + plre.GateCount(plre.ResourceZ.resource_rep(), 1), + ] + assert plre.ResourceT.adjoint_resource_decomp() == expected + + pow_data = ( + (1, [plre.GateCount(plre.ResourceT.resource_rep(), 1)]), + (2, [plre.GateCount(plre.ResourceS.resource_rep(), 1)]), + ( + 3, + [ + plre.GateCount(plre.ResourceS.resource_rep(), 1), + plre.GateCount(plre.ResourceT.resource_rep(), 1), + ], + ), + ( + 7, + [ + plre.GateCount(plre.ResourceZ.resource_rep(), 1), + plre.GateCount(plre.ResourceS.resource_rep(), 1), + plre.GateCount(plre.ResourceT.resource_rep(), 1), + ], + ), + (8, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + ( + 14, + [ + plre.GateCount(plre.ResourceZ.resource_rep(), 1), + plre.GateCount(plre.ResourceS.resource_rep(), 1), + ], + ), + ( + 15, + [ + plre.GateCount(plre.ResourceZ.resource_rep(), 1), + plre.GateCount(plre.ResourceS.resource_rep(), 1), + plre.GateCount(plre.ResourceT.resource_rep(), 1), + ], + ), + (16, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_pow_decomp(self, z, expected_res): + """Test that the pow decomposition is correct.""" + op = plre.ResourceT + assert op.pow_resource_decomp(z) == expected_res + + +class TestX: + """Tests for the ResourceX gate""" + + def test_resources(self): + """Tests for the ResourceX gate""" + expected = [ + plre.GateCount(plre.ResourceHadamard.resource_rep(), 2), + plre.GateCount(plre.ResourceS.resource_rep(), 2), + ] + assert plre.ResourceX.resource_decomp() == expected + + def test_resource_params(self): + """Test that the resource params are correct""" + op = plre.ResourceX(0) + assert op.resource_params == {} + + def test_resource_rep(self): + """Test that the compact representation is correct""" + expected = plre.CompressedResourceOp(plre.ResourceX, {}) + assert plre.ResourceX.resource_rep() == expected + + def test_adjoint_decomposition(self): + """Test that the adjoint resources are correct.""" + expected = [plre.GateCount(plre.ResourceX.resource_rep(), 1)] + assert plre.ResourceX.adjoint_resource_decomp() == expected + + pow_data = ( + (1, [plre.GateCount(plre.ResourceX.resource_rep(), 1)]), + (2, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + (3, [plre.GateCount(plre.ResourceX.resource_rep(), 1)]), + (4, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_pow_decomp(self, z, expected_res): + """Test that the pow decomposition is correct.""" + op = plre.ResourceX(0) + assert op.pow_resource_decomp(z) == expected_res + + +class TestY: + """Tests for the ResourceY gate""" + + def test_resources(self): + """Test that ResourceT does not implement a decomposition""" + expected = [ + plre.GateCount(plre.ResourceS.resource_rep(), 6), + plre.GateCount(plre.ResourceHadamard.resource_rep(), 2), + ] + assert plre.ResourceY.resource_decomp() == expected + + def test_resource_params(self): + """Test that the resource params are correct""" + op = plre.ResourceY(0) + assert op.resource_params == {} + + def test_resource_rep(self): + """Test that the compact representation is correct""" + expected = plre.CompressedResourceOp(plre.ResourceY, {}) + assert plre.ResourceY.resource_rep() == expected + + def test_adjoint_decomposition(self): + """Test that the adjoint resources are correct.""" + expected = [plre.GateCount(plre.ResourceY.resource_rep(), 1)] + assert plre.ResourceY.adjoint_resource_decomp() == expected + + pow_data = ( + (1, [plre.GateCount(plre.ResourceY.resource_rep())]), + (2, [plre.GateCount(plre.ResourceIdentity.resource_rep())]), + (3, [plre.GateCount(plre.ResourceY.resource_rep())]), + (4, [plre.GateCount(plre.ResourceIdentity.resource_rep())]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_pow_decomp(self, z, expected_res): + """Test that the pow decomposition is correct.""" + op = plre.ResourceY(0) + assert op.pow_resource_decomp(z) == expected_res + + +class TestZ: + """Tests for the ResourceZ gate""" + + def test_resources(self): + """Test that ResourceZ implements the correct decomposition""" + expected = [plre.GateCount(plre.ResourceS.resource_rep(), 2)] + assert plre.ResourceZ.resource_decomp() == expected + + def test_resource_params(self): + """Test that the resource params are correct""" + op = plre.ResourceZ(0) + assert op.resource_params == {} + + def test_resource_rep(self): + """Test that the compact representation is correct""" + expected = plre.CompressedResourceOp(plre.ResourceZ, {}) + assert plre.ResourceZ.resource_rep() == expected + + def test_adjoint_decomposition(self): + """Test that the adjoint resources are correct.""" + expected = [plre.GateCount(plre.ResourceZ.resource_rep(), 1)] + assert plre.ResourceZ.adjoint_resource_decomp() == expected + + pow_data = ( + (1, [plre.GateCount(plre.ResourceZ.resource_rep())]), + (2, [plre.GateCount(plre.ResourceIdentity.resource_rep())]), + (3, [plre.GateCount(plre.ResourceZ.resource_rep())]), + (4, [plre.GateCount(plre.ResourceIdentity.resource_rep())]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_pow_decomp(self, z, expected_res): + """Test that the pow decomposition is correct.""" + op = plre.ResourceZ(0) + assert op.pow_resource_decomp(z) == expected_res diff --git a/pennylane/labs/tests/resource_estimation/ops/test_identity.py b/pennylane/labs/tests/resource_estimation/ops/test_identity.py new file mode 100644 index 00000000000..c9743fd4d67 --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/ops/test_identity.py @@ -0,0 +1,132 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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 identity resource operators +""" +import pytest + +import pennylane.labs.resource_estimation as plre + +# pylint: disable=no-self-use,use-implicit-booleaness-not-comparison + + +class TestIdentity: + """Test ResourceIdentity""" + + def test_resources(self): + """ResourceIdentity should have empty resources""" + op = plre.ResourceIdentity() + assert op.resource_decomp() == [] + + def test_resource_rep(self): + """Test the compressed representation""" + expected = plre.CompressedResourceOp(plre.ResourceIdentity, {}) + assert plre.ResourceIdentity.resource_rep() == expected + + def test_resource_params(self): + """Test the resource params are correct""" + op = plre.ResourceIdentity(0) + assert op.resource_params == {} + + def test_resources_from_rep(self): + """Test that the resources can be computed from the compressed representation""" + op = plre.ResourceIdentity() + expected = [] + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resource_decomp(**op_resource_params) == expected + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + op = plre.ResourceIdentity(0) + assert op.adjoint_resource_decomp() == [ + plre.GateCount(plre.ResourceIdentity.resource_rep(), 1) + ] + + identity_ctrl_data = ( + ([1], [1], [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + ([1, 2], [1, 1], [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + ([1, 2, 3], [1, 0, 0], [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("ctrl_wires, ctrl_values, expected_res", identity_ctrl_data) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + op = plre.ResourceIdentity(0) + assert op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + + identity_pow_data = ( + (1, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + (2, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + (5, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", identity_pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + op = plre.ResourceIdentity(0) + assert op.pow_resource_decomp(z) == expected_res + + +class TestGlobalPhase: + """Test ResourceGlobalPhase""" + + def test_resources(self): + """ResourceGlobalPhase should have empty resources""" + op = plre.ResourceGlobalPhase(wires=0) + assert op.resource_decomp() == [] + + def test_resource_rep(self): + """Test the compressed representation""" + expected = plre.CompressedResourceOp(plre.ResourceGlobalPhase, {}) + assert plre.ResourceGlobalPhase.resource_rep() == expected + + def test_resource_params(self): + """Test the resource params are correct""" + op = plre.ResourceGlobalPhase() + assert op.resource_params == {} + + def test_resources_from_rep(self): + """Test that the resources can be computed from the compressed representation""" + op = plre.ResourceGlobalPhase(wires=0) + expected = [] + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resource_decomp(**op_resource_params) == expected + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + op = plre.ResourceGlobalPhase(wires=0) + assert op.adjoint_resource_decomp() == [ + plre.GateCount(plre.ResourceGlobalPhase.resource_rep(), 1) + ] + + globalphase_pow_data = ( + (1, [plre.GateCount(plre.ResourceGlobalPhase.resource_rep(), 1)]), + (2, [plre.GateCount(plre.ResourceGlobalPhase.resource_rep(), 1)]), + (5, [plre.GateCount(plre.ResourceGlobalPhase.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", globalphase_pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + op = plre.ResourceGlobalPhase() + assert op.pow_resource_decomp(z) == expected_res From d75ff355fcad8ef1c218e440246734e0f2c5c9e2 Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Mon, 16 Jun 2025 12:50:58 -0400 Subject: [PATCH 13/18] Add parametric single qubit operators (#7541) **Context:** Add Parametric single qubit operator decompositions as ResourceOperator classes. **Description of the Change:** **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** --------- Co-authored-by: Jay Soni Co-authored-by: Utkarsh Co-authored-by: Soran Jahangiri <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 4 + .../labs/resource_estimation/__init__.py | 10 + .../labs/resource_estimation/ops/__init__.py | 14 +- .../resource_estimation/ops/qubit/__init__.py | 7 + .../ops/qubit/parametric_ops_single_qubit.py | 623 ++++++++++++++++++ .../qubit/test_parametric_ops_single_qubit.py | 213 ++++++ 6 files changed, 870 insertions(+), 1 deletion(-) create mode 100644 pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py create mode 100644 pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 3dddbfb3ee6..6ad864bd109 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -575,6 +575,10 @@ estimation for non-parametric single qubit gates. [(#7540)](https://github.com/PennyLaneAI/pennylane/pull/7540) +* Added the `pennylane.labs.ResourceOperator` templates which will be used to perform resource + estimation for parametric single qubit gates. + [(#7541)](https://github.com/PennyLaneAI/pennylane/pull/7541) + * A new module :mod:`pennylane.labs.zxopt ` provides access to the basic optimization passes from [pyzx](https://pyzx.readthedocs.io/en/latest/) for PennyLane circuits. diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 57d9b03b84b..10acbb67b8d 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -71,6 +71,11 @@ ~ResourceX ~ResourceY ~ResourceZ + ~ResourceRX + ~ResourceRY + ~ResourceRZ + ~ResourceRot + ~ResourcePhaseShift """ @@ -98,6 +103,11 @@ ResourceGlobalPhase, ResourceHadamard, ResourceIdentity, + ResourcePhaseShift, + ResourceRot, + ResourceRX, + ResourceRY, + ResourceRZ, ResourceS, ResourceT, ResourceX, diff --git a/pennylane/labs/resource_estimation/ops/__init__.py b/pennylane/labs/resource_estimation/ops/__init__.py index fec35db4dc4..869a5a97f08 100644 --- a/pennylane/labs/resource_estimation/ops/__init__.py +++ b/pennylane/labs/resource_estimation/ops/__init__.py @@ -14,4 +14,16 @@ r"""This module contains resource operators for PennyLane Operators""" from .identity import ResourceGlobalPhase, ResourceIdentity -from .qubit import ResourceHadamard, ResourceS, ResourceT, ResourceX, ResourceY, ResourceZ +from .qubit import ( + ResourceHadamard, + ResourcePhaseShift, + ResourceRot, + ResourceRX, + ResourceRY, + ResourceRZ, + ResourceS, + ResourceT, + ResourceX, + ResourceY, + ResourceZ, +) diff --git a/pennylane/labs/resource_estimation/ops/qubit/__init__.py b/pennylane/labs/resource_estimation/ops/qubit/__init__.py index f6a082e8412..4d7f72fb919 100644 --- a/pennylane/labs/resource_estimation/ops/qubit/__init__.py +++ b/pennylane/labs/resource_estimation/ops/qubit/__init__.py @@ -21,3 +21,10 @@ ResourceY, ResourceZ, ) +from .parametric_ops_single_qubit import ( + ResourcePhaseShift, + ResourceRot, + ResourceRX, + ResourceRY, + ResourceRZ, +) diff --git a/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py b/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py new file mode 100644 index 00000000000..af992a9a2a2 --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py @@ -0,0 +1,623 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""Resource operators for parametric single qubit operations.""" + +import numpy as np + +import pennylane.labs.resource_estimation as plre +from pennylane.labs.resource_estimation.resource_operator import ( + CompressedResourceOp, + GateCount, + ResourceOperator, + resource_rep, +) + +# pylint: disable=arguments-differ + + +def _rotation_resources(epsilon=10e-3): + r"""An estimate on the number of T gates needed to implement a Pauli rotation. + + The expected T-count is taken from (the 'Simulation Results' section) `Efficient + Synthesis of Universal Repeat-Until-Success Circuits `_. + The cost is given as: + + .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil + + Args: + epsilon (float): the acceptable error threshold for the approximation + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + num_gates = round(1.149 * np.log2(1 / epsilon) + 9.2) + t = resource_rep(plre.ResourceT) + return [GateCount(t, num_gates)] + + +class ResourcePhaseShift(ResourceOperator): + r"""Resource class for the PhaseShift gate. + + Keyword Args: + eps (float, optional): The error threshold for clifford plus T decomposition of this operation. + The default value is `None` which corresponds to using the epsilon stated in the config. + wires (Any or Wires, optional): the wire the operation acts on + + Resources: + The phase shift gate is equivalent to a Z-rotation upto some global phase, + as defined from the following identity: + + .. math:: R_\phi(\phi) = e^{i\phi/2}R_z(\phi) = \begin{bmatrix} + 1 & 0 \\ + 0 & e^{i\phi} + \end{bmatrix}. + + .. seealso:: :class:`~.PhaseShift` + + **Example** + + The resources for this operation are computed as: + + >>> plre.ResourcePhaseShift.resource_decomp() + [(1 x RZ), (1 x GlobalPhase)] + """ + + num_wires = 1 + resource_keys = {"eps"} + + def __init__(self, epsilon=None, wires=None) -> None: + self.eps = epsilon + super().__init__(wires=wires) + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + A dictionary containing the resource parameters: + * eps (Union[float, None]): error threshold for the approximation + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Resources: + The phase shift gate is equivalent to a Z-rotation upto some global phase, + as defined from the following identity: + + .. math:: R_\phi(\phi) = e^{i\phi/2}R_z(\phi) = \begin{bmatrix} + 1 & 0 \\ + 0 & e^{i\phi} + \end{bmatrix}. + """ + rz = resource_rep(ResourceRZ, {"eps": eps}) + global_phase = resource_rep(plre.ResourceGlobalPhase) + return [GateCount(rz), GateCount(global_phase)] + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of a phase shift operator just changes the sign of the phase, thus + the resources of the adjoint operation results in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps=eps))] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + Taking arbitrary powers of a phase shift produces a sum of shifts. + The resources simplify to just one total phase shift operator. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + + return [GateCount(cls.resource_rep(eps=eps))] + + +class ResourceRX(ResourceOperator): + r"""Resource class for the RX gate. + + Keyword Args: + eps (float): error threshold for clifford plus T decomposition of this operation + wires (Any, Wires, optional): the wire the operation acts on + + Resources: + A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The + resources are approximating the gate with a series of T gates. The expected T-count is taken + from (the 'Simulation Results' section) `Efficient Synthesis of Universal Repeat-Until-Success + Circuits `_. The cost is given as: + + .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil + + .. seealso:: :class:`~.RX` + + **Example** + + The resources for this operation are computed as: + + >>> op = plre.estimate_resources(plre.ResourceRX)() + >>> print(op) + --- Resources: --- + Total qubits: 1 + Total gates : 21 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'T': 21} + + The operation does not require any parameters directly, however, it will depend on the single + qubit error threshold, which can be set using a config dictionary. + + >>> config = {"error_rx": 1e-4} + >>> op = plre.estimate_resources(plre.ResourceRX, config=config)() + >>> print(op) + --- Resources: --- + Total qubits: 1 + Total gates : 24 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'T': 24} + """ + + num_wires = 1 + resource_keys = {"eps"} + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * eps (Union[float, None]): the number of qubits the operation is controlled on + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Keyword Args: + eps (float): the error threshold + + Resources: + A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The + resources are approximating the gate with a series of T gates. The expected T-count is taken + from (the 'Simulation Results' section) `Efficient Synthesis of Universal Repeat-Until-Success + Circuits `_. The cost is given as: + + .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil + + """ + eps = eps or kwargs["config"]["error_rx"] + return _rotation_resources(epsilon=eps) + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of a single qubit rotation changes the sign of the rotation angle, + thus the resources of the adjoint operation result in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps))] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + Taking arbitrary powers of a single qubit rotation produces a sum of rotations. + The resources simplify to just one total single qubit rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + + return [GateCount(cls.resource_rep(eps))] + + +class ResourceRY(ResourceOperator): + r"""Resource class for the RY gate. + + Keyword Args: + eps (float): error threshold for clifford plus T decomposition of this operation + wires (Any, Wires, optional): the wire the operation acts on + + Resources: + A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The + resources are approximating the gate with a series of T gates. The expected T-count is taken + from (the 'Simulation Results' section) `Efficient Synthesis of Universal Repeat-Until-Success + Circuits `_. The cost is given as: + + .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil + + .. seealso:: :class:`~.RY` + + **Example** + + The resources for this operation are computed using: + + >>> op = plre.estimate_resources(plre.ResourceRY)() + >>> print(op) + --- Resources: --- + Total qubits: 1 + Total gates : 1 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'T': 21} + + The operation does not require any parameters directly, however, it will depend on the single + qubit error threshold, which can be set using a config dictionary. + + >>> config = {"error_ry": 1e-4} + >>> op = plre.estimate_resources(plre.ResourceRY, config=config)() + --- Resources: --- + Total qubits: 1 + Total gates : 24 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'T': 24} + """ + + num_wires = 1 + resource_keys = {"eps"} + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + A dictionary containing the resource parameters: + * eps (Union[float, None]): error threshold for the approximation + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Keyword Args: + eps (float): the error threshold + + Resources: + A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The + resources are approximating the gate with a series of T gates. The expected T-count is taken + from (the 'Simulation Results' section) `Efficient Synthesis of Universal Repeat-Until-Success + Circuits `_. The cost is given as: + + .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil + + Args: + config (dict): a dictionary containing the error threshold + """ + eps = eps or kwargs["config"]["error_ry"] + return _rotation_resources(epsilon=eps) + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of a single qubit rotation changes the sign of the rotation angle, + thus the resources of the adjoint operation result in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps))] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + Taking arbitrary powers of a single qubit rotation produces a sum of rotations. + The resources simplify to just one total single qubit rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + + return [GateCount(cls.resource_rep(eps))] + + +class ResourceRZ(ResourceOperator): + r"""Resource class for the RZ gate. + + Keyword Args: + eps (float): error threshold for clifford plus T decomposition of this operation + wires (Any, Wires, optional): the wire the operation acts on + + Resources: + A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The + resources are approximating the gate with a series of T gates. The expected T-count is taken + from (the 'Simulation Results' section) `Efficient Synthesis of Universal Repeat-Until-Success + Circuits `_. The cost is given as: + + .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil + + .. seealso:: :class:`~.RZ` + + **Example** + + The resources for this operation are computed using: + + >>> op = plre.estimate_resources(plre.ResourceRZ)() + >>> op + --- Resources: --- + Total qubits: 1 + Total gates : 21 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'T': 21} + + The operation does not require any parameters directly, however, it will depend on the single + qubit error threshold, which can be set using a config dictionary. + + >>> config = {"error_rz": 1e-4} + >>> op = plre.estimate_resources(plre.ResourceRZ, config=config)() + >>> print(op) + --- Resources: --- + Total qubits: 1 + Total gates : 24 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'T': 24} + """ + + num_wires = 1 + resource_keys = {"eps"} + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + A dictionary containing the resource parameters: + * eps (Union[float, None]): error threshold for the approximation + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Resources: + A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The + resources are approximating the gate with a series of T gates. The expected T-count is taken + from (the 'Simulation Results' section) `Efficient Synthesis of Universal Repeat-Until-Success + Circuits `_. The cost is given as: + + .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil + + Args: + config (dict): a dictionary containing the error threshold + """ + eps = eps or kwargs["config"]["error_rz"] + return _rotation_resources(epsilon=eps) + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of a single qubit rotation changes the sign of the rotation angle, + thus the resources of the adjoint operation result in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps))] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + Taking arbitrary powers of a single qubit rotation produces a sum of rotations. + The resources simplify to just one total single qubit rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps))] + + +class ResourceRot(ResourceOperator): + r"""Resource class for the Rot-gate. + + Args: + eps (float): error threshold for clifford plus T decomposition of this operation + wires (Any, Wires, optional): the wire the operation acts on + + Resources: + The resources are obtained according to the definition of the :class:`Rot` gate: + + .. math:: \hat{R}(\omega, \theta, \phi) = \hat{RZ}(\omega) \cdot \hat{RY}(\theta) \cdot \hat{RZ}(\phi). + + .. seealso:: :class:`~.Rot` + + **Example** + + The resources for this operation are computed using: + + >>> plre.ResourceRot.resource_decomp() + [(1 x RY), (2 x RZ)] + """ + + num_wires = 1 + resource_keys = {"eps"} + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + A dictionary containing the resource parameters: + * eps (Union[float, None]): error threshold for the approximation + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Resources: + The resources are obtained according to the definition of the :class:`Rot` gate: + + .. math:: \hat{R}(\omega, \theta, \phi) = \hat{RZ}(\omega) \cdot \hat{RY}(\theta) \cdot \hat{RZ}(\phi). + + """ + ry = resource_rep(ResourceRY, {"eps": eps}) + rz = resource_rep(ResourceRZ, {"eps": eps}) + + return [GateCount(ry), GateCount(rz, 2)] + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of a general single qubit rotation changes the sign of the rotation angles, + thus the resources of the adjoint operation results in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps))] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + Taking arbitrary powers of a general single qubit rotation produces a sum of rotations. + The resources simplify to just one total single qubit rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + + return [GateCount(cls.resource_rep(eps))] diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py new file mode 100644 index 00000000000..4dd37df9f40 --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py @@ -0,0 +1,213 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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 parametric single qubit resource operators. +""" +import pytest + +import pennylane.labs.resource_estimation as plre +from pennylane.labs.resource_estimation.ops.qubit.parametric_ops_single_qubit import ( + _rotation_resources, +) + +# pylint: disable=no-self-use, use-implicit-booleaness-not-comparison,too-many-arguments + +params = list(zip([10e-3, 10e-4, 10e-5], [17, 21, 24])) + + +@pytest.mark.parametrize("epsilon, expected", params) +def test_rotation_resources(epsilon, expected): + """Test the hardcoded resources used for RX, RY, RZ""" + gate_types = [plre.GateCount(plre.CompressedResourceOp(plre.ResourceT, {}), expected)] + + assert gate_types == _rotation_resources(epsilon=epsilon) + + +class TestPauliRotation: + """Test ResourceRX, ResourceRY, and ResourceRZ""" + + params_classes = [plre.ResourceRX, plre.ResourceRY, plre.ResourceRZ] + params_errors = [10e-3, 10e-4, 10e-5] + + @pytest.mark.parametrize("resource_class", params_classes) + @pytest.mark.parametrize("epsilon", params_errors) + def test_resources(self, resource_class, epsilon): + """Test the resources method""" + + label = "error_" + resource_class.__name__.replace("Resource", "").lower() + config = {label: epsilon} + op = resource_class(wires=0) + assert op.resource_decomp(config=config) == _rotation_resources(epsilon=epsilon) + + @pytest.mark.parametrize("resource_class", params_classes) + @pytest.mark.parametrize("epsilon", params_errors) + def test_resource_rep(self, resource_class, epsilon): # pylint: disable=unused-argument + """Test the compact representation""" + op = resource_class(wires=0) + expected = plre.CompressedResourceOp(resource_class, {"eps": None}) + assert op.resource_rep() == expected + + @pytest.mark.parametrize("resource_class", params_classes) + @pytest.mark.parametrize("epsilon", params_errors) + def test_resources_from_rep(self, resource_class, epsilon): + """Test the resources can be obtained from the compact representation""" + + label = "error_" + resource_class.__name__.replace("Resource", "").lower() + config = {label: epsilon} + op = resource_class(wires=0) + expected = _rotation_resources(epsilon=epsilon) + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resource_decomp(**op_resource_params, config=config) == expected + + @pytest.mark.parametrize("resource_class", params_classes) + @pytest.mark.parametrize("epsilon", params_errors) + def test_resource_params(self, resource_class, epsilon): # pylint: disable=unused-argument + """Test that the resource params are correct""" + op = resource_class(epsilon, wires=0) + assert op.resource_params == {"eps": epsilon} + + @pytest.mark.parametrize("resource_class", params_classes) + @pytest.mark.parametrize("epsilon", params_errors) + def test_adjoint_decomposition(self, resource_class, epsilon): + """Test that the adjoint decompositions are correct.""" + + expected = [plre.GateCount(resource_class(epsilon).resource_rep(), 1)] + assert resource_class(epsilon).adjoint_resource_decomp() == expected + + @pytest.mark.parametrize("resource_class", params_classes) + @pytest.mark.parametrize("epsilon", params_errors) + @pytest.mark.parametrize("z", list(range(1, 10))) + def test_pow_decomposition(self, resource_class, epsilon, z): + """Test that the pow decompositions are correct.""" + + expected = [ + ( + plre.GateCount(resource_class(epsilon).resource_rep(), 1) + if z + else plre.GateCount(plre.ResourceIdentity.resource_rep(), 1) + ) + ] + assert resource_class(epsilon).pow_resource_decomp(z) == expected + + +class TestRot: + """Test ResourceRot""" + + def test_resources(self): + """Test the resources method""" + op = plre.ResourceRot(wires=0) + ry = plre.ResourceRY.resource_rep() + rz = plre.ResourceRZ.resource_rep() + expected = [plre.GateCount(ry, 1), plre.GateCount(rz, 2)] + + assert op.resource_decomp() == expected + + def test_resource_rep(self): + """Test the compressed representation""" + op = plre.ResourceRot(wires=0) + expected = plre.CompressedResourceOp(plre.ResourceRot, {"eps": None}) + assert op.resource_rep() == expected + + def test_resources_from_rep(self): + """Test that the resources can be obtained from the compact representation""" + op = plre.ResourceRot(wires=0) + ry = plre.ResourceRY.resource_rep() + rz = plre.ResourceRZ.resource_rep() + expected = [plre.GateCount(ry, 1), plre.GateCount(rz, 2)] + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resource_decomp(**op_resource_params) == expected + + def test_resource_params(self): + """Test that the resource params are correct""" + op = plre.ResourceRot(wires=0) + assert op.resource_params == {"eps": None} + + def test_adjoint_decomp(self): + """Test that the adjoint decomposition is correct""" + + expected = [plre.GateCount(plre.ResourceRot.resource_rep(), 1)] + assert plre.ResourceRot.adjoint_resource_decomp() == expected + + pow_data = ( + (1, [plre.GateCount(plre.ResourceRot.resource_rep(), 1)]), + (2, [plre.GateCount(plre.ResourceRot.resource_rep(), 1)]), + (5, [plre.GateCount(plre.ResourceRot.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + op = plre.ResourceRot() + assert op.pow_resource_decomp(z) == expected_res + + +class TestPhaseShift: + """Test ResourcePhaseShift""" + + def test_resources(self): + """Test the resources method""" + op = plre.ResourcePhaseShift(0.1, wires=0) + rz = plre.ResourceRZ.resource_rep() + global_phase = plre.ResourceGlobalPhase.resource_rep() + + expected = [plre.GateCount(rz, 1), plre.GateCount(global_phase, 1)] + + assert op.resource_decomp() == expected + + def test_resource_rep(self): + """Test the compressed representation""" + op = plre.ResourcePhaseShift(wires=0) + expected = plre.CompressedResourceOp(plre.ResourcePhaseShift, {"eps": None}) + assert op.resource_rep() == expected + + def test_resources_from_rep(self): + """Test that the resources can be obtained from the compact representation""" + op = plre.ResourcePhaseShift(0.1) + global_phase = plre.ResourceGlobalPhase.resource_rep() + rz = plre.ResourceRZ.resource_rep(0.1) + expected = [plre.GateCount(rz, 1), plre.GateCount(global_phase, 1)] + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resource_decomp(**op_resource_params) == expected + + def test_resource_params(self): + """Test that the resource params are correct""" + op = plre.ResourcePhaseShift() + assert op.resource_params == {"eps": None} + + def test_adjoint_decomp(self): + """Test that the adjoint decomposition is correct""" + + expected = [plre.GateCount(plre.ResourcePhaseShift.resource_rep(), 1)] + assert plre.ResourcePhaseShift.adjoint_resource_decomp() == expected + + pow_data = ( + (1, [plre.GateCount(plre.ResourcePhaseShift.resource_rep(), 1)]), + (2, [plre.GateCount(plre.ResourcePhaseShift.resource_rep(), 1)]), + (5, [plre.GateCount(plre.ResourcePhaseShift.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + op = plre.ResourcePhaseShift() + assert op.pow_resource_decomp(z) == expected_res From 1370ed73a0cd65984a857aba30e6c60b06dcb252 Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Tue, 17 Jun 2025 10:05:50 -0400 Subject: [PATCH 14/18] Added controlled resource operators (#7526) **Context:** Added controlled ResourceOperator classes **Description of the Change:** **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** [sc-91020] --------- Co-authored-by: Jay Soni Co-authored-by: Utkarsh Co-authored-by: Soran Jahangiri <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 4 + .../labs/resource_estimation/__init__.py | 46 +- .../labs/resource_estimation/ops/__init__.py | 19 + .../ops/op_math/__init__.py | 31 + .../ops/op_math/controlled_ops.py | 1705 +++++++++++++++++ .../resource_estimation/ops/qubit/__init__.py | 1 + .../ops/qubit/non_parametric_ops.py | 183 ++ .../ops/op_math/test_controlled_ops.py | 725 +++++++ .../ops/qubit/test_non_parametric_ops.py | 100 + 9 files changed, 2806 insertions(+), 8 deletions(-) create mode 100644 pennylane/labs/resource_estimation/ops/op_math/__init__.py create mode 100644 pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py create mode 100644 pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 6ad864bd109..42733d71111 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -579,6 +579,10 @@ estimation for parametric single qubit gates. [(#7541)](https://github.com/PennyLaneAI/pennylane/pull/7541) +* Added the `pennylane.labs.ResourceOperator` templates which will be used to perform resource + estimation for controlled gates. + [(#7526)](https://github.com/PennyLaneAI/pennylane/pull/7526) + * A new module :mod:`pennylane.labs.zxopt ` provides access to the basic optimization passes from [pyzx](https://pyzx.readthedocs.io/en/latest/) for PennyLane circuits. diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 10acbb67b8d..d2a2df917f3 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -76,6 +76,21 @@ ~ResourceRZ ~ResourceRot ~ResourcePhaseShift + ~ResourceSWAP + ~ResourceCH + ~ResourceCY + ~ResourceCZ + ~ResourceCSWAP + ~ResourceCCZ + ~ResourceCNOT + ~ResourceToffoli + ~ResourceMultiControlledX + ~ResourceCRX + ~ResourceCRY + ~ResourceCRZ + ~ResourceCRot + ~ResourceControlledPhaseShift + ~ResourceTempAND """ @@ -100,17 +115,32 @@ estimate_resources, ) from .ops import ( - ResourceGlobalPhase, ResourceHadamard, - ResourceIdentity, - ResourcePhaseShift, - ResourceRot, - ResourceRX, - ResourceRY, - ResourceRZ, ResourceS, - ResourceT, ResourceX, ResourceY, ResourceZ, + ResourceRX, + ResourceRY, + ResourceRZ, + ResourceT, + ResourcePhaseShift, + ResourceGlobalPhase, + ResourceRot, + ResourceIdentity, + ResourceSWAP, + ResourceCH, + ResourceCY, + ResourceCZ, + ResourceCSWAP, + ResourceCCZ, + ResourceCNOT, + ResourceToffoli, + ResourceMultiControlledX, + ResourceCRX, + ResourceCRY, + ResourceCRZ, + ResourceCRot, + ResourceControlledPhaseShift, + ResourceTempAND, ) diff --git a/pennylane/labs/resource_estimation/ops/__init__.py b/pennylane/labs/resource_estimation/ops/__init__.py index 869a5a97f08..3b1395ced7e 100644 --- a/pennylane/labs/resource_estimation/ops/__init__.py +++ b/pennylane/labs/resource_estimation/ops/__init__.py @@ -14,6 +14,24 @@ r"""This module contains resource operators for PennyLane Operators""" from .identity import ResourceGlobalPhase, ResourceIdentity + +from .op_math import ( + ResourceCCZ, + ResourceCH, + ResourceCNOT, + ResourceControlledPhaseShift, + ResourceCRot, + ResourceCRX, + ResourceCRY, + ResourceCRZ, + ResourceCSWAP, + ResourceCY, + ResourceCZ, + ResourceMultiControlledX, + ResourceTempAND, + ResourceToffoli, +) + from .qubit import ( ResourceHadamard, ResourcePhaseShift, @@ -22,6 +40,7 @@ ResourceRY, ResourceRZ, ResourceS, + ResourceSWAP, ResourceT, ResourceX, ResourceY, diff --git a/pennylane/labs/resource_estimation/ops/op_math/__init__.py b/pennylane/labs/resource_estimation/ops/op_math/__init__.py new file mode 100644 index 00000000000..92bace670d7 --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/op_math/__init__.py @@ -0,0 +1,31 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""This module contains experimental resource estimation functionality.""" + +from .controlled_ops import ( + ResourceCCZ, + ResourceCH, + ResourceCNOT, + ResourceControlledPhaseShift, + ResourceCRot, + ResourceCRX, + ResourceCRY, + ResourceCRZ, + ResourceCSWAP, + ResourceCY, + ResourceCZ, + ResourceMultiControlledX, + ResourceTempAND, + ResourceToffoli, +) diff --git a/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py b/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py new file mode 100644 index 00000000000..24b7d1e8cd1 --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py @@ -0,0 +1,1705 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""Resource operators for controlled operations.""" + +import pennylane.labs.resource_estimation as re +from pennylane.labs.resource_estimation.qubit_manager import AllocWires, FreeWires +from pennylane.labs.resource_estimation.resource_operator import ( + CompressedResourceOp, + GateCount, + ResourceOperator, + resource_rep, +) + +# pylint: disable=arguments-differ,too-many-ancestors,too-many-arguments,too-many-positional-arguments + + +class ResourceCH(ResourceOperator): + r"""Resource class for the CH gate. + + Args: + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources are derived from the following identities (as presented in this + `blog post `_): + + .. math:: + + \begin{align} + \hat{H} &= \hat{R}_{y}(\frac{\pi}{4}) \cdot \hat{Z} \cdot \hat{R}_{y}(\frac{-\pi}{4}), \\ + \hat{Z} &= \hat{H} \cdot \hat{X} \cdot \hat{H}. + \end{align} + + Specifically, the resources are defined as two :class:`~.ResourceRY`, two + :class:`~.ResourceHadamard` and one :class:`~.ResourceCNOT` gates. + + .. seealso:: :class:`~.CH` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceCH.resource_decomp() + [(2 x Hadamard), (2 x RY), (1 x CNOT)] + + """ + + num_wires = 2 + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + The resources are derived from the following identities (as presented in this + `blog post `_): + + .. math:: + + \begin{align} + \hat{H} &= \hat{R}_{y}(\frac{\pi}{4}) \cdot \hat{Z} \cdot \hat{R}_{y}(\frac{-\pi}{4}), \\ + \hat{Z} &= \hat{H} \cdot \hat{X} \cdot \hat{H}. + \end{align} + + Specifically, the resources are defined as two :class:`~.ResourceRY`, two + :class:`~.ResourceHadamard` and one :class:`~.ResourceCNOT` gates. + """ + ry = resource_rep(re.ResourceRY) + h = resource_rep(re.ResourceHadamard) + cnot = resource_rep(ResourceCNOT) + return [GateCount(h, 2), GateCount(ry, 2), GateCount(cnot, 1)] + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + This operation is self-inverse, thus when raised to even integer powers acts like + the identity operator and raised to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return ( + [GateCount(resource_rep(re.ResourceIdentity))] + if pow_z % 2 == 0 + else [GateCount(cls.resource_rep())] + ) + + +class ResourceCY(ResourceOperator): + r"""Resource class for the CY gate. + + Args: + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources are derived from the following identity: + + .. math:: \hat{Y} = \hat{S} \cdot \hat{X} \cdot \hat{S}^{\dagger}. + + By replacing the :class:`~.ResourceX` gate with a :class:`~.ResourceCNOT` we + obtain the controlled decomposition. Specifically, the resources are defined as a + :class:`~.ResourceCNOT` gate conjugated by a pair of :class:`~.ResourceS` gates. + + .. seealso:: :class:`~.CY` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceCY.resource_decomp() + [(1 x CNOT), (1 x S), (1 x Adjoint(S))] + """ + + num_wires = 2 + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + The resources are derived from the following identity: + + .. math:: \hat{Y} = \hat{S} \cdot \hat{X} \cdot \hat{S}^{\dagger}. + + By replacing the :class:`~.ResourceX` gate with a :class:`~.ResourceCNOT` we + obtain the controlled decomposition. Specifically, the resources are defined as a + :class:`~.ResourceCNOT` gate conjugated by a pair of :class:`~.ResourceS` gates. + """ + # TODO: To be added after the symbolic PR + raise re.ResourcesNotDefined + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + This operation is self-inverse, thus when raised to even integer powers acts like + the identity operator and raised to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return ( + [GateCount(resource_rep(re.ResourceIdentity))] + if pow_z % 2 == 0 + else [GateCount(cls.resource_rep())] + ) + + +class ResourceCZ(ResourceOperator): + r"""Resource class for the CZ gate. + + Args: + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources are derived from the following identity: + + .. math:: \hat{Z} = \hat{H} \cdot \hat{X} \cdot \hat{H}. + + By replacing the :class:`~.ResourceX` gate with a :class:`~.ResourceCNOT` we obtain + the controlled decomposition. Specifically, the resources are defined as a + :class:`~.ResourceCNOT` gate conjugated by a pair of :class:`~.ResourceHadamard` gates. + + .. seealso:: :class:`~.CZ` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceCZ.resource_decomp() + [(1 x CNOT), (2 x Hadamard)] + """ + + num_wires = 2 + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + The resources are derived from the following identity: + + .. math:: \hat{Z} = \hat{H} \cdot \hat{X} \cdot \hat{H}. + + By replacing the :class:`~.ResourceX` gate with a :class:`~.ResourceCNOT` we obtain + the controlled decomposition. Specifically, the resources are defined as a + :class:`~.ResourceCNOT` gate conjugated by a pair of :class:`~.ResourceHadamard` gates. + """ + cnot = resource_rep(ResourceCNOT) + h = resource_rep(re.ResourceHadamard) + + return [GateCount(cnot), GateCount(h, 2)] + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + This operation is self-inverse, thus when raised to even integer powers acts like + the identity operator and raised to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return ( + [GateCount(resource_rep(re.ResourceIdentity))] + if pow_z % 2 == 0 + else [GateCount(cls.resource_rep())] + ) + + +class ResourceCSWAP(ResourceOperator): + r"""Resource class for the CSWAP gate. + + Args: + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources are taken from Figure 1d of `arXiv:2305.18128 `_. + + The circuit which applies the SWAP operation on wires (1, 2) and controlled on wire (0) is + defined as: + + .. code-block:: bash + + 0: ────╭●────┤ + 1: ─╭X─├●─╭X─┤ + 2: ─╰●─╰X─╰●─┤ + + .. seealso:: :class:`~.CSWAP` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceCSWAP.resource_decomp() + [(1 x Toffoli), (2 x CNOT)] + """ + + num_wires = 3 + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + The resources are taken from Figure 1d of `arXiv:2305.18128 `_. + + The circuit which applies the SWAP operation on wires (1, 2) and controlled on wire (0) is + defined as: + + .. code-block:: bash + + 0: ────╭●────┤ + 1: ─╭X─├●─╭X─┤ + 2: ─╰●─╰X─╰●─┤ + """ + tof = resource_rep(ResourceToffoli) + cnot = resource_rep(ResourceCNOT) + return [GateCount(tof), GateCount(cnot, 2)] + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + This operation is self-inverse, thus when raised to even integer powers acts like + the identity operator and raised to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return ( + [GateCount(resource_rep(re.ResourceIdentity))] + if pow_z % 2 == 0 + else [GateCount(cls.resource_rep())] + ) + + +class ResourceCCZ(ResourceOperator): + r"""Resource class for the CCZ gate. + + Args: + wires (Sequence[int], optional): the subsystem the gate acts on + + Resources: + The resources are derived from the following identity: + + .. math:: \hat{Z} = \hat{H} \cdot \hat{X} \cdot \hat{H}. + + By replacing the :class:`~.ResourceX` gate with a :class:`~.ResourceToffoli` we obtain + the controlled decomposition. Specifically, the resources are defined as a + :class:`~.ResourceToffoli` gate conjugated by a pair of :class:`~.ResourceHadamard` gates. + + .. seealso:: :class:`~.CCZ` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceCCZ.resource_decomp() + [(1 x Toffoli), (2 x Hadamard)] + """ + + num_wires = 3 + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + The resources are derived from the following identity: + + .. math:: \hat{Z} = \hat{H} \cdot \hat{X} \cdot \hat{H}. + + By replacing the :class:`~.ResourceX` gate with a :class:`~.ResourceToffoli` we obtain + the controlled decomposition. Specifically, the resources are defined as a + :class:`~.ResourceToffoli` gate conjugated by a pair of :class:`~.ResourceHadamard` gates. + """ + toffoli = resource_rep(ResourceToffoli) + h = resource_rep(re.ResourceHadamard) + return [GateCount(toffoli), GateCount(h, 2)] + + @classmethod + def default_adjoint_resource_decomp(cls, **kwargs): + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + This operation is self-inverse, thus when raised to even integer powers acts like + the identity operator and raised to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return ( + [GateCount(resource_rep(re.ResourceIdentity))] + if pow_z % 2 == 0 + else [GateCount(cls.resource_rep())] + ) + + +class ResourceCNOT(ResourceOperator): + r"""Resource class for the CNOT gate. + + Args: + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The CNOT gate is treated as a fundamental gate and thus it cannot be decomposed + further. Requesting the resources of this gate raises a :code:`ResourcesNotDefined` error. + + .. seealso:: :class:`~.CNOT` + + """ + + num_wires = 2 + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + The CNOT gate is treated as a fundamental gate and thus it cannot be decomposed + further. Requesting the resources of this gate raises a :code:`ResourcesNotDefined` error. + + Raises: + ResourcesNotDefined: This gate is fundamental, no further decomposition defined. + """ + raise re.ResourcesNotDefined + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + The resources are expressed as one general :class:`~.ResourceMultiControlledX` gate. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1 and ctrl_num_ctrl_values == 0: + return [GateCount(resource_rep(ResourceToffoli))] + + mcx = resource_rep( + ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires + 1, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [ + GateCount(mcx), + ] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + This operation is self-inverse, thus when raised to even integer powers acts like + the identity operator and raised to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return ( + [GateCount(resource_rep(re.ResourceIdentity))] + if pow_z % 2 == 0 + else [GateCount(cls.resource_rep())] + ) + + +class ResourceTempAND(ResourceOperator): + r"""Resource class representing a temporary `AND`-gate. + + Args: + wires (Sequence[int], optional): the wires the operation acts on + + This gate was introduced in Fig 4 of `Babbush 2018 `_ along + with it's adjoint (uncompute). + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceTempAND.resource_decomp() + [(1 x Toffoli)] + """ + + num_wires = 3 + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + The resources are obtained from Figure 4 of `Babbush 2018 `_. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + tof = resource_rep(ResourceToffoli, {"elbow": "left"}) + return [GateCount(tof)] + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The resources are obtained from Figure 4 of `Babbush 2018 `_. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + h = resource_rep(re.ResourceHadamard) + cz = resource_rep(ResourceCZ) + return [GateCount(h), GateCount(cz)] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + The resources are expressed as one general :class:`~.ResourceMultiControlledX` gate. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + mcx = resource_rep( + re.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires + 2, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(mcx)] + + +class ResourceToffoli(ResourceOperator): + r"""Resource class for the Toffoli gate. + + Args: + wires (Sequence[int], optional): the subsystem the gate acts on + elbow (Union[str, None]): String identifier to determine if this is a special type of + Toffoli gate (left or right elbow). Default value is `None`. + + Resources: + If `elbow` is provided, resources are obtained from Figure 4 of + `Babbush 2018 `_. + + If `elbow` is `None`, the resources are obtained from Figure 1 of + `Jones 2012 `_. + + The circuit which applies the Toffoli gate on target wire 'target' with control wires + ('c1', 'c2') is defined as: + + .. code-block:: bash + + c1: ─╭●────╭X──T†────────╭X────╭●───────────────╭●─┤ + c2: ─│──╭X─│──╭●───T†─╭●─│──╭X─│────────────────╰Z─┤ + aux1: ─╰X─│──│──╰X───T──╰X─│──│──╰X────────────────║─┤ + aux2: ──H─╰●─╰●──T─────────╰●─╰●──H──S─╭●──H──┤↗├──║─┤ + target: ─────────────────────────────────╰X──────║───║─┤ + ╚═══╝ + + Specifically, the resources are defined as nine :class:`~.ResourceCNOT` gates, three + :class:`~.ResourceHadamard` gates, one :class:`~.ResourceCZ` gate, one :class:`~.ResourceS` + gate, two :class:`~.ResourceT` gates and two adjoint :class:`~.ResourceT` gates. + + .. seealso:: :class:`~.Toffoli` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceToffoli.resource_decomp() + [AllocWires(2), (9 x CNOT), (3 x Hadamard), (1 x S), (1 x CZ), (2 x T), (2 x Adjoint(T)), FreeWires(2)] + """ + + num_wires = 3 + resource_keys = {"elbow"} + + def __init__(self, elbow=None, wires=None) -> None: + self.elbow = elbow + super().__init__(wires=wires) + + @classmethod + def default_resource_decomp(cls, elbow=None, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + If `elbow` is provided, resources are obtained from Figure 4 of + `arXiv:1805.03662 `_. + + If `elbow` is `None`, the resources are obtained from Figure 1 of + `Jones 2012 `_. + + The circuit which applies the Toffoli gate on target wire 'target' with control wires + ('c1', 'c2') is defined as: + + .. code-block:: bash + + c1: ─╭●────╭X──T†────────╭X────╭●───────────────╭●─┤ + c2: ─│──╭X─│──╭●───T†─╭●─│──╭X─│────────────────╰Z─┤ + aux1: ─╰X─│──│──╰X───T──╰X─│──│──╰X────────────────║─┤ + aux2: ──H─╰●─╰●──T─────────╰●─╰●──H──S─╭●──H──┤↗├──║─┤ + target: ─────────────────────────────────╰X──────║───║─┤ + ╚═══╝ + + Specifically, the resources are defined as nine :class:`~.ResourceCNOT` gates, three + :class:`~.ResourceHadamard` gates, one :class:`~.ResourceCZ` gate, one :class:`~.ResourceS` + gate, two :class:`~.ResourceT` gates and two adjoint :class:`~.ResourceT` gates. + """ + # TODO: To be added after the symbolic op PR + raise re.ResourcesNotDefined + + @classmethod + def textbook_resource_decomp(cls, elbow=None, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + If `elbow` is provided, resources are obtained from Figure 4 of + `arXiv:1805.03662 `_. + + If `elbow` is `None`, the resources are taken from Figure 4.9 of `Nielsen, M. A., & Chuang, I. L. (2010) + `_. + + The circuit is defined as: + + .. code-block:: bash + + 0: ───────────╭●───────────╭●────╭●──T──╭●─┤ + 1: ────╭●─────│─────╭●─────│───T─╰X──T†─╰X─┤ + 2: ──H─╰X──T†─╰X──T─╰X──T†─╰X──T──H────────┤ + + Specifically, the resources are defined as six :class:`~.ResourceCNOT` gates, two + :class:`~.ResourceHadamard` gates, four :class:`~.ResourceT` gates and three adjoint + :class:`~.ResourceT` gates. + """ + # TODO: To be added after the symbolic op PR + raise re.ResourcesNotDefined + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + A dictionary containing the resource parameters: + * elbow (Union[str, None]): String identifier to determine if this is a special type of Toffoli gate (left or right elbow). + + """ + return {"elbow": self.elbow} + + @classmethod + def resource_rep(cls, elbow=None) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + return CompressedResourceOp(cls, {"elbow": elbow}) + + @classmethod + def default_adjoint_resource_decomp(cls, elbow=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if elbow is None: + return [GateCount(cls.resource_rep())] + + adj_elbow = "left" if elbow == "right" else "right" + return [GateCount(cls.resource_rep(elbow=adj_elbow))] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + elbow (Union[str, None]): String identifier to determine if this is a special type of Toffoli gate (left or right elbow). + Default value is `None`. + Resources: + The resources are expressed as one general :class:`~.ResourceMultiControlledX` gate. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + mcx = resource_rep( + re.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires + 2, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(mcx)] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, elbow=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + This operation is self-inverse, thus when raised to even integer powers acts like + the identity operator and raised to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return ( + [GateCount(resource_rep(re.ResourceIdentity))] + if pow_z % 2 == 0 + else [GateCount(cls.resource_rep(elbow=elbow))] + ) + + +class ResourceMultiControlledX(ResourceOperator): + r"""Resource class for the MultiControlledX gate. + + Args: + num_ctrl_wires (int): the number of qubits the operation is controlled on + num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + wires (Sequence[int], optional): the wires this operation acts on + + Resources: + The resources are obtained based on the unary iteration technique described in + `Babbush 2018 `_. + + .. seealso:: :class:`~.MultiControlledX` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceMultiControlledX.resource_decomp(num_ctrl_wires=5, num_ctrl_values=2) + [(4 x X), AllocWires(3), (3 x TempAND), (3 x Toffoli), (1 x Toffoli), FreeWires(3)] + """ + + resource_keys = {"num_ctrl_wires", "num_ctrl_values"} + + def __init__(self, num_ctrl_wires, num_ctrl_values, wires=None) -> None: + self.num_ctrl_wires = num_ctrl_wires + self.num_ctrl_values = num_ctrl_values + + self.num_wires = num_ctrl_wires + 1 + super().__init__(wires=wires) + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * num_ctrl_wires (int): the number of qubits the operation is controlled on + * num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + """ + + return { + "num_ctrl_wires": self.num_ctrl_wires, + "num_ctrl_values": self.num_ctrl_values, + } + + @classmethod + def resource_rep(cls, num_ctrl_wires, num_ctrl_values) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources. + + Args: + num_ctrl_wires (int): the number of qubits the operation is controlled on + num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp( + cls, + { + "num_ctrl_wires": num_ctrl_wires, + "num_ctrl_values": num_ctrl_values, + }, + ) + + @classmethod + def default_resource_decomp( + cls, + num_ctrl_wires, + num_ctrl_values, + **kwargs, # pylint: disable=unused-argument + ) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Args: + num_ctrl_wires (int): the number of qubits the operation is controlled on + num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + The resources are obtained based on the unary iteration technique described in + `Babbush 2018 `_. + """ + gate_lst = [] + + x = resource_rep(re.ResourceX) + if num_ctrl_wires == 0: + if num_ctrl_values: + return [] + + return [GateCount(x)] + + if num_ctrl_values: + gate_lst.append(GateCount(x, num_ctrl_values * 2)) + + cnot = resource_rep(ResourceCNOT) + if num_ctrl_wires == 1: + gate_lst.append(GateCount(cnot)) + return gate_lst + + toffoli = resource_rep(ResourceToffoli) + if num_ctrl_wires == 2: + gate_lst.append(GateCount(toffoli)) + return gate_lst + + if num_ctrl_wires == 3: # assuming one work wire: + res = [AllocWires(1), GateCount(cnot, 2), GateCount(toffoli, 2), FreeWires(1)] + gate_lst.extend(res) + return gate_lst + + l_elbow = resource_rep(ResourceTempAND) + r_elbow = resource_rep(ResourceToffoli, {"elbow": "right"}) + + res = [ + AllocWires(num_ctrl_wires - 2), + GateCount(l_elbow, num_ctrl_wires - 2), + GateCount(r_elbow, num_ctrl_wires - 2), + GateCount(toffoli, 1), + FreeWires(num_ctrl_wires - 2), + ] + gate_lst.extend(res) + return gate_lst + + @classmethod + def default_adjoint_resource_decomp(cls, num_ctrl_wires, num_ctrl_values) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Args: + num_ctrl_wires (int): the number of qubits the operation is controlled on + num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(num_ctrl_wires, num_ctrl_values))] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + num_ctrl_wires, + num_ctrl_values, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): The number of control qubits to further control the base + controlled operation upon. + ctrl_num_ctrl_values (int): The subset of those control qubits, which further control + the base controlled operation, which are controlled when in the :math:`|0\rangle` state. + num_ctrl_wires (int): the number of control qubits of the operation + num_ctrl_values (int): The subset of control qubits of the operation, that are controlled + when in the :math:`|0\rangle` state. + + Resources: + The resources are derived by combining the control qubits, control-values and + into a single instance of :class:`~.ResourceMultiControlledX` gate, controlled + on the whole set of control-qubits. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [ + GateCount( + cls.resource_rep( + ctrl_num_ctrl_wires + num_ctrl_wires, + ctrl_num_ctrl_values + num_ctrl_values, + ) + ) + ] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, num_ctrl_wires, num_ctrl_values) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + num_ctrl_wires (int): the number of qubits the operation is controlled on + num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + This operation is self-inverse, thus when raised to even integer powers acts like + the identity operator and raised to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return ( + [GateCount(resource_rep(re.ResourceIdentity))] + if pow_z % 2 == 0 + else [GateCount(cls.resource_rep(num_ctrl_wires, num_ctrl_values))] + ) + + +class ResourceCRX(ResourceOperator): + r"""Resource class for the CRX gate. + + Args: + wires (Sequence[int], optional): the wire the operation acts on + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay + `_. In combination with the following identity: + + .. math:: \hat{RX} = \hat{H} \cdot \hat{RZ} \cdot \hat{H}, + + we can express the :code:`CRX` gate as a :code:`CRZ` gate conjugated by :code:`Hadamard` + gates. The expression for controlled-RZ gates is used as defined in the reference above. + Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates, two + :class:`~.ResourceHadamard` gates and two :class:`~.ResourceRZ` gates. + + .. seealso:: :class:`~.CRX` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceCRX.resource_decomp() + [(2 x CNOT), (2 x RZ), (2 x Hadamard)] + """ + + num_wires = 2 + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + A dictionary containing the resource parameters: + * eps (Union[float, None]): error threshold for the approximation + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay + `_. In combination with the following identity: + + .. math:: \hat{RX} = \hat{H} \cdot \hat{RZ} \cdot \hat{H}, + + we can express the :code:`CRX` gate as a :code:`CRZ` gate conjugated by :code:`Hadamard` + gates. The expression for controlled-RZ gates is used as defined in the reference above. + Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates, two + :class:`~.ResourceHadamard` gates and two :class:`~.ResourceRZ` gates. + """ + h = resource_rep(re.ResourceHadamard) + rz = resource_rep(re.ResourceRZ, {"eps": eps}) + cnot = resource_rep(ResourceCNOT) + + return [GateCount(cnot, 2), GateCount(rz, 2), GateCount(h, 2)] + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of a single qubit rotation changes the sign of the rotation angle, + thus the resources of the adjoint operation result in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps))] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + Taking arbitrary powers of a single qubit rotation produces a sum of rotations. + The resources simplify to just one total single qubit rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + + return [GateCount(cls.resource_rep(eps))] + + +class ResourceCRY(ResourceOperator): + r"""Resource class for the CRY gate. + + Args: + wires (Sequence[int], optional): the wire the operation acts on + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay + `_. In combination with the following identity: + + .. math:: \hat{RY}(\theta) = \hat{X} \cdot \hat{RY}(- \theta) \cdot \hat{X}. + + By replacing the :class:`~.ResourceX` gates with :class:`~.ResourceCNOT` gates, we obtain a + controlled-version of this identity. Thus we are able to constructively or destructively + interfere the gates based on the value of the control qubit. Specifically, the resources are + defined as two :class:`~.ResourceCNOT` gates and two :class:`~.ResourceRY` gates. + + .. seealso:: :class:`~.CRY` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceCRY.resource_decomp() + [(2 x CNOT), (2 x RY)] + """ + + num_wires = 2 + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + A dictionary containing the resource parameters: + * eps (Union[float, None]): error threshold for the approximation + + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay + `_. In combination with the following identity: + + .. math:: \hat{RY}(\theta) = \hat{X} \cdot \hat{RY}(- \theta) \cdot \hat{X}. + + By replacing the :code:`X` gates with :code:`CNOT` gates, we obtain a controlled-version of this + identity. Thus we are able to constructively or destructively interfere the gates based on the value + of the control qubit. Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates + and two :class:`~.ResourceRY` gates. + """ + cnot = resource_rep(ResourceCNOT) + ry = resource_rep(re.ResourceRY, {"eps": eps}) + return [GateCount(cnot, 2), GateCount(ry, 2)] + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of a single qubit rotation changes the sign of the rotation angle, + thus the resources of the adjoint operation result in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps))] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + Taking arbitrary powers of a single qubit rotation produces a sum of rotations. + The resources simplify to just one total single qubit rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + + return [GateCount(cls.resource_rep(eps))] + + +class ResourceCRZ(ResourceOperator): + r"""Resource class for the CRZ gate. + + Args: + wires (Sequence[int], optional): the wire the operation acts on + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay + `_. In combination with the following identity: + + .. math:: \hat{RZ}(\theta) = \hat{X} \cdot \hat{RZ}(- \theta) \cdot \hat{X}. + + By replacing the :code:`X` gates with :code:`CNOT` gates, we obtain a controlled-version of this + identity. Thus we are able to constructively or destructively interfere the gates based on the value + of the control qubit. Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates + and two :class:`~.ResourceRZ` gates. + + .. seealso:: :class:`~.CRZ` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceCRZ.resource_decomp() + [(2 x CNOT), (2 x RZ)] + """ + + num_wires = 2 + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + A dictionary containing the resource parameters: + * eps (Union[float, None]): error threshold for the approximation + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay + `_. In combination with the following identity: + + .. math:: \hat{RZ}(\theta) = \hat{X} \cdot \hat{RZ}(- \theta) \cdot \hat{X}. + + By replacing the :code:`X` gates with :code:`CNOT` gates, we obtain a controlled-version of this + identity. Thus we are able to constructively or destructively interfere the gates based on the value + of the control qubit. Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates + and two :class:`~.ResourceRZ` gates. + """ + cnot = resource_rep(ResourceCNOT) + rz = resource_rep(re.ResourceRZ, {"eps": eps}) + return [GateCount(cnot, 2), GateCount(rz, 2)] + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of a single qubit rotation changes the sign of the rotation angle, + thus the resources of the adjoint operation result in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps))] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + Taking arbitrary powers of a single qubit rotation produces a sum of rotations. + The resources simplify to just one total single qubit rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + + return [GateCount(cls.resource_rep(eps))] + + +class ResourceCRot(ResourceOperator): + r"""Resource class for the CRot gate. + + Args: + wires (Sequence[int], optional): the wire the operation acts on + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay + `_. In combination with the following identity: + + .. math:: + + \begin{align} + \hat{RZ}(\theta) = \hat{X} \cdot \hat{RZ}(- \theta) \cdot \hat{X}, \\ + \hat{RY}(\theta) = \hat{X} \cdot \hat{RY}(- \theta) \cdot \hat{X}. + \end{align} + + This identity is applied along with some clever choices for the angle values to combine rotation; + the final circuit takes the form: + + .. code-block:: bash + + ctrl: ─────╭●─────────╭●─────────┤ + trgt: ──RZ─╰X──RZ──RY─╰X──RY──RZ─┤ + + .. seealso:: :class:`~.CRot` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceCRot.resource_decomp() + [(2 x CNOT), (3 x RZ), (2 x RY)] + """ + + num_wires = 2 + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + A dictionary containing the resource parameters: + * eps (Union[float, None]): error threshold for the approximation + + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + + Resources: + The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay + `_. In combination with the following identity: + + .. math:: + + \begin{align} + \hat{RZ}(\theta) = \hat{X} \cdot \hat{RZ}(- \theta) \cdot \hat{X}, \\ + \hat{RY}(\theta) = \hat{X} \cdot \hat{RY}(- \theta) \cdot \hat{X}. + \end{align} + + This identity is applied along with some clever choices for the angle values to combine rotation; + the final circuit takes the form: + + .. code-block:: bash + + ctrl: ─────╭●─────────╭●─────────┤ + trgt: ──RZ─╰X──RZ──RY─╰X──RY──RZ─┤ + + """ + cnot = resource_rep(ResourceCNOT) + rz = resource_rep(re.ResourceRZ, {"eps": eps}) + ry = resource_rep(re.ResourceRY, {"eps": eps}) + + return [GateCount(cnot, 2), GateCount(rz, 3), GateCount(ry, 2)] + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of a general rotation flips the sign of the rotation angle, + thus the resources of the adjoint operation result in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps))] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + Taking arbitrary powers of a general single qubit rotation produces a sum of rotations. + The resources simplify to just one total single qubit rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + + return [GateCount(cls.resource_rep(eps))] + + +class ResourceControlledPhaseShift(ResourceOperator): + r"""Resource class for the ControlledPhaseShift gate. + + Args: + wires (Sequence[int], optional): the wire the operation acts on + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + The resources are derived using the fact that a :class:`~.ResourcePhaseShift` gate is + identical to the :class:`~.ResourceRZ` gate up to some global phase. Furthermore, a controlled + global phase simplifies to a :class:`~.ResourcePhaseShift` gate. This gives rise to the + following identity: + + .. math:: CR_\phi(\phi) = (R_\phi(\phi/2) \otimes I) \cdot CNOT \cdot (I \otimes R_\phi(-\phi/2)) \cdot CNOT \cdot (I \otimes R_\phi(\phi/2)) + + Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates and three + :class:`~.ResourceRZ` gates. + + .. seealso:: :class:`~.ControlledPhaseShift` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceControlledPhaseShift.resource_decomp() + [(2 x CNOT), (3 x RZ)] + """ + + num_wires = 2 + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + A dictionary containing the resource parameters: + * eps (Union[float, None]): error threshold for the approximation + + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources.""" + + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the resources of the operator. + Each GateCount object specifies a gate type and its total occurrence count. + """ + cnot = resource_rep(ResourceCNOT) + rz = resource_rep(re.ResourceRZ, {"eps": eps}) + return [GateCount(cnot, 2), GateCount(rz, 3)] + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Resources: + The adjoint of a phase shift just flips the sign of the phase angle, + thus the resources of the adjoint operation result in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps))] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + Taking arbitrary powers of a phase shift produces a sum of shifts. + The resources simplify to just one total phase shift operator. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + + return [GateCount(cls.resource_rep(eps))] diff --git a/pennylane/labs/resource_estimation/ops/qubit/__init__.py b/pennylane/labs/resource_estimation/ops/qubit/__init__.py index 4d7f72fb919..2fb306edce9 100644 --- a/pennylane/labs/resource_estimation/ops/qubit/__init__.py +++ b/pennylane/labs/resource_estimation/ops/qubit/__init__.py @@ -20,6 +20,7 @@ ResourceX, ResourceY, ResourceZ, + ResourceSWAP, ) from .parametric_ops_single_qubit import ( ResourcePhaseShift, diff --git a/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py b/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py index 3a56f030aeb..8a1c7d71483 100644 --- a/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py +++ b/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py @@ -199,6 +199,189 @@ def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: return [GateCount(resource_rep(ResourceZ)), GateCount(cls.resource_rep())] +class ResourceSWAP(ResourceOperator): + r"""Resource class for the SWAP gate. + + Args: + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources come from the following identity expressing SWAP as the product of + three :class:`~.CNOT` gates: + + .. math:: + + SWAP = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 0\\ + 0 & 1 & 0 & 0\\ + 0 & 0 & 0 & 1 + \end{bmatrix} + = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0\\ + 0 & 0 & 0 & 1\\ + 0 & 0 & 1 & 0 + \end{bmatrix} + \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 1\\ + 0 & 0 & 1 & 0\\ + 0 & 1 & 0 & 0 + \end{bmatrix} + \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0\\ + 0 & 0 & 0 & 1\\ + 0 & 0 & 1 & 0 + \end{bmatrix}. + + .. seealso:: :class:`~.SWAP` + + **Example** + + The resources for this operation are computed using: + + >>> plre.ResourceSWAP.resource_decomp() + [(3 x CNOT)] + + """ + + num_wires = 2 + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: Empty dictionary. The resources of this operation don't depend on any additional parameters. + """ + return {} + + @classmethod + def resource_rep(cls) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation.""" + return CompressedResourceOp(cls, {}) + + @classmethod + def default_resource_decomp(cls, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a quantum gate + and the number of times it occurs in the decomposition. + + Resources: + The resources come from the following identity expressing SWAP as the product of + three CNOT gates: + + .. math:: + + SWAP = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & 1 & 0\\ + 0 & 1 & 0 & 0\\ + 0 & 0 & 0 & 1 + \end{bmatrix} + = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0\\ + 0 & 0 & 0 & 1\\ + 0 & 0 & 1 & 0 + \end{bmatrix} + \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & 0 & 1\\ + 0 & 0 & 1 & 0\\ + 0 & 1 & 0 & 0 + \end{bmatrix} + \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 1 & 0 & 0\\ + 0 & 0 & 0 & 1\\ + 0 & 0 & 1 & 0 + \end{bmatrix}. + """ + cnot = resource_rep(plre.ResourceCNOT) + return [GateCount(cnot, 3)] + + @classmethod + def default_adjoint_resource_decomp(cls) -> list[GateCount]: + r"""Returns a dictionary representing the resources for the adjoint of the operator. + + Resources: + This operation is self-adjoint, so the resources of the adjoint operation results + in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep())] + + @classmethod + def default_controlled_resource_decomp( + cls, ctrl_num_ctrl_wires, ctrl_num_ctrl_values + ) -> list[GateCount]: + r"""Returns a dictionary representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + For a single control wire, the cost is a single instance of :class:`~.ResourceCSWAP`. + Two additional :class:`~.ResourceX` gates are used to flip the control qubit if + it is zero-controlled. + + In the case where multiple controlled wires are provided, the resources are given by + two :class:`~.ResourceCNOT` gates and one :class:`~.ResourceMultiControlledX` gate. This + is because of the symmetric resource decomposition of the SWAP gate. By controlling on + the middle CNOT gate, we obtain the required controlled operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1: + gate_types = [GateCount(resource_rep(plre.ResourceCSWAP))] + + if ctrl_num_ctrl_values: + gate_types.append(GateCount(resource_rep(ResourceX), 2)) + + return gate_types + + cnot = resource_rep(plre.ResourceCNOT) + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires + 1, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(cnot, 2), GateCount(mcx)] + + @classmethod + def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: + r"""Returns a dictionary representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + + Resources: + The SWAP gate raised to even powers produces identity and raised + to odd powers it produces itself. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if pow_z % 2 == 0: + return [GateCount(resource_rep(plre.ResourceIdentity))] + return [GateCount(cls.resource_rep())] + + class ResourceT(ResourceOperator): r"""Resource class for the T-gate. diff --git a/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py b/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py new file mode 100644 index 00000000000..7a4a9871e1d --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py @@ -0,0 +1,725 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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 controlled resource operators. +""" +import pytest + +import pennylane.labs.resource_estimation as re +from pennylane.labs.resource_estimation.qubit_manager import AllocWires, FreeWires + +# pylint: disable=no-self-use, use-implicit-booleaness-not-comparison,too-many-arguments,too-many-positional-arguments + + +class TestResourceCH: + """Test the ResourceCH operation""" + + op = re.ResourceCH(wires=[0, 1]) + + def test_resources(self): + """Test that the resources method produces the expected resources.""" + + expected_resources = [ + re.GateCount(re.ResourceHadamard.resource_rep(), 2), + re.GateCount(re.ResourceRY.resource_rep(), 2), + re.GateCount(re.ResourceCNOT.resource_rep(), 1), + ] + assert self.op.resource_decomp(**self.op.resource_params) == expected_resources + + def test_resource_rep(self): + """Test the resource_rep produces the correct compressed representation.""" + expected_rep = re.CompressedResourceOp(re.ResourceCH, {}) + assert self.op.resource_rep(**self.op.resource_params) == expected_rep + + def test_resource_params(self): + """Test that the resource_params are produced as expected.""" + expected_params = {} + assert self.op.resource_params == expected_params + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + expected_res = [re.GateCount(self.op.resource_rep(), 1)] + + assert self.op.adjoint_resource_decomp() == expected_res + + pow_data = ( + (1, [re.GateCount(op.resource_rep(), 1)]), + (2, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), + (5, [re.GateCount(op.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + + assert self.op.pow_resource_decomp(z) == expected_res + + +class TestResourceCY: + """Test the ResourceCY operation""" + + op = re.ResourceCY(wires=[0, 1]) + + def test_resource_rep(self): + """Test the resource_rep produces the correct compressed representation.""" + expected_rep = re.CompressedResourceOp(re.ResourceCY, {}) + assert self.op.resource_rep(**self.op.resource_params) == expected_rep + + def test_resource_params(self): + """Test that the resource_params are produced as expected.""" + expected_params = {} + assert self.op.resource_params == expected_params + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + expected_res = [re.GateCount(self.op.resource_rep(), 1)] + + assert self.op.adjoint_resource_decomp() == expected_res + + pow_data = ( + (1, [re.GateCount(op.resource_rep(), 1)]), + (2, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), + (5, [re.GateCount(op.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + + assert self.op.pow_resource_decomp(z) == expected_res + + +class TestResourceCZ: + """Test the ResourceCZ operation""" + + op = re.ResourceCZ(wires=[0, 1]) + + def test_resources(self): + """Test that the resources method produces the expected resources.""" + + expected_resources = [ + re.GateCount(re.ResourceCNOT.resource_rep(), 1), + re.GateCount(re.ResourceHadamard.resource_rep(), 2), + ] + assert self.op.resource_decomp(**self.op.resource_params) == expected_resources + + def test_resource_rep(self): + """Test the resource_rep produces the correct compressed representation.""" + expected_rep = re.CompressedResourceOp(re.ResourceCZ, {}) + assert self.op.resource_rep() == expected_rep + + def test_resource_params(self): + """Test that the resource_params are produced as expected.""" + expected_params = {} + assert self.op.resource_params == expected_params + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + expected_res = [re.GateCount(self.op.resource_rep(), 1)] + + assert self.op.adjoint_resource_decomp() == expected_res + + pow_data = [ + (1, [re.GateCount(op.resource_rep(), 1)]), + (2, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), + (5, [re.GateCount(op.resource_rep(), 1)]), + ] + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + + assert self.op.pow_resource_decomp(z) == expected_res + + +class TestResourceCSWAP: + """Test the ResourceCSWAP operation""" + + op = re.ResourceCSWAP(wires=[0, 1, 2]) + + def test_resources(self): + """Test that the resources method produces the expected resources.""" + expected_resources = [ + re.GateCount(re.ResourceToffoli.resource_rep(), 1), + re.GateCount(re.ResourceCNOT.resource_rep(), 2), + ] + assert self.op.resource_decomp(**self.op.resource_params) == expected_resources + + def test_resource_rep(self): + """Test the resource_rep produces the correct compressed representation.""" + expected_rep = re.CompressedResourceOp(re.ResourceCSWAP, {}) + assert self.op.resource_rep(**self.op.resource_params) == expected_rep + + def test_resource_params(self): + """Test that the resource_params are produced as expected.""" + expected_params = {} + assert self.op.resource_params == expected_params + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + expected_res = [re.GateCount(self.op.resource_rep(), 1)] + assert self.op.adjoint_resource_decomp() == expected_res + + pow_data = ( + (1, [re.GateCount(op.resource_rep(), 1)]), + (2, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), + (5, [re.GateCount(op.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + + assert self.op.pow_resource_decomp(z) == expected_res + + +class TestResourceCCZ: + """Test the ResourceCZZ operation""" + + op = re.ResourceCCZ(wires=[0, 1, 2]) + + def test_resources(self): + """Test that the resources method produces the expected resources.""" + expected_resources = [ + re.GateCount(re.ResourceToffoli.resource_rep(), 1), + re.GateCount(re.ResourceHadamard.resource_rep(), 2), + ] + assert self.op.resource_decomp(**self.op.resource_params) == expected_resources + + def test_resource_rep(self): + """Test the resource_rep produces the correct compressed representation.""" + expected_rep = re.CompressedResourceOp(re.ResourceCCZ, {}) + assert self.op.resource_rep(**self.op.resource_params) == expected_rep + + def test_resource_params(self): + """Test that the resource_params are produced as expected.""" + expected_params = {} + assert self.op.resource_params == expected_params + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + expected_res = [re.GateCount(self.op.resource_rep(), 1)] + + assert self.op.adjoint_resource_decomp() == expected_res + + pow_data = ( + (1, [re.GateCount(op.resource_rep(), 1)]), + (2, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), + (5, [re.GateCount(op.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + + assert self.op.pow_resource_decomp(z) == expected_res + + +class TestResourceCNOT: + """Test ResourceCNOT operation""" + + op = re.ResourceCNOT([0, 1]) + + def test_resources(self): + """Test that the resources method is not implemented""" + with pytest.raises(re.ResourcesNotDefined): + self.op.resource_decomp() + + def test_resource_rep(self): + """Test the resource_rep produces the correct compressed representation.""" + expected = re.CompressedResourceOp(re.ResourceCNOT, {}) + assert self.op.resource_rep() == expected + + def test_resource_params(self): + """Test that the resource_params are produced as expected.""" + expected_params = {} + assert self.op.resource_params == expected_params + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + expected_res = [re.GateCount(self.op.resource_rep(), 1)] + + assert self.op.adjoint_resource_decomp() == expected_res + + ctrl_data = ( + ( + ["c1"], + [1], + [re.GateCount(re.ResourceToffoli.resource_rep(), 1)], + ), + ( + ["c1", "c2"], + [1, 1], + [re.GateCount(re.ResourceMultiControlledX.resource_rep(3, 0), 1)], + ), + ( + ["c1", "c2", "c3"], + [1, 0, 0], + [re.GateCount(re.ResourceMultiControlledX.resource_rep(4, 2), 1)], + ), + ) + + @pytest.mark.parametrize( + "ctrl_wires, ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + assert self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + + pow_data = ( + (1, [re.GateCount(op.resource_rep(), 1)]), + (2, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), + (5, [re.GateCount(op.resource_rep(), 1)]), + (8, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + + assert self.op.pow_resource_decomp(z) == expected_res + + +class TestResourceToffoli: + """Test the ResourceToffoli operation""" + + op = re.ResourceToffoli(wires=[0, 1, 2]) + + def test_resource_rep(self): + """Test the resource_rep produces the correct compressed representation.""" + expected_rep = re.CompressedResourceOp(re.ResourceToffoli, {"elbow": None}) + assert self.op.resource_rep(**self.op.resource_params) == expected_rep + + def test_resource_params(self): + """Test that the resource_params are produced as expected.""" + expected_params = {"elbow": None} + assert self.op.resource_params == expected_params + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + expected_res = [re.GateCount(self.op.resource_rep(), 1)] + + assert self.op.adjoint_resource_decomp() == expected_res + + ctrl_data = ( + ( + ["c1"], + [1], + [re.GateCount(re.ResourceMultiControlledX.resource_rep(3, 0), 1)], + ), + ( + ["c1", "c2"], + [1, 1], + [re.GateCount(re.ResourceMultiControlledX.resource_rep(4, 0), 1)], + ), + ( + ["c1", "c2", "c3"], + [1, 0, 0], + [re.GateCount(re.ResourceMultiControlledX.resource_rep(5, 2), 1)], + ), + ) + + @pytest.mark.parametrize( + "ctrl_wires, ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + assert self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + + pow_data = ( + (1, [re.GateCount(op.resource_rep(), 1)]), + (2, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), + (5, [re.GateCount(op.resource_rep(), 1)]), + (8, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + + assert self.op.pow_resource_decomp(z) == expected_res + + +class TestResourceMultiControlledX: + """Test the ResourceMultiControlledX operation""" + + res_ops = ( + re.ResourceMultiControlledX(1, 0), + re.ResourceMultiControlledX(2, 0), + re.ResourceMultiControlledX(3, 0), + re.ResourceMultiControlledX(5, 0), + re.ResourceMultiControlledX(1, 1), + re.ResourceMultiControlledX(2, 1), + re.ResourceMultiControlledX(3, 2), + re.ResourceMultiControlledX(5, 3), + ) + + res_params = ( + (1, 0), + (2, 0), + (3, 0), + (5, 0), + (1, 1), + (2, 1), + (3, 2), + (5, 3), + ) + + expected_resources = ( + [re.GateCount(re.ResourceCNOT.resource_rep(), 1)], + [re.GateCount(re.ResourceToffoli.resource_rep(), 1)], + [ + AllocWires(1), + re.GateCount(re.ResourceCNOT.resource_rep(), 2), + re.GateCount(re.ResourceToffoli.resource_rep(), 2), + FreeWires(1), + ], + [ + AllocWires(3), + re.GateCount(re.resource_rep(re.ResourceTempAND), 3), + re.GateCount(re.resource_rep(re.ResourceToffoli, {"elbow": "right"}), 3), + re.GateCount(re.resource_rep(re.ResourceToffoli, {}), 1), + FreeWires(3), + ], + [ + re.GateCount(re.ResourceX.resource_rep(), 2), + re.GateCount(re.ResourceCNOT.resource_rep(), 1), + ], + [ + re.GateCount(re.ResourceX.resource_rep(), 2), + re.GateCount(re.ResourceToffoli.resource_rep(), 1), + ], + [ + re.GateCount(re.resource_rep(re.ResourceX), 4), + AllocWires(1), + re.GateCount(re.resource_rep(re.ResourceCNOT), 2), + re.GateCount(re.resource_rep(re.ResourceToffoli), 2), + FreeWires(1), + ], + [ + re.GateCount(re.resource_rep(re.ResourceX), 6), + AllocWires(3), + re.GateCount(re.resource_rep(re.ResourceTempAND), 3), + re.GateCount(re.resource_rep(re.ResourceToffoli, {"elbow": "right"}), 3), + re.GateCount(re.resource_rep(re.ResourceToffoli, {}), 1), + FreeWires(3), + ], + ) + + @staticmethod + def _prep_params(num_control, num_control_values): + return { + "num_ctrl_wires": num_control, + "num_ctrl_values": num_control_values, + } + + @pytest.mark.parametrize("params, expected_res", zip(res_params, expected_resources)) + def test_resources(self, params, expected_res): + """Test that the resources method produces the expected resources.""" + op_resource_params = self._prep_params(*params) + assert repr(re.ResourceMultiControlledX.resource_decomp(**op_resource_params)) == repr( + expected_res + ) + + @pytest.mark.parametrize("op, params", zip(res_ops, res_params)) + def test_resource_rep(self, op, params): + """Test the resource_rep produces the correct compressed representation.""" + op_resource_params = self._prep_params(*params) + expected_rep = re.CompressedResourceOp(re.ResourceMultiControlledX, op_resource_params) + assert op.resource_rep(**op.resource_params) == expected_rep + + @pytest.mark.parametrize("op, params", zip(res_ops, res_params)) + def test_resource_params(self, op, params): + """Test that the resource_params are produced as expected.""" + expected_params = self._prep_params(*params) + print("params", params, expected_params, op.resource_params) + assert op.resource_params == expected_params + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + op = re.ResourceMultiControlledX(5, 3) + + expected_res = [re.GateCount(op.resource_rep(**op.resource_params), 1)] + + assert op.adjoint_resource_decomp(**op.resource_params) == expected_res + + pow_data = ( + (1, [re.GateCount(re.ResourceMultiControlledX.resource_rep(5, 3), 1)]), + (2, [re.GateCount(re.ResourceIdentity.resource_rep())]), + (5, [re.GateCount(re.ResourceMultiControlledX.resource_rep(5, 3), 1)]), + (6, [re.GateCount(re.ResourceIdentity.resource_rep())]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + op = re.ResourceMultiControlledX(5, 3) + + assert op.pow_resource_decomp(z, **op.resource_params) == expected_res + + +class TestResourceCRX: + """Test the ResourceCRX operation""" + + op = re.ResourceCRX(wires=[0, 1]) + + def test_resources(self): + """Test that the resources method produces the expected resources.""" + + expected_resources = [ + re.GateCount(re.ResourceCNOT.resource_rep(), 2), + re.GateCount(re.ResourceRZ.resource_rep(), 2), + re.GateCount(re.ResourceHadamard.resource_rep(), 2), + ] + assert self.op.resource_decomp(**self.op.resource_params) == expected_resources + + def test_resource_rep(self): + """Test the resource_rep produces the correct compressed representation.""" + expected_rep = re.CompressedResourceOp(re.ResourceCRX, {"eps": None}) + assert self.op.resource_rep(**self.op.resource_params) == expected_rep + + def test_resource_params(self): + """Test that the resource_params are produced as expected.""" + expected_params = {"eps": None} + assert self.op.resource_params == expected_params + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + expected_res = [re.GateCount(self.op.resource_rep(), 1)] + + assert self.op.adjoint_resource_decomp() == expected_res + + pow_data = ( + (1, [re.GateCount(op.resource_rep(), 1)]), + (2, [re.GateCount(op.resource_rep(), 1)]), + (5, [re.GateCount(op.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + + assert self.op.pow_resource_decomp(z) == expected_res + + +class TestResourceCRY: + """Test the ResourceCRY operation""" + + op = re.ResourceCRY(wires=[0, 1]) + + def test_resources(self): + """Test that the resources method produces the expected resources.""" + + expected_resources = [ + re.GateCount(re.ResourceCNOT.resource_rep(), 2), + re.GateCount(re.ResourceRY.resource_rep(), 2), + ] + assert self.op.resource_decomp(**self.op.resource_params) == expected_resources + + def test_resource_rep(self): + """Test the resource_rep produces the correct compressed representation.""" + expected_rep = re.CompressedResourceOp(re.ResourceCRY, {"eps": None}) + assert self.op.resource_rep(**self.op.resource_params) == expected_rep + + def test_resource_params(self): + """Test that the resource_params are produced as expected.""" + expected_params = {"eps": None} + assert self.op.resource_params == expected_params + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + expected_res = [re.GateCount(self.op.resource_rep(), 1)] + + assert self.op.adjoint_resource_decomp() == expected_res + + pow_data = ( + (1, [re.GateCount(op.resource_rep(), 1)]), + (2, [re.GateCount(op.resource_rep(), 1)]), + (5, [re.GateCount(op.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + + assert self.op.pow_resource_decomp(z) == expected_res + + +class TestResourceCRZ: + """Test the ResourceCRZ operation""" + + op = re.ResourceCRZ(wires=[0, 1]) + + def test_resources(self): + """Test that the resources method produces the expected resources.""" + + expected_resources = [ + re.GateCount(re.ResourceCNOT.resource_rep(), 2), + re.GateCount(re.ResourceRZ.resource_rep(), 2), + ] + assert self.op.resource_decomp(**self.op.resource_params) == expected_resources + + def test_resource_rep(self): + """Test the resource_rep produces the correct compressed representation.""" + expected_rep = re.CompressedResourceOp(re.ResourceCRZ, {"eps": None}) + assert self.op.resource_rep(**self.op.resource_params) == expected_rep + + def test_resource_params(self): + """Test that the resource_params are produced as expected.""" + expected_params = {"eps": None} + assert self.op.resource_params == expected_params + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + expected_res = [re.GateCount(self.op.resource_rep(), 1)] + + assert self.op.adjoint_resource_decomp() == expected_res + + pow_data = ( + (1, [re.GateCount(op.resource_rep(), 1)]), + (2, [re.GateCount(op.resource_rep(), 1)]), + (5, [re.GateCount(op.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + + assert self.op.pow_resource_decomp(z) == expected_res + + +class TestResourceCRot: + """Test the ResourceCRot operation""" + + op = re.ResourceCRot(wires=[0, 1]) + + def test_resources(self): + """Test that the resources method produces the expected resources.""" + expected_resources = [ + re.GateCount(re.ResourceCNOT.resource_rep(), 2), + re.GateCount(re.ResourceRZ.resource_rep(), 3), + re.GateCount(re.ResourceRY.resource_rep(), 2), + ] + assert self.op.resource_decomp(**self.op.resource_params) == expected_resources + + def test_resource_rep(self): + """Test the resource_rep produces the correct compressed representation.""" + expected_rep = re.CompressedResourceOp(re.ResourceCRot, {"eps": None}) + assert self.op.resource_rep(**self.op.resource_params) == expected_rep + + def test_resource_params(self): + """Test that the resource_params are produced as expected.""" + expected_params = {"eps": None} + assert self.op.resource_params == expected_params + + def test_resource_adjoint(self): + """Test that the adjoint resources are as expected""" + expected_res = [re.GateCount(self.op.resource_rep(), 1)] + + assert self.op.adjoint_resource_decomp() == expected_res + + pow_data = ( + (1, [re.GateCount(op.resource_rep(), 1)]), + (2, [re.GateCount(op.resource_rep(), 1)]), + (5, [re.GateCount(op.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_resource_pow(self, z, expected_res): + """Test that the pow resources are as expected""" + + assert self.op.pow_resource_decomp(z) == expected_res + + +class TestResourceControlledPhaseShift: + """Test ResourceControlledPhaseShift""" + + def test_resources(self): + """Test the resources method""" + + op = re.ResourceControlledPhaseShift() + + expected = [ + re.GateCount(re.CompressedResourceOp(re.ResourceCNOT, {}), 2), + re.GateCount(re.CompressedResourceOp(re.ResourceRZ, {"eps": None}), 3), + ] + + assert op.resource_decomp(**op.resource_params) == expected + + def test_resource_params(self): + """Test the resource parameters""" + + op = re.ResourceControlledPhaseShift() + assert op.resource_params == { + "eps": None + } # pylint: disable=use-implicit-booleaness-not-comparison + + def test_resource_rep(self): + """Test the compressed representation""" + + op = re.ResourceControlledPhaseShift() + expected = re.CompressedResourceOp(re.ResourceControlledPhaseShift, {"eps": None}) + + assert op.resource_rep() == expected + + def test_resource_rep_from_op(self): + """Test resource_rep_from_op method""" + + op = re.ResourceControlledPhaseShift() + assert op.resource_rep_from_op() == re.ResourceControlledPhaseShift.resource_rep( + **op.resource_params + ) + + def test_resources_from_rep(self): + """Compute the resources from the compressed representation""" + + op = re.ResourceControlledPhaseShift() + + expected = [ + re.GateCount(re.CompressedResourceOp(re.ResourceCNOT, {}), 2), + re.GateCount(re.CompressedResourceOp(re.ResourceRZ, {"eps": None}), 3), + ] + + op_compressed_rep = op.resource_rep_from_op() + op_resource_params = op_compressed_rep.params + op_compressed_rep_type = op_compressed_rep.op_type + + assert op_compressed_rep_type.resource_decomp(**op_resource_params) == expected + + def test_adjoint_decomp(self): + """Test that the adjoint resources are correct.""" + + op = re.ResourceControlledPhaseShift() + + assert op.default_adjoint_resource_decomp() == [ + re.GateCount(re.ResourceControlledPhaseShift.resource_rep(), 1) + ] + + pow_data = ((1, [re.GateCount(re.ResourceControlledPhaseShift.resource_rep(), 1)]),) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_pow_decomp(self, z, expected_res): + """Test that the adjoint resources are correct.""" + + op = re.ResourceControlledPhaseShift + + assert op.default_pow_resource_decomp(z) == expected_res diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py index 3c5d8f71c39..d19e9d6a221 100644 --- a/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py +++ b/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py @@ -62,6 +62,106 @@ def test_pow_decomp(self, z, expected_res): assert op.pow_resource_decomp(z) == expected_res +class TestSWAP: + """Tests for ResourceSWAP""" + + def test_resources(self): + """Test that SWAP decomposes into three CNOTs""" + op = plre.ResourceSWAP([0, 1]) + cnot = plre.ResourceCNOT.resource_rep() + expected = [plre.GateCount(cnot, 3)] + + assert op.resource_decomp() == expected + + def test_resource_params(self): + """Test that the resource params are correct""" + op = plre.ResourceSWAP([0, 1]) + assert op.resource_params == {} + + def test_resource_rep(self): + """Test the compact representation""" + expected = plre.CompressedResourceOp(plre.ResourceSWAP, {}) + assert plre.ResourceSWAP.resource_rep() == expected + + def test_resources_from_rep(self): + """Test that the resources can be computed from the compressed representation""" + + op = plre.ResourceSWAP([0, 1]) + expected = [plre.GateCount(plre.ResourceCNOT.resource_rep(), 3)] + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resource_decomp(**op_resource_params) == expected + + def test_adjoint_decomp(self): + """Test that the adjoint decomposition is correct.""" + swap = plre.ResourceSWAP([0, 1]) + expected = [plre.GateCount(swap.resource_rep(), 1)] + + assert swap.adjoint_resource_decomp() == expected + + ctrl_data = ( + ( + ["c1"], + [1], + [ + plre.GateCount(plre.ResourceCSWAP.resource_rep(), 1), + ], + ), + ( + ["c1"], + [0], + [ + plre.GateCount(plre.ResourceCSWAP.resource_rep(), 1), + plre.GateCount(plre.ResourceX.resource_rep(), 2), + ], + ), + ( + ["c1", "c2"], + [1, 1], + [ + plre.GateCount(plre.ResourceCNOT.resource_rep(), 2), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(2, 0), 1), + ], + ), + ( + ["c1", "c2", "c3"], + [1, 0, 0], + [ + plre.GateCount(plre.ResourceCNOT.resource_rep(), 2), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 2), 1), + ], + ), + ) + + @pytest.mark.parametrize( + "ctrl_wires, ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + op = plre.ResourceSWAP([0, 1]) + + assert op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + + pow_data = ( + (1, [plre.GateCount(plre.ResourceSWAP.resource_rep(), 1)]), + (2, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + (3, [plre.GateCount(plre.ResourceSWAP.resource_rep(), 1)]), + (4, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), + ) + + @pytest.mark.parametrize("z, expected_res", pow_data) + def test_pow_decomp(self, z, expected_res): + """Test that the pow decomposition is correct.""" + op = plre.ResourceSWAP([0, 1]) + assert op.pow_resource_decomp(z) == expected_res + + class TestS: """Tests for ResourceS""" From 76cba836832ca7811a4a02f0b00fb9db6e50346e Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Fri, 20 Jun 2025 14:23:16 -0400 Subject: [PATCH 15/18] Add Symbolic Resources Ops (#7584) **Context:** Adding Symbolic ops to be compatible with the resource estimation functionality. **Description of the Change:** **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** --------- Co-authored-by: Diksha Dhawan Co-authored-by: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Co-authored-by: Utkarsh Co-authored-by: Soran Jahangiri <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 4 + .../labs/resource_estimation/__init__.py | 17 + .../labs/resource_estimation/ops/__init__.py | 5 + .../labs/resource_estimation/ops/identity.py | 48 +- .../ops/op_math/__init__.py | 11 +- .../ops/op_math/controlled_ops.py | 500 +++++++- .../ops/op_math/symbolic.py | 1121 +++++++++++++++++ .../ops/qubit/non_parametric_ops.py | 321 ++++- .../ops/qubit/parametric_ops_single_qubit.py | 297 ++++- .../labs/resource_estimation/qubit_manager.py | 3 + .../resource_estimation/resource_operator.py | 1 - .../resource_estimation/resource_tracking.py | 10 +- .../ops/op_math/test_controlled_ops.py | 357 +++++- .../ops/op_math/test_symbolic_ops.py | 357 ++++++ .../ops/qubit/test_non_parametric_ops.py | 355 +++++- .../qubit/test_parametric_ops_single_qubit.py | 171 +++ .../resource_estimation/ops/test_identity.py | 39 + .../test_resource_tracking.py | 2 +- 18 files changed, 3527 insertions(+), 92 deletions(-) create mode 100644 pennylane/labs/resource_estimation/ops/op_math/symbolic.py create mode 100644 pennylane/labs/tests/resource_estimation/ops/op_math/test_symbolic_ops.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 90232d2b619..182ee299c61 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -633,6 +633,10 @@ estimation for controlled gates. [(#7526)](https://github.com/PennyLaneAI/pennylane/pull/7526) +* Added the `pennylane.labs.ResourceOperator` templates which will be used to perform resource + estimation for symbolic operators of gates. + [(#7584)](https://github.com/PennyLaneAI/pennylane/pull/7584) + * A new module :mod:`pennylane.labs.zxopt ` provides access to the basic optimization passes from [pyzx](https://pyzx.readthedocs.io/en/latest/) for PennyLane circuits. diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index d2a2df917f3..8e0f25eb8fd 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -57,6 +57,18 @@ ~AllocWires ~FreeWires +Arithmetic Operators: +~~~~~~~~~~~~~~~~~~~~~ + +.. autosummary:: + :toctree: api + + ~ResourceAdjoint + ~ResourceChangeBasisOp + ~ResourceControlled + ~ResourcePow + ~ResourceProd + Operators: ~~~~~~~~~~ @@ -143,4 +155,9 @@ ResourceCRot, ResourceControlledPhaseShift, ResourceTempAND, + ResourceAdjoint, + ResourceControlled, + ResourceProd, + ResourceChangeBasisOp, + ResourcePow, ) diff --git a/pennylane/labs/resource_estimation/ops/__init__.py b/pennylane/labs/resource_estimation/ops/__init__.py index 3b1395ced7e..7efb423af23 100644 --- a/pennylane/labs/resource_estimation/ops/__init__.py +++ b/pennylane/labs/resource_estimation/ops/__init__.py @@ -16,9 +16,12 @@ from .identity import ResourceGlobalPhase, ResourceIdentity from .op_math import ( + ResourceAdjoint, ResourceCCZ, ResourceCH, + ResourceChangeBasisOp, ResourceCNOT, + ResourceControlled, ResourceControlledPhaseShift, ResourceCRot, ResourceCRX, @@ -28,6 +31,8 @@ ResourceCY, ResourceCZ, ResourceMultiControlledX, + ResourcePow, + ResourceProd, ResourceTempAND, ResourceToffoli, ) diff --git a/pennylane/labs/resource_estimation/ops/identity.py b/pennylane/labs/resource_estimation/ops/identity.py index be159e9cc2d..e6ce9aad824 100644 --- a/pennylane/labs/resource_estimation/ops/identity.py +++ b/pennylane/labs/resource_estimation/ops/identity.py @@ -1,4 +1,4 @@ -# Copyright 2024 Xanadu Quantum Technologies Inc. +# Copyright 2025 Xanadu Quantum Technologies Inc. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,10 +13,12 @@ # limitations under the License. r"""Resource operators for identity and global phase operations.""" +from pennylane.labs import resource_estimation as plre from pennylane.labs.resource_estimation.resource_operator import ( CompressedResourceOp, GateCount, ResourceOperator, + resource_rep, ) # pylint: disable=arguments-differ,no-self-use,too-many-ancestors @@ -211,3 +213,47 @@ def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: in the decomposition. """ return [GateCount(cls.resource_rep())] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires: int, + ctrl_num_ctrl_values: int, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): The number of control qubits, that are controlled when + in the :math:`|0\rangle` state. + + Resources: + The resources are generated from the fact that a global phase controlled on a + single qubit is equivalent to a local phase shift on that control qubit. + This idea can be generalized to a multi-qubit global phase by introducing one + 'clean' auxilliary qubit which gets reset at the end of the computation. In this + case, we sandwich the phase shift operation with two multi-controlled X gates. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1: + gate_types = [GateCount(resource_rep(plre.ResourcePhaseShift))] + + if ctrl_num_ctrl_values: + gate_types.append(GateCount(resource_rep(plre.ResourceX), 2)) + + return gate_types + + ps = resource_rep(plre.ResourcePhaseShift) + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + + return [GateCount(ps), GateCount(mcx, 2)] diff --git a/pennylane/labs/resource_estimation/ops/op_math/__init__.py b/pennylane/labs/resource_estimation/ops/op_math/__init__.py index 92bace670d7..8d2d399c429 100644 --- a/pennylane/labs/resource_estimation/ops/op_math/__init__.py +++ b/pennylane/labs/resource_estimation/ops/op_math/__init__.py @@ -11,7 +11,8 @@ # 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. -r"""This module contains experimental resource estimation functionality.""" +r"""This module contains classes which integrate arithemtic operators with +resource estimation.""" from .controlled_ops import ( ResourceCCZ, @@ -29,3 +30,11 @@ ResourceTempAND, ResourceToffoli, ) + +from .symbolic import ( + ResourceAdjoint, + ResourceChangeBasisOp, + ResourceControlled, + ResourcePow, + ResourceProd, +) diff --git a/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py b/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py index 24b7d1e8cd1..df463080dd1 100644 --- a/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py +++ b/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py @@ -22,7 +22,7 @@ resource_rep, ) -# pylint: disable=arguments-differ,too-many-ancestors,too-many-arguments,too-many-positional-arguments +# pylint: disable=arguments-differ,too-many-ancestors,too-many-arguments,too-many-positional-arguments,unused-argument class ResourceCH(ResourceOperator): @@ -112,6 +112,38 @@ def default_adjoint_resource_decomp(cls) -> list[GateCount]: """ return [GateCount(cls.resource_rep())] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources + are computed according to the :code:`controlled_resource_decomp()` of the base + :class:`~.ResourceHadamard` class. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + ctrl_h = resource_rep( + re.ResourceControlled, + { + "base_cmpr_op": resource_rep(re.ResourceHadamard), + "num_ctrl_wires": ctrl_num_ctrl_wires + 1, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(ctrl_h)] + @classmethod def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -191,8 +223,11 @@ def default_resource_decomp(cls, **kwargs) -> list[GateCount]: obtain the controlled decomposition. Specifically, the resources are defined as a :class:`~.ResourceCNOT` gate conjugated by a pair of :class:`~.ResourceS` gates. """ - # TODO: To be added after the symbolic PR - raise re.ResourcesNotDefined + cnot = resource_rep(ResourceCNOT) + s = resource_rep(re.ResourceS) + s_dag = resource_rep(re.ResourceAdjoint, {"base_cmpr_op": s}) + + return [GateCount(cnot), GateCount(s), GateCount(s_dag)] @classmethod def default_adjoint_resource_decomp(cls) -> list[GateCount]: @@ -209,6 +244,38 @@ def default_adjoint_resource_decomp(cls) -> list[GateCount]: """ return [GateCount(cls.resource_rep())] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources + are computed according to the :code:`controlled_resource_decomp()` of the base + :class:`~.ResourceY` class. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + ctrl_y = resource_rep( + re.ResourceControlled, + { + "base_cmpr_op": resource_rep(re.ResourceY), + "num_ctrl_wires": ctrl_num_ctrl_wires + 1, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(ctrl_y)] + @classmethod def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -308,6 +375,39 @@ def default_adjoint_resource_decomp(cls) -> list[GateCount]: """ return [GateCount(cls.resource_rep())] + @classmethod + def default_controlled_resource_decomp( + cls, ctrl_num_ctrl_wires, ctrl_num_ctrl_values + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources + are computed according to the :code:`controlled_resource_decomp()` of the base + :class:`~.ResourceZ` class. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1 and ctrl_num_ctrl_values == 0: + return [GateCount(resource_rep(re.ResourceCCZ))] + + ctrl_z = resource_rep( + re.ResourceControlled, + { + "base_cmpr_op": resource_rep(re.ResourceZ), + "num_ctrl_wires": ctrl_num_ctrl_wires + 1, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(ctrl_z)] + @classmethod def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -412,6 +512,36 @@ def default_adjoint_resource_decomp(cls) -> list[GateCount]: """ return [GateCount(cls.resource_rep())] + @classmethod + def default_controlled_resource_decomp( + cls, ctrl_num_ctrl_wires, ctrl_num_ctrl_values + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources + are computed according to the :code:`controlled_resource_decomp()` of the base + :class:`~.ResourceSWAP` class. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + ctrl_swap = resource_rep( + re.ResourceControlled, + { + "base_cmpr_op": resource_rep(re.ResourceSWAP), + "num_ctrl_wires": ctrl_num_ctrl_wires + 1, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(ctrl_swap)] + @classmethod def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -510,6 +640,39 @@ def default_adjoint_resource_decomp(cls, **kwargs): """ return [GateCount(cls.resource_rep())] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources + are computed according to the :code:`controlled_resource_decomp()` of the base + :class:`~.ResourceZ` class. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + ctrl_z = resource_rep( + re.ResourceControlled, + { + "base_cmpr_op": resource_rep(re.ResourceZ), + "num_ctrl_wires": ctrl_num_ctrl_wires + 2, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + + return [GateCount(ctrl_z)] + @classmethod def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -794,6 +957,43 @@ def __init__(self, elbow=None, wires=None) -> None: self.elbow = elbow super().__init__(wires=wires) + @staticmethod + def elbow_decomp(elbow="left"): + """A function that prepares the resource decomposition obtained from Figure 4 of + `Babbush 2018 `_. + + Args: + elbow (str, optional): One of "left" or "right". Defaults to "left". + + Returns: + list[GateCount]: The resources of decomposing the elbow gates. + """ + gate_types = [] + t = resource_rep(re.ResourceT) + t_dag = resource_rep( + re.ResourceAdjoint, + {"base_cmpr_op": t}, + ) + h = resource_rep(re.ResourceHadamard) + cnot = resource_rep(ResourceCNOT) + s_dag = resource_rep( + re.ResourceAdjoint, + {"base_cmpr_op": resource_rep(re.ResourceS)}, + ) + cz = resource_rep(ResourceCZ) + + if elbow == "left": + gate_types.append(GateCount(t, 2)) + gate_types.append(GateCount(t_dag, 2)) + gate_types.append(GateCount(cnot, 3)) + gate_types.append(GateCount(s_dag)) + + if elbow == "right": + gate_types.append(GateCount(h)) + gate_types.append(GateCount(cz)) + + return gate_types + @classmethod def default_resource_decomp(cls, elbow=None, **kwargs) -> list[GateCount]: r"""Returns a list of GateCount objects representing the resources of the operator. @@ -822,8 +1022,29 @@ def default_resource_decomp(cls, elbow=None, **kwargs) -> list[GateCount]: :class:`~.ResourceHadamard` gates, one :class:`~.ResourceCZ` gate, one :class:`~.ResourceS` gate, two :class:`~.ResourceT` gates and two adjoint :class:`~.ResourceT` gates. """ - # TODO: To be added after the symbolic op PR - raise re.ResourcesNotDefined + if elbow: + return ResourceToffoli.elbow_decomp(elbow) + + cnot = resource_rep(ResourceCNOT) + t = resource_rep(re.ResourceT) + h = resource_rep(re.ResourceHadamard) + s = resource_rep(re.ResourceS) + cz = resource_rep(ResourceCZ) + t_dag = resource_rep( + re.ResourceAdjoint, + {"base_cmpr_op": t}, + ) + + return [ + AllocWires(2), + GateCount(cnot, 9), + GateCount(h, 3), + GateCount(s), + GateCount(cz), + GateCount(t, 2), + GateCount(t_dag, 2), + FreeWires(2), + ] @classmethod def textbook_resource_decomp(cls, elbow=None, **kwargs) -> list[GateCount]: @@ -849,8 +1070,18 @@ def textbook_resource_decomp(cls, elbow=None, **kwargs) -> list[GateCount]: :class:`~.ResourceHadamard` gates, four :class:`~.ResourceT` gates and three adjoint :class:`~.ResourceT` gates. """ - # TODO: To be added after the symbolic op PR - raise re.ResourcesNotDefined + if elbow: + return ResourceToffoli.elbow_decomp(elbow) + + cnot = resource_rep(ResourceCNOT) + t = resource_rep(re.ResourceT) + h = resource_rep(re.ResourceHadamard) + t_dag = resource_rep( + re.ResourceAdjoint, + {"base_cmpr_op": t}, + ) + + return [GateCount(cnot, 6), GateCount(h, 2), GateCount(t, 4), GateCount(t_dag, 3)] @property def resource_params(self) -> dict: @@ -893,6 +1124,7 @@ def default_controlled_resource_decomp( cls, ctrl_num_ctrl_wires, ctrl_num_ctrl_values, + elbow=None, ) -> list[GateCount]: r"""Returns a list representing the resources for a controlled version of the operator. @@ -951,7 +1183,16 @@ class ResourceMultiControlledX(ResourceOperator): Resources: The resources are obtained based on the unary iteration technique described in - `Babbush 2018 `_. + `Babbush 2018 `_. Specifically, the + resources are defined as the following rules: + + * If there are no control qubits, treat the operation as a :class:`~.labs.resource_estimation.ResourceX` gate. + + * If there is only one control qubit, treat the resources as a :class:`~.labs.resource_estimation.ResourceCNOT` gate. + + * If there are two control qubits, treat the resources as a :class:`~.labs.resource_estimation.ResourceToffoli` gate. + + * If there are three or more control qubits (:math:`n`), the resources obtained based on the unary iteration technique described in `Babbush 2018 `_. Specifically, it requires :math:`n - 2` clean qubits, and produces :math:`n - 2` elbow gates and a single :class:`~.labs.resource_estimation.ResourceToffoli`. .. seealso:: :class:`~.MultiControlledX` @@ -1023,7 +1264,17 @@ def default_resource_decomp( Resources: The resources are obtained based on the unary iteration technique described in - `Babbush 2018 `_. + `Babbush 2018 `_. Specifically, the + resources are defined as the following rules: + + * If there are no control qubits, treat the operation as a :class:`~.labs.resource_estimation.ResourceX` gate. + + * If there is only one control qubit, treat the resources as a :class:`~.labs.resource_estimation.ResourceCNOT` gate. + + * If there are two control qubits, treat the resources as a :class:`~.labs.resource_estimation.ResourceToffoli` gate. + + * If there are three or more control qubits (:math:`n`), the resources obtained based on the unary iteration technique described in `Babbush 2018 `_. Specifically, it requires :math:`n - 2` clean qubits, and produces :math:`n - 2` elbow gates and a single :class:`~.labs.resource_estimation.ResourceToffoli`. + """ gate_lst = [] @@ -1047,13 +1298,8 @@ def default_resource_decomp( gate_lst.append(GateCount(toffoli)) return gate_lst - if num_ctrl_wires == 3: # assuming one work wire: - res = [AllocWires(1), GateCount(cnot, 2), GateCount(toffoli, 2), FreeWires(1)] - gate_lst.extend(res) - return gate_lst - l_elbow = resource_rep(ResourceTempAND) - r_elbow = resource_rep(ResourceToffoli, {"elbow": "right"}) + r_elbow = resource_rep(re.ResourceAdjoint, {"base_cmpr_op": l_elbow}) res = [ AllocWires(num_ctrl_wires - 2), @@ -1204,6 +1450,10 @@ def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: r"""Returns a list of GateCount objects representing the resources of the operator. Each GateCount object specifies a gate type and its total occurrence count. + Args: + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + Resources: The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay `_. In combination with the following identity: @@ -1236,6 +1486,41 @@ def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: """ return [GateCount(cls.resource_rep(eps))] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources + are computed according to the :code:`controlled_resource_decomp()` of the base + :class:`~.ResourceRX` class. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + ctrl_rx = resource_rep( + re.ResourceControlled, + { + "base_cmpr_op": resource_rep(re.ResourceRX, {"eps": eps}), + "num_ctrl_wires": ctrl_num_ctrl_wires + 1, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(ctrl_rx)] + @classmethod def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -1254,7 +1539,6 @@ def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: represents a specific quantum gate and the number of times it appears in the decomposition. """ - return [GateCount(cls.resource_rep(eps))] @@ -1308,7 +1592,6 @@ def resource_params(self) -> dict: def resource_rep(cls, eps=None) -> CompressedResourceOp: r"""Returns a compressed representation containing only the parameters of the Operator that are needed to compute the resources.""" - return CompressedResourceOp(cls, {"eps": eps}) @classmethod @@ -1316,6 +1599,10 @@ def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: r"""Returns a list of GateCount objects representing the resources of the operator. Each GateCount object specifies a gate type and its total occurrence count. + Args: + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + Resources: The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay `_. In combination with the following identity: @@ -1346,6 +1633,41 @@ def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: """ return [GateCount(cls.resource_rep(eps))] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources + are computed according to the :code:`controlled_resource_decomp()` of the base + :class:`~.ResourceRY` class. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + ctrl_ry = resource_rep( + re.ResourceControlled, + { + "base_cmpr_op": resource_rep(re.ResourceRY, {"eps": eps}), + "num_ctrl_wires": ctrl_num_ctrl_wires + 1, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(ctrl_ry)] + @classmethod def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -1364,7 +1686,6 @@ def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: represents a specific quantum gate and the number of times it appears in the decomposition. """ - return [GateCount(cls.resource_rep(eps))] @@ -1425,6 +1746,10 @@ def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: r"""Returns a list of GateCount objects representing the resources of the operator. Each GateCount object specifies a gate type and its total occurrence count. + Args: + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + Resources: The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay `_. In combination with the following identity: @@ -1455,6 +1780,41 @@ def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: """ return [GateCount(cls.resource_rep(eps))] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources + are computed according to the :code:`controlled_resource_decomp()` of the base + :class:`~.ResourceRZ` class. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + ctrl_rz = resource_rep( + re.ResourceControlled, + { + "base_cmpr_op": resource_rep(re.ResourceRZ, {"eps": eps}), + "num_ctrl_wires": ctrl_num_ctrl_wires + 1, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(ctrl_rz)] + @classmethod def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -1473,7 +1833,6 @@ def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: represents a specific quantum gate and the number of times it appears in the decomposition. """ - return [GateCount(cls.resource_rep(eps))] @@ -1535,7 +1894,6 @@ def resource_params(self) -> dict: def resource_rep(cls, eps=None) -> CompressedResourceOp: r"""Returns a compressed representation containing only the parameters of the Operator that are needed to compute the resources.""" - return CompressedResourceOp(cls, {"eps": eps}) @classmethod @@ -1543,6 +1901,10 @@ def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: r"""Returns a list of GateCount objects representing the resources of the operator. Each GateCount object specifies a gate type and its total occurrence count. + Args: + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + Resources: The resources are taken from Figure 1b of `Gheorghiu, V., Mosca, M. & Mukhopadhyay `_. In combination with the following identity: @@ -1584,6 +1946,41 @@ def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: """ return [GateCount(cls.resource_rep(eps))] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources + are computed according to the :code:`controlled_resource_decomp()` of the base + :class:`~.ResourceRot` class. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + ctrl_rot = resource_rep( + re.ResourceControlled, + { + "base_cmpr_op": resource_rep(re.ResourceRot, {"eps": eps}), + "num_ctrl_wires": ctrl_num_ctrl_wires + 1, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(ctrl_rot)] + @classmethod def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -1602,7 +1999,6 @@ def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: represents a specific quantum gate and the number of times it appears in the decomposition. """ - return [GateCount(cls.resource_rep(eps))] @@ -1656,13 +2052,35 @@ def resource_params(self): def resource_rep(cls, eps=None) -> CompressedResourceOp: r"""Returns a compressed representation containing only the parameters of the Operator that are needed to compute the resources.""" - return CompressedResourceOp(cls, {"eps": eps}) @classmethod def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: r"""Returns a list of GateCount objects representing the resources of the operator. - Each GateCount object specifies a gate type and its total occurrence count. + + Args: + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + The resources are derived using the fact that a :class:`~.ResourcePhaseShift` gate is + identical to the :class:`~.ResourceRZ` gate up to some global phase. Furthermore, a controlled + global phase simplifies to a :class:`~.ResourcePhaseShift` gate. This gives rise to the + following identity: + + .. math:: CR_\phi(\phi) = (R_\phi(\phi/2) \otimes I) \cdot CNOT \cdot (I \otimes R_\phi(-\phi/2)) \cdot CNOT \cdot (I \otimes R_\phi(\phi/2)) + + Specifically, the resources are defined as two :class:`~.ResourceCNOT` gates and three + :class:`~.ResourceRZ` gates. + + .. seealso:: :class:`~.ControlledPhaseShift` + + **Example** + + The resources for this operation are computed using: + + >>> re.ResourceControlledPhaseShift.resource_decomp() + [(2 x CNOT), (3 x RZ)] """ cnot = resource_rep(ResourceCNOT) rz = resource_rep(re.ResourceRZ, {"eps": eps}) @@ -1683,6 +2101,41 @@ def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: """ return [GateCount(cls.resource_rep(eps))] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float, optional): The error threshold for clifford plus T decomposition of the rotation gate. + The default value is `None` which corresponds to using the epsilon stated in the config. + + Resources: + The resources are expressed using the symbolic :class:`~.ResourceControlled`. The resources + are computed according to the :code:`controlled_resource_decomp()` of the base + :class:`~.ResourcePhaseShift` class. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + ctrl_ps = resource_rep( + re.ResourceControlled, + { + "base_cmpr_op": resource_rep(re.ResourcePhaseShift, {"eps": eps}), + "num_ctrl_wires": ctrl_num_ctrl_wires + 1, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(ctrl_ps)] + @classmethod def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -1701,5 +2154,4 @@ def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: represents a specific quantum gate and the number of times it appears in the decomposition. """ - return [GateCount(cls.resource_rep(eps))] diff --git a/pennylane/labs/resource_estimation/ops/op_math/symbolic.py b/pennylane/labs/resource_estimation/ops/op_math/symbolic.py new file mode 100644 index 00000000000..96315465a6a --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/op_math/symbolic.py @@ -0,0 +1,1121 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""Resource operators for symbolic operations.""" +from functools import singledispatch +from typing import Dict, Iterable, Tuple, Union + +import pennylane.labs.resource_estimation as re +from pennylane.labs.resource_estimation.qubit_manager import AllocWires, FreeWires +from pennylane.labs.resource_estimation.resource_operator import ( + CompressedResourceOp, + GateCount, + ResourceOperator, + ResourcesNotDefined, + resource_rep, +) +from pennylane.queuing import QueuingManager +from pennylane.wires import Wires + +# pylint: disable=too-many-ancestors,arguments-differ,protected-access,too-many-arguments,too-many-positional-arguments,super-init-not-called + + +class ResourceAdjoint(ResourceOperator): + r"""Resource class for the symbolic Adjoint operation. + + A symbolic class used to represent the adjoint of some base operation. + + Args: + base_op (~.pennylane.labs.resource_estimation.ResourceOperator): The operator that we + want the adjoint of. + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + This symbolic operation represents the adjoint of some base operation. The resources are + determined as follows. If the base operation implements the + :code:`.default_adjoint_resource_decomp()` method, then the resources are obtained from + this. + + Otherwise, the adjoint resources are given as the adjoint of each operation in the + base operation's resources. + + .. seealso:: :class:`~.ops.op_math.adjoint.AdjointOperation` + + **Example** + + The adjoint operation can be constructed like this: + + >>> qft = plre.ResourceQFT(num_wires=3) + >>> adj_qft = plre.ResourceAdjoint(qft) + + We can see how the resources differ by choosing a suitable gateset and estimating resources: + + >>> gate_set = { + ... "SWAP", + ... "Adjoint(SWAP)", + ... "Hadamard", + ... "Adjoint(Hadamard)", + ... "ControlledPhaseShift", + ... "Adjoint(ControlledPhaseShift)", + ... } + >>> + >>> print(plre.estimate_resources(qft, gate_set)) + --- Resources: --- + Total qubits: 3 + Total gates : 7 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3 + Gate breakdown: + {'Hadamard': 3, 'SWAP': 1, 'ControlledPhaseShift': 3} + >>> + >>> print(plre.estimate_resources(adj_qft, gate_set)) + --- Resources: --- + Total qubits: 3 + Total gates : 7 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3 + Gate breakdown: + {'Adjoint(ControlledPhaseShift)': 3, 'Adjoint(SWAP)': 1, 'Adjoint(Hadamard)': 3} + + """ + + resource_keys = {"base_cmpr_op"} + + def __init__(self, base_op: ResourceOperator, wires=None) -> None: + self.queue(remove_op=base_op) + base_cmpr_op = base_op.resource_rep_from_op() + + self.base_op = base_cmpr_op + + if wires: + self.wires = Wires(wires) + self.num_wires = len(self.wires) + else: + self.wires = None or base_op.wires + self.num_wires = base_op.num_wires + + def queue(self, remove_op, context: QueuingManager = QueuingManager): + """Append the operator to the Operator queue.""" + context.remove(remove_op) + context.append(self) + return self + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * base_cmpr_op (~.pennylane.labs.resource_estimation.ResourceOperator): The operator + that we want the adjoint of. + + """ + return {"base_cmpr_op": self.base_op} + + @classmethod + def resource_rep(cls, base_cmpr_op) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + base_cmpr_op (~.pennylane.labs.resource_estimation.ResourceOperator): The operator + that we want the adjoint of. + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"base_cmpr_op": base_cmpr_op}) + + @classmethod + def default_resource_decomp(cls, base_cmpr_op: CompressedResourceOp, **kwargs): + r"""Returns a list representing the resources of the operator. Each object represents a + quantum gate and the number of times it occurs in the decomposition. + + Args: + base_cmpr_op (~.pennylane.labs.resource_estimation.CompressedResourceOp): A + compressed resource representation for the operator we want the adjoint of. + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + This symbolic operation represents the adjoint of some base operation. The resources are + determined as follows. If the base operation implements the + :code:`.default_adjoint_resource_decomp()` method, then the resources are obtained from + this. + + Otherwise, the adjoint resources are given as the adjoint of each operation in the + base operation's resources. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + + .. seealso:: :class:`~.ops.op_math.adjoint.AdjointOperation` + + **Example** + + The adjoint operation can be constructed like this: + + >>> qft = plre.ResourceQFT(num_wires=3) + >>> adj_qft = plre.ResourceAdjoint(qft) + + We can see how the resources differ by choosing a suitable gateset and estimating resources: + + >>> gate_set = { + ... "SWAP", + ... "Adjoint(SWAP)", + ... "Hadamard", + ... "Adjoint(Hadamard)", + ... "ControlledPhaseShift", + ... "Adjoint(ControlledPhaseShift)", + ... } + >>> + >>> print(plre.estimate_resources(qft, gate_set)) + --- Resources: --- + Total qubits: 3 + Total gates : 7 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3 + Gate breakdown: + {'Hadamard': 3, 'SWAP': 1, 'ControlledPhaseShift': 3} + >>> + >>> print(plre.estimate_resources(adj_qft, gate_set)) + --- Resources: --- + Total qubits: 3 + Total gates : 7 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3 + Gate breakdown: + {'Adjoint(ControlledPhaseShift)': 3, 'Adjoint(SWAP)': 1, 'Adjoint(Hadamard)': 3} + + """ + base_class, base_params = (base_cmpr_op.op_type, base_cmpr_op.params) + try: + return base_class.adjoint_resource_decomp(**base_params) + except ResourcesNotDefined: + gate_lst = [] + decomp = base_class.resource_decomp(**base_params, **kwargs) + + for gate in decomp[::-1]: # reverse the order + gate_lst.append(_apply_adj(gate)) + return gate_lst + + @classmethod + def default_adjoint_resource_decomp(cls, base_cmpr_op: CompressedResourceOp): + r"""Returns a list representing the resources for the adjoint of the operator. + + Args: + base_cmpr_op (~.pennylane.labs.resource_estimation.CompressedResourceOp): A + compressed resource representation for the operator we want the adjoint of. + + Resources: + The adjoint of an adjointed operation is just the original operation. The resources + are given as one instance of the base operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(base_cmpr_op)] + + @staticmethod + def tracking_name(base_cmpr_op: CompressedResourceOp) -> str: + r"""Returns the tracking name built with the operator's parameters.""" + base_name = base_cmpr_op.name + return f"Adjoint({base_name})" + + +class ResourceControlled(ResourceOperator): + r"""Resource class for the symbolic Controlled operation. + + A symbolic class used to represent the application of some base operation controlled on the + state of some control qubits. + + Args: + base_op (~.pennylane.labs.resource_estimation.ResourceOperator): The base operator to be + controlled. + num_ctrl_wires (int): the number of qubits the operation is controlled on + num_ctrl_values (int): the number of control qubits, that are controlled when in the + :math:`|0\rangle` state + + Resources: + The resources are determined as follows. If the base operator implements the + :code:`.controlled_resource_decomp()` method, then the resources are obtained directly from + this. + + Otherwise, the controlled resources are given in two steps. Firstly, any control qubits + which should be triggered when in the :math:`|0\rangle` state, are flipped. This corresponds + to an additional cost of two :class:`~.ResourceX` gates per :code:`num_ctrl_values`. + Secondly, the base operation resources are extracted and we add to the cost the controlled + variant of each operation in the resources. + + .. seealso:: :class:`~.ops.op_math.controlled.ControlledOp` + + **Example** + + The controlled operation can be constructed like this: + + >>> x = plre.ResourceX() + >>> cx = plre.ResourceControlled(x, num_ctrl_wires=1, num_ctrl_values=0) + >>> ccx = plre.ResourceControlled(x, num_ctrl_wires=2, num_ctrl_values=2) + + We can observe the expected gates when we estimate the resources. + + >>> print(plre.estimate_resources(cx)) + --- Resources: --- + Total qubits: 2 + Total gates : 1 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 2 + Gate breakdown: + {'CNOT': 1} + >>> + >>> print(plre.estimate_resources(ccx)) + --- Resources: --- + Total qubits: 3 + Total gates : 5 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3 + Gate breakdown: + {'X': 4, 'Toffoli': 1} + + """ + + resource_keys = {"base_cmpr_op", "num_ctrl_wires", "num_ctrl_values"} + + def __init__( + self, + base_op: ResourceOperator, + num_ctrl_wires: int, + num_ctrl_values: int, + wires=None, + ) -> None: + self.queue(remove_base_op=base_op) + base_cmpr_op = base_op.resource_rep_from_op() + + self.base_op = base_cmpr_op + self.num_ctrl_wires = num_ctrl_wires + self.num_ctrl_values = num_ctrl_values + + if wires: + self.wires = Wires(wires) + self.num_wires = len(self.wires) + else: + self.wires = None + num_base_wires = base_op.num_wires + self.num_wires = num_ctrl_wires + num_base_wires + + def queue(self, remove_base_op, context: QueuingManager = QueuingManager): + """Append the operator to the Operator queue.""" + context.remove(remove_base_op) + context.append(self) + return self + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * base_cmpr_op (~.pennylane.labs.resource_estimation.CompressedResourceOp): The base + operator to be controlled. + * num_ctrl_wires (int): the number of qubits the operation is controlled on + * num_ctrl_values (int): the number of control qubits, that are controlled when in the + :math:`|0\rangle` state + """ + + return { + "base_cmpr_op": self.base_op, + "num_ctrl_wires": self.num_ctrl_wires, + "num_ctrl_values": self.num_ctrl_values, + } + + @classmethod + def resource_rep( + cls, + base_cmpr_op, + num_ctrl_wires, + num_ctrl_values, + ) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + base_cmpr_op (~.pennylane.labs.resource_estimation.CompressedResourceOp): The base + operator to be controlled. + num_ctrl_wires (int): the number of qubits the operation is controlled on + num_ctrl_values (int): the number of control qubits, that are controlled when in the + :math:`|0\rangle` state + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp( + cls, + { + "base_cmpr_op": base_cmpr_op, + "num_ctrl_wires": num_ctrl_wires, + "num_ctrl_values": num_ctrl_values, + }, + ) + + @classmethod + def default_resource_decomp( + cls, base_cmpr_op, num_ctrl_wires, num_ctrl_values, **kwargs + ) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a + quantum gate and the number of times it occurs in the decomposition. + + Args: + base_cmpr_op (~.pennylane.labs.resource_estimation.CompressedResourceOp): The base + operator to be controlled. + num_ctrl_wires (int): the number of qubits the operation is controlled on + num_ctrl_values (int): the number of control qubits, that are controlled when in the + :math:`|0\rangle` state + + Resources: + The resources are determined as follows. If the base operator implements the + :code:`.controlled_resource_decomp()` method, then the resources are obtained directly from + this. + + Otherwise, the controlled resources are given in two steps. Firstly, any control qubits + which should be triggered when in the :math:`|0\rangle` state, are flipped. This corresponds + to an additional cost of two :class:`~.ResourceX` gates per :code:`num_ctrl_values`. + Secondly, the base operation resources are extracted and we add to the cost the controlled + variant of each operation in the resources. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + + .. seealso:: :class:`~.ops.op_math.controlled.ControlledOp` + + **Example** + + The controlled operation can be constructed like this: + + >>> x = plre.ResourceX() + >>> cx = plre.ResourceControlled(x, num_ctrl_wires=1, num_ctrl_values=0) + >>> ccx = plre.ResourceControlled(x, num_ctrl_wires=2, num_ctrl_values=2) + + We can observe the expected gates when we estimate the resources. + + >>> print(plre.estimate_resources(cx)) + --- Resources: --- + Total qubits: 2 + Total gates : 1 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 2 + Gate breakdown: + {'CNOT': 1} + >>> + >>> print(plre.estimate_resources(ccx)) + --- Resources: --- + Total qubits: 3 + Total gates : 5 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3 + Gate breakdown: + {'X': 4, 'Toffoli': 1} + + """ + + base_class, base_params = (base_cmpr_op.op_type, base_cmpr_op.params) + try: + return base_class.controlled_resource_decomp( + ctrl_num_ctrl_wires=num_ctrl_wires, + ctrl_num_ctrl_values=num_ctrl_values, + **base_params, + ) + except re.ResourcesNotDefined: + pass + + gate_lst = [] + if num_ctrl_values != 0: + x = resource_rep(re.ResourceX) + gate_lst.append(GateCount(x, 2 * num_ctrl_values)) + + decomp = base_class.resource_decomp(**base_params, **kwargs) + for action in decomp: + if isinstance(action, GateCount): + gate = action.gate + c_gate = cls.resource_rep( + gate, + num_ctrl_wires, + num_ctrl_values=0, # we flipped already and added the X gates above + ) + gate_lst.append(GateCount(c_gate, action.count)) + + else: + gate_lst.append(action) + + return gate_lst + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + base_cmpr_op, + num_ctrl_wires, + num_ctrl_values, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): The number of control qubits to further control the base + controlled operation upon. + ctrl_num_ctrl_values (int): The subset of those control qubits, which further control + the base controlled operation, which are controlled when in the :math:`|0\rangle` state. + base_cmpr_op (~.pennylane.labs.resource_estimation.CompressedResourceOp): The base + operator to be controlled. + num_ctrl_wires (int): the number of control qubits of the operation + num_ctrl_values (int): The subset of control qubits of the operation, that are controlled + when in the :math:`|0\rangle` state. + + Resources: + The resources are derived by simply combining the control qubits, control-values and + work qubits into a single instance of :class:`~.ResourceControlled` gate, controlled + on the whole set of control-qubits. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [ + GateCount( + cls.resource_rep( + base_cmpr_op, + ctrl_num_ctrl_wires + num_ctrl_wires, + ctrl_num_ctrl_values + num_ctrl_values, + ) + ), + ] + + @staticmethod + def tracking_name( + base_cmpr_op: CompressedResourceOp, + num_ctrl_wires: int, + num_ctrl_values: int, + ): + r"""Returns the tracking name built with the operator's parameters.""" + base_name = base_cmpr_op.name + return f"C({base_name}, num_ctrl_wires={num_ctrl_wires},num_ctrl_values={num_ctrl_values})" + + +class ResourcePow(ResourceOperator): + r"""Resource class for the symbolic Pow operation. + + A symbolic class used to represent some base operation raised to a power. + + Args: + base_op (~.pennylane.labs.resource_estimation.ResourceOperator): The operator that we + want to exponentiate. + z (float): the exponent (default value is 1) + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources are determined as follows. If the power :math:`z = 0`, then we have the identitiy + gate and we have no resources. If the base operation class :code:`base_class` implements the + :code:`.pow_resource_decomp()` method, then the resources are obtained from this. Otherwise, + the resources of the operation raised to the power :math:`z` are given by extracting the base + operation's resources (via :code:`.resources()`) and raising each operation to the same power. + + .. seealso:: :class:`~.ops.op_math.pow.PowOperation` + + **Example** + + The operation raised to a power :math:`z` can be constructed like this: + + >>> z = plre.ResourceZ() + >>> z_2 = plre.ResourcePow(z, 2) + >>> z_5 = plre.ResourcePow(z, 5) + + We obtain the expected resources. + + >>> print(plre.estimate_resources(z_2, gate_set={"Identity", "Z"})) + --- Resources: --- + Total qubits: 1 + Total gates : 1 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'Identity': 1} + >>> + >>> print(plre.estimate_resources(z_5, gate_set={"Identity", "Z"})) + --- Resources: --- + Total qubits: 1 + Total gates : 1 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'Z': 1} + + """ + + resource_keys = {"base_cmpr_op", "z"} + + def __init__(self, base_op: ResourceOperator, z: int, wires=None) -> None: + self.queue(remove_op=base_op) + base_cmpr_op = base_op.resource_rep_from_op() + + self.z = z + self.base_op = base_cmpr_op + + if wires: + self.wires = Wires(wires) + self.num_wires = len(self.wires) + else: + self.wires = None or base_op.wires + self.num_wires = base_op.num_wires + + def queue(self, remove_op, context: QueuingManager = QueuingManager): + """Append the operator to the Operator queue.""" + context.remove(remove_op) + context.append(self) + return self + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * base_class (Type[~.pennylane.labs.resource_estimation.ResourceOperator]): The class type of the base operator to be raised to some power. + * base_params (dict): the resource parameters required to extract the cost of the base operator + * z (int): the power that the operator is being raised to + """ + return { + "base_cmpr_op": self.base_op, + "z": self.z, + } + + @classmethod + def resource_rep(cls, base_cmpr_op, z) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + base_class (Type[~.pennylane.labs.resource_estimation.ResourceOperator]): The class type of the base operator to be raised to some power. + base_params (dict): the resource parameters required to extract the cost of the base operator + z (int): the power that the operator is being raised to + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"base_cmpr_op": base_cmpr_op, "z": z}) + + @classmethod + def default_resource_decomp(cls, base_cmpr_op, z, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object represents a + quantum gate and the number of times it occurs in the decomposition. + + Args: + base_cmpr_op (~.pennylane.labs.resource_estimation.CompressedResourceOp): A + compressed resource representation for the operator we want to exponentiate. + z (float): the exponent (default value is 1) + + Resources: + The resources are determined as follows. If the power :math:`z = 0`, then we have the identitiy + gate and we have no resources. If the base operation class :code:`base_class` implements the + :code:`.pow_resource_decomp()` method, then the resources are obtained from this. Otherwise, + the resources of the operation raised to the power :math:`z` are given by extracting the base + operation's resources (via :code:`.resources()`) and raising each operation to the same power. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + + .. seealso:: :class:`~.ops.op_math.pow.PowOperation` + + **Example** + + The operation raised to a power :math:`z` can be constructed like this: + + >>> z = plre.ResourceZ() + >>> z_2 = plre.ResourcePow(z, 2) + >>> z_5 = plre.ResourcePow(z, 5) + + We obtain the expected resources. + + >>> print(plre.estimate_resources(z_2, gate_set={"Identity", "Z"})) + --- Resources: --- + Total qubits: 1 + Total gates : 1 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'Identity': 1} + >>> + >>> print(plre.estimate_resources(z_5, gate_set={"Identity", "Z"})) + --- Resources: --- + Total qubits: 1 + Total gates : 1 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'Z': 1} + + """ + base_class, base_params = (base_cmpr_op.op_type, base_cmpr_op.params) + + if z == 0: + return [GateCount(resource_rep(re.ResourceIdentity))] + + if z == 1: + return [GateCount(base_cmpr_op)] + + try: + return base_class.pow_resource_decomp(pow_z=z, **base_params) + except re.ResourcesNotDefined: + return [GateCount(base_cmpr_op, z)] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, base_cmpr_op, z): + r"""Returns a list representing the resources of the operator. Each object represents a + quantum gate and the number of times it occurs in the decomposition. + + Args: + pow_z (int): the exponent that the pow-operator is being raised to + base_cmpr_op (~.pennylane.labs.resource_estimation.CompressedResourceOp): A + compressed resource representation for the operator we want to exponentiate. + z (float): the exponent that the base operator is being raised to (default value is 1) + + Resources: + The resources are derived by simply adding together the :math:`z` exponent and the + :math:`z_{0}` exponent into a single instance of :class:`~.ResourcePow` gate, raising + the base operator to the power :math:`z + z_{0}`. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(base_cmpr_op, pow_z * z))] + + @staticmethod + def tracking_name(base_cmpr_op: CompressedResourceOp, z: int) -> str: + r"""Returns the tracking name built with the operator's parameters.""" + base_name = base_cmpr_op.name + return f"Pow({base_name}, {z})" + + +class ResourceProd(ResourceOperator): + r"""Resource class for the symbolic Prod operation. + + A symbolic class used to represent a product of some base operations. + + Args: + res_ops (tuple[~.pennylane.labs.resource_estimation.ResourceOperator]): A tuple of + resource operators or a nested tuple of resource operators and counts. + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + This symbolic class represents a product of operations. The resources are defined trivially + as the counts for each operation in the product. + + .. seealso:: :class:`~.ops.op_math.prod.Prod` + + **Example** + + The product of operations can be constructed from a list of operations or + a nested tuple where each operator is accompanied with the number of counts. + Note, each operation in the product must be a valid :class:`~.pennylane.labs.resource_estimation.ResourceOperator` + + We can construct a product operator as follows: + + >>> factors = [plre.ResourceX(), plre.ResourceY(), plre.ResourceZ()] + >>> prod_xyz = plre.ResourceProd(factors) + >>> + >>> print(plre.estimate_resources(prod_xyz)) + --- Resources: --- + Total qubits: 1 + Total gates : 3 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'X': 1, 'Y': 1, 'Z': 1} + + We can also specify the factors as a tuple with + + >>> factors = [(plre.ResourceX(), 2), (plre.ResourceZ(), 3)] + >>> prod_x2z3 = plre.ResourceProd(factors) + >>> + >>> print(plre.estimate_resources(prod_x2z3)) + --- Resources: --- + Total qubits: 1 + Total gates : 5 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'X': 2, 'Z': 3} + + """ + + resource_keys = {"cmpr_factors_and_counts"} + + def __init__( + self, + res_ops: Iterable[Union[ResourceOperator, Tuple[int, ResourceOperator]]], + wires=None, + ) -> None: + + ops = [] + counts = [] + for op_or_tup in res_ops: + op, count = op_or_tup if isinstance(op_or_tup, tuple) else (op_or_tup, 1) + + ops.append(op) + counts.append(count) + + self.queue(ops) + + try: + cmpr_ops = tuple(op.resource_rep_from_op() for op in ops) + except AttributeError as error: + raise ValueError( + "All factors of the Product must be instances of `ResourceOperator` in order to obtain resources." + ) from error + + self.cmpr_factors_and_counts = tuple(zip(cmpr_ops, counts)) + + if wires: + self.wires = Wires(wires) + self.num_wires = len(self.wires) + else: + ops_wires = [op.wires for op in ops if op.wires is not None] + if len(ops_wires) == 0: + self.wires = None + self.num_wires = max((op.num_wires for op in ops)) + else: + self.wires = Wires.all_wires(ops_wires) + self.num_wires = len(self.wires) + + def queue(self, ops_to_remove, context: QueuingManager = QueuingManager): + """Append the operator to the Operator queue.""" + for op in ops_to_remove: + context.remove(op) + context.append(self) + return self + + @property + def resource_params(self) -> Dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * cmpr_factors_and_counts (Tuple[Tuple[~.labs.resource_estimation.CompressedResourceOp, int]]): + A sequence of tuples containing the operations, in the compressed representation, and + a count for how many times they are repeated corresponding to the factors in the product. + """ + return {"cmpr_factors_and_counts": self.cmpr_factors_and_counts} + + @classmethod + def resource_rep(cls, cmpr_factors_and_counts) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + cmpr_factors_and_counts (Tuple[Tuple[~.labs.resource_estimation.CompressedResourceOp, int]]): + A sequence of tuples containing the operations, in the compressed representation, and + a count for how many times they are repeated corresponding to the factors in the product. + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"cmpr_factors_and_counts": cmpr_factors_and_counts}) + + @classmethod + def default_resource_decomp(cls, cmpr_factors_and_counts, **kwargs): + r"""Returns a list representing the resources of the operator. Each object represents a + quantum gate and the number of times it occurs in the decomposition. + + Args: + cmpr_factors_and_counts (Tuple[Tuple[~.labs.resource_estimation.CompressedResourceOp, int]]): + A sequence of tuples containing the operations, in the compressed representation, and + a count for how many times they are repeated corresponding to the factors in the product. + + Resources: + This symbolic class represents a product of operations. The resources are defined + trivially as the counts for each operation in the product. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + + .. seealso:: :class:`~.ops.op_math.prod.Prod` + + **Example** + + The product of operations can be constructed as follows. Note, each operation in the + product must be a valid :class:`~.pennylane.labs.resource_estimation.ResourceOperator` + + >>> factors = [plre.ResourceX(), plre.ResourceY(), plre.ResourceZ()] + >>> prod_xyz = plre.ResourceProd(factors) + >>> + >>> print(plre.estimate_resources(prod_xyz)) + --- Resources: --- + Total qubits: 1 + Total gates : 3 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'X': 1, 'Y': 1, 'Z': 1} + + We can also specify the factors as a tuple with + + >>> factors = [(plre.ResourceX(), 2), (plre.ResourceZ(), 3)] + >>> prod_x2z3 = plre.ResourceProd(factors) + >>> + >>> print(plre.estimate_resources(prod_x2z3)) + --- Resources: --- + Total qubits: 1 + Total gates : 5 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'X': 2, 'Z': 3} + + """ + return [GateCount(cmpr_op, count) for cmpr_op, count in cmpr_factors_and_counts] + + +class ResourceChangeBasisOp(ResourceOperator): + r"""Change of Basis resource operator. + + A symbolic class used to represent a change of basis operation. This is a special + type of operator which can be expressed as + :math:`\hat{U}_{compute} \cdot \hat{V} \cdot \hat{U}_{uncompute}`. If no :code:`uncompute_op` is + provided then the adjoint of the :code:`compute_op` is used by default. + + Args: + compute_op (~.pennylane.labs.resource_estimation.ResourceOperator): A resource operator + representing the basis change operation. + base_op (~.pennylane.labs.resource_estimation.ResourceOperator): A resource operator + representing the base operation. + uncompute_op (~.pennylane.labs.resource_estimation.ResourceOperator, optional): An optional + resource operator representing the inverse of the basis change operation. + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + This symbolic class represents a product of the three provided operations. The resources are + defined trivially as the sum of the costs of each. + + .. seealso:: :class:`~.ops.op_math.prod.Prod` + + **Example** + + Note, each operation in the product must be a valid :class:`~.pennylane.labs.resource_estimation.ResourceOperator` + The change of basis operation can be constructed as follows: + + >>> compute_u = plre.ResourceS() + >>> base_v = plre.ResourceZ() + >>> cb_op = plre.ResourceChangeBasisOp(compute_u, base_v) + >>> print(plre.estimate_resources(cb_op, gate_set={"Z", "S", "Adjoint(S)"})) + --- Resources: --- + Total qubits: 1 + Total gates : 3 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'S': 1, 'Z': 1, 'Adjoint(S)': 1} + + We can also set the :code:`uncompute_op` directly. + + >>> uncompute_u = plre.ResourceProd([plre.ResourceZ(), plre.ResourceS()]) + >>> cb_op = plre.ResourceChangeBasisOp(compute_u, base_v, uncompute_u) + >>> print(plre.estimate_resources(cb_op, gate_set={"Z", "S", "Adjoint(S)"})) + --- Resources: --- + Total qubits: 1 + Total gates : 4 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'S': 2, 'Z': 2} + + """ + + resource_keys = {"cmpr_compute_op", "cmpr_base_op", "cmpr_uncompute_op"} + + def __init__( + self, + compute_op: ResourceOperator, + base_op: ResourceOperator, + uncompute_op: Union[None, ResourceOperator] = None, + wires=None, + ) -> None: + uncompute_op = uncompute_op or ResourceAdjoint(compute_op) + ops_to_remove = [compute_op, base_op, uncompute_op] + + self.queue(ops_to_remove) + + try: + self.cmpr_compute_op = compute_op.resource_rep_from_op() + self.cmpr_base_op = base_op.resource_rep_from_op() + self.cmpr_uncompute_op = uncompute_op.resource_rep_from_op() + + except AttributeError as error: + raise ValueError( + "All ops of the ChangeofBasisOp must be instances of `ResourceOperator` in order to obtain resources." + ) from error + + if wires: + self.wires = Wires(wires) + self.num_wires = len(self.wires) + else: + ops_wires = [op.wires for op in ops_to_remove if op.wires is not None] + if len(ops_wires) == 0: + self.wires = None + self.num_wires = max((op.num_wires for op in ops_to_remove)) + else: + self.wires = Wires.all_wires(ops_wires) + self.num_wires = len(self.wires) + + def queue(self, ops_to_remove, context: QueuingManager = QueuingManager): + """Append the operator to the Operator queue.""" + for op in ops_to_remove: + context.remove(op) + context.append(self) + return self + + @property + def resource_params(self) -> Dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * cmpr_compute_op (CompressedResourceOp): A compressed resource operator, corresponding + to the compute operation. + * cmpr_base_op (CompressedResourceOp): A compressed resource operator, corresponding + to the base operation. + * cmpr_uncompute_op (CompressedResourceOp): A compressed resource operator, corresponding + to the uncompute operation. + + """ + return { + "cmpr_compute_op": self.cmpr_compute_op, + "cmpr_base_op": self.cmpr_base_op, + "cmpr_uncompute_op": self.cmpr_uncompute_op, + } + + @classmethod + def resource_rep( + cls, cmpr_compute_op, cmpr_base_op, cmpr_uncompute_op=None + ) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to estimate the resources. + + Args: + cmpr_compute_op (CompressedResourceOp): A compressed resource operator, corresponding + to the compute operation. + cmpr_base_op (CompressedResourceOp): A compressed resource operator, corresponding + to the base operation. + cmpr_uncompute_op (CompressedResourceOp): A compressed resource operator, corresponding + to the uncompute operation. + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + cmpr_uncompute_op = cmpr_uncompute_op or resource_rep( + ResourceAdjoint, {"base_cmpr_op": cmpr_compute_op} + ) + return CompressedResourceOp( + cls, + { + "cmpr_compute_op": cmpr_compute_op, + "cmpr_base_op": cmpr_base_op, + "cmpr_uncompute_op": cmpr_uncompute_op, + }, + ) + + @classmethod + def default_resource_decomp(cls, cmpr_compute_op, cmpr_base_op, cmpr_uncompute_op, **kwargs): + r"""Returns a list representing the resources of the operator. Each object represents a + quantum gate and the number of times it occurs in the decomposition. + + Args: + cmpr_compute_op (CompressedResourceOp): A compressed resource operator, corresponding + to the compute operation. + cmpr_base_op (CompressedResourceOp): A compressed resource operator, corresponding + to the base operation. + cmpr_uncompute_op (CompressedResourceOp): A compressed resource operator, corresponding + to the uncompute operation. + + Resources: + This symbolic class represents a product of the three provided operations. The resources are + defined trivially as the sum of the costs of each. + + .. seealso:: :class:`~.ops.op_math.prod.Prod` + + **Example** + + Note, each operation in the product must be a valid :class:`~.pennylane.labs.resource_estimation.ResourceOperator` + The change of basis operation can be constructed as follows: + + >>> compute_u = plre.ResourceS() + >>> base_v = plre.ResourceZ() + >>> cb_op = plre.ResourceChangeBasisOp(compute_u, base_v) + >>> print(plre.estimate_resources(cb_op, gate_set={"Z", "S", "Adjoint(S)"})) + --- Resources: --- + Total qubits: 1 + Total gates : 3 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'S': 1, 'Z': 1, 'Adjoint(S)': 1} + + We can also set the :code:`uncompute_op` directly. + + >>> uncompute_u = plre.ResourceProd([plre.ResourceZ(), plre.ResourceS()]) + >>> cb_op = plre.ResourceChangeBasisOp(compute_u, base_v, uncompute_u) + >>> print(plre.estimate_resources(cb_op, gate_set={"Z", "S", "Adjoint(S)"})) + --- Resources: --- + Total qubits: 1 + Total gates : 4 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 1 + Gate breakdown: + {'S': 2, 'Z': 2} + + """ + return [ + GateCount(cmpr_compute_op), + GateCount(cmpr_base_op), + GateCount(cmpr_uncompute_op), + ] + + +@singledispatch +def _apply_adj(action): + raise TypeError(f"Unsupported type {action}") + + +@_apply_adj.register +def _(action: GateCount): + gate = action.gate + return GateCount(resource_rep(ResourceAdjoint, {"base_cmpr_op": gate}), action.count) + + +@_apply_adj.register +def _(action: AllocWires): + return FreeWires(action.num_wires) + + +@_apply_adj.register +def _(action: FreeWires): + return AllocWires(action.num_wires) diff --git a/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py b/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py index 8a1c7d71483..3cf3b098f9a 100644 --- a/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py +++ b/pennylane/labs/resource_estimation/ops/qubit/non_parametric_ops.py @@ -83,6 +83,67 @@ def default_adjoint_resource_decomp(cls) -> list[GateCount]: """ return [GateCount(cls.resource_rep(), 1)] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires: int, + ctrl_num_ctrl_values: int, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + For a single control wire, the cost is a single instance of :class:`~.ResourceCH`. + Two additional :class:`~.ResourceX` gates are used to flip the control qubit if + it is zero-controlled. + In the case where multiple controlled wires are provided, the resources are derived from + the following identities (as presented in this `blog post `_): + + .. math:: + + \begin{align} + \hat{H} &= \hat{R}_{y}(\frac{\pi}{4}) \cdot \hat{Z} \cdot \hat{R}_{y}(\frac{-\pi}{4}), \\ + \hat{Z} &= \hat{H} \cdot \hat{X} \cdot \hat{H}. + \end{align} + + Specifically, the resources are given by two :class:`~.ResourceRY` gates, two + :class:`~.ResourceHadamard` gates and a :class:`~.ResourceX` gate. By replacing the + :class:`~.ResourceX` gate with :class:`~.ResourceMultiControlledX` gate, we obtain a + controlled-version of this identity. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1: + gate_lst = [GateCount(resource_rep(plre.ResourceCH))] + + if ctrl_num_ctrl_values: + gate_lst.append(GateCount(resource_rep(plre.ResourceX), 2)) + + return gate_lst + + gate_lst = [] + + ry = resource_rep(plre.ResourceRY) + h = cls.resource_rep() + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + + gate_lst.append(GateCount(h, 2)) + gate_lst.append(GateCount(ry, 2)) + gate_lst.append(GateCount(mcx)) + return gate_lst + @classmethod def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -167,6 +228,52 @@ def default_adjoint_resource_decomp(cls) -> list[GateCount]: z = resource_rep(ResourceZ) return [GateCount(z, 1), GateCount(cls.resource_rep(), 1)] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ): + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + The S-gate is equivalent to the PhaseShift gate for some fixed phase. Given a single + control wire, the cost is therefore a single instance of + :class:`~.ResourceControlledPhaseShift`. Two additional :class:`~.ResourceX` gates are + used to flip the control qubit if it is zero-controlled. + + In the case where multiple controlled wires are provided, we can collapse the control + wires by introducing one 'clean' auxilliary qubit (which gets reset at the end). + In this case the cost increases by two additional :class:`~.ResourceMultiControlledX` gates, + as described in (Lemma 7.11) `Barenco et al. `_. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1: + gate_lst = [GateCount(resource_rep(plre.ResourceControlledPhaseShift))] + + if ctrl_num_ctrl_values: + gate_lst.append(GateCount(resource_rep(plre.ResourceX), 2)) + + return gate_lst + + cs = resource_rep(plre.ResourceControlledPhaseShift) + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(cs, 1), GateCount(mcx, 2)] + @classmethod def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -305,7 +412,7 @@ def default_resource_decomp(cls, **kwargs) -> list[GateCount]: @classmethod def default_adjoint_resource_decomp(cls) -> list[GateCount]: - r"""Returns a dictionary representing the resources for the adjoint of the operator. + r"""Returns a list representing the resources for the adjoint of the operator. Resources: This operation is self-adjoint, so the resources of the adjoint operation results @@ -322,7 +429,7 @@ def default_adjoint_resource_decomp(cls) -> list[GateCount]: def default_controlled_resource_decomp( cls, ctrl_num_ctrl_wires, ctrl_num_ctrl_values ) -> list[GateCount]: - r"""Returns a dictionary representing the resources for a controlled version of the operator. + r"""Returns a list representing the resources for a controlled version of the operator. Args: ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on @@ -363,7 +470,7 @@ def default_controlled_resource_decomp( @classmethod def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: - r"""Returns a dictionary representing the resources for an operator raised to a power. + r"""Returns a list representing the resources for an operator raised to a power. Args: pow_z (int): the power that the operator is being raised to @@ -444,6 +551,48 @@ def default_adjoint_resource_decomp(cls) -> list[GateCount]: s = resource_rep(ResourceS) return [GateCount(cls.resource_rep()), GateCount(s), GateCount(z)] + @classmethod + def default_controlled_resource_decomp(cls, ctrl_num_ctrl_wires, ctrl_num_ctrl_values): + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + The T-gate is equivalent to the PhaseShift gate for some fixed phase. Given a single + control wire, the cost is therefore a single instance of + :class:`~.ResourceControlledPhaseShift`. Two additional :class:`~.ResourceX` gates are + used to flip the control qubit if it is zero-controlled. + + In the case where multiple controlled wires are provided, we can collapse the control + wires by introducing one 'clean' auxilliary qubit (which gets reset at the end). + In this case the cost increases by two additional :class:`~.ResourceMultiControlledX` gates, + as described in (Lemma 7.11) `Barenco et al. `_. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1: + gate_types = [GateCount(resource_rep(plre.ResourceControlledPhaseShift))] + + if ctrl_num_ctrl_values: + gate_types.append(GateCount(resource_rep(ResourceX), 2)) + + return gate_types + + ct = resource_rep(plre.ResourceControlledPhaseShift) + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(ct), GateCount(mcx, 2)] + @classmethod def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -576,6 +725,53 @@ def default_adjoint_resource_decomp(cls) -> list[GateCount]: """ return [GateCount(cls.resource_rep())] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ): + r"""Returns a list representing the resources for a controlled version of the operator. + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + Resources: + For one or two control wires, the cost is one of :class:`~.ResourceCNOT` + or :class:`~.ResourceToffoli` respectively. Two additional :class:`~.ResourceX` gates + per control qubit are used to flip the control qubits if they are zero-controlled. + In the case where multiple controlled wires are provided, the cost is one general + :class:`~.ResourceMultiControlledX` gate. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires > 2: + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(mcx)] + + gate_lst = [] + if ctrl_num_ctrl_values: + gate_lst.append(GateCount(resource_rep(ResourceX), 2 * ctrl_num_ctrl_values)) + + if ctrl_num_ctrl_wires == 0: + gate_lst.append(GateCount(resource_rep(ResourceX))) + + elif ctrl_num_ctrl_wires == 1: + gate_lst.append(GateCount(resource_rep(plre.ResourceCNOT))) + + else: + gate_lst.append(GateCount(resource_rep(plre.ResourceToffoli))) + + return gate_lst + @classmethod def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -611,12 +807,10 @@ class ResourceY(ResourceOperator): \begin{align} \hat{Y} &= \hat{S} \cdot \hat{X} \cdot \hat{S}^{\dagger}, \\ \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ - \hat{Z} &= \hat{S}^{2}, \\ - \hat{S}^{\dagger} &= 3 \hat{S}. \end{align} - Thus the resources for a Y-gate are six :class:`~.ResourceS` gates and - two :class:`~.ResourceHadamard` gates. + Thus the resources for a Y-gate are one S-gate, one Adjoint(S)-gate, one Z-gate + and two Hadamard gates. .. seealso:: :class:`~.Y` @@ -625,7 +819,7 @@ class ResourceY(ResourceOperator): The resources for this operation are computed using: >>> plre.ResourceY.resource_decomp() - [(6 x S), (2 x Hadamard)] + [(1 x S), (1 x Z), (1 x Adjoint(S)), (2 x Hadamard)] """ num_wires = 1 @@ -643,17 +837,17 @@ def default_resource_decomp(cls, **kwargs) -> list[GateCount]: \begin{align} \hat{Y} &= \hat{S} \cdot \hat{X} \cdot \hat{S}^{\dagger}, \\ \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ - \hat{Z} &= \hat{S}^{2}, \\ - \hat{S}^{\dagger} &= 3 \hat{S}. \end{align} - Thus the resources for a Y-gate are six :class:`~.ResourceS` gates and - two :class:`~.ResourceHadamard` gates. + Thus the resources for a Y-gate are one S-gate, one Adjoint(S)-gate, one Z-gate + and two Hadamard gates. """ + z = resource_rep(ResourceZ) s = resource_rep(ResourceS) + s_adj = resource_rep(plre.ResourceAdjoint, {"base_cmpr_op": s}) h = resource_rep(ResourceHadamard) - return [GateCount(s, 6), GateCount(h, 2)] + return [GateCount(s), GateCount(z), GateCount(s_adj), GateCount(h, 2)] @property def resource_params(self) -> dict: @@ -685,6 +879,53 @@ def default_adjoint_resource_decomp(cls) -> list[GateCount]: """ return [GateCount(cls.resource_rep())] + @classmethod + def default_controlled_resource_decomp( + cls, ctrl_num_ctrl_wires, ctrl_num_ctrl_values + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + For a single control wire, the cost is a single instance of :class:`~.ResourceCY`. + Two additional :class:`~.ResourceX` gates are used to flip the control qubit if + it is zero-controlled. + In the case where multiple controlled wires are provided, the resources are derived from + the following identity: + + .. math:: \hat{Y} = \hat{S} \cdot \hat{X} \cdot \hat{S}^{\dagger}. + + Specifically, the resources are given by a :class:`~.ResourceX` gate conjugated with + a pair of :class:`~.ResourceS` gates. By replacing the :class:`~.ResourceX` gate with a + :class:`~.ResourceMultiControlledX` gate, we obtain a controlled-version of this identity. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1: + gate_types = [GateCount(resource_rep(plre.ResourceCY))] + + if ctrl_num_ctrl_values: + gate_types.append(GateCount(resource_rep(ResourceX), 2)) + + return gate_types + + s = resource_rep(ResourceS) + s_dagg = resource_rep(plre.ResourceAdjoint, {"base_cmpr_op": s}) + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(s), GateCount(s_dagg), GateCount(mcx)] + @classmethod def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -776,6 +1017,60 @@ def default_adjoint_resource_decomp(cls) -> list[GateCount]: """ return [GateCount(cls.resource_rep())] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + + Resources: + For one or two control wires, the cost is one of :class:`~.ResourceCZ` + or :class:`~.ResourceCCZ` respectively. Two additional :class:`~.ResourceX` gates + per control qubit are used to flip the control qubits if they are zero-controlled. + In the case where multiple controlled wires are provided, the resources are derived from + the following identity: + + .. math:: \hat{Z} = \hat{H} \cdot \hat{X} \cdot \hat{H}. + + Specifically, the resources are given by a :class:`~.ResourceX` gate conjugated with + a pair of :class:`~.ResourceHadamard` gates. By replacing the :class:`~.ResourceX` gate + with a :class:`~.ResourceMultiControlledX` gate, we obtain a controlled-version of this + identity. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires > 2: + h = resource_rep(ResourceHadamard) + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(h, 2), GateCount(mcx)] + + gate_list = [] + if ctrl_num_ctrl_wires == 1: + gate_list.append(GateCount(resource_rep(plre.ResourceCZ))) + + if ctrl_num_ctrl_wires == 2: + gate_list.append(GateCount(resource_rep(plre.ResourceCCZ))) + + if ctrl_num_ctrl_values: + gate_list.append(GateCount(resource_rep(ResourceX), 2 * ctrl_num_ctrl_values)) + + return gate_list + @classmethod def default_pow_resource_decomp(cls, pow_z) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. diff --git a/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py b/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py index af992a9a2a2..fc2a457512e 100644 --- a/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py +++ b/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_single_qubit.py @@ -16,6 +16,7 @@ import numpy as np import pennylane.labs.resource_estimation as plre +from pennylane.labs.resource_estimation.qubit_manager import AllocWires, FreeWires from pennylane.labs.resource_estimation.resource_operator import ( CompressedResourceOp, GateCount, @@ -104,6 +105,9 @@ def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: r"""Returns a list representing the resources of the operator. Each object represents a quantum gate and the number of times it occurs in the decomposition. + Keyword Args: + eps (float): error threshold for clifford plus T decomposition of this operation + Resources: The phase shift gate is equivalent to a Z-rotation upto some global phase, as defined from the following identity: @@ -112,6 +116,11 @@ def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: 1 & 0 \\ 0 & e^{i\phi} \end{bmatrix}. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. """ rz = resource_rep(ResourceRZ, {"eps": eps}) global_phase = resource_rep(plre.ResourceGlobalPhase) @@ -132,6 +141,53 @@ def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: """ return [GateCount(cls.resource_rep(eps=eps))] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float): error threshold for clifford plus T decomposition of this operation + + Resources: + For a single control wire, the cost is a single instance of + :class:`~.ResourceControlledPhaseShift`. Two additional :class:`~.ResourceX` gates are used + to flip the control qubit if it is zero-controlled. + + In the case where multiple controlled wires are provided, we can collapse the control + wires by introducing one 'clean' auxilliary qubit (which gets reset at the end). + In this case the cost increases by two additional :class:`~.ResourceMultiControlledX` gates, + as described in (lemma 7.11) `Elementary gates for quantum computation `_. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1: + gate_types = [GateCount(resource_rep(plre.ResourceControlledPhaseShift, {"eps": eps}))] + + if ctrl_num_ctrl_values: + gate_types.append(GateCount(resource_rep(plre.ResourceX), 2)) + + return gate_types + + c_ps = resource_rep(plre.ResourceControlledPhaseShift, {"eps": eps}) + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [AllocWires(1), GateCount(c_ps), GateCount(mcx, 2), FreeWires(1)] + @classmethod def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -148,7 +204,6 @@ def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: represents a specific quantum gate and the number of times it appears in the decomposition. """ - return [GateCount(cls.resource_rep(eps=eps))] @@ -228,7 +283,7 @@ def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: and the number of times it occurs in the decomposition. Keyword Args: - eps (float): the error threshold + eps (float): error threshold for clifford plus T decomposition of this operation Resources: A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The @@ -257,6 +312,61 @@ def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: """ return [GateCount(cls.resource_rep(eps))] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float): error threshold for clifford plus T decomposition of this operation + + Resources: + For a single control wire, the cost is a single instance of :class:`~.ResourceCRX`. + Two additional :class:`~.ResourceX` gates are used to flip the control qubit if + it is zero-controlled. + + In the case where multiple controlled wires are provided, the resources are taken + from Figure 1b of the paper `T-count and T-depth of any multi-qubit unitary + `_. In combination with the following identity: + + .. math:: \hat{RX} = \hat{H} \cdot \hat{RZ} \cdot \hat{H}, + + we can express the :code:`CRX` gate as a :code:`CRZ` gate conjugated by :code:`Hadamard` + gates. The expression for controlled-RZ gates is used as defined in the reference above. + By replacing the :code:`X` gates with multi-controlled :code:`X` gates, we obtain a + controlled-version of that identity. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1: + + gate_types = [GateCount(resource_rep(plre.ResourceCRX, {"eps": eps}))] + + if ctrl_num_ctrl_values: + gate_types.append(GateCount(resource_rep(plre.ResourceX), 2)) + + return gate_types + + h = resource_rep(plre.ResourceHadamard) + rz = resource_rep(ResourceRZ, {"eps": eps}) + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + return [GateCount(h, 2), GateCount(rz, 2), GateCount(mcx, 2)] + @classmethod def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -273,7 +383,6 @@ def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: represents a specific quantum gate and the number of times it appears in the decomposition. """ - return [GateCount(cls.resource_rep(eps))] @@ -343,7 +452,6 @@ def resource_params(self) -> dict: def resource_rep(cls, eps=None) -> CompressedResourceOp: r"""Returns a compressed representation containing only the parameters of the Operator that are needed to compute the resources.""" - return CompressedResourceOp(cls, {"eps": eps}) @classmethod @@ -352,7 +460,7 @@ def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: and the number of times it occurs in the decomposition. Keyword Args: - eps (float): the error threshold + eps (float): error threshold for clifford plus T decomposition of this operation Resources: A single qubit rotation gate can be approximately synthesised from Clifford and T gates. The @@ -362,8 +470,6 @@ def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil - Args: - config (dict): a dictionary containing the error threshold """ eps = eps or kwargs["config"]["error_ry"] return _rotation_resources(epsilon=eps) @@ -383,12 +489,67 @@ def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: """ return [GateCount(cls.resource_rep(eps))] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float): error threshold for clifford plus T decomposition of this operation + + Resources: + For a single control wire, the cost is a single instance of :class:`~.ResourceCRY`. + Two additional :class:`~.ResourceX` gates are used to flip the control qubit if + it is zero-controlled. + + In the case where multiple controlled wires are provided, the resources are taken + from Figure 1b of the paper `T-count and T-depth of any multi-qubit + unitary `_. The resources are derived with the + following identity: + + .. math:: \hat{RY}(\theta) = \hat{X} \cdot \hat{RY}(- \theta) \cdot \hat{X}. + + By replacing the :code:`X` gates with multi-controlled :code:`X` gates, we obtain a + controlled-version of this identity. Thus we are able to constructively or destructively + interfere the gates based on the value of the control qubits. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1: + gate_types = [GateCount(resource_rep(plre.ResourceCRY, {"eps": eps}))] + + if ctrl_num_ctrl_values: + gate_types.append(GateCount(resource_rep(plre.ResourceX), 2)) + + return gate_types + + ry = resource_rep(ResourceRY, {"eps": eps}) + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + + return [GateCount(ry, 2), GateCount(mcx, 2)] + @classmethod def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. Args: pow_z (int): the power that the operator is being raised to + eps (float): error threshold for clifford plus T decomposition of this operation Resources: Taking arbitrary powers of a single qubit rotation produces a sum of rotations. @@ -399,7 +560,6 @@ def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: represents a specific quantum gate and the number of times it appears in the decomposition. """ - return [GateCount(cls.resource_rep(eps))] @@ -470,7 +630,6 @@ def resource_params(self) -> dict: def resource_rep(cls, eps=None) -> CompressedResourceOp: r"""Returns a compressed representation containing only the parameters of the Operator that are needed to compute the resources.""" - return CompressedResourceOp(cls, {"eps": eps}) @classmethod @@ -487,7 +646,7 @@ def default_resource_decomp(cls, eps=None, **kwargs) -> list[GateCount]: .. math:: T_{count} = \lceil(1.149 * log_{2}(\frac{1}{\epsilon}) + 9.2)\rceil Args: - config (dict): a dictionary containing the error threshold + eps (float): error threshold for clifford plus T decomposition of this operation """ eps = eps or kwargs["config"]["error_rz"] return _rotation_resources(epsilon=eps) @@ -507,6 +666,59 @@ def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: """ return [GateCount(cls.resource_rep(eps))] + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + For a single control wire, the cost is a single instance of :class:`~.ResourceCRY`. + Two additional :class:`~.ResourceX` gates are used to flip the control qubit if + it is zero-controlled. + + In the case where multiple controlled wires are provided, the resources are obtained + from Figure 1b of the paper `T-count and T-depth of any multi-qubit unitary + `_. They are derived from the following identity: + + .. math:: \hat{RZ}(\theta) = \hat{X} \cdot \hat{RZ}(- \theta) \cdot \hat{X}. + + By replacing the :code:`X` gates with multi-controlled :code:`X` gates, we obtain a + controlled-version of this identity. Thus we are able to constructively or destructively + interfere the gates based on the value of the control qubits. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1: + gate_types = [GateCount(resource_rep(plre.ResourceCRZ, {"eps": eps}))] + + if ctrl_num_ctrl_values: + gate_types.append(GateCount(resource_rep(plre.ResourceX), 2)) + + return gate_types + + rz = resource_rep(ResourceRZ, {"eps": eps}) + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + + return [GateCount(rz, 2), GateCount(mcx, 2)] + @classmethod def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -569,7 +781,6 @@ def resource_params(self): def resource_rep(cls, eps=None) -> CompressedResourceOp: r"""Returns a compressed representation containing only the parameters of the Operator that are needed to compute the resources.""" - return CompressedResourceOp(cls, {"eps": eps}) @classmethod @@ -603,6 +814,69 @@ def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: """ return [GateCount(cls.resource_rep(eps))] + @classmethod + def default_controlled_resource_decomp( + cls, ctrl_num_ctrl_wires, ctrl_num_ctrl_values, eps=None + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float): error threshold for clifford plus T decomposition of this operation + + Resources: + For a single control wire, the cost is a single instance of :class:`~.ResourceCRot`. + Two additional :class:`~.ResourceX` gates are used to flip the control qubit if + it is zero-controlled. + In the case where multiple controlled wires are provided, the resources are derived + from Figure 1b of the paper `T-count and T-depth of any multi-qubit unitary + `_. The resources are derived with the following + identities: + + .. math:: + + \begin{align} + \hat{RZ}(\theta) = \hat{X} \cdot \hat{RZ}(- \theta) \cdot \hat{X}, \\ + \hat{RY}(\theta) = \hat{X} \cdot \hat{RY}(- \theta) \cdot \hat{X}. + \end{align} + + This identity is applied along with some clever choices for the angle values to combine + rotations; the final circuit takes the form: + + .. code-block:: bash + + ctrl: ─────╭●─────────╭●─────────┤ + trgt: ──RZ─╰X──RZ──RY─╰X──RY──RZ─┤ + + The :code:`CNOT` gates are replaced with multi-controlled X-gates to generalize to the + multi-controlled case. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if ctrl_num_ctrl_wires == 1: + gate_types = [GateCount(resource_rep(plre.ResourceCRot, {"eps": eps}))] + + if ctrl_num_ctrl_values: + gate_types.append(GateCount(resource_rep(plre.ResourceX), 2)) + + return gate_types + + rz = resource_rep(ResourceRZ, {"eps": eps}) + ry = resource_rep(ResourceRY, {"eps": eps}) + mcx = resource_rep( + plre.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + + return [GateCount(mcx, 2), GateCount(rz, 3), GateCount(ry, 2)] + @classmethod def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: r"""Returns a list representing the resources for an operator raised to a power. @@ -619,5 +893,4 @@ def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: represents a specific quantum gate and the number of times it appears in the decomposition. """ - return [GateCount(cls.resource_rep(eps))] diff --git a/pennylane/labs/resource_estimation/qubit_manager.py b/pennylane/labs/resource_estimation/qubit_manager.py index eaa42268987..6028212ed6c 100644 --- a/pennylane/labs/resource_estimation/qubit_manager.py +++ b/pennylane/labs/resource_estimation/qubit_manager.py @@ -173,6 +173,9 @@ def queue(self, context=qml.QueuingManager): context.append(self) return self + def __eq__(self, other: "_WireAction") -> bool: + return self.num_wires == other.num_wires + class AllocWires(_WireAction): r"""Allows users to allocate clean work wires. diff --git a/pennylane/labs/resource_estimation/resource_operator.py b/pennylane/labs/resource_estimation/resource_operator.py index 043b0466a49..b62220253f5 100644 --- a/pennylane/labs/resource_estimation/resource_operator.py +++ b/pennylane/labs/resource_estimation/resource_operator.py @@ -79,7 +79,6 @@ def __eq__(self, other: CompressedResourceOp) -> bool: ) def __repr__(self) -> str: - class_name = self.__class__.__qualname__ op_type_name = self.op_type.__name__ diff --git a/pennylane/labs/resource_estimation/resource_tracking.py b/pennylane/labs/resource_estimation/resource_tracking.py index 992f6a605db..bb6c6e10f5c 100644 --- a/pennylane/labs/resource_estimation/resource_tracking.py +++ b/pennylane/labs/resource_estimation/resource_tracking.py @@ -65,11 +65,11 @@ # parameters for further configuration of the decompositions resource_config = { - "error_rx": 1e-5, - "error_ry": 1e-5, - "error_rz": 1e-5, - "precision_multiplexer": 1e-3, - "precision_qrom_state_prep": 1e-3, + "error_rx": 1e-9, + "error_ry": 1e-9, + "error_rz": 1e-9, + "precision_multiplexer": 1e-9, + "precision_qrom_state_prep": 1e-9, } diff --git a/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py b/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py index 7a4a9871e1d..6d47f83c5e2 100644 --- a/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py +++ b/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py @@ -18,6 +18,7 @@ import pennylane.labs.resource_estimation as re from pennylane.labs.resource_estimation.qubit_manager import AllocWires, FreeWires +from pennylane.labs.resource_estimation.resource_operator import GateCount # pylint: disable=no-self-use, use-implicit-booleaness-not-comparison,too-many-arguments,too-many-positional-arguments @@ -50,9 +51,22 @@ def test_resource_params(self): def test_resource_adjoint(self): """Test that the adjoint resources are as expected""" expected_res = [re.GateCount(self.op.resource_rep(), 1)] - assert self.op.adjoint_resource_decomp() == expected_res + def test_resource_controlled(self): + """Test that the controlled resources are as expected""" + num_ctrl_wires = 3 + num_ctrl_values = 1 + + expected_op = re.ResourceControlled( + re.ResourceHadamard(), + num_ctrl_wires=4, + num_ctrl_values=1, + ) + expected_res = [GateCount(expected_op.resource_rep_from_op())] + + assert self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + pow_data = ( (1, [re.GateCount(op.resource_rep(), 1)]), (2, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), @@ -71,6 +85,16 @@ class TestResourceCY: op = re.ResourceCY(wires=[0, 1]) + def test_resources(self): + """Test that the resources method produces the expected resources.""" + + expected_resources = [ + re.GateCount(re.ResourceCNOT.resource_rep(), 1), + re.GateCount(re.ResourceS.resource_rep(), 1), + re.GateCount(re.ResourceAdjoint.resource_rep(re.resource_rep(re.ResourceS)), 1), + ] + assert self.op.resource_decomp(**self.op.resource_params) == expected_resources + def test_resource_rep(self): """Test the resource_rep produces the correct compressed representation.""" expected_rep = re.CompressedResourceOp(re.ResourceCY, {}) @@ -84,9 +108,35 @@ def test_resource_params(self): def test_resource_adjoint(self): """Test that the adjoint resources are as expected""" expected_res = [re.GateCount(self.op.resource_rep(), 1)] - assert self.op.adjoint_resource_decomp() == expected_res + ctrl_data = ( + ( + 1, + 0, + GateCount(re.ResourceControlled(re.ResourceY(), 2, 0).resource_rep_from_op()), + ), + ( + 2, + 0, + GateCount(re.ResourceControlled(re.ResourceY(), 3, 0).resource_rep_from_op()), + ), + ( + 3, + 2, + GateCount(re.ResourceControlled(re.ResourceY(), 4, 2).resource_rep_from_op()), + ), + ) + + @pytest.mark.parametrize( + "num_ctrl_wires, num_ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, num_ctrl_wires, num_ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + + assert self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == [expected_res] + pow_data = ( (1, [re.GateCount(op.resource_rep(), 1)]), (2, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), @@ -130,6 +180,32 @@ def test_resource_adjoint(self): assert self.op.adjoint_resource_decomp() == expected_res + ctrl_data = ( + ( + 1, + 0, + GateCount(re.ResourceCCZ().resource_rep_from_op()), + ), + ( + 2, + 0, + GateCount(re.ResourceControlled(re.ResourceZ(), 3, 0).resource_rep_from_op()), + ), + ( + 3, + 2, + GateCount(re.ResourceControlled(re.ResourceZ(), 4, 2).resource_rep_from_op()), + ), + ) + + @pytest.mark.parametrize( + "num_ctrl_wires, num_ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, num_ctrl_wires, num_ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + assert self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == [expected_res] + pow_data = [ (1, [re.GateCount(op.resource_rep(), 1)]), (2, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), @@ -171,6 +247,32 @@ def test_resource_adjoint(self): expected_res = [re.GateCount(self.op.resource_rep(), 1)] assert self.op.adjoint_resource_decomp() == expected_res + ctrl_data = ( + ( + 1, + 0, + GateCount(re.ResourceControlled(re.ResourceSWAP(), 2, 0).resource_rep_from_op()), + ), + ( + 2, + 0, + GateCount(re.ResourceControlled(re.ResourceSWAP(), 3, 0).resource_rep_from_op()), + ), + ( + 3, + 2, + GateCount(re.ResourceControlled(re.ResourceSWAP(), 4, 2).resource_rep_from_op()), + ), + ) + + @pytest.mark.parametrize( + "num_ctrl_wires, num_ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, num_ctrl_wires, num_ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + assert self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == [expected_res] + pow_data = ( (1, [re.GateCount(op.resource_rep(), 1)]), (2, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), @@ -180,7 +282,6 @@ def test_resource_adjoint(self): @pytest.mark.parametrize("z, expected_res", pow_data) def test_resource_pow(self, z, expected_res): """Test that the pow resources are as expected""" - assert self.op.pow_resource_decomp(z) == expected_res @@ -213,6 +314,32 @@ def test_resource_adjoint(self): assert self.op.adjoint_resource_decomp() == expected_res + ctrl_data = ( + ( + 1, + 0, + GateCount(re.ResourceControlled(re.ResourceZ(), 3, 0).resource_rep_from_op()), + ), + ( + 2, + 0, + GateCount(re.ResourceControlled(re.ResourceZ(), 4, 0).resource_rep_from_op()), + ), + ( + 3, + 2, + GateCount(re.ResourceControlled(re.ResourceZ(), 5, 2).resource_rep_from_op()), + ), + ) + + @pytest.mark.parametrize( + "num_ctrl_wires, num_ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, num_ctrl_wires, num_ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + assert self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == [expected_res] + pow_data = ( (1, [re.GateCount(op.resource_rep(), 1)]), (2, [re.GateCount(re.ResourceIdentity.resource_rep(), 1)]), @@ -222,7 +349,6 @@ def test_resource_adjoint(self): @pytest.mark.parametrize("z, expected_res", pow_data) def test_resource_pow(self, z, expected_res): """Test that the pow resources are as expected""" - assert self.op.pow_resource_decomp(z) == expected_res @@ -249,7 +375,6 @@ def test_resource_params(self): def test_resource_adjoint(self): """Test that the adjoint resources are as expected""" expected_res = [re.GateCount(self.op.resource_rep(), 1)] - assert self.op.adjoint_resource_decomp() == expected_res ctrl_data = ( @@ -291,7 +416,6 @@ def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): @pytest.mark.parametrize("z, expected_res", pow_data) def test_resource_pow(self, z, expected_res): """Test that the pow resources are as expected""" - assert self.op.pow_resource_decomp(z) == expected_res @@ -300,6 +424,21 @@ class TestResourceToffoli: op = re.ResourceToffoli(wires=[0, 1, 2]) + def test_resources(self): + """Test that the resources method produces the expected resources.""" + + expected_resources = [ + AllocWires(2), + re.GateCount(re.ResourceCNOT.resource_rep(), 9), + re.GateCount(re.ResourceHadamard.resource_rep(), 3), + re.GateCount(re.ResourceS.resource_rep(), 1), + re.GateCount(re.ResourceCZ.resource_rep(), 1), + re.GateCount(re.ResourceT.resource_rep(), 2), + re.GateCount(re.ResourceAdjoint.resource_rep(re.resource_rep(re.ResourceT)), 2), + FreeWires(2), + ] + assert self.op.resource_decomp(**self.op.resource_params) == expected_resources + def test_resource_rep(self): """Test the resource_rep produces the correct compressed representation.""" expected_rep = re.CompressedResourceOp(re.ResourceToffoli, {"elbow": None}) @@ -355,7 +494,6 @@ def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): @pytest.mark.parametrize("z, expected_res", pow_data) def test_resource_pow(self, z, expected_res): """Test that the pow resources are as expected""" - assert self.op.pow_resource_decomp(z) == expected_res @@ -389,15 +527,16 @@ class TestResourceMultiControlledX: [re.GateCount(re.ResourceToffoli.resource_rep(), 1)], [ AllocWires(1), - re.GateCount(re.ResourceCNOT.resource_rep(), 2), - re.GateCount(re.ResourceToffoli.resource_rep(), 2), + re.GateCount(re.ResourceTempAND.resource_rep(), 1), + re.GateCount(re.ResourceAdjoint.resource_rep(re.ResourceTempAND.resource_rep()), 1), + re.GateCount(re.ResourceToffoli.resource_rep(), 1), FreeWires(1), ], [ AllocWires(3), - re.GateCount(re.resource_rep(re.ResourceTempAND), 3), - re.GateCount(re.resource_rep(re.ResourceToffoli, {"elbow": "right"}), 3), - re.GateCount(re.resource_rep(re.ResourceToffoli, {}), 1), + re.GateCount(re.ResourceTempAND.resource_rep(), 3), + re.GateCount(re.ResourceAdjoint.resource_rep(re.ResourceTempAND.resource_rep()), 3), + re.GateCount(re.ResourceToffoli.resource_rep(), 1), FreeWires(3), ], [ @@ -411,16 +550,17 @@ class TestResourceMultiControlledX: [ re.GateCount(re.resource_rep(re.ResourceX), 4), AllocWires(1), - re.GateCount(re.resource_rep(re.ResourceCNOT), 2), - re.GateCount(re.resource_rep(re.ResourceToffoli), 2), + re.GateCount(re.ResourceTempAND.resource_rep(), 1), + re.GateCount(re.ResourceAdjoint.resource_rep(re.ResourceTempAND.resource_rep()), 1), + re.GateCount(re.ResourceToffoli.resource_rep(), 1), FreeWires(1), ], [ re.GateCount(re.resource_rep(re.ResourceX), 6), AllocWires(3), - re.GateCount(re.resource_rep(re.ResourceTempAND), 3), - re.GateCount(re.resource_rep(re.ResourceToffoli, {"elbow": "right"}), 3), - re.GateCount(re.resource_rep(re.ResourceToffoli, {}), 1), + re.GateCount(re.ResourceTempAND.resource_rep(), 3), + re.GateCount(re.ResourceAdjoint.resource_rep(re.ResourceTempAND.resource_rep()), 3), + re.GateCount(re.ResourceToffoli.resource_rep(), 1), FreeWires(3), ], ) @@ -436,9 +576,7 @@ def _prep_params(num_control, num_control_values): def test_resources(self, params, expected_res): """Test that the resources method produces the expected resources.""" op_resource_params = self._prep_params(*params) - assert repr(re.ResourceMultiControlledX.resource_decomp(**op_resource_params)) == repr( - expected_res - ) + assert re.ResourceMultiControlledX.resource_decomp(**op_resource_params) == expected_res @pytest.mark.parametrize("op, params", zip(res_ops, res_params)) def test_resource_rep(self, op, params): @@ -457,11 +595,47 @@ def test_resource_params(self, op, params): def test_resource_adjoint(self): """Test that the adjoint resources are as expected""" op = re.ResourceMultiControlledX(5, 3) - expected_res = [re.GateCount(op.resource_rep(**op.resource_params), 1)] assert op.adjoint_resource_decomp(**op.resource_params) == expected_res + ctrl_data = ( + ( + ["c1"], + [1], + [re.GateCount(re.ResourceMultiControlledX.resource_rep(4, 2))], + ), + ( + ["c1", "c2"], + [1, 1], + [ + re.GateCount(re.ResourceMultiControlledX.resource_rep(5, 2)), + ], + ), + ( + ["c1", "c2", "c3", "c4"], + [1, 0, 0, 1], + [ + re.GateCount(re.ResourceMultiControlledX.resource_rep(7, 4)), + ], + ), + ) + + @pytest.mark.parametrize( + "ctrl_wires, ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + op = re.ResourceMultiControlledX(3, 2) + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + assert ( + op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, **op.resource_params) + == expected_res + ) + pow_data = ( (1, [re.GateCount(re.ResourceMultiControlledX.resource_rep(5, 3), 1)]), (2, [re.GateCount(re.ResourceIdentity.resource_rep())]), @@ -473,7 +647,6 @@ def test_resource_adjoint(self): def test_resource_pow(self, z, expected_res): """Test that the pow resources are as expected""" op = re.ResourceMultiControlledX(5, 3) - assert op.pow_resource_decomp(z, **op.resource_params) == expected_res @@ -505,9 +678,34 @@ def test_resource_params(self): def test_resource_adjoint(self): """Test that the adjoint resources are as expected""" expected_res = [re.GateCount(self.op.resource_rep(), 1)] - assert self.op.adjoint_resource_decomp() == expected_res + ctrl_data = ( + ( + 1, + 0, + GateCount(re.ResourceControlled(re.ResourceRX(), 2, 0).resource_rep_from_op()), + ), + ( + 2, + 0, + GateCount(re.ResourceControlled(re.ResourceRX(), 3, 0).resource_rep_from_op()), + ), + ( + 3, + 2, + GateCount(re.ResourceControlled(re.ResourceRX(), 4, 2).resource_rep_from_op()), + ), + ) + + @pytest.mark.parametrize( + "num_ctrl_wires, num_ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, num_ctrl_wires, num_ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + assert self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == [expected_res] + pow_data = ( (1, [re.GateCount(op.resource_rep(), 1)]), (2, [re.GateCount(op.resource_rep(), 1)]), @@ -548,9 +746,34 @@ def test_resource_params(self): def test_resource_adjoint(self): """Test that the adjoint resources are as expected""" expected_res = [re.GateCount(self.op.resource_rep(), 1)] - assert self.op.adjoint_resource_decomp() == expected_res + ctrl_data = ( + ( + 1, + 0, + GateCount(re.ResourceControlled(re.ResourceRY(), 2, 0).resource_rep_from_op()), + ), + ( + 2, + 0, + GateCount(re.ResourceControlled(re.ResourceRY(), 3, 0).resource_rep_from_op()), + ), + ( + 3, + 2, + GateCount(re.ResourceControlled(re.ResourceRY(), 4, 2).resource_rep_from_op()), + ), + ) + + @pytest.mark.parametrize( + "num_ctrl_wires, num_ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, num_ctrl_wires, num_ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + assert self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == [expected_res] + pow_data = ( (1, [re.GateCount(op.resource_rep(), 1)]), (2, [re.GateCount(op.resource_rep(), 1)]), @@ -560,7 +783,6 @@ def test_resource_adjoint(self): @pytest.mark.parametrize("z, expected_res", pow_data) def test_resource_pow(self, z, expected_res): """Test that the pow resources are as expected""" - assert self.op.pow_resource_decomp(z) == expected_res @@ -591,9 +813,34 @@ def test_resource_params(self): def test_resource_adjoint(self): """Test that the adjoint resources are as expected""" expected_res = [re.GateCount(self.op.resource_rep(), 1)] - assert self.op.adjoint_resource_decomp() == expected_res + ctrl_data = ( + ( + 1, + 0, + GateCount(re.ResourceControlled(re.ResourceRZ(), 2, 0).resource_rep_from_op()), + ), + ( + 2, + 0, + GateCount(re.ResourceControlled(re.ResourceRZ(), 3, 0).resource_rep_from_op()), + ), + ( + 3, + 2, + GateCount(re.ResourceControlled(re.ResourceRZ(), 4, 2).resource_rep_from_op()), + ), + ) + + @pytest.mark.parametrize( + "num_ctrl_wires, num_ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, num_ctrl_wires, num_ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + assert self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == [expected_res] + pow_data = ( (1, [re.GateCount(op.resource_rep(), 1)]), (2, [re.GateCount(op.resource_rep(), 1)]), @@ -603,7 +850,6 @@ def test_resource_adjoint(self): @pytest.mark.parametrize("z, expected_res", pow_data) def test_resource_pow(self, z, expected_res): """Test that the pow resources are as expected""" - assert self.op.pow_resource_decomp(z) == expected_res @@ -634,9 +880,34 @@ def test_resource_params(self): def test_resource_adjoint(self): """Test that the adjoint resources are as expected""" expected_res = [re.GateCount(self.op.resource_rep(), 1)] - assert self.op.adjoint_resource_decomp() == expected_res + ctrl_data = ( + ( + 1, + 0, + GateCount(re.ResourceControlled(re.ResourceRot(), 2, 0).resource_rep_from_op()), + ), + ( + 2, + 0, + GateCount(re.ResourceControlled(re.ResourceRot(), 3, 0).resource_rep_from_op()), + ), + ( + 3, + 2, + GateCount(re.ResourceControlled(re.ResourceRot(), 4, 2).resource_rep_from_op()), + ), + ) + + @pytest.mark.parametrize( + "num_ctrl_wires, num_ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, num_ctrl_wires, num_ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + assert self.op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == [expected_res] + pow_data = ( (1, [re.GateCount(op.resource_rep(), 1)]), (2, [re.GateCount(op.resource_rep(), 1)]), @@ -646,7 +917,6 @@ def test_resource_adjoint(self): @pytest.mark.parametrize("z, expected_res", pow_data) def test_resource_pow(self, z, expected_res): """Test that the pow resources are as expected""" - assert self.op.pow_resource_decomp(z) == expected_res @@ -721,5 +991,32 @@ def test_pow_decomp(self, z, expected_res): """Test that the adjoint resources are correct.""" op = re.ResourceControlledPhaseShift - assert op.default_pow_resource_decomp(z) == expected_res + + ctrl_data = ( + ( + 1, + 0, + GateCount(re.ResourceControlled(re.ResourcePhaseShift(), 2, 0).resource_rep_from_op()), + ), + ( + 2, + 0, + GateCount(re.ResourceControlled(re.ResourcePhaseShift(), 3, 0).resource_rep_from_op()), + ), + ( + 3, + 2, + GateCount(re.ResourceControlled(re.ResourcePhaseShift(), 4, 2).resource_rep_from_op()), + ), + ) + + @pytest.mark.parametrize( + "num_ctrl_wires, num_ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, num_ctrl_wires, num_ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + op = re.ResourceControlledPhaseShift() + + assert op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == [expected_res] diff --git a/pennylane/labs/tests/resource_estimation/ops/op_math/test_symbolic_ops.py b/pennylane/labs/tests/resource_estimation/ops/op_math/test_symbolic_ops.py new file mode 100644 index 00000000000..2bc76e32696 --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/ops/op_math/test_symbolic_ops.py @@ -0,0 +1,357 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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 symbolic resource operators. +""" +import pytest + +import pennylane.labs.resource_estimation as plre +from pennylane.labs.resource_estimation.resource_operator import GateCount +from pennylane.queuing import AnnotatedQueue +from pennylane.wires import Wires + +# pylint: disable=no-self-use, + + +class TestResourceAdjoint: + """Tests for the Adjoint resource Op""" + + @pytest.mark.parametrize( + "base_type, base_args", + ( + (plre.ResourceS, {}), + (plre.ResourceRZ, {"eps": 1e-3}), + (plre.ResourceCNOT, {"wires": ["ctrl", "trgt"]}), + ), + ) + def test_init(self, base_type, base_args): + """Test that the operator is instantiated correctly""" + with AnnotatedQueue() as q: + base_op = base_type(**base_args) + adj_base_op = plre.ResourceAdjoint(base_op) + + assert base_op not in q.queue + assert adj_base_op.num_wires == base_op.num_wires + assert base_op.wires == adj_base_op.wires + + def test_resource_decomp(self): + """Test that we can obtain the resources as expected""" + op = plre.ResourceS() # has default_adjoint_decomp defined + adj_op = plre.ResourceAdjoint(op) + assert adj_op.resource_decomp(**adj_op.resource_params) == op.adjoint_resource_decomp( + **op.resource_params + ) + + class ResourceDummyS(plre.ResourceS): + """Dummy class with no default adjoint decomp""" + + @classmethod + def default_adjoint_resource_decomp(cls, **kwargs) -> list[GateCount]: + """No default resources""" + raise plre.ResourcesNotDefined + + op = ResourceDummyS() # no default_adjoint_decomp defined + adj_op = plre.ResourceAdjoint(op) + expected_res = [ + GateCount( + plre.resource_rep( + plre.ResourceAdjoint, + {"base_cmpr_op": plre.resource_rep(plre.ResourceT)}, + ), + 2, + ) + ] + assert adj_op.resource_decomp(**adj_op.resource_params) == expected_res + + @pytest.mark.parametrize( + "base_op", + ( + plre.ResourceS(), + plre.ResourceRZ(eps=1e-3), + plre.ResourceCNOT(wires=["ctrl", "trgt"]), + ), + ) + def test_adj_resource_decomp(self, base_op): + """Test that the adjoint of this operator produces resources as expected.""" + adj_op = plre.ResourceAdjoint(base_op) + adj_adj_op = plre.ResourceAdjoint(adj_op) + + expected_res = [GateCount(base_op.resource_rep_from_op())] + assert adj_adj_op.resource_decomp(**adj_adj_op.resource_params) == expected_res + + +class TestResourcePow: + """Tests for the Pow resource Op""" + + @pytest.mark.parametrize("z", (0, 1, 2, 3)) + @pytest.mark.parametrize( + "base_type, base_args", + ( + (plre.ResourceS, {}), + (plre.ResourceRZ, {"eps": 1e-3}), + (plre.ResourceCNOT, {"wires": ["ctrl", "trgt"]}), + ), + ) + def test_init(self, z, base_type, base_args): + """Test that the operator is instantiated correctly""" + with AnnotatedQueue() as q: + base_op = base_type(**base_args) + pow_base_op = plre.ResourcePow(base_op, z) + + assert base_op not in q.queue + assert pow_base_op.num_wires == base_op.num_wires + assert base_op.wires == pow_base_op.wires + + def test_resource_decomp(self): + """Test that we can obtain the resources as expected""" + op = plre.ResourceX() # has default_pow_decomp defined + z_and_expected_res = ( + (0, [GateCount(plre.resource_rep(plre.ResourceIdentity()))]), + (1, [GateCount(op.resource_rep_from_op())]), + (2, op.pow_resource_decomp(2, **op.resource_params)), + (3, op.pow_resource_decomp(3, **op.resource_params)), + ) + + for z, res in z_and_expected_res: + pow_op = plre.ResourcePow(op, z) + assert pow_op.resource_decomp(**pow_op.resource_params) == res + + class ResourceDummyX(plre.ResourceX): + """Dummy class with no default pow decomp""" + + @classmethod + def default_pow_resource_decomp(cls, pow_z, **kwargs) -> list[GateCount]: + """No default resources""" + raise plre.ResourcesNotDefined + + op = ResourceDummyX() # no default_pow_decomp defined + pow_op = plre.ResourcePow(op, 7) + expected_res = [GateCount(op.resource_rep_from_op(), 7)] + assert pow_op.resource_decomp(**pow_op.resource_params) == expected_res + + @pytest.mark.parametrize("z", (0, 1, 2, 3)) + @pytest.mark.parametrize( + "base_op", + ( + plre.ResourceS(), + plre.ResourceRZ(eps=1e-3), + plre.ResourceCNOT(wires=["ctrl", "trgt"]), + ), + ) + def test_pow_resource_decomp(self, base_op, z): + """Test that the power of this operator produces resources as expected.""" + pow_op = plre.ResourcePow(base_op, z) + pow_pow_op = plre.ResourcePow(pow_op, z=5) + + expected_res = [ + GateCount( + plre.ResourcePow.resource_rep( + base_op.resource_rep_from_op(), + z=5 * z, + ) + ) + ] + assert pow_pow_op.resource_decomp(**pow_pow_op.resource_params) == expected_res + + +class TestResourceControlled: + """Tests for the Controlled resource Op""" + + @pytest.mark.parametrize( + "ctrl_wires, ctrl_values", + ( + (1, 0), + (2, 0), + (3, 2), + ), + ) + @pytest.mark.parametrize( + "base_type, base_args", + ( + (plre.ResourceS, {}), + (plre.ResourceRZ, {"eps": 1e-3}), + (plre.ResourceCNOT, {"wires": ["ctrl", "trgt"]}), + ), + ) + def test_init(self, ctrl_wires, ctrl_values, base_type, base_args): + """Test that the operator is instantiated correctly""" + with AnnotatedQueue() as q: + base_op = base_type(**base_args) + ctrl_base_op = plre.ResourceControlled(base_op, ctrl_wires, ctrl_values) + + assert base_op not in q.queue + assert ctrl_base_op.num_wires == base_op.num_wires + ctrl_wires + + def test_resource_decomp(self): + """Test that we can obtain the resources as expected""" + op = plre.ResourceZ() # has default_ctrl_decomp defined + ctrl_params_and_expected_res = ( + ((1, 0), op.controlled_resource_decomp(1, 0, **op.resource_params)), + ((2, 0), op.controlled_resource_decomp(2, 0, **op.resource_params)), + ((3, 2), op.controlled_resource_decomp(3, 2, **op.resource_params)), + ) + + for (ctrl_wires, ctrl_values), res in ctrl_params_and_expected_res: + ctrl_op = plre.ResourceControlled(op, ctrl_wires, ctrl_values) + assert ctrl_op.resource_decomp(**ctrl_op.resource_params) == res + + class ResourceDummyZ(plre.ResourceZ): + """Dummy class with no default ctrl decomp""" + + @classmethod + def default_controlled_resource_decomp( + cls, ctrl_num_ctrl_wires, ctrl_num_ctrl_values, **kwargs + ) -> list[GateCount]: + """No default resources""" + raise plre.ResourcesNotDefined + + op = ResourceDummyZ() # no default_ctrl_decomp defined + ctrl_op = plre.ResourceControlled(op, num_ctrl_wires=3, num_ctrl_values=2) + expected_res = [ + GateCount(plre.resource_rep(plre.ResourceX), 4), + GateCount( + plre.ResourceControlled.resource_rep( + plre.resource_rep(plre.ResourceS), + num_ctrl_wires=3, + num_ctrl_values=0, + ), + 2, + ), + ] + assert ctrl_op.resource_decomp(**ctrl_op.resource_params) == expected_res + + @pytest.mark.parametrize( + "ctrl_wires, ctrl_values", + ( + (1, 0), + (2, 0), + (3, 2), + ), + ) + @pytest.mark.parametrize( + "base_op", + ( + plre.ResourceS(), + plre.ResourceRZ(eps=1e-3), + plre.ResourceCNOT(wires=["ctrl", "trgt"]), + ), + ) + def test_ctrl_resource_decomp(self, ctrl_wires, ctrl_values, base_op): + """Test that the control of this operator produces resources as expected.""" + ctrl_op = plre.ResourceControlled(base_op, ctrl_wires, ctrl_values) + ctrl_ctrl_op = plre.ResourceControlled(ctrl_op, num_ctrl_wires=2, num_ctrl_values=1) + + expected_res = [ + GateCount( + plre.ResourceControlled.resource_rep( + base_op.resource_rep_from_op(), + num_ctrl_wires=ctrl_wires + 2, + num_ctrl_values=ctrl_values + 1, + ) + ) + ] + assert ctrl_ctrl_op.resource_decomp(**ctrl_ctrl_op.resource_params) == expected_res + + +class TestResourceProd: + """Tests for the Prod resource Op""" + + def test_init(self): + """Test that the operator is instantiated correctly""" + with AnnotatedQueue() as q: + ops = [ + plre.ResourceX(wires=0), + plre.ResourceCZ(wires=[1, 2]), + (plre.ResourceHadamard(), 4), + plre.ResourceRX(eps=1e-4, wires=3), + (plre.ResourceCNOT(), 2), + ] + prod_op = plre.ResourceProd(res_ops=ops) + + for op in ops: + if isinstance(op, tuple): + op = op[0] + assert op not in q.queue + + assert prod_op.num_wires == 4 + assert prod_op.wires == Wires([0, 1, 2, 3]) + + def test_resource_decomp(self): + """Test that we can obtain the resources as expected""" + ops = [ + plre.ResourceX(wires=0), + plre.ResourceCZ(wires=[1, 2]), + (plre.ResourceHadamard(), 4), + plre.ResourceRX(eps=1e-4, wires=3), + (plre.ResourceCNOT(), 2), + ] + prod_op = plre.ResourceProd(res_ops=ops) + + expected_res = [ + GateCount(plre.resource_rep(plre.ResourceX)), + GateCount(plre.resource_rep(plre.ResourceCZ)), + GateCount(plre.resource_rep(plre.ResourceHadamard), 4), + GateCount(plre.resource_rep(plre.ResourceRX, {"eps": 1e-4})), + GateCount(plre.resource_rep(plre.ResourceCNOT), 2), + ] + assert prod_op.resource_decomp(**prod_op.resource_params) == expected_res + + +class TestResourceChangeBasisOp: + """Tests for the ChangeBasis resource Op""" + + def test_init(self): + """Test that the operator is instantiated correctly""" + with AnnotatedQueue() as q: + compute_op = plre.ResourceS(wires=0) + base_op = plre.ResourceProd(((plre.ResourceZ(), 3),), wires=[0, 1, 2]) + uncompute_op = plre.ResourcePow(plre.ResourceT(wires=0), 6) + + cb_op = plre.ResourceChangeBasisOp(compute_op, base_op, uncompute_op) + + for op in [compute_op, base_op, uncompute_op]: + assert op not in q.queue + + assert cb_op.num_wires == 3 + assert cb_op.wires == Wires([0, 1, 2]) + + def test_resource_decomp(self): + """Test that we can obtain the resources as expected""" + compute_op = plre.ResourceS(wires=0) + base_op = plre.ResourceProd([(plre.ResourceZ(), 3)], wires=[0, 1, 2]) + uncompute_op = plre.ResourcePow(plre.ResourceT(wires=0), 6) + + cb_op = plre.ResourceChangeBasisOp(compute_op, base_op, uncompute_op) + expected_res = [ + GateCount(plre.resource_rep(plre.ResourceS)), + GateCount( + plre.resource_rep( + plre.ResourceProd, + { + "cmpr_factors_and_counts": ((plre.ResourceZ.resource_rep(), 3),), + }, + ), + ), + GateCount( + plre.resource_rep( + plre.ResourcePow, + { + "base_cmpr_op": (plre.ResourceT.resource_rep()), + "z": 6, + }, + ), + ), + ] + + assert cb_op.resource_decomp(**cb_op.resource_params) == expected_res diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py index d19e9d6a221..aa8dfbe4004 100644 --- a/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py +++ b/pennylane/labs/tests/resource_estimation/ops/qubit/test_non_parametric_ops.py @@ -48,6 +48,57 @@ def test_adjoint_decomp(self): expected = [plre.GateCount(plre.ResourceHadamard.resource_rep(), 1)] assert h_dag == expected + ctrl_data = ( + ( + ["c1"], + [1], + [ + plre.GateCount(plre.ResourceCH.resource_rep(), 1), + ], + ), + ( + ["c1"], + [0], + [ + plre.GateCount(plre.ResourceCH.resource_rep(), 1), + plre.GateCount(plre.ResourceX.resource_rep(), 2), + ], + ), + ( + ["c1", "c2"], + [1, 1], + [ + plre.GateCount(plre.ResourceHadamard.resource_rep(), 2), + plre.GateCount(plre.ResourceRY.resource_rep(), 2), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(2, 0), 1), + ], + ), + ( + ["c1", "c2", "c3"], + [1, 0, 0], + [ + plre.GateCount(plre.ResourceHadamard.resource_rep(), 2), + plre.GateCount(plre.ResourceRY.resource_rep(), 2), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 2), 1), + ], + ), + ) + + @pytest.mark.parametrize( + "ctrl_wires, ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + op = plre.ResourceHadamard(0) + op2 = plre.ResourceControlled(op, num_ctrl_wires, num_ctrl_values) + + assert op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + assert op2.resource_decomp(**op2.resource_params) == expected_res + pow_data = ( (1, [plre.GateCount(plre.ResourceHadamard.resource_rep(), 1)]), (2, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), @@ -122,7 +173,7 @@ def test_adjoint_decomp(self): [1, 1], [ plre.GateCount(plre.ResourceCNOT.resource_rep(), 2), - plre.GateCount(plre.ResourceMultiControlledX.resource_rep(2, 0), 1), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 0), 1), ], ), ( @@ -130,7 +181,7 @@ def test_adjoint_decomp(self): [1, 0, 0], [ plre.GateCount(plre.ResourceCNOT.resource_rep(), 2), - plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 2), 1), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(4, 2), 1), ], ), ) @@ -183,7 +234,6 @@ def test_resource_rep(self): def test_resources_from_rep(self): """Test that the resources can be computed from the compressed representation""" - op = plre.ResourceS(0) expected = [plre.GateCount(plre.ResourceT.resource_rep(), 2)] @@ -200,6 +250,59 @@ def test_adjoint_decomposition(self): ] assert plre.ResourceS.adjoint_resource_decomp() == expected + ctrl_data = ( + ( + ["c1"], + [1], + [ + plre.GateCount(plre.ResourceControlledPhaseShift.resource_rep(), 1), + ], + ), + ( + ["c1"], + [0], + [ + plre.GateCount(plre.ResourceControlledPhaseShift.resource_rep(), 1), + plre.GateCount(plre.ResourceX.resource_rep(), 2), + ], + ), + ( + ["c1", "c2"], + [1, 1], + [ + plre.GateCount(plre.ResourceControlledPhaseShift.resource_rep(), 1), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(2, 0), 2), + ], + ), + ( + ["c1", "c2", "c3"], + [1, 0, 0], + [ + plre.GateCount(plre.ResourceControlledPhaseShift.resource_rep(), 1), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 2), 2), + ], + ), + ) + + @pytest.mark.parametrize( + "ctrl_wires, ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + op = plre.ResourceS(0) + op2 = plre.ResourceControlled( + op, + num_ctrl_wires, + num_ctrl_values, + ) + + assert op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + assert op2.resource_decomp(**op2.resource_params) == expected_res + pow_data = ( (1, [plre.GateCount(plre.ResourceS.resource_rep(), 1)]), (2, [plre.GateCount(plre.ResourceZ.resource_rep(), 1)]), @@ -264,6 +367,55 @@ def test_adjoint_decomposition(self): ] assert plre.ResourceT.adjoint_resource_decomp() == expected + ctrl_data = ( + ( + ["c1"], + [1], + [ + plre.GateCount(plre.ResourceControlledPhaseShift.resource_rep(), 1), + ], + ), + ( + ["c1"], + [0], + [ + plre.GateCount(plre.ResourceControlledPhaseShift.resource_rep(), 1), + plre.GateCount(plre.ResourceX.resource_rep(), 2), + ], + ), + ( + ["c1", "c2"], + [1, 1], + [ + plre.GateCount(plre.ResourceControlledPhaseShift.resource_rep(), 1), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(2, 0), 2), + ], + ), + ( + ["c1", "c2", "c3"], + [1, 0, 0], + [ + plre.GateCount(plre.ResourceControlledPhaseShift.resource_rep(), 1), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 2), 2), + ], + ), + ) + + @pytest.mark.parametrize( + "ctrl_wires, ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + op = plre.ResourceT(0) + op2 = plre.ResourceControlled(op, num_ctrl_wires, num_ctrl_values) + + assert op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + assert op2.resource_decomp(**op2.resource_params) == expected_res + pow_data = ( (1, [plre.GateCount(plre.ResourceT.resource_rep(), 1)]), (2, [plre.GateCount(plre.ResourceS.resource_rep(), 1)]), @@ -334,6 +486,68 @@ def test_adjoint_decomposition(self): expected = [plre.GateCount(plre.ResourceX.resource_rep(), 1)] assert plre.ResourceX.adjoint_resource_decomp() == expected + ctrl_data = ( + ( + ["c1"], + [1], + [ + plre.GateCount(plre.ResourceCNOT.resource_rep(), 1), + ], + ), + ( + ["c1"], + [0], + [ + plre.GateCount(plre.ResourceX.resource_rep(), 2), + plre.GateCount(plre.ResourceCNOT.resource_rep(), 1), + ], + ), + ( + ["c1", "c2"], + [1, 1], + [ + plre.GateCount(plre.ResourceToffoli.resource_rep(), 1), + ], + ), + ( + ["c1", "c2"], + [0, 0], + [ + plre.GateCount(plre.ResourceX.resource_rep(), 4), + plre.GateCount(plre.ResourceToffoli.resource_rep(), 1), + ], + ), + ( + ["c1", "c2", "c3"], + [1, 0, 0], + [ + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 2), 1), + ], + ), + ( + ["c1", "c2", "c3", "c4"], + [1, 0, 0, 1], + [ + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(4, 2), 1), + ], + ), + ) + + @pytest.mark.parametrize( + "ctrl_wires, ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + op = plre.ResourceX(0) + op2 = plre.ResourceControlled(op, num_ctrl_wires, num_ctrl_values) + + assert op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + assert op2.resource_decomp(**op2.resource_params) == expected_res + pow_data = ( (1, [plre.GateCount(plre.ResourceX.resource_rep(), 1)]), (2, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), @@ -354,7 +568,9 @@ class TestY: def test_resources(self): """Test that ResourceT does not implement a decomposition""" expected = [ - plre.GateCount(plre.ResourceS.resource_rep(), 6), + plre.GateCount(plre.ResourceS.resource_rep(), 1), + plre.GateCount(plre.ResourceZ.resource_rep(), 1), + plre.GateCount(plre.ResourceAdjoint.resource_rep(plre.ResourceS.resource_rep()), 1), plre.GateCount(plre.ResourceHadamard.resource_rep(), 2), ] assert plre.ResourceY.resource_decomp() == expected @@ -374,6 +590,81 @@ def test_adjoint_decomposition(self): expected = [plre.GateCount(plre.ResourceY.resource_rep(), 1)] assert plre.ResourceY.adjoint_resource_decomp() == expected + ctrl_data = ( + ( + ["c1"], + [1], + [ + plre.GateCount(plre.ResourceCY.resource_rep(), 1), + ], + ), + ( + ["c1"], + [0], + [ + plre.GateCount(plre.ResourceCY.resource_rep(), 1), + plre.GateCount(plre.ResourceX.resource_rep(), 2), + ], + ), + ( + ["c1", "c2"], + [1, 1], + [ + plre.GateCount(plre.ResourceS.resource_rep(), 1), + plre.GateCount( + plre.resource_rep( + plre.ResourceAdjoint, {"base_cmpr_op": plre.ResourceS.resource_rep()} + ), + 1, + ), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(2, 0), 1), + ], + ), + ( + ["c1", "c2"], + [0, 0], + [ + plre.GateCount(plre.ResourceS.resource_rep(), 1), + plre.GateCount( + plre.resource_rep( + plre.ResourceAdjoint, {"base_cmpr_op": plre.ResourceS.resource_rep()} + ), + 1, + ), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(2, 2), 1), + ], + ), + ( + ["c1", "c2", "c3"], + [1, 0, 0], + [ + plre.GateCount(plre.ResourceS.resource_rep(), 1), + plre.GateCount( + plre.resource_rep( + plre.ResourceAdjoint, {"base_cmpr_op": plre.ResourceS.resource_rep()} + ), + 1, + ), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 2), 1), + ], + ), + ) + + @pytest.mark.parametrize( + "ctrl_wires, ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + op = plre.ResourceY(0) + op2 = plre.ResourceControlled(op, num_ctrl_wires, num_ctrl_values) + + assert op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + assert op2.resource_decomp(**op2.resource_params) == expected_res + pow_data = ( (1, [plre.GateCount(plre.ResourceY.resource_rep())]), (2, [plre.GateCount(plre.ResourceIdentity.resource_rep())]), @@ -411,6 +702,62 @@ def test_adjoint_decomposition(self): expected = [plre.GateCount(plre.ResourceZ.resource_rep(), 1)] assert plre.ResourceZ.adjoint_resource_decomp() == expected + ctrl_data = ( + ( + ["c1"], + [1], + [ + plre.GateCount(plre.ResourceCZ.resource_rep(), 1), + ], + ), + ( + ["c1"], + [0], + [ + plre.GateCount(plre.ResourceCZ.resource_rep(), 1), + plre.GateCount(plre.ResourceX.resource_rep(), 2), + ], + ), + ( + ["c1", "c2"], + [1, 1], + [ + plre.GateCount(plre.ResourceCCZ.resource_rep(), 1), + ], + ), + ( + ["c1", "c2"], + [0, 0], + [ + plre.GateCount(plre.ResourceCCZ.resource_rep(), 1), + plre.GateCount(plre.ResourceX.resource_rep(), 4), + ], + ), + ( + ["c1", "c2", "c3"], + [1, 0, 0], + [ + plre.GateCount(plre.ResourceHadamard.resource_rep(), 2), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 2), 1), + ], + ), + ) + + @pytest.mark.parametrize( + "ctrl_wires, ctrl_values, expected_res", + ctrl_data, + ) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + op = plre.ResourceZ(0) + op2 = plre.ResourceControlled(op, num_ctrl_wires, num_ctrl_values) + + assert op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + assert op2.resource_decomp(**op2.resource_params) == expected_res + pow_data = ( (1, [plre.GateCount(plre.ResourceZ.resource_rep())]), (2, [plre.GateCount(plre.ResourceIdentity.resource_rep())]), diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py index 4dd37df9f40..a2cfc1ac614 100644 --- a/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py +++ b/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_single_qubit.py @@ -14,6 +14,8 @@ """ Tests for parametric single qubit resource operators. """ +import copy + import pytest import pennylane.labs.resource_estimation as plre @@ -39,6 +41,18 @@ class TestPauliRotation: params_classes = [plre.ResourceRX, plre.ResourceRY, plre.ResourceRZ] params_errors = [10e-3, 10e-4, 10e-5] + params_ctrl_res = [ + [ + plre.GateCount(plre.ResourceHadamard.resource_rep(), 2), + plre.GateCount(plre.ResourceRZ.resource_rep(), 2), + ], + [ + plre.GateCount(plre.ResourceRY.resource_rep(), 2), + ], + [ + plre.GateCount(plre.ResourceRZ.resource_rep(), 2), + ], + ] @pytest.mark.parametrize("resource_class", params_classes) @pytest.mark.parametrize("epsilon", params_errors) @@ -103,6 +117,75 @@ def test_pow_decomposition(self, resource_class, epsilon, z): ] assert resource_class(epsilon).pow_resource_decomp(z) == expected + params_ctrl_classes = ( + (plre.ResourceRX, plre.ResourceCRX), + (plre.ResourceRY, plre.ResourceCRY), + (plre.ResourceRZ, plre.ResourceCRZ), + ) + + @pytest.mark.parametrize("resource_class, controlled_class", params_ctrl_classes) + @pytest.mark.parametrize("epsilon", params_errors) + def test_controlled_decomposition_single_control( + self, resource_class, controlled_class, epsilon + ): + """Test that the controlled decompositions are correct.""" + expected = [plre.GateCount(controlled_class.resource_rep(), 1)] + assert resource_class.controlled_resource_decomp(1, 0) == expected + + expected = [ + plre.GateCount(controlled_class.resource_rep(), 1), + plre.GateCount(plre.ResourceX.resource_rep(), 2), + ] + assert resource_class.controlled_resource_decomp(1, 1) == expected + + op = resource_class(wires=0) + c_op = plre.ResourceControlled(op, 1, 0) + + c = controlled_class(wires=[0, 1]) + + config = {"error_rx": epsilon, "error_ry": epsilon, "error_rz": epsilon} + + r1 = plre.estimate_resources(c, config=config) + r2 = plre.estimate_resources(c_op, config=config) + + assert r1 == r2 + + ctrl_res_data = ( + ( + [1, 2], + [1, 1], + [plre.GateCount(plre.ResourceMultiControlledX.resource_rep(2, 0), 2)], + ), + ( + [1, 2], + [1, 0], + [plre.GateCount(plre.ResourceMultiControlledX.resource_rep(2, 1), 2)], + ), + ( + [1, 2, 3], + [1, 0, 0], + [plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 2), 2)], + ), + ) + + @pytest.mark.parametrize("resource_class, local_res", zip(params_classes, params_ctrl_res)) + @pytest.mark.parametrize("ctrl_wires, ctrl_values, general_res", ctrl_res_data) + def test_controlled_decomposition_multi_controlled( + self, resource_class, local_res, ctrl_wires, ctrl_values, general_res + ): + """Test that the controlled docomposition is correct when controlled on multiple wires.""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + op = resource_class(wires=0) + op2 = plre.ResourceControlled(op, num_ctrl_wires, num_ctrl_values) + + expected_resources = copy.copy(local_res) + expected_resources.extend(general_res) + + assert op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_resources + assert op2.resource_decomp(**op2.resource_params) == expected_resources + class TestRot: """Test ResourceRot""" @@ -145,6 +228,48 @@ def test_adjoint_decomp(self): expected = [plre.GateCount(plre.ResourceRot.resource_rep(), 1)] assert plre.ResourceRot.adjoint_resource_decomp() == expected + ctrl_data = ( + ([1], [1], [plre.GateCount(plre.ResourceCRot.resource_rep(), 1)]), + ( + [1], + [0], + [ + plre.GateCount(plre.ResourceCRot.resource_rep(), 1), + plre.GateCount(plre.ResourceX.resource_rep(), 2), + ], + ), + ( + [1, 2], + [1, 1], + [ + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(2, 0), 2), + plre.GateCount(plre.ResourceRZ.resource_rep(), 3), + plre.GateCount(plre.ResourceRY.resource_rep(), 2), + ], + ), + ( + [1, 2, 3], + [1, 0, 0], + [ + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 2), 2), + plre.GateCount(plre.ResourceRZ.resource_rep(), 3), + plre.GateCount(plre.ResourceRY.resource_rep(), 2), + ], + ), + ) + + @pytest.mark.parametrize("ctrl_wires, ctrl_values, expected_res", ctrl_data) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + op = plre.ResourceRot(wires=0) + op2 = plre.ResourceControlled(op, num_ctrl_wires, num_ctrl_values) + + assert op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + assert op2.resource_decomp(**op2.resource_params) == expected_res + pow_data = ( (1, [plre.GateCount(plre.ResourceRot.resource_rep(), 1)]), (2, [plre.GateCount(plre.ResourceRot.resource_rep(), 1)]), @@ -200,6 +325,52 @@ def test_adjoint_decomp(self): expected = [plre.GateCount(plre.ResourcePhaseShift.resource_rep(), 1)] assert plre.ResourcePhaseShift.adjoint_resource_decomp() == expected + ctrl_data = ( + ([1], [1], [plre.GateCount(plre.ResourceControlledPhaseShift.resource_rep(), 1)]), + ( + [1], + [0], + [ + plre.GateCount(plre.ResourceControlledPhaseShift.resource_rep(), 1), + plre.GateCount(plre.ResourceX.resource_rep(), 2), + ], + ), + ( + [1, 2], + [1, 1], + [ + plre.AllocWires(1), + plre.GateCount(plre.ResourceControlledPhaseShift.resource_rep(), 1), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(2, 0), 2), + plre.FreeWires(1), + ], + ), + ( + [1, 2, 3], + [1, 0, 0], + [ + plre.AllocWires(1), + plre.GateCount(plre.ResourceControlledPhaseShift.resource_rep(), 1), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 2), 2), + plre.FreeWires(1), + ], + ), + ) + + @pytest.mark.parametrize("ctrl_wires, ctrl_values, expected_res", ctrl_data) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + op = plre.ResourcePhaseShift(wires=0) + op2 = plre.ResourceControlled(op, num_ctrl_wires, num_ctrl_values) + + assert repr(op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values)) == repr( + expected_res + ) + assert repr(op2.resource_decomp(**op2.resource_params)) == repr(expected_res) + pow_data = ( (1, [plre.GateCount(plre.ResourcePhaseShift.resource_rep(), 1)]), (2, [plre.GateCount(plre.ResourcePhaseShift.resource_rep(), 1)]), diff --git a/pennylane/labs/tests/resource_estimation/ops/test_identity.py b/pennylane/labs/tests/resource_estimation/ops/test_identity.py index c9743fd4d67..7615eb193a7 100644 --- a/pennylane/labs/tests/resource_estimation/ops/test_identity.py +++ b/pennylane/labs/tests/resource_estimation/ops/test_identity.py @@ -69,7 +69,10 @@ def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): num_ctrl_values = len([v for v in ctrl_values if not v]) op = plre.ResourceIdentity(0) + op2 = plre.ResourceControlled(op, num_ctrl_wires, num_ctrl_values) + assert op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) == expected_res + assert op2.resource_decomp(**op2.resource_params) == expected_res identity_pow_data = ( (1, [plre.GateCount(plre.ResourceIdentity.resource_rep(), 1)]), @@ -119,6 +122,42 @@ def test_resource_adjoint(self): plre.GateCount(plre.ResourceGlobalPhase.resource_rep(), 1) ] + globalphase_ctrl_data = ( + ([1], [1], [plre.GateCount(plre.ResourcePhaseShift.resource_rep(), 1)]), + ( + [1, 2], + [1, 1], + [ + plre.GateCount(plre.ResourcePhaseShift.resource_rep(), 1), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(2, 0), 2), + ], + ), + ( + [1, 2, 3], + [1, 0, 0], + [ + plre.GateCount(plre.ResourcePhaseShift.resource_rep(), 1), + plre.GateCount(plre.ResourceMultiControlledX.resource_rep(3, 2), 2), + ], + ), + ) + + @pytest.mark.parametrize("ctrl_wires, ctrl_values, expected_res", globalphase_ctrl_data) + def test_resource_controlled(self, ctrl_wires, ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + num_ctrl_wires = len(ctrl_wires) + num_ctrl_values = len([v for v in ctrl_values if not v]) + + op = plre.ResourceGlobalPhase(wires=0) + op2 = plre.ResourceControlled(op, num_ctrl_wires, num_ctrl_values) + print( + "oper: ", expected_res, op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values) + ) + assert repr(op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values)) == repr( + expected_res + ) + assert repr(op2.resource_decomp(**op2.resource_params)) == repr(expected_res) + globalphase_pow_data = ( (1, [plre.GateCount(plre.ResourceGlobalPhase.resource_rep(), 1)]), (2, [plre.GateCount(plre.ResourceGlobalPhase.resource_rep(), 1)]), diff --git a/pennylane/labs/tests/resource_estimation/test_resource_tracking.py b/pennylane/labs/tests/resource_estimation/test_resource_tracking.py index 89976999ba5..42f0e4fbdf8 100644 --- a/pennylane/labs/tests/resource_estimation/test_resource_tracking.py +++ b/pennylane/labs/tests/resource_estimation/test_resource_tracking.py @@ -217,7 +217,7 @@ def my_circuit(): expected_gates = defaultdict( int, { - resource_rep(ResourceTestT): round(1 / 1e-2) + round(1 / 1e-5), + resource_rep(ResourceTestT): round(1 / 1e-2) + round(1 / 1e-9), resource_rep(ResourceTestCNOT): 7, resource_rep(ResourceTestHadamard): 10, }, From 0f7aaab2f21cda20319e7de4dd1415ea9a44754b Mon Sep 17 00:00:00 2001 From: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:21:42 -0400 Subject: [PATCH 16/18] Add multi-qubit ResourceOperators (#7549) **Context:** Added classes for multi-qubit ResourceOperator classes. **Description of the Change:** **Benefits:** **Possible Drawbacks:** **Related GitHub Issues:** [sc-91014] --------- Co-authored-by: Jay Soni Co-authored-by: Utkarsh Co-authored-by: Soran Jahangiri <40344468+soranjh@users.noreply.github.com> --- doc/releases/changelog-dev.md | 4 + .../labs/resource_estimation/__init__.py | 14 + .../labs/resource_estimation/ops/__init__.py | 7 + .../resource_estimation/ops/qubit/__init__.py | 9 + .../ops/qubit/parametric_ops_multi_qubit.py | 1445 +++++++++++++++++ .../qubit/test_parametric_ops_multi_qubit.py | 686 ++++++++ 6 files changed, 2165 insertions(+) create mode 100644 pennylane/labs/resource_estimation/ops/qubit/parametric_ops_multi_qubit.py create mode 100644 pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_multi_qubit.py diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 182ee299c61..161d2d4fb2f 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -637,6 +637,10 @@ estimation for symbolic operators of gates. [(#7584)](https://github.com/PennyLaneAI/pennylane/pull/7584) +* Added the `pennylane.labs.ResourceOperator` templates which will be used to perform resource + estimation for multi-qubit parametic gates. + [(#7549)](https://github.com/PennyLaneAI/pennylane/pull/7549) + * A new module :mod:`pennylane.labs.zxopt ` provides access to the basic optimization passes from [pyzx](https://pyzx.readthedocs.io/en/latest/) for PennyLane circuits. diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 8e0f25eb8fd..5db16ba0495 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -103,6 +103,13 @@ ~ResourceCRot ~ResourceControlledPhaseShift ~ResourceTempAND + ~ResourceMultiRZ + ~ResourcePauliRot + ~ResourceIsingXX + ~ResourceIsingYY + ~ResourceIsingXY + ~ResourceIsingZZ + ~ResourcePSWAP """ @@ -154,6 +161,13 @@ ResourceCRZ, ResourceCRot, ResourceControlledPhaseShift, + ResourceMultiRZ, + ResourcePauliRot, + ResourceIsingXX, + ResourceIsingYY, + ResourceIsingXY, + ResourceIsingZZ, + ResourcePSWAP, ResourceTempAND, ResourceAdjoint, ResourceControlled, diff --git a/pennylane/labs/resource_estimation/ops/__init__.py b/pennylane/labs/resource_estimation/ops/__init__.py index 7efb423af23..7ebf79ea74e 100644 --- a/pennylane/labs/resource_estimation/ops/__init__.py +++ b/pennylane/labs/resource_estimation/ops/__init__.py @@ -50,4 +50,11 @@ ResourceX, ResourceY, ResourceZ, + ResourceMultiRZ, + ResourcePauliRot, + ResourceIsingXX, + ResourceIsingYY, + ResourceIsingXY, + ResourceIsingZZ, + ResourcePSWAP, ) diff --git a/pennylane/labs/resource_estimation/ops/qubit/__init__.py b/pennylane/labs/resource_estimation/ops/qubit/__init__.py index 2fb306edce9..6d3574bb704 100644 --- a/pennylane/labs/resource_estimation/ops/qubit/__init__.py +++ b/pennylane/labs/resource_estimation/ops/qubit/__init__.py @@ -29,3 +29,12 @@ ResourceRY, ResourceRZ, ) +from .parametric_ops_multi_qubit import ( + ResourceMultiRZ, + ResourcePauliRot, + ResourceIsingXX, + ResourceIsingYY, + ResourceIsingXY, + ResourceIsingZZ, + ResourcePSWAP, +) diff --git a/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_multi_qubit.py b/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_multi_qubit.py new file mode 100644 index 00000000000..dd922686e54 --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/qubit/parametric_ops_multi_qubit.py @@ -0,0 +1,1445 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""Resource operators for parametric multi qubit operations.""" + +import pennylane.labs.resource_estimation as re +from pennylane.labs.resource_estimation.resource_operator import ( + CompressedResourceOp, + GateCount, + ResourceOperator, +) + +# pylint: disable=arguments-differ + + +class ResourceMultiRZ(ResourceOperator): + r"""Resource class for the MultiRZ gate. + + Args: + num_wires (int): the number of qubits the operation acts upon + eps (float, optional): error threshold for Clifford+T decomposition of this operation + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources come from Section VIII (Figure 3) of `The Bravyi-Kitaev transformation for + quantum computation of electronic structure `_ paper. + + Specifically, the resources are given by one :class:`~.ResourceRZ` gate and a cascade of + :math:`2 * (n - 1)` :class:`~.ResourceCNOT` gates where :math:`n` is the number of qubits + the gate acts on. + + .. seealso:: :class:`~.MultiRZ` + + **Example** + + The resources for this operation are computed using: + + >>> multi_rz = plre.ResourceMultiRZ(num_wires=3) + >>> gate_set = {"CNOT", "RZ"} + >>> + >>> print(plre.estimate_resources(multi_rz, gate_set)) + --- Resources: --- + Total qubits: 3 + Total gates : 5 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3 + Gate breakdown: + {'CNOT': 4, 'RZ': 1} + + """ + + resource_keys = {"num_wires", "eps"} + + def __init__(self, num_wires, eps=None, wires=None) -> None: + self.num_wires = num_wires + self.eps = eps + super().__init__(wires=wires) + + @classmethod + def default_resource_decomp(cls, num_wires, eps=None, **kwargs): + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + num_wires (int): the number of qubits the operation acts upon + eps (float): error threshold for clifford plus T decomposition of this operation + + Resources: + The resources come from Section VIII (Figure 3) of `The Bravyi-Kitaev transformation for + quantum computation of electronic structure `_ paper. + + Specifically, the resources are given by one :class:`~.ResourceRZ` gate and a cascade of + :math:`2 * (n - 1)` :class:`~.ResourceCNOT` gates where :math:`n` is the number of qubits + the gate acts on. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + cnot = re.ResourceCNOT.resource_rep() + rz = re.ResourceRZ.resource_rep(eps=eps) + + return [GateCount(cnot, 2 * (num_wires - 1)), GateCount(rz)] + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * num_wires (int): the number of qubits the operation acts upon + * eps (float): error threshold for clifford plus T decomposition of this operation + """ + return {"num_wires": self.num_wires, "eps": self.eps} + + @classmethod + def resource_rep(cls, num_wires, eps=None): + """Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + num_wires (int): the number of qubits the operation acts upon + eps (float): error threshold for clifford plus T decomposition of this operation + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"num_wires": num_wires, "eps": eps}) + + @classmethod + def default_adjoint_resource_decomp(cls, num_wires, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Args: + num_wires (int): the number of qubits the operation acts upon + eps (float): error threshold for clifford plus T decomposition of this operation + + Resources: + The adjoint of this operator just changes the sign of the phase, thus + the resources of the adjoint operation results in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(num_wires=num_wires, eps=eps))] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + num_wires, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + num_wires (int): the number of qubits the base operation acts upon + eps (float): error threshold for clifford plus T decomposition of this operation + + Resources: + The resources are derived from the following identity. If an operation :math:`\hat{A}` + can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` + then the controlled operation :math:`C\hat{A}` can be expressed as: + + .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} + + Specifically, the resources are one multi-controlled RZ-gate and a cascade of + :math:`2 * (n - 1)` :class:`~.ResourceCNOT` gates where :math:`n` is the number of qubits + the gate acts on. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + cnot = re.resource_rep(re.ResourceCNOT) + ctrl_rz = re.resource_rep( + re.ResourceControlled, + { + "base_cmpr_op": re.resource_rep(re.ResourceRZ, {"eps": eps}), + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + + return [GateCount(cnot, 2 * (num_wires - 1)), GateCount(ctrl_rz)] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, num_wires, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + num_wires (int): the number of qubits the base operation acts upon + eps (float): error threshold for clifford plus T decomposition of this operation + + Resources: + Taking arbitrary powers of a general rotation produces a sum of rotations. + The resources simplify to just one total multi-RZ rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(num_wires=num_wires, eps=eps))] + + +class ResourcePauliRot(ResourceOperator): + r"""Resource class for the PauliRot gate. + + Args: + pauli_string (str): a string describing the pauli operators that define the rotation + eps (float, optional): error threshold for clifford plus T decomposition of this operation + wires (Sequence[int], optional): the wire the operation acts on + + Resources: + When the :code:`pauli_string` is a single Pauli operator (:code:`X, Y, Z, Identity`) + the cost is the associated single qubit rotation (:code:`RX, RY, RZ, GlobalPhase`). + + The resources come from Section VIII (Figures 3 & 4) of `The Bravyi-Kitaev transformation + for quantum computation of electronic structure `_ paper, + in combination with the following identity: + + .. math:: + + \begin{align} + \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ + \hat{Y} &= \hat{S} \cdot \hat{H} \cdot \hat{Z} \cdot \hat{H} \cdot \hat{S}^{\dagger}. + \end{align} + + Specifically, the resources are given by one :class:`~.ResourceRZ` gate and a cascade of + :math:`2 * (n - 1)` :class:`~.ResourceCNOT` gates where :math:`n` is the number of qubits + the gate acts on. Additionally, for each :code:`X` gate in the Pauli word we conjugate by + a pair of :class:`~.ResourceHadamard` gates, and for each :code:`Y` gate in the Pauli word we + conjugate by a pair of :class:`~.ResourceHadamard` and a pair of :class:`~.ResourceS` gates. + + .. seealso:: :class:`~.PauliRot` + + **Example** + + The resources for this operation are computed using: + + >>> pr = plre.ResourcePauliRot(pauli_string="XYZ") + >>> print(plre.estimate_resources(pr, plre.StandardGate\ + Set)) + --- Resources: --- + Total qubits: 3 + Total gates : 11 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3 + Gate breakdown: + {'Hadamard': 4, 'S': 1, 'Adjoint(S)': 1, 'RZ': 1, 'CNOT': 4} + + """ + + resource_keys = {"pauli_string", "eps"} + + def __init__(self, pauli_string, eps=None, wires=None) -> None: + self.eps = eps + self.pauli_string = pauli_string + self.num_wires = len(pauli_string) + super().__init__(wires=wires) + + @classmethod + def default_resource_decomp(cls, pauli_string, eps=None, **kwargs): + r"""Returns a list of GateCount objects representing the operator's resources. + + Args: + pauli_string (str): a string describing the pauli operators that define the rotation + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + When the :code:`pauli_string` is a single Pauli operator (:code:`X, Y, Z, Identity`) + the cost is the associated single qubit rotation (:code:`RX, RY, RZ, GlobalPhase`). + + The resources come from Section VIII (Figures 3 & 4) of `The Bravyi-Kitaev transformation + for quantum computation of electronic structure `_ paper, + in combination with the following identity: + + .. math:: + + \begin{align} + \hat{X} &= \hat{H} \cdot \hat{Z} \cdot \hat{H}, \\ + \hat{Y} &= \hat{S} \cdot \hat{H} \cdot \hat{Z} \cdot \hat{H} \cdot \hat{S}^{\dagger}. + \end{align} + + Specifically, the resources are given by one :class:`~.ResourceRZ` gate and a cascade of + :math:`2 * (n - 1)` :class:`~.ResourceCNOT` gates where :math:`n` is the number of qubits + the gate acts on. Additionally, for each :code:`X` gate in the Pauli word we conjugate by + a pair of :class:`~.ResourceHadamard` gates, and for each :code:`Y` gate in the Pauli word we + conjugate by a pair of :class:`~.ResourceHadamard` and a pair of :class:`~.ResourceS` gates. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if (set(pauli_string) == {"I"}) or (len(pauli_string) == 0): + gp = re.resource_rep(re.ResourceGlobalPhase) + return [GateCount(gp)] + + if pauli_string == "X": + return [GateCount(re.resource_rep(re.ResourceRX, {"eps": eps}))] + if pauli_string == "Y": + return [GateCount(re.resource_rep(re.ResourceRY, {"eps": eps}))] + if pauli_string == "Z": + return [GateCount(re.resource_rep(re.ResourceRZ, {"eps": eps}))] + + active_wires = len(pauli_string.replace("I", "")) + + h = re.resource_rep(re.ResourceHadamard) + s = re.resource_rep(re.ResourceS) + rz = re.resource_rep(re.ResourceRZ, {"eps": eps}) + s_dagg = re.resource_rep( + re.ResourceAdjoint, + {"base_cmpr_op": re.resource_rep(re.ResourceS)}, + ) + cnot = re.resource_rep(re.ResourceCNOT) + + h_count = 0 + s_count = 0 + + for gate in pauli_string: + if gate == "X": + h_count += 1 + if gate == "Y": + h_count += 1 + s_count += 1 + + gate_types = [] + if h_count: + gate_types.append(GateCount(h, 2 * h_count)) + + if s_count: + gate_types.append(GateCount(s, s_count)) + gate_types.append(GateCount(s_dagg, s_count)) + + gate_types.append(GateCount(rz)) + gate_types.append(GateCount(cnot, 2 * (active_wires - 1))) + + return gate_types + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * pauli_string (str): a string describing the pauli operators that define the rotation + * eps (float): error threshold for clifford plus T decomposition of this operation + """ + return { + "pauli_string": self.pauli_string, + "eps": self.eps, + } + + @classmethod + def resource_rep(cls, pauli_string, eps=None): + """Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + pauli_string (str): a string describing the pauli operators that define the rotation + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"pauli_string": pauli_string, "eps": eps}) + + @classmethod + def default_adjoint_resource_decomp(cls, pauli_string, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Args: + pauli_string (str): a string describing the pauli operators that define the rotation + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The adjoint of this operator just changes the sign of the phase, thus + the resources of the adjoint operation results in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(pauli_string=pauli_string, eps=eps))] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + pauli_string, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + pauli_string (str): a string describing the pauli operators that define the rotation + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + When the :code:`pauli_string` is a single Pauli operator (:code:`X, Y, Z, Identity`) + the cost is the associated controlled single qubit rotation gate: (:class:`~.ResourceCRX`, + :class:`~.ResourceCRY`, :class:`~.ResourceCRZ`, controlled-:class:`~.ResourceGlobalPhase`). + + The resources are derived from the following identity. If an operation :math:`\hat{A}` + can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` + then the controlled operation :math:`C\hat{A}` can be expressed as: + + .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} + + Specifically, the resources are one multi-controlled RZ-gate and a cascade of + :math:`2 * (n - 1)` :class:`~.ResourceCNOT` gates where :math:`n` is the number of qubits + the gate acts on. Additionally, for each :code:`X` gate in the Pauli word we conjugate by + a pair of :class:`~.ResourceHadamard` gates, and for each :code:`Y` gate in the Pauli word + we conjugate by a pair of :class:`~.ResourceHadamard` and a pair of :class:`~.ResourceS` gates. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + + if (set(pauli_string) == {"I"}) or (len(pauli_string) == 0): + ctrl_gp = re.ResourceControlled.resource_rep( + re.resource_rep(re.ResourceGlobalPhase), + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ) + return [GateCount(ctrl_gp)] + + if pauli_string == "X": + return [ + GateCount( + re.ResourceControlled.resource_rep( + re.resource_rep(re.ResourceRX, {"eps": eps}), + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ) + ) + ] + if pauli_string == "Y": + return [ + GateCount( + re.ResourceControlled.resource_rep( + re.resource_rep(re.ResourceRY, {"eps": eps}), + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ) + ) + ] + if pauli_string == "Z": + return [ + GateCount( + re.ResourceControlled.resource_rep( + re.resource_rep(re.ResourceRZ, {"eps": eps}), + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ) + ) + ] + + active_wires = len(pauli_string.replace("I", "")) + + h = re.ResourceHadamard.resource_rep() + s = re.ResourceS.resource_rep() + crz = re.ResourceControlled.resource_rep( + re.resource_rep(re.ResourceRZ, {"eps": eps}), + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + ) + s_dagg = re.resource_rep( + re.ResourceAdjoint, + {"base_cmpr_op": re.resource_rep(re.ResourceS)}, + ) + cnot = re.ResourceCNOT.resource_rep() + + h_count = 0 + s_count = 0 + + for gate in pauli_string: + if gate == "X": + h_count += 1 + if gate == "Y": + h_count += 1 + s_count += 1 + + gate_types = [] + if h_count: + gate_types.append(GateCount(h, 2 * h_count)) + + if s_count: + gate_types.append(GateCount(s, s_count)) + gate_types.append(GateCount(s_dagg, s_count)) + + gate_types.append(GateCount(crz)) + gate_types.append(GateCount(cnot, 2 * (active_wires - 1))) + + return gate_types + + @classmethod + def default_pow_resource_decomp(cls, pow_z, pauli_string, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + pauli_string (str): a string describing the pauli operators that define the rotation + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + Taking arbitrary powers of a general rotation produces a sum of rotations. + The resources simplify to just one total pauli rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(pauli_string=pauli_string, eps=eps))] + + +class ResourceIsingXX(ResourceOperator): + r"""Resource class for the IsingXX gate. + + Args: + eps (float, optional): error threshold for Clifford+T decomposition of this operation + wires (Sequence[int], optional): the wire the operation acts on + + Resources: + Ising XX coupling gate + + .. math:: XX(\phi) = \exp\left(-i \frac{\phi}{2} (X \otimes X)\right) = + \begin{bmatrix} = + \cos(\phi / 2) & 0 & 0 & -i \sin(\phi / 2) \\ + 0 & \cos(\phi / 2) & -i \sin(\phi / 2) & 0 \\ + 0 & -i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ + -i \sin(\phi / 2) & 0 & 0 & \cos(\phi / 2) + \end{bmatrix}. + + The circuit implementing this transformation is given by: + + .. code-block:: bash + + 0: ─╭●─────RX────╭●─┤ + 1: ─╰X───────────╰X─┤ + + .. seealso:: :class:`~.IsingXX` + + **Example** + + The resources for this operation are computed using: + + >>> ising_xx = plre.ResourceIsingXX() + >>> gate_set = {"CNOT", "RX"} + >>> print(plre.estimate_resources(ising_xx, gate_set)) + --- Resources: --- + Total qubits: 2 + Total gates : 3 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 2 + Gate breakdown: + {'CNOT': 2, 'RX': 1} + + """ + + num_wires = 2 + resource_keys = {"eps"} + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs): + r"""Returns a list of GateCount objects representing the operator's resources. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + Ising XX coupling gate + + .. math:: XX(\phi) = \exp\left(-i \frac{\phi}{2} (X \otimes X)\right) = + \begin{bmatrix} = + \cos(\phi / 2) & 0 & 0 & -i \sin(\phi / 2) \\ + 0 & \cos(\phi / 2) & -i \sin(\phi / 2) & 0 \\ + 0 & -i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ + -i \sin(\phi / 2) & 0 & 0 & \cos(\phi / 2) + \end{bmatrix}. + + The cost for implementing this transformation is given by: + + .. code-block:: bash + + 0: ─╭●─────RX────╭●─┤ + 1: ─╰X───────────╰X─┤ + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + cnot = re.ResourceCNOT.resource_rep() + rx = re.ResourceRX.resource_rep(eps=eps) + return [GateCount(cnot, 2), GateCount(rx)] + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * eps (float): error threshold for clifford plus T decomposition of this operation + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None): + """Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The adjoint of this operator just changes the sign of the phase angle, thus + the resources of the adjoint operation results in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps=eps))] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The resources are derived from the following identity. If an operation :math:`\hat{A}` + can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` + then the controlled operation :math:`C\hat{A}` can be expressed as: + + .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} + + Specifically, the resources are one multi-controlled RX-gate and a pair of + :class:`~.ResourceCNOT` gates. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + + """ + cnot = re.ResourceCNOT.resource_rep() + ctrl_rx = re.ResourceControlled.resource_rep( + base_cmpr_op=re.ResourceRX.resource_rep(eps=eps), + num_ctrl_wires=ctrl_num_ctrl_wires, + num_ctrl_values=ctrl_num_ctrl_values, + ) + + return [GateCount(cnot, 2), GateCount(ctrl_rx)] + + @classmethod + def default_pow_resource_decomp( + cls, + pow_z, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + Taking arbitrary powers of a rotation produces a sum of rotations. + The resources simplify to just one total Ising rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps=eps))] + + +class ResourceIsingYY(ResourceOperator): + r"""Resource class for the IsingYY gate. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + wires (Sequence[int], optional): the wire the operation acts on + + Resources: + Ising YY coupling gate + + .. math:: \mathtt{YY}(\phi) = \exp\left(-i \frac{\phi}{2} (Y \otimes Y)\right) = + \begin{bmatrix} + \cos(\phi / 2) & 0 & 0 & i \sin(\phi / 2) \\ + 0 & \cos(\phi / 2) & -i \sin(\phi / 2) & 0 \\ + 0 & -i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ + i \sin(\phi / 2) & 0 & 0 & \cos(\phi / 2) + \end{bmatrix}. + + The cost for implementing this transformation is given by: + + .. code-block:: bash + + 0: ─╭●─────RY────╭●─┤ + 1: ─╰Y───────────╰Y─┤ + + .. seealso:: :class:`~.IsingYY` + + **Example** + + The resources for this operation are computed using: + + >>> ising_yy = plre.ResourceIsingYY() + >>> gate_set = {"CY", "RY"} + >>> print(plre.estimate_resources(ising_yy, gate_set)) + --- Resources: --- + Total qubits: 2 + Total gates : 3 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 2 + Gate breakdown: + {'CY': 2, 'RY': 1} + + """ + + num_wires = 2 + resource_keys = {"eps"} + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs): + r"""Returns a list of GateCount objects representing the operator's resources. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + Ising YY coupling gate + + .. math:: \mathtt{YY}(\phi) = \exp\left(-i \frac{\phi}{2} (Y \otimes Y)\right) = + \begin{bmatrix} + \cos(\phi / 2) & 0 & 0 & i \sin(\phi / 2) \\ + 0 & \cos(\phi / 2) & -i \sin(\phi / 2) & 0 \\ + 0 & -i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ + i \sin(\phi / 2) & 0 & 0 & \cos(\phi / 2) + \end{bmatrix}. + + The cost for implementing this transformation is given by: + + .. code-block:: bash + + 0: ─╭●─────RY────╭●─┤ + 1: ─╰Y───────────╰Y─┤ + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + cy = re.ops.ResourceCY.resource_rep() + ry = re.ops.ResourceRY.resource_rep(eps=eps) + return [GateCount(cy, 2), GateCount(ry)] + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * eps (float): error threshold for clifford plus T decomposition of this operation + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None): + """Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The adjoint of this operator just changes the sign of the phase angle, thus + the resources of the adjoint operation results in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps=eps))] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The resources are derived from the following identity. If an operation :math:`\hat{A}` + can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` + then the controlled operation :math:`C\hat{A}` can be expressed as: + + .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} + + Specifically, the resources are one multi-controlled RY-gate and a pair of + :class:`~.ResourceCY` gates. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + cy = re.ops.ResourceCY.resource_rep() + ctrl_ry = re.ResourceControlled.resource_rep( + base_cmpr_op=re.ResourceRY.resource_rep(eps=eps), + num_ctrl_wires=ctrl_num_ctrl_wires, + num_ctrl_values=ctrl_num_ctrl_values, + ) + + return [GateCount(cy, 2), GateCount(ctrl_ry)] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + Taking arbitrary powers of a rotation produces a sum of rotations. + The resources simplify to just one total Ising rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps=eps))] + + +class ResourceIsingXY(ResourceOperator): + r"""Resource class for the IsingXY gate. + + Args: + eps (float, optional): error threshold for Clifford+T decomposition of this operation + wires (Sequence[int], optional): the wire the operation acts on + + Resources: + Ising (XX + YY) coupling gate + + .. math:: \mathtt{XY}(\phi) = \exp\left(i \frac{\theta}{4} (X \otimes X + Y \otimes Y)\right) = + \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & \cos(\phi / 2) & i \sin(\phi / 2) & 0 \\ + 0 & i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix}. + + The cost for implementing this transformation is given by: + + .. code-block:: bash + + 0: ──H─╭●─────RY────╭●──H─┤ + 1: ────╰Y─────RX────╰Y────┤ + + .. seealso:: :class:`~.IsingXY` + + **Example** + + The resources for this operation are computed using: + + >>> ising_xy = plre.ResourceIsingXY() + >>> gate_set = {"Hadamard", "CY", "RY", "RX"} + >>> print(plre.estimate_resources(ising_xy, gate_set)) + --- Resources: --- + Total qubits: 2 + Total gates : 6 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 2 + Gate breakdown: + {'Hadamard': 2, 'CY': 2, 'RY': 1, 'RX': 1} + + """ + + num_wires = 2 + resource_keys = {"eps"} + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs): + r"""Returns a list of GateCount objects representing the operator's resources. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + IsingXY coupling gate + + .. math:: \mathtt{XY}(\phi) = \exp\left(i \frac{\theta}{4} (X \otimes X + Y \otimes Y)\right) = + \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & \cos(\phi / 2) & i \sin(\phi / 2) & 0 \\ + 0 & i \sin(\phi / 2) & \cos(\phi / 2) & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix}. + + The cost for implementing this transformation is given by: + + .. code-block:: bash + + 0: ──H─╭●─────RY────╭●──H─┤ + 1: ────╰Y─────RX────╰Y────┤ + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + h = re.ResourceHadamard.resource_rep() + cy = re.ResourceCY.resource_rep() + ry = re.ResourceRY.resource_rep(eps=eps) + rx = re.ResourceRX.resource_rep(eps=eps) + + return [GateCount(h, 2), GateCount(cy, 2), GateCount(ry), GateCount(rx)] + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * eps (float): error threshold for clifford plus T decomposition of this operation + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None): + """Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The adjoint of this operator just changes the sign of the phase angle, thus + the resources of the adjoint operation results in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps=eps))] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The resources are derived from the following identity. If an operation :math:`\hat{A}` + can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` + then the controlled operation :math:`C\hat{A}` can be expressed as: + + .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} + + Specifically, the resources are one multi-controlled RY-gate, one multi-controlled RX-gate, + a pair of :class:`~.ResourceCY` gates and a pair of :class:`~.ResourceHadamard` gates. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + h = re.ResourceHadamard.resource_rep() + cy = re.ResourceCY.resource_rep() + ctrl_rx = re.ResourceControlled.resource_rep( + base_cmpr_op=re.ResourceRX.resource_rep(eps=eps), + num_ctrl_wires=ctrl_num_ctrl_wires, + num_ctrl_values=ctrl_num_ctrl_values, + ) + ctrl_ry = re.ResourceControlled.resource_rep( + base_cmpr_op=re.ResourceRY.resource_rep(eps=eps), + num_ctrl_wires=ctrl_num_ctrl_wires, + num_ctrl_values=ctrl_num_ctrl_values, + ) + + return [GateCount(h, 2), GateCount(cy, 2), GateCount(ctrl_ry), GateCount(ctrl_rx)] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + Taking arbitrary powers of a rotation produces a sum of rotations. + The resources simplify to just one total Ising rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps=eps))] + + +class ResourceIsingZZ(ResourceOperator): + r"""Resource class for the IsingZZ gate. + + Args: + eps (float, optional): error threshold for Clifford+T decomposition of this operation + wires (Sequence[int], optional): the wire the operation acts on + + Resources: + Ising ZZ coupling gate + + .. math:: ZZ(\phi) = \exp\left(-i \frac{\phi}{2} (Z \otimes Z)\right) = + \begin{bmatrix} + e^{-i \phi / 2} & 0 & 0 & 0 \\ + 0 & e^{i \phi / 2} & 0 & 0 \\ + 0 & 0 & e^{i \phi / 2} & 0 \\ + 0 & 0 & 0 & e^{-i \phi / 2} + \end{bmatrix}. + + The cost for implmenting this transformation is given by: + + .. code-block:: bash + + 0: ─╭●───────────╭●─┤ + 1: ─╰X─────RZ────╰X─┤ + + .. seealso:: :class:`~.IsingZZ` + + **Example** + + The resources for this operation are computed using: + + >>> ising_zz = plre.ResourceIsingZZ() + >>> gate_set = {"CNOT", "RZ"} + >>> print(plre.estimate_resources(ising_zz, gate_set)) + --- Resources: --- + Total qubits: 2 + Total gates : 3 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 2 + Gate breakdown: + {'CNOT': 2, 'RZ': 1} + + """ + + num_wires = 2 + resource_keys = {"eps"} + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs): + r"""Returns a list of GateCount objects representing the operator's resources. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + Ising ZZ coupling gate + + .. math:: ZZ(\phi) = \exp\left(-i \frac{\phi}{2} (Z \otimes Z)\right) = + \begin{bmatrix} + e^{-i \phi / 2} & 0 & 0 & 0 \\ + 0 & e^{i \phi / 2} & 0 & 0 \\ + 0 & 0 & e^{i \phi / 2} & 0 \\ + 0 & 0 & 0 & e^{-i \phi / 2} + \end{bmatrix}. + + The cost for implmenting this transformation is given by: + + .. code-block:: bash + + 0: ─╭●───────────╭●─┤ + 1: ─╰X─────RZ────╰X─┤ + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + cnot = re.ResourceCNOT.resource_rep() + rz = re.ResourceRZ.resource_rep(eps=eps) + + gate_types = {} + gate_types[cnot] = 2 + gate_types[rz] = 1 + + return [GateCount(cnot, 2), GateCount(rz)] + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * eps (float): error threshold for clifford plus T decomposition of this operation + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None): + """Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The adjoint of this operator just changes the sign of the phase angle, thus + the resources of the adjoint operation results in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps=eps))] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The resources are derived from the following identity. If an operation :math:`\hat{A}` + can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` + then the controlled operation :math:`C\hat{A}` can be expressed as: + + .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} + + Specifically, the resources are one multi-controlled RZ-gate and a pair of + :class:`~.ResourceCNOT` gates. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + cnot = re.ResourceCNOT.resource_rep() + ctrl_rz = re.ResourceControlled.resource_rep( + base_cmpr_op=re.ResourceRZ.resource_rep(eps=eps), + num_ctrl_wires=ctrl_num_ctrl_wires, + num_ctrl_values=ctrl_num_ctrl_values, + ) + return [GateCount(cnot, 2), GateCount(ctrl_rz)] + + @classmethod + def default_pow_resource_decomp(cls, pow_z, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for an operator raised to a power. + + Args: + pow_z (int): the power that the operator is being raised to + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + Taking arbitrary powers of a rotation produces a sum of rotations. + The resources simplify to just one total Ising rotation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + return [GateCount(cls.resource_rep(eps=eps))] + + +class ResourcePSWAP(ResourceOperator): + r"""Resource class for the PSWAP gate. + + Args: + eps (float, optional): error threshold for Clifford+T decomposition of this operation + wires (Sequence[int], optional): the wire the operation acts on + + Resources: + The :code:`PSWAP` gate is defined as: + + .. math:: PSWAP(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & e^{i \phi} & 0 \\ + 0 & e^{i \phi} & 0 & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix}. + + The cost for implementing this transformation is given by: + + .. code-block:: bash + + 0: ─╭SWAP─╭●───────────╭●─┤ + 1: ─╰SWAP─╰X─────Rϕ────╰X─┤ + + .. seealso:: :class:`~.PSWAP` + + **Example** + + The resources for this operation are computed using: + + >>> pswap = plre.ResourcePSWAP() + >>> gate_set = {"CNOT", "SWAP", "PhaseShift"} + >>> print(plre.estimate_resources(pswap, gate_set)) + --- Resources: --- + Total qubits: 2 + Total gates : 4 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 2 + Gate breakdown: + {'SWAP': 1, 'PhaseShift': 1, 'CNOT': 2} + + """ + + num_wires = 2 + resource_keys = {"eps"} + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs): + r"""Returns a list of GateCount objects representing the operator's resources. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The :code:`PSWAP` gate is defined as: + + .. math:: PSWAP(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & 0 & e^{i \phi} & 0 \\ + 0 & e^{i \phi} & 0 & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix}. + + The cost for implementing this transformation is given by: + + .. code-block:: bash + + 0: ─╭SWAP─╭●───────────╭●─┤ + 1: ─╰SWAP─╰X─────Rϕ────╰X─┤ + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + swap = re.ResourceSWAP.resource_rep() + cnot = re.ResourceCNOT.resource_rep() + phase = re.ResourcePhaseShift.resource_rep(eps=eps) + + return [GateCount(swap), GateCount(phase), GateCount(cnot, 2)] + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * eps (float): error threshold for clifford plus T decomposition of this operation + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None): + """Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"eps": eps}) + + @classmethod + def default_adjoint_resource_decomp(cls, eps=None) -> list[GateCount]: + r"""Returns a list representing the resources for the adjoint of the operator. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The adjoint of this operator just changes the sign of the phase angle, thus + the resources of the adjoint operation results in the original operation. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + + """ + return [GateCount(cls.resource_rep(eps=eps))] + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires, + ctrl_num_ctrl_values, + eps=None, + ) -> list[GateCount]: + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The resources are derived from the following identity. If an operation :math:`\hat{A}` + can be expressed as :math:`\hat{A} \ = \ \hat{U} \cdot \hat{B} \cdot \hat{U}^{\dagger}` + then the controlled operation :math:`C\hat{A}` can be expressed as: + + .. math:: C\hat{A} \ = \ \hat{U} \cdot C\hat{B} \cdot \hat{U}^{\dagger} + + Specifically, the resources are one multi-controlled phase shift gate, one multi-controlled + SWAP gate and a pair of :class:`~.ResourceCNOT` gates. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + cnot = re.ResourceCNOT.resource_rep() + ctrl_swap = re.ResourceControlled.resource_rep( + base_cmpr_op=re.ResourceSWAP.resource_rep(), + num_ctrl_wires=ctrl_num_ctrl_wires, + num_ctrl_values=ctrl_num_ctrl_values, + ) + ctrl_ps = re.ResourceControlled.resource_rep( + base_cmpr_op=re.ResourcePhaseShift.resource_rep(eps=eps), + num_ctrl_wires=ctrl_num_ctrl_wires, + num_ctrl_values=ctrl_num_ctrl_values, + ) + + return [GateCount(ctrl_swap), GateCount(cnot, 2), GateCount(ctrl_ps)] diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_multi_qubit.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_multi_qubit.py new file mode 100644 index 00000000000..ec66fd128bb --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/ops/qubit/test_parametric_ops_multi_qubit.py @@ -0,0 +1,686 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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 parametric multi qubit resource operators.""" + +import pytest + +import pennylane.labs.resource_estimation as re + +# pylint: disable=use-implicit-booleaness-not-comparison,no-self-use,too-many-arguments + + +class TestMultiRZ: + """Test the ResourceMultiRZ class.""" + + @pytest.mark.parametrize("eps", (None, 1e-3)) + @pytest.mark.parametrize("num_wires", range(1, 5)) + def test_resource_params(self, num_wires, eps): + """Test that the resource params are correct.""" + if eps: + op = re.ResourceMultiRZ(num_wires, eps=eps) + else: + op = re.ResourceMultiRZ(num_wires) + + assert op.resource_params == {"num_wires": num_wires, "eps": eps} + + @pytest.mark.parametrize("eps", (None, 1e-3)) + @pytest.mark.parametrize("num_wires", range(1, 5)) + def test_resource_rep(self, num_wires, eps): + """Test that the compressed representation is correct.""" + expected = re.CompressedResourceOp(re.ResourceMultiRZ, {"num_wires": num_wires, "eps": eps}) + assert re.ResourceMultiRZ.resource_rep(num_wires, eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + @pytest.mark.parametrize("num_wires", range(1, 5)) + def test_resources(self, num_wires, eps): + """Test that the resources are correct.""" + expected = [ + re.GateCount(re.ResourceCNOT.resource_rep(), 2 * (num_wires - 1)), + re.GateCount(re.ResourceRZ.resource_rep(eps=eps)), + ] + assert re.ResourceMultiRZ.resource_decomp(num_wires, eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + @pytest.mark.parametrize("num_wires", range(1, 5)) + def test_resources_from_rep(self, num_wires, eps): + """Test that the resources can be computed from the compressed representation and params.""" + op = re.ResourceMultiRZ(num_wires, eps) + expected = [ + re.GateCount(re.ResourceCNOT.resource_rep(), 2 * (num_wires - 1)), + re.GateCount(re.ResourceRZ.resource_rep(eps=eps)), + ] + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resource_decomp(**op_resource_params) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + @pytest.mark.parametrize("num_wires", range(1, 5)) + def test_adjoint_decomp(self, num_wires, eps): + """Test that the adjoint decomposition is correct.""" + expected = [re.GateCount(re.ResourceMultiRZ.resource_rep(num_wires=num_wires, eps=eps))] + assert re.ResourceMultiRZ.adjoint_resource_decomp(num_wires=num_wires, eps=eps) == expected + + ctrl_data = ( + ( + 1, + 0, + [ + re.GateCount(re.resource_rep(re.ResourceCNOT()), 4), + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceRZ.resource_rep(1e-3), 1, 0) + ), + ], + ), + ( + 1, + 1, + [ + re.GateCount(re.resource_rep(re.ResourceCNOT()), 4), + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceRZ.resource_rep(1e-3), 1, 1) + ), + ], + ), + ( + 2, + 0, + [ + re.GateCount(re.resource_rep(re.ResourceCNOT()), 4), + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceRZ.resource_rep(1e-3), 2, 0) + ), + ], + ), + ( + 3, + 2, + [ + re.GateCount(re.resource_rep(re.ResourceCNOT()), 4), + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceRZ.resource_rep(1e-3), 3, 2) + ), + ], + ), + ) + + @pytest.mark.parametrize("num_ctrl_wires, num_ctrl_values, expected_res", ctrl_data) + def test_resource_controlled(self, num_ctrl_wires, num_ctrl_values, expected_res): + """Test that the controlled resources are as expected""" + + op = re.ResourceMultiRZ(num_wires=3, eps=1e-3) + op2 = re.ResourceControlled( + op, + num_ctrl_wires, + num_ctrl_values, + ) + + assert ( + op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, **op.resource_params) + == expected_res + ) + assert op2.resource_decomp(**op2.resource_params) == expected_res + + @pytest.mark.parametrize("z", range(1, 5)) + @pytest.mark.parametrize("eps", (None, 1e-3)) + @pytest.mark.parametrize("num_wires", range(1, 5)) + def test_pow_decomp(self, z, num_wires, eps): + """Test that the pow decomposition is correct.""" + op = re.ResourceMultiRZ(num_wires, eps=eps) + expected_res = [re.GateCount(re.ResourceMultiRZ.resource_rep(num_wires, eps))] + assert op.pow_resource_decomp(z, **op.resource_params) == expected_res + + +class TestPauliRot: + """Test the ResourcePauliRot class.""" + + pauli_words = ("I", "XYZ", "XXX", "XIYIZIX", "III") + + @pytest.mark.parametrize("eps", (None, 1e-3)) + @pytest.mark.parametrize("pauli_string", pauli_words) + def test_resource_params(self, pauli_string, eps): + """Test that the resource params are correct.""" + op = re.ResourcePauliRot(pauli_string=pauli_string, eps=eps) + assert op.resource_params == {"pauli_string": pauli_string, "eps": eps} + + @pytest.mark.parametrize("eps", (None, 1e-3)) + @pytest.mark.parametrize("pauli_string", pauli_words) + def test_resource_rep(self, pauli_string, eps): + """Test that the compressed representation is correct.""" + expected = re.CompressedResourceOp( + re.ResourcePauliRot, {"pauli_string": pauli_string, "eps": eps} + ) + assert re.ResourcePauliRot.resource_rep(pauli_string, eps) == expected + + expected_h_count = (0, 4, 6, 6, 0) + expected_s_count = (0, 1, 0, 1, 0) + params = zip(pauli_words, expected_h_count, expected_s_count) + + @pytest.mark.parametrize("eps", (None, 1e-3)) + @pytest.mark.parametrize("pauli_string, expected_h_count, expected_s_count", params) + def test_resources(self, pauli_string, expected_h_count, expected_s_count, eps): + """Test that the resources are correct.""" + active_wires = len(pauli_string.replace("I", "")) + + if set(pauli_string) == {"I"}: + expected = [re.GateCount(re.ResourceGlobalPhase.resource_rep())] + else: + expected = [] + + if expected_h_count: + expected.append(re.GateCount(re.ResourceHadamard.resource_rep(), expected_h_count)) + + if expected_s_count: + expected.append(re.GateCount(re.ResourceS.resource_rep(), expected_s_count)) + expected.append( + re.GateCount( + re.ResourceAdjoint.resource_rep(re.ResourceS.resource_rep()), + expected_s_count, + ) + ) + + expected.append(re.GateCount(re.ResourceRZ.resource_rep(eps=eps))) + expected.append(re.GateCount(re.ResourceCNOT.resource_rep(), 2 * (active_wires - 1))) + + assert re.ResourcePauliRot.resource_decomp(pauli_string, eps=eps) == expected + + def test_resources_empty_pauli_string(self): + """Test that the resources method produces the correct result for an empty pauli string.""" + expected = [re.GateCount(re.ResourceGlobalPhase.resource_rep())] + assert re.ResourcePauliRot.resource_decomp(pauli_string="") == expected + + @pytest.mark.parametrize("pauli_string, expected_h_count, expected_s_count", params) + def test_resources_from_rep(self, pauli_string, expected_h_count, expected_s_count): + """Test that the resources can be computed from the compressed representation and params.""" + op = re.ResourcePauliRot(0.5, pauli_string, wires=range(len(pauli_string))) + active_wires = len(pauli_string.replace("I", "")) + + if set(pauli_string) == {"I"}: + expected = [re.GateCount(re.ResourceGlobalPhase.resource_rep())] + else: + expected = [ + re.GateCount(re.ResourceRZ.resource_rep()), + re.GateCount(re.ResourceCNOT.resource_rep(), 2 * (active_wires - 1)), + ] + + if expected_h_count: + expected.append(re.GateCount(re.ResourceHadamard.resource_rep(), expected_h_count)) + + if expected_s_count: + expected.append(re.GateCount(re.ResourceS.resource_rep(), expected_s_count)) + expected.append( + re.GateCount( + re.ResourceAdjoint.resource_rep(re.ResourceS.resource_rep()), + expected_s_count, + ) + ) + + op_compressed_rep = op.resource_rep_from_op() + op_resource_type = op_compressed_rep.op_type + op_resource_params = op_compressed_rep.params + assert op_resource_type.resource_decomp(**op_resource_params) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + @pytest.mark.parametrize("pauli_word", pauli_words) + def test_adjoint_decomp(self, pauli_word, eps): + """Test that the adjoint decomposition is correct.""" + expected = [ + re.GateCount(re.ResourcePauliRot.resource_rep(pauli_string=pauli_word, eps=eps)) + ] + assert ( + re.ResourcePauliRot.adjoint_resource_decomp(pauli_string=pauli_word, eps=eps) + == expected + ) + + ctrl_data = ( + ( + "XXX", + 1, + 0, + [ + re.GateCount(re.ResourceHadamard.resource_rep(), 6), + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceRZ.resource_rep(eps=1e-5), 1, 0), + 1, + ), + re.GateCount(re.ResourceCNOT.resource_rep(), 4), + ], + ), + ( + "XXX", + 1, + 1, + [ + re.GateCount(re.ResourceHadamard.resource_rep(), 6), + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceRZ.resource_rep(eps=1e-5), 1, 1), + 1, + ), + re.GateCount(re.ResourceCNOT.resource_rep(), 4), + ], + ), + ( + "XXX", + 2, + 0, + [ + re.GateCount(re.ResourceHadamard.resource_rep(), 6), + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceRZ.resource_rep(eps=1e-5), 2, 0), + 1, + ), + re.GateCount(re.ResourceCNOT.resource_rep(), 4), + ], + ), + ( + "XIYIZIX", + 1, + 0, + [ + re.GateCount(re.ResourceHadamard.resource_rep(), 6), + re.GateCount(re.ResourceS.resource_rep(), 1), + re.GateCount(re.ResourceAdjoint.resource_rep(re.ResourceS.resource_rep()), 1), + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceRZ.resource_rep(eps=1e-5), 1, 0), + 1, + ), + re.GateCount(re.ResourceCNOT.resource_rep(), 6), + ], + ), + ( + "XIYIZIX", + 1, + 1, + [ + re.GateCount(re.ResourceHadamard.resource_rep(), 6), + re.GateCount(re.ResourceS.resource_rep(), 1), + re.GateCount(re.ResourceAdjoint.resource_rep(re.ResourceS.resource_rep()), 1), + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceRZ.resource_rep(eps=1e-5), 1, 1), + 1, + ), + re.GateCount(re.ResourceCNOT.resource_rep(), 6), + ], + ), + ( + "XIYIZIX", + 2, + 0, + [ + re.GateCount(re.ResourceHadamard.resource_rep(), 6), + re.GateCount(re.ResourceS.resource_rep(), 1), + re.GateCount(re.ResourceAdjoint.resource_rep(re.ResourceS.resource_rep()), 1), + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceRZ.resource_rep(eps=1e-5), 2, 0), + 1, + ), + re.GateCount(re.ResourceCNOT.resource_rep(), 6), + ], + ), + ( + "III", + 1, + 0, + [ + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceGlobalPhase.resource_rep(), 1, 0) + ) + ], + ), + ( + "X", + 1, + 1, + [ + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceRX.resource_rep(eps=1e-5), 1, 1) + ) + ], + ), + ( + "Y", + 2, + 0, + [ + re.GateCount( + re.ResourceControlled.resource_rep(re.ResourceRY.resource_rep(eps=1e-5), 2, 0) + ) + ], + ), + ) + + @pytest.mark.parametrize("pauli_word, num_ctrl_wires, num_ctrl_values, expected_res", ctrl_data) + def test_resource_controlled(self, num_ctrl_wires, num_ctrl_values, pauli_word, expected_res): + """Test that the controlled resources are as expected""" + + op = re.ResourcePauliRot(pauli_word, eps=1e-5) + op2 = re.ResourceControlled(op, num_ctrl_wires, num_ctrl_values) + + assert ( + op.controlled_resource_decomp(num_ctrl_wires, num_ctrl_values, **op.resource_params) + == expected_res + ) + assert op2.resource_decomp(**op2.resource_params) == expected_res + + @pytest.mark.parametrize("z", range(1, 5)) + @pytest.mark.parametrize("eps", (None, 1e-3)) + @pytest.mark.parametrize("pauli_word", pauli_words) + def test_pow_decomp(self, z, pauli_word, eps): + """Test that the pow decomposition is correct.""" + op = re.ResourcePauliRot(pauli_string=pauli_word, eps=eps) + expected_res = [ + re.GateCount(re.ResourcePauliRot.resource_rep(pauli_string=pauli_word, eps=eps)) + ] + assert op.pow_resource_decomp(z, **op.resource_params) == expected_res + + op2 = re.ResourcePow(op, z) + assert op2.resource_decomp(**op2.resource_params) == expected_res + + +class TestIsingXX: + """Test the IsingXX class.""" + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resource_params(self, eps): + """Test that the resource params are correct.""" + if eps: + op = re.ResourceIsingXX(eps=eps) + else: + op = re.ResourceIsingXX() + + assert op.resource_params == {"eps": eps} + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resource_rep(self, eps): + """Test that the compressed representation is correct.""" + expected = re.CompressedResourceOp(re.ResourceIsingXX, {"eps": eps}) + assert re.ResourceIsingXX.resource_rep(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources(self, eps): + """Test that the resources are correct.""" + expected = [ + re.GateCount(re.ResourceCNOT.resource_rep(), 2), + re.GateCount(re.ResourceRX.resource_rep(eps=eps)), + ] + assert re.ResourceIsingXX.resource_decomp(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_pow(self, eps): + """Test that the pow resources are correct.""" + expected = [re.GateCount(re.ResourceIsingXX.resource_rep(eps=eps))] + assert re.ResourceIsingXX.pow_resource_decomp(pow_z=3, eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_adjoint(self, eps): + """Test that the adjoint resources are correct.""" + expected = [re.GateCount(re.ResourceIsingXX.resource_rep(eps=eps))] + assert re.ResourceIsingXX.adjoint_resource_decomp(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_controlled(self, eps): + """Test that the controlled resources are correct.""" + expected = [ + re.GateCount(re.ResourceCNOT.resource_rep(), 2), + re.GateCount( + re.ResourceControlled.resource_rep( + re.ResourceRX.resource_rep(eps=eps), + 3, + 2, + ) + ), + ] + op = re.ResourceControlled(re.ResourceIsingXX(eps=eps), num_ctrl_wires=3, num_ctrl_values=2) + assert op.resource_decomp(**op.resource_params) == expected + + +class TestIsingXY: + """Test the IsingXY class.""" + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resource_params(self, eps): + """Test that the resource params are correct.""" + if eps: + op = re.ResourceIsingXY(eps=eps) + else: + op = re.ResourceIsingXY() + + assert op.resource_params == {"eps": eps} + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resource_rep(self, eps): + """Test that the compressed representation is correct.""" + expected = re.CompressedResourceOp(re.ResourceIsingXY, {"eps": eps}) + assert re.ResourceIsingXY.resource_rep(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources(self, eps): + """Test that the resources are correct.""" + expected = [ + re.GateCount(re.ResourceHadamard.resource_rep(), 2), + re.GateCount(re.ResourceCY.resource_rep(), 2), + re.GateCount(re.ResourceRY.resource_rep(eps=eps)), + re.GateCount(re.ResourceRX.resource_rep(eps=eps)), + ] + assert re.ResourceIsingXY.resource_decomp(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_pow(self, eps): + """Test that the pow resources are correct.""" + expected = [re.GateCount(re.ResourceIsingXY.resource_rep(eps=eps))] + assert re.ResourceIsingXY.pow_resource_decomp(pow_z=3, eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_adjoint(self, eps): + """Test that the adjoint resources are correct.""" + expected = [re.GateCount(re.ResourceIsingXY.resource_rep(eps=eps))] + assert re.ResourceIsingXY.adjoint_resource_decomp(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_controlled(self, eps): + """Test that the controlled resources are correct.""" + expected = [ + re.GateCount(re.ResourceHadamard.resource_rep(), 2), + re.GateCount(re.ResourceCY.resource_rep(), 2), + re.GateCount( + re.ResourceControlled.resource_rep( + re.ResourceRY.resource_rep(eps=eps), + 3, + 2, + ) + ), + re.GateCount( + re.ResourceControlled.resource_rep( + re.ResourceRX.resource_rep(eps=eps), + 3, + 2, + ) + ), + ] + op = re.ResourceControlled(re.ResourceIsingXY(eps=eps), num_ctrl_wires=3, num_ctrl_values=2) + assert op.resource_decomp(**op.resource_params) == expected + + +class TestIsingYY: + """Test the IsingYY class.""" + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resource_params(self, eps): + """Test that the resource params are correct.""" + if eps: + op = re.ResourceIsingYY(eps=eps) + else: + op = re.ResourceIsingYY() + + assert op.resource_params == {"eps": eps} + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resource_rep(self, eps): + """Test that the compressed representation is correct.""" + expected = re.CompressedResourceOp(re.ResourceIsingYY, {"eps": eps}) + assert re.ResourceIsingYY.resource_rep(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources(self, eps): + """Test that the resources are correct.""" + expected = [ + re.GateCount(re.ResourceCY.resource_rep(), 2), + re.GateCount(re.ResourceRY.resource_rep(eps=eps)), + ] + assert re.ResourceIsingYY.resource_decomp(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_pow(self, eps): + """Test that the pow resources are correct.""" + expected = [re.GateCount(re.ResourceIsingYY.resource_rep(eps=eps))] + assert re.ResourceIsingYY.pow_resource_decomp(pow_z=3, eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_adjoint(self, eps): + """Test that the adjoint resources are correct.""" + expected = [re.GateCount(re.ResourceIsingYY.resource_rep(eps=eps))] + assert re.ResourceIsingYY.adjoint_resource_decomp(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_controlled(self, eps): + """Test that the controlled resources are correct.""" + expected = [ + re.GateCount(re.ResourceCY.resource_rep(), 2), + re.GateCount( + re.ResourceControlled.resource_rep( + re.ResourceRY.resource_rep(eps=eps), + 3, + 2, + ) + ), + ] + op = re.ResourceControlled(re.ResourceIsingYY(eps=eps), num_ctrl_wires=3, num_ctrl_values=2) + assert op.resource_decomp(**op.resource_params) == expected + + +class TestIsingZZ: + """Test the IsingZZ class.""" + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resource_params(self, eps): + """Test that the resource params are correct.""" + if eps: + op = re.ResourceIsingZZ(eps=eps) + else: + op = re.ResourceIsingZZ() + + assert op.resource_params == {"eps": eps} + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resource_rep(self, eps): + """Test that the compressed representation is correct.""" + expected = re.CompressedResourceOp(re.ResourceIsingZZ, {"eps": eps}) + assert re.ResourceIsingZZ.resource_rep(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources(self, eps): + """Test that the resources are correct.""" + expected = [ + re.GateCount(re.ResourceCNOT.resource_rep(), 2), + re.GateCount(re.ResourceRZ.resource_rep(eps=eps)), + ] + assert re.ResourceIsingZZ.resource_decomp(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_pow(self, eps): + """Test that the pow resources are correct.""" + expected = [re.GateCount(re.ResourceIsingZZ.resource_rep(eps=eps))] + assert re.ResourceIsingZZ.pow_resource_decomp(pow_z=3, eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_adjoint(self, eps): + """Test that the adjoint resources are correct.""" + expected = [re.GateCount(re.ResourceIsingZZ.resource_rep(eps=eps))] + assert re.ResourceIsingZZ.adjoint_resource_decomp(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_controlled(self, eps): + """Test that the controlled resources are correct.""" + expected = [ + re.GateCount(re.ResourceCNOT.resource_rep(), 2), + re.GateCount( + re.ResourceControlled.resource_rep( + re.ResourceRZ.resource_rep(eps=eps), + 3, + 2, + ) + ), + ] + op = re.ResourceControlled(re.ResourceIsingZZ(eps=eps), num_ctrl_wires=3, num_ctrl_values=2) + assert op.resource_decomp(**op.resource_params) == expected + + +class TestPSWAP: + """Test the PSWAP class.""" + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resource_params(self, eps): + """Test that the resource params are correct.""" + if eps: + op = re.ResourcePSWAP(eps=eps) + else: + op = re.ResourcePSWAP() + + assert op.resource_params == {"eps": eps} + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resource_rep(self, eps): + """Test that the compressed representation is correct.""" + expected = re.CompressedResourceOp(re.ResourcePSWAP, {"eps": eps}) + assert re.ResourcePSWAP.resource_rep(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources(self, eps): + """Test that the resources are correct.""" + expected = [ + re.GateCount(re.ResourceSWAP.resource_rep()), + re.GateCount(re.ResourcePhaseShift.resource_rep(eps=eps)), + re.GateCount(re.ResourceCNOT.resource_rep(), 2), + ] + assert re.ResourcePSWAP.resource_decomp(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_adjoint(self, eps): + """Test that the adjoint resources are correct.""" + expected = [re.GateCount(re.ResourcePSWAP.resource_rep(eps=eps))] + assert re.ResourcePSWAP.adjoint_resource_decomp(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources_controlled(self, eps): + """Test that the controlled resources are correct.""" + expected = [ + re.GateCount( + re.ResourceControlled.resource_rep( + re.ResourceSWAP.resource_rep(), + 3, + 2, + ) + ), + re.GateCount(re.ResourceCNOT.resource_rep(), 2), + re.GateCount( + re.ResourceControlled.resource_rep( + re.ResourcePhaseShift.resource_rep(eps=eps), + 3, + 2, + ) + ), + ] + op = re.ResourceControlled(re.ResourcePSWAP(eps=eps), num_ctrl_wires=3, num_ctrl_values=2) + assert op.resource_decomp(**op.resource_params) == expected From 418d5b1fbcee238cce8623b5047d01c7a28f4207 Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Wed, 25 Jun 2025 17:04:19 -0400 Subject: [PATCH 17/18] Adding Resource estimation support for some core Templates (#7725) **Context:** Certain important templates are needed to unlock `CompactHamiltonian` support for Trotterization **Description of the Change:** Adding some core templates such as: - BasisRotation - OutMultiplier - SemiAdder - OutOfPlaceSquare - PhaseGradient - Select --------- Co-authored-by: Diksha Dhawan Co-authored-by: Diksha Dhawan <40900030+ddhawan11@users.noreply.github.com> Co-authored-by: Utkarsh Co-authored-by: Soran Jahangiri <40344468+soranjh@users.noreply.github.com> --- .github/workflows/tests-labs.yml | 2 - doc/releases/changelog-dev.md | 4 + .../labs/resource_estimation/__init__.py | 26 + .../labs/resource_estimation/ops/__init__.py | 1 + .../resource_estimation/ops/qubit/__init__.py | 1 + .../ops/qubit/qchem_ops.py | 146 +++ .../resource_estimation/templates/__init__.py | 24 + .../templates/subroutines.py | 1133 +++++++++++++++++ .../ops/qubit/test_qchem_ops.py | 59 + .../templates/test_subroutines.py | 466 +++++++ 10 files changed, 1860 insertions(+), 2 deletions(-) create mode 100644 pennylane/labs/resource_estimation/ops/qubit/qchem_ops.py create mode 100644 pennylane/labs/resource_estimation/templates/__init__.py create mode 100644 pennylane/labs/resource_estimation/templates/subroutines.py create mode 100644 pennylane/labs/tests/resource_estimation/ops/qubit/test_qchem_ops.py create mode 100644 pennylane/labs/tests/resource_estimation/templates/test_subroutines.py diff --git a/.github/workflows/tests-labs.yml b/.github/workflows/tests-labs.yml index 2f8ce42405f..b6cc6feb76c 100644 --- a/.github/workflows/tests-labs.yml +++ b/.github/workflows/tests-labs.yml @@ -1,8 +1,6 @@ name: PennyLane Labs Unit-Tests on: pull_request: - branches: - - master concurrency: group: qml-labs-${{ github.workflow }}-${{ github.ref }} diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md index 47ce4a52ab9..2e4dcee4123 100644 --- a/doc/releases/changelog-dev.md +++ b/doc/releases/changelog-dev.md @@ -792,6 +792,10 @@ estimation for multi-qubit parametic gates. [(#7549)](https://github.com/PennyLaneAI/pennylane/pull/7549) +* Added `pennylane.labs.ResourceOperator` templates for various algorithms required for + supporting compact hamiltonian development. + [(#7725)](https://github.com/PennyLaneAI/pennylane/pull/7725) + * A new module :mod:`pennylane.labs.zxopt ` provides access to the basic optimization passes from [pyzx](https://pyzx.readthedocs.io/en/latest/) for PennyLane circuits. diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 5db16ba0495..c5dfc1d00d8 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -110,6 +110,21 @@ ~ResourceIsingXY ~ResourceIsingZZ ~ResourcePSWAP + ~ResourceSingleExcitation + +Templates: +~~~~~~~~~~ + +.. autosummary:: + :toctree: api + + ~ResourceOutOfPlaceSquare + ~ResourcePhaseGradient + ~ResourceOutMultiplier + ~ResourceSemiAdder + ~ResourceBasisRotation + ~ResourceSelect + ~ResourceQROM """ @@ -169,9 +184,20 @@ ResourceIsingZZ, ResourcePSWAP, ResourceTempAND, + ResourceSingleExcitation, ResourceAdjoint, ResourceControlled, ResourceProd, ResourceChangeBasisOp, ResourcePow, ) + +from .templates import ( + ResourceOutOfPlaceSquare, + ResourcePhaseGradient, + ResourceOutMultiplier, + ResourceSemiAdder, + ResourceBasisRotation, + ResourceSelect, + ResourceQROM, +) diff --git a/pennylane/labs/resource_estimation/ops/__init__.py b/pennylane/labs/resource_estimation/ops/__init__.py index 7ebf79ea74e..c3167172512 100644 --- a/pennylane/labs/resource_estimation/ops/__init__.py +++ b/pennylane/labs/resource_estimation/ops/__init__.py @@ -57,4 +57,5 @@ ResourceIsingXY, ResourceIsingZZ, ResourcePSWAP, + ResourceSingleExcitation, ) diff --git a/pennylane/labs/resource_estimation/ops/qubit/__init__.py b/pennylane/labs/resource_estimation/ops/qubit/__init__.py index 6d3574bb704..4c1984c676d 100644 --- a/pennylane/labs/resource_estimation/ops/qubit/__init__.py +++ b/pennylane/labs/resource_estimation/ops/qubit/__init__.py @@ -38,3 +38,4 @@ ResourceIsingZZ, ResourcePSWAP, ) +from .qchem_ops import ResourceSingleExcitation diff --git a/pennylane/labs/resource_estimation/ops/qubit/qchem_ops.py b/pennylane/labs/resource_estimation/ops/qubit/qchem_ops.py new file mode 100644 index 00000000000..3395966538b --- /dev/null +++ b/pennylane/labs/resource_estimation/ops/qubit/qchem_ops.py @@ -0,0 +1,146 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""Resource operators for qchem operations.""" +import pennylane.labs.resource_estimation as re +from pennylane.labs.resource_estimation.resource_operator import ( + CompressedResourceOp, + GateCount, + ResourceOperator, + resource_rep, +) + +# pylint: disable=arguments-differ + + +class ResourceSingleExcitation(ResourceOperator): + r"""Resource class for the SingleExcitation gate. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources are obtained by decomposing the following matrix into fundamental gates. + + .. math:: U(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & \cos(\phi/2) & -\sin(\phi/2) & 0 \\ + 0 & \sin(\phi/2) & \cos(\phi/2) & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix}. + + The cost for implementing this transformation is given by: + + .. code-block:: bash + + 0: ──T†──H───S─╭X──RZ-─╭X──S†──H──T─┤ + 1: ──T†──S†──H─╰●──RY──╰●──H───S──T─┤ + + .. seealso:: :class:`~.SingleExcitation` + + **Example** + + The resources for this operation are computed using: + + >>> se = plre.ResourceSingleExcitation() + >>> print(plre.estimate_resources(se, plre.StandardGateSet)) + --- Resources: --- + Total qubits: 2 + Total gates : 16 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 2 + Gate breakdown: + {'Adjoint(T)': 2, 'Hadamard': 4, 'S': 2, 'Adjoint(S)': 2, 'CNOT': 2, 'RZ': 1, 'RY': 1, 'T': 2} + + """ + + num_wires = 2 + resource_keys = {"eps"} + + def __init__(self, eps=None, wires=None) -> None: + self.eps = eps + super().__init__(wires=wires) + + @classmethod + def default_resource_decomp(cls, eps=None, **kwargs): + r"""Returns a list of GateCount objects representing the operator's resources. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Resources: + The resources are obtained by decomposing the following matrix into fundamental gates. + + .. math:: U(\phi) = \begin{bmatrix} + 1 & 0 & 0 & 0 \\ + 0 & \cos(\phi/2) & -\sin(\phi/2) & 0 \\ + 0 & \sin(\phi/2) & \cos(\phi/2) & 0 \\ + 0 & 0 & 0 & 1 + \end{bmatrix}. + + The cost for implementing this transformation is given by: + + .. code-block:: bash + + 0: ──T†──H───S─╭X──RZ-─╭X──S†──H──T─┤ + 1: ──T†──S†──H─╰●──RY──╰●──H───S──T─┤ + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + h = resource_rep(re.ResourceHadamard) + s = resource_rep(re.ResourceS) + s_dag = resource_rep(re.ResourceAdjoint, {"base_cmpr_op": s}) + cnot = resource_rep(re.ResourceCNOT) + rz = resource_rep(re.ResourceRZ, {"eps": eps}) + ry = resource_rep(re.ResourceRY, {"eps": eps}) + t = resource_rep(re.ResourceT) + t_dag = resource_rep(re.ResourceAdjoint, {"base_cmpr_op": t}) + + gate_types = [ + GateCount(t_dag, 2), + GateCount(h, 4), + GateCount(s, 2), + GateCount(s_dag, 2), + GateCount(cnot, 2), + GateCount(rz), + GateCount(ry), + GateCount(t, 2), + ] + return gate_types + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * eps (float): error threshold for clifford plus T decomposition of this operation + """ + return {"eps": self.eps} + + @classmethod + def resource_rep(cls, eps=None): + """Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + eps (float, optional): error threshold for clifford plus T decomposition of this operation + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"eps": eps}) diff --git a/pennylane/labs/resource_estimation/templates/__init__.py b/pennylane/labs/resource_estimation/templates/__init__.py new file mode 100644 index 00000000000..8a7f28ed31c --- /dev/null +++ b/pennylane/labs/resource_estimation/templates/__init__.py @@ -0,0 +1,24 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""This module contains experimental resource estimation functionality.""" + +from .subroutines import ( + ResourceOutOfPlaceSquare, + ResourcePhaseGradient, + ResourceOutMultiplier, + ResourceSemiAdder, + ResourceBasisRotation, + ResourceSelect, + ResourceQROM, +) diff --git a/pennylane/labs/resource_estimation/templates/subroutines.py b/pennylane/labs/resource_estimation/templates/subroutines.py new file mode 100644 index 00000000000..8628fadca76 --- /dev/null +++ b/pennylane/labs/resource_estimation/templates/subroutines.py @@ -0,0 +1,1133 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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. +r"""Resource operators for PennyLane subroutine templates.""" +import math +from collections import defaultdict +from typing import Dict + +from pennylane import numpy as qnp +from pennylane.labs import resource_estimation as re +from pennylane.labs.resource_estimation.qubit_manager import AllocWires, FreeWires +from pennylane.labs.resource_estimation.resource_operator import ( + CompressedResourceOp, + GateCount, + ResourceOperator, + resource_rep, +) +from pennylane.queuing import QueuingManager +from pennylane.wires import Wires + +# pylint: disable=arguments-differ,protected-access,too-many-arguments,unused-argument,super-init-not-called + + +class ResourceOutOfPlaceSquare(ResourceOperator): + r"""Resource class for the OutofPlaceSquare gate. + + This operation takes two quantum registers. The input register is of size :code:`register_size` + and the output register of size :code:`2 * register_size`. The number encoded in the input is + squared and returned in the output register. + + Args: + register_size (int): the size of the input register + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources are obtained from appendix G, lemma 7 in `PRX Quantum, 2, 040332 (2021) + `_. Specifically, + the resources are given as :math:`(n - 1)^2` Toffoli gates, and :math:`n` CNOT gates. + + **Example** + + The resources for this operation are computed using: + + >>> out_square = plre.ResourceOutOfPlaceSquare(register_size=3) + >>> print(plre.estimate_resources(out_square)) + --- Resources: --- + Total qubits: 9 + Total gates : 7 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 9 + Gate breakdown: + {'Toffoli': 4, 'CNOT': 3} + """ + + resource_keys = {"register_size"} + + def __init__(self, register_size: int, wires=None): + self.register_size = register_size + self.num_wires = 3 * register_size + super().__init__(wires=wires) + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * register_size (int): the size of the input register + """ + return {"register_size": self.register_size} + + @classmethod + def resource_rep(cls, register_size): + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + register_size (int): the size of the input register + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"register_size": register_size}) + + @classmethod + def default_resource_decomp(cls, register_size, **kwargs): + r"""Returns a dictionary representing the resources of the operator. The + keys are the operators and the associated values are the counts. + + Args: + register_size (int): the size of the input register + + Resources: + The resources are obtained from appendix G, lemma 7 in `PRX Quantum, 2, 040332 (2021) + `_. Specifically, + the resources are given as :math:`(n - 1)^2` Toffoli gates, and :math:`n` CNOT gates. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + gate_lst = [] + + gate_lst.append(GateCount(resource_rep(re.ResourceToffoli), (register_size - 1) ** 2)) + gate_lst.append(GateCount(resource_rep(re.ResourceCNOT), register_size)) + + return gate_lst + + +class ResourcePhaseGradient(ResourceOperator): + r"""Resource class for the PhaseGradient gate. + + This operation prepares the phase gradient state + :math:`\frac{1}{\sqrt{2^b}} \cdot \sum_{k=0}^{2^b - 1} e^{-i2\pi \frac{k}{2^b}}\ket{k}`. + + Args: + num_wires (int): the number of qubits to prepare in the phase gradient state + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The phase gradient state is defined as an + equal superposition of phaseshifts where each shift is progressively more precise. This + is achieved by applying Hadamard gates to each qubit and then applying RZ-rotations to each + qubit with progressively smaller rotation angle. The first three rotations can be compiled to + a Z-gate, S-gate and a T-gate. + + **Example** + + The resources for this operation are computed using: + + >>> phase_grad = plre.ResourcePhaseGradient(num_wires=5) + >>> gate_set={"Z", "S", "T", "RZ", "Hadamard"} + >>> print(plre.estimate_resources(phase_grad, gate_set)) + --- Resources: --- + Total qubits: 5 + Total gates : 10 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 5 + Gate breakdown: + {'Hadamard': 5, 'Z': 1, 'S': 1, 'T': 1, 'RZ': 2} + """ + + resource_keys = {"num_wires"} + + def __init__(self, num_wires, wires=None): + self.num_wires = num_wires + super().__init__(wires=wires) + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * num_wires (int): the number of qubits to prepare in the phase gradient state + """ + return {"num_wires": self.num_wires} + + @classmethod + def resource_rep(cls, num_wires) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources. + + Args: + num_wires (int): the number of qubits to prepare in the phase gradient state + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"num_wires": num_wires}) + + @classmethod + def default_resource_decomp(cls, num_wires, **kwargs): + r"""Returns a list representing the resources of the operator. Each object in the list represents a gate and the + number of times it occurs in the circuit. + + Args: + num_wires (int): the number of qubits to prepare in the phase gradient state + + Resources: + The resources are obtained by construction. The phase gradient state is defined as an + equal superposition of phaseshifts where each shift is progressively more precise. This + is achieved by applying Hadamard gates to each qubit and then applying RZ-rotations to each + qubit with progressively smaller rotation angle. The first three rotations can be compiled to + a Z-gate, S-gate and a T-gate. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + gate_counts = [GateCount(resource_rep(re.ResourceHadamard), num_wires)] + if num_wires > 0: + gate_counts.append(GateCount(resource_rep(re.ResourceZ))) + + if num_wires > 1: + gate_counts.append(GateCount(resource_rep(re.ResourceS))) + + if num_wires > 2: + gate_counts.append(GateCount(resource_rep(re.ResourceT))) + + if num_wires > 3: + gate_counts.append(GateCount(resource_rep(re.ResourceRZ), num_wires - 3)) + + return gate_counts + + +class ResourceOutMultiplier(ResourceOperator): + r"""Resource class for the OutMultiplier gate. + + Args: + a_num_qubits (int): the size of the first input register + b_num_qubits (int): the size of the second input register + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources are obtained from appendix G, lemma 10 in `PRX Quantum, 2, 040332 (2021) + `_. + + .. seealso:: :class:`~.OutMultiplier` + + **Example** + + The resources for this operation are computed using: + + >>> out_mul = plre.ResourceOutMultiplier(4, 4) + >>> print(plre.estimate_resources(out_mul)) + --- Resources: --- + Total qubits: 16 + Total gates : 70 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 16 + Gate breakdown: + {'Toffoli': 14, 'Hadamard': 42, 'CNOT': 14} + """ + + resource_keys = {"a_num_qubits", "b_num_qubits"} + + def __init__(self, a_num_qubits, b_num_qubits, wires=None) -> None: + self.num_wires = a_num_qubits + b_num_qubits + 2 * max((a_num_qubits, b_num_qubits)) + self.a_num_qubits = a_num_qubits + self.b_num_qubits = b_num_qubits + super().__init__(wires=wires) + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * a_num_qubits (int): the size of the first input register + * b_num_qubits (int): the size of the second input register + """ + return {"a_num_qubits": self.a_num_qubits, "b_num_qubits": self.b_num_qubits} + + @classmethod + def resource_rep(cls, a_num_qubits, b_num_qubits) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + a_num_qubits (int): the size of the first input register + b_num_qubits (int): the size of the second input register + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp( + cls, {"a_num_qubits": a_num_qubits, "b_num_qubits": b_num_qubits} + ) + + @classmethod + def default_resource_decomp(cls, a_num_qubits, b_num_qubits, **kwargs) -> list[GateCount]: + r"""Returns a dictionary representing the resources of the operator. The + keys are the operators and the associated values are the counts. + + Args: + a_num_qubits (int): the size of the first input register + b_num_qubits (int): the size of the second input register + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources are obtained from appendix G, lemma 10 in `PRX Quantum, 2, 040332 (2021) + `_. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + l = max(a_num_qubits, b_num_qubits) + + toff = resource_rep(re.ResourceToffoli) + l_elbow = resource_rep(re.ResourceTempAND) + r_elbow = resource_rep(re.ResourceAdjoint, {"base_cmpr_op": l_elbow}) + + toff_count = 2 * a_num_qubits * b_num_qubits - l + elbow_count = toff_count // 2 + toff_count = toff_count - (elbow_count * 2) + + gate_lst = [ + GateCount(l_elbow, elbow_count), + GateCount(r_elbow, elbow_count), + ] + + if toff_count: + gate_lst.append(GateCount(toff)) + return gate_lst + + +class ResourceSemiAdder(ResourceOperator): + r"""Resource class for the SemiOutAdder gate. + + Args: + max_register_size (int): the size of the larger of the two registers being added together + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources are obtained from figures 1 and 2 in `Gidney (2018) + `_. + + .. seealso:: :class:`~.SemiAdder` + + **Example** + + The resources for this operation are computed using: + + >>> semi_add = plre.ResourceSemiAdder(max_register_size=4) + >>> print(plre.estimate_resources(semi_add)) + --- Resources: --- + Total qubits: 11 + Total gates : 30 + Qubit breakdown: + clean qubits: 3, dirty qubits: 0, algorithmic qubits: 8 + Gate breakdown: + {'CNOT': 18, 'Toffoli': 3, 'Hadamard': 9} + """ + + resource_keys = {"max_register_size"} + + def __init__(self, max_register_size, wires=None): + self.max_register_size = max_register_size + self.num_wires = 2 * max_register_size + super().__init__(wires=wires) + + @property + def resource_params(self): + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * max_register_size (int): the size of the larger of the two registers being added together + + """ + return {"max_register_size": self.max_register_size} + + @classmethod + def resource_rep(cls, max_register_size): + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute the resources. + + Args: + max_register_size (int): the size of the larger of the two registers being added together + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + return CompressedResourceOp(cls, {"max_register_size": max_register_size}) + + @classmethod + def default_resource_decomp(cls, max_register_size, **kwargs): + r"""Returns a dictionary representing the resources of the operator. The + keys are the operators and the associated values are the counts. + + Args: + max_register_size (int): the size of the larger of the two registers being added together + + Resources: + The resources are obtained from figures 1 and 2 in `Gidney (2018) + `_. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + cnot = resource_rep(re.ResourceCNOT) + if max_register_size == 1: + return [GateCount(cnot)] + + x = resource_rep(re.ResourceX) + toff = resource_rep(re.ResourceToffoli) + if max_register_size == 2: + return [GateCount(cnot, 2), GateCount(x, 2), GateCount(toff)] + + cnot_count = (6 * (max_register_size - 2)) + 3 + elbow_count = max_register_size - 1 + + l_elbow = resource_rep(re.ResourceTempAND) + r_elbow = resource_rep(re.ResourceAdjoint, {"base_cmpr_op": l_elbow}) + return [ + AllocWires(max_register_size - 1), + GateCount(cnot, cnot_count), + GateCount(l_elbow, elbow_count), + GateCount(r_elbow, elbow_count), + FreeWires(max_register_size - 1), + ] # Obtained resource from Fig1 and Fig2 https://quantum-journal.org/papers/q-2018-06-18-74/pdf/ + + @classmethod + def default_controlled_resource_decomp( + cls, ctrl_num_ctrl_wires, ctrl_num_ctrl_values, max_register_size, **kwargs + ): + r"""Returns a list representing the resources of the operator. Each object in the list represents a gate and the + number of times it occurs in the circuit. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + max_register_size (int): the size of the larger of the two registers being added together + + Resources: + The resources are obtained from figure 4a in `Gidney (2018) + `_. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + if max_register_size > 2: + gate_lst = [] + + if ctrl_num_ctrl_wires > 1: + mcx = resource_rep( + re.ResourceMultiControlledX, + { + "num_ctrl_wires": ctrl_num_ctrl_wires, + "num_ctrl_values": ctrl_num_ctrl_values, + }, + ) + gate_lst.append(AllocWires(1)) + gate_lst.append(GateCount(mcx, 2)) + + cnot_count = (7 * (max_register_size - 2)) + 3 + elbow_count = 2 * (max_register_size - 1) + + x = resource_rep(re.ResourceX) + cnot = resource_rep(re.ResourceCNOT) + l_elbow = resource_rep(re.ResourceTempAND) + r_elbow = resource_rep(re.ResourceAdjoint, {"base_cmpr_op": l_elbow}) + gate_lst.extend( + [ + AllocWires(max_register_size - 1), + GateCount(cnot, cnot_count), + GateCount(l_elbow, elbow_count), + GateCount(r_elbow, elbow_count), + FreeWires(max_register_size - 1), + ], + ) + + if ctrl_num_ctrl_wires > 1: + gate_lst.append(FreeWires(1)) + elif ctrl_num_ctrl_values > 0: + gate_lst.append(GateCount(x, 2 * ctrl_num_ctrl_values)) + + return gate_lst # Obtained resource from Fig 4a https://quantum-journal.org/papers/q-2018-06-18-74/pdf/ + + raise re.ResourcesNotDefined + + +class ResourceBasisRotation(ResourceOperator): + r"""Resource class for the BasisRotation gate. + + Args: + dim_N (int): The dimensions of the input :code:`unitary_matrix`. This is computed as the + number of columns of the matrix. + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources are obtained from the construction scheme given in `Optica, 3, 1460 (2016) + `_. Specifically, + the resources are given as :math:`dim_N \times (dim_N - 1) / 2` instances of the + :class:`~.ResourceSingleExcitation` gate, and :math:`dim_N \times (1 + (dim_N - 1) / 2)` instances + of the :class:`~.ResourcePhaseShift` gate. + + .. seealso:: :class:`~.BasisRotation` + + **Example** + + The resources for this operation are computed using: + + >>> basis_rot = plre.ResourceBasisRotation(dim_N = 5) + >>> print(plre.estimate_resources(basis_rot)) + --- Resources: --- + Total qubits: 5 + Total gates : 1.740E+3 + Qubit breakdown: + clean qubits: 0, dirty qubits: 0, algorithmic qubits: 5 + Gate breakdown: + {'T': 1.580E+3, 'S': 60, 'Z': 40, 'Hadamard': 40, 'CNOT': 20} + """ + + resource_keys = {"dim_N"} + + def __init__(self, dim_N, wires=None): + self.num_wires = dim_N + super().__init__(wires=wires) + + @classmethod + def default_resource_decomp(cls, dim_N, **kwargs) -> list[GateCount]: + r"""Returns a dictionary representing the resources of the operator. The + keys are the operators and the associated values are the counts. + + Args: + dim_N (int): The dimensions of the input :code:`unitary_matrix`. This is computed + as the number of columns of the matrix. + + Resources: + The resources are obtained from the construction scheme given in `Optica, 3, 1460 (2016) + `_. Specifically, + the resources are given as :math:`dim_N * (dim_N - 1) / 2` instances of the + :class:`~.ResourceSingleExcitation` gate, and :math:`dim_N * (1 + (dim_N - 1) / 2)` instances + of the :class:`~.ResourcePhaseShift` gate. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + phase_shift = resource_rep(re.ResourcePhaseShift) + single_excitation = resource_rep(re.ResourceSingleExcitation) + + se_count = dim_N * (dim_N - 1) // 2 + ps_count = dim_N + se_count + + return [GateCount(phase_shift, ps_count), GateCount(single_excitation, se_count)] + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * dim_N (int): The dimensions of the input :code:`unitary_matrix`. This is computed as the number of columns of the matrix. + + """ + return {"dim_N": self.num_wires} + + @classmethod + def resource_rep(cls, dim_N) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + dim_N (int): The dimensions of the input :code:`unitary_matrix`. This is computed + as the number of columns of the matrix. + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + params = {"dim_N": dim_N} + return CompressedResourceOp(cls, params) + + @classmethod + def tracking_name(cls, dim_N) -> str: + r"""Returns the tracking name built with the operator's parameters.""" + return f"BasisRotation({dim_N})" + + +class ResourceSelect(ResourceOperator): + r"""Resource class for the Select gate. + + Args: + select_ops (list[~.ResourceOperator]): the set of operations to select over + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources are based on the analysis in `Babbush et al. (2018) `_ section III.A, + 'Unary Iteration and Indexed Operations'. See Figures 4, 6, and 7. + + Note: This implementation assumes we have access to :math:`n - 1` additional work qubits, + where :math:`n = \left\lceil log_{2}(N) \right\rceil` and :math:`N` is the number of batches of unitaries + to select. + + .. seealso:: :class:`~.Select` + + **Example** + + The resources for this operation are computed using: + + >>> ops = [plre.ResourceX(), plre.ResourceY(), plre.ResourceZ()] + >>> select_op = plre.ResourceSelect(select_ops=ops) + >>> print(plre.estimate_resources(select_op)) + --- Resources: --- + Total qubits: 4 + Total gates : 24 + Qubit breakdown: + clean qubits: 1, dirty qubits: 0, algorithmic qubits: 3 + Gate breakdown: + {'CNOT': 7, 'S': 2, 'Z': 1, 'Hadamard': 8, 'X': 4, 'Toffoli': 2} + """ + + resource_keys = {"cmpr_ops"} + + def __init__(self, select_ops, wires=None) -> None: + self.queue(select_ops) + num_select_ops = len(select_ops) + num_ctrl_wires = math.ceil(math.log2(num_select_ops)) + + try: + cmpr_ops = tuple(op.resource_rep_from_op() for op in select_ops) + self.cmpr_ops = cmpr_ops + except AttributeError as error: + raise ValueError( + "All factors of the Select must be instances of `ResourceOperator` in order to obtain resources." + ) from error + + if wires is not None: + self.wires = Wires(wires) + self.num_wires = len(self.wires) + else: + ops_wires = [op.wires for op in select_ops if op.wires is not None] + if len(ops_wires) == 0: + self.wires = None + self.num_wires = max((op.num_wires for op in select_ops)) + num_ctrl_wires + else: + self.wires = Wires.all_wires(ops_wires) + self.num_wires = len(self.wires) + num_ctrl_wires + + def queue(self, ops_to_remove, context: QueuingManager = QueuingManager): + """Append the operator to the Operator queue.""" + for op in ops_to_remove: + context.remove(op) + context.append(self) + return self + + @classmethod + def default_resource_decomp(cls, cmpr_ops, **kwargs): # pylint: disable=unused-argument + r"""The resources for a select implementation taking advantage of the unary iterator trick. + + Args: + cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed + representation, to be applied according to the selected qubits. + + Resources: + The resources are based on the analysis in `Babbush et al. (2018) `_ section III.A, + 'Unary Iteration and Indexed Operations'. See Figures 4, 6, and 7. + + Note: This implementation assumes we have access to :math:`n - 1` additional work qubits, + where :math:`n = \ceil{log_{2}(N)}` and :math:`N` is the number of batches of unitaries + to select. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + gate_types = [] + x = re.ResourceX.resource_rep() + cnot = re.ResourceCNOT.resource_rep() + l_elbow = resource_rep(re.ResourceTempAND) + r_elbow = resource_rep(re.ResourceAdjoint, {"base_cmpr_op": l_elbow}) + + num_ops = len(cmpr_ops) + work_qubits = math.ceil(math.log2(num_ops)) - 1 + + gate_types.append(AllocWires(work_qubits)) + for cmp_rep in cmpr_ops: + ctrl_op = re.ResourceControlled.resource_rep(cmp_rep, 1, 0) + gate_types.append(GateCount(ctrl_op)) + + gate_types.append(GateCount(x, 2 * (num_ops - 1))) # conjugate 0 controlled toffolis + gate_types.append(GateCount(cnot, num_ops - 1)) + gate_types.append(GateCount(l_elbow, num_ops - 1)) + gate_types.append(GateCount(r_elbow, num_ops - 1)) + + gate_types.append(FreeWires(work_qubits)) + return gate_types + + @staticmethod + def textbook_resources(cmpr_ops, **kwargs) -> list[GateCount]: + r"""Returns a list representing the resources of the operator. Each object in the list represents a gate and the + number of times it occurs in the circuit. + + Args: + cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed + representation, to be applied according to the selected qubits. + + Resources: + The resources correspond directly to the definition of the operation. Specifically, + for each operator in :code:`cmpr_ops`, the cost is given as a controlled version of the operator + controlled on the associated bitstring. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + gate_types = defaultdict(int) + x = re.ResourceX.resource_rep() + + num_ops = len(cmpr_ops) + num_ctrl_wires = int(qnp.ceil(qnp.log2(num_ops))) + num_total_ctrl_possibilities = 2**num_ctrl_wires # 2^n + + num_zero_controls = num_total_ctrl_possibilities // 2 + gate_types[x] = num_zero_controls * 2 # conjugate 0 controls + + for cmp_rep in cmpr_ops: + ctrl_op = re.ResourceControlled.resource_rep( + cmp_rep, + num_ctrl_wires, + 0, + ) + gate_types[ctrl_op] += 1 + + return gate_types + + @property + def resource_params(self) -> dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed representation, to be applied according to the selected qubits. + + """ + return {"cmpr_ops": self.cmpr_ops} + + @classmethod + def resource_rep(cls, cmpr_ops) -> CompressedResourceOp: + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + cmpr_ops (list[CompressedResourceOp]): The list of operators, in the compressed + representation, to be applied according to the selected qubits. + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + params = {"cmpr_ops": cmpr_ops} + return CompressedResourceOp(cls, params) + + +class ResourceQROM(ResourceOperator): + """Resource class for the QROM template. + + Args: + num_bitstrings (int): the number of bitstrings that are to be encoded + size_bitstring (int): the length of each bitstring + num_bit_flips (int, optional): The total number of :math:`1`'s in the dataset. Defaults to + :code:`(num_bitstrings * size_bitstring) // 2`, which is half the dataset. + clean (bool, optional): Determine if allocated qubits should be reset after the computation + (at the cost of higher gate counts). Defaults to :code`True`. + select_swap_depth (Union[int, None], optional): A natural number that determines if data + will be loaded in parallel by adding more rows following Figure 1.C of `Low et al. (2024) `_. + Defaults to :code:`None`, which internally determines the optimal depth. + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources for QROM are taken from the following two papers: + `Low et al. (2024) `_ (Figure 1.C) for + :code:`clean = False` and `Berry et al. (2019) `_ + (Figure 4) for :code:`clean = True`. + + .. seealso:: :class:`~.QROM` + + **Example** + + The resources for this operation are computed using: + + >>> qrom = plre.ResourceQROM( + ... num_bitstrings=10, + ... size_bitstring=4, + ... ) + >>> print(plre.estimate_resources(qrom)) + --- Resources: --- + Total qubits: 11 + Total gates : 178.0 + Qubit breakdown: + clean qubits: 3, dirty qubits: 0, algorithmic qubits: 8 + Gate breakdown: + {'Hadamard': 56, 'X': 34, 'CNOT': 72.0, 'Toffoli': 16} + + """ + + resource_keys = { + "num_bitstrings", + "size_bitstring", + "num_bit_flips", + "select_swap_depth", + "clean", + } + + @staticmethod + def _t_optimized_select_swap_width(num_bitstrings, size_bitstring): + opt_width_continuous = math.sqrt((2 / 3) * (num_bitstrings / size_bitstring)) + w1 = 2 ** math.floor(math.log2(opt_width_continuous)) + w2 = 2 ** math.ceil(math.log2(opt_width_continuous)) + + if w1 < 1 and w2 < 1: + return 1 + + def t_cost_func(w): + return 4 * (math.ceil(num_bitstrings / w) - 2) + 6 * (w - 1) * size_bitstring + + if t_cost_func(w2) < t_cost_func(w1) and w2 >= 1: + return w2 + return w1 + + def __init__( + self, + num_bitstrings, + size_bitstring, + num_bit_flips=None, + clean=True, + select_swap_depth=None, + wires=None, + ) -> None: + self.clean = clean + self.num_bitstrings = num_bitstrings + self.size_bitstring = size_bitstring + self.num_bit_flips = num_bit_flips or (num_bitstrings * size_bitstring / 2) + + if wires is not None: + self.num_wires = len(wires) + assert self.num_wires > size_bitstring + self.num_control_wires = self.num_wires - size_bitstring + assert self.num_control_wires >= math.ceil(math.log2(num_bitstrings)) + + else: + self.num_control_wires = math.ceil(math.log2(num_bitstrings)) + self.num_wires = size_bitstring + self.num_control_wires + + self.select_swap_depth = select_swap_depth + super().__init__(wires=wires) + + @classmethod + def default_resource_decomp( + cls, + num_bitstrings, + size_bitstring, + num_bit_flips, + select_swap_depth=None, + clean=True, + **kwargs, + ) -> list[GateCount]: + r"""Returns a list of GateCount objects representing the operator's resources. + + Args: + num_bitstrings (int): the number of bitstrings that are to be encoded + size_bitstring (int): the length of each bitstring + num_bit_flips (int, optional): The total number of :math:`1`'s in the dataset. Defaults to + :code:`(num_bitstrings * size_bitstring) // 2`, which is half the dataset. + clean (bool, optional): Determine if allocated qubits should be reset after the computation + (at the cost of higher gate counts). Defaults to :code`True`. + select_swap_depth (Union[int, None], optional): A natural number that determines if data + will be loaded in parallel by adding more rows following Figure 1.C of `Low et al. (2024) `_. + Defaults to :code:`None`, which internally determines the optimal depth. + wires (Sequence[int], optional): the wires the operation acts on + + Resources: + The resources for QROM are taken from the following two papers: + `Low et al. (2024) `_ (Figure 1.C) for + :code:`clean = False` and `Berry et al. (2019) `_ + (Figure 4) for :code:`clean = True`. + + Note: we use the unary iterator trick to implement the Select. This + implementation assumes we have access to :math:`n - 1` additional + work qubits, where :math:`n = \left\lceil log_{2}(N) \right\rceil` and :math:`N` is + the number of batches of unitaries to select. + """ + + if select_swap_depth: + select_swap_depth = 2 ** math.floor(math.log2(select_swap_depth)) + W_opt = select_swap_depth or ResourceQROM._t_optimized_select_swap_width( + num_bitstrings, size_bitstring + ) + L_opt = math.ceil(num_bitstrings / W_opt) + l = math.ceil(math.log2(L_opt)) + + gate_cost = [] + gate_cost.append( + AllocWires((W_opt - 1) * size_bitstring + (l - 1)) + ) # Swap registers + work_wires for UI trick + + x = resource_rep(re.ResourceX) + cnot = resource_rep(re.ResourceCNOT) + l_elbow = resource_rep(re.ResourceTempAND) + r_elbow = resource_rep(re.ResourceAdjoint, {"base_cmpr_op": l_elbow}) + hadamard = resource_rep(re.ResourceHadamard) + + swap_clean_prefactor = 1 + select_clean_prefactor = 1 + + if clean: + gate_cost.append(GateCount(hadamard, 2 * size_bitstring)) + swap_clean_prefactor = 4 + select_clean_prefactor = 2 + + # SELECT cost: + gate_cost.append( + GateCount(x, select_clean_prefactor * (2 * (L_opt - 2) + 1)) + ) # conjugate 0 controlled toffolis + 1 extra X gate from un-controlled unary iterator decomp + gate_cost.append( + GateCount( + cnot, select_clean_prefactor * (L_opt - 2) + select_clean_prefactor * num_bit_flips + ) # num CNOTs in unary iterator trick + each unitary in the select is just a CNOT + ) + gate_cost.append(GateCount(l_elbow, select_clean_prefactor * (L_opt - 2))) + gate_cost.append(GateCount(r_elbow, select_clean_prefactor * (L_opt - 2))) + + gate_cost.append(FreeWires(l - 1)) # release UI trick work wires + + # # SWAP cost: + ctrl_swap = resource_rep(re.ResourceCSWAP) + gate_cost.append(GateCount(ctrl_swap, swap_clean_prefactor * (W_opt - 1) * size_bitstring)) + + if clean: + gate_cost.append(FreeWires((W_opt - 1) * size_bitstring)) # release Swap registers + + return gate_cost + + @classmethod + def single_controlled_res_decomp( + cls, + num_bitstrings, + size_bitstring, + num_bit_flips, + select_swap_depth, + clean, + ): + r"""The resource decomposition for QROM controlled on a single wire.""" + W_opt = select_swap_depth or ResourceQROM._t_optimized_select_swap_width( + num_bitstrings, size_bitstring + ) + L_opt = math.ceil(num_bitstrings / W_opt) + l = math.ceil(math.log2(L_opt)) + + gate_cost = [] + gate_cost.append( + FreeWires((W_opt - 1) * size_bitstring + l) + ) # Swap registers + work_wires for UI trick + + x = resource_rep(re.ResourceX) + cnot = resource_rep(re.ResourceCNOT) + l_elbow = resource_rep(re.ResourceTempAND) + r_elbow = resource_rep(re.ResourceAdjoint, {"base_cmpr_op": l_elbow}) + hadamard = resource_rep(re.ResourceHadamard) + + swap_clean_prefactor = 1 + select_clean_prefactor = 1 + + if clean: + gate_cost.append(GateCount(hadamard, 2 * size_bitstring)) + swap_clean_prefactor = 4 + select_clean_prefactor = 2 + + # SELECT cost: + gate_cost.append( + GateCount(x, select_clean_prefactor * (2 * (L_opt - 1))) + ) # conjugate 0 controlled toffolis + gate_cost.append( + GateCount( + cnot, select_clean_prefactor * (L_opt - 1) + select_clean_prefactor * num_bit_flips + ) # num CNOTs in unary iterator trick + each unitary in the select is just a CNOT + ) + gate_cost.append(GateCount(l_elbow, select_clean_prefactor * (L_opt - 1))) + gate_cost.append(GateCount(r_elbow, select_clean_prefactor * (L_opt - 1))) + + gate_cost.append(FreeWires(l)) # release UI trick work wires + + # SWAP cost: + w = math.ceil(math.log2(W_opt)) + ctrl_swap = re.ResourceCSWAP.resource_rep() + gate_cost.append(AllocWires(1)) # need one temporary qubit for l/r-elbow to control SWAP + + gate_cost.append(GateCount(l_elbow, w)) + gate_cost.append(GateCount(ctrl_swap, swap_clean_prefactor * (W_opt - 1) * size_bitstring)) + gate_cost.append(GateCount(r_elbow, w)) + + gate_cost.append(FreeWires(1)) # temp wires + if clean: + gate_cost.append( + FreeWires((W_opt - 1) * size_bitstring) + ) # release Swap registers + temp wires + return gate_cost + + @classmethod + def default_controlled_resource_decomp( + cls, + ctrl_num_ctrl_wires: int, + ctrl_num_ctrl_values: int, + num_bitstrings, + size_bitstring, + num_bit_flips=None, + select_swap_depth=None, + clean=True, + **kwargs, + ): + r"""Returns a list representing the resources for a controlled version of the operator. + + Args: + ctrl_num_ctrl_wires (int): the number of qubits the operation is controlled on + ctrl_num_ctrl_values (int): the number of control qubits, that are controlled when in the :math:`|0\rangle` state + num_bitstrings (int): the number of bitstrings that are to be encoded + size_bitstring (int): the length of each bitstring + num_bit_flips (int, optional): The total number of :math:`1`'s in the dataset. Defaults to + :code:`(num_bitstrings * size_bitstring) // 2`, which is half the dataset. + clean (bool, optional): Determine if allocated qubits should be reset after the computation + (at the cost of higher gate counts). Defaults to :code`True`. + select_swap_depth (Union[int, None], optional): A natural number that determines if data + will be loaded in parallel by adding more rows following Figure 1.C of `Low et al. (2024) `_. + Defaults to :code:`None`, which internally determines the optimal depth. + + Resources: + The resources for QROM are taken from the following two papers: + `Low et al. (2024) `_ (Figure 1.C) for + :code:`clean = False` and `Berry et al. (2019) `_ + (Figure 4) for :code:`clean = True`. + + Note: we use the single-controlled unary iterator trick to implement the Select. This + implementation assumes we have access to :math:`n - 1` additional work qubits, + where :math:`n = \ceil{log_{2}(N)}` and :math:`N` is the number of batches of + unitaries to select. + + Returns: + list[GateCount]: A list of GateCount objects, where each object + represents a specific quantum gate and the number of times it appears + in the decomposition. + """ + gate_cost = [] + if ctrl_num_ctrl_values: + x = re.ResourceX.resource_rep() + gate_cost.append(GateCount(x, 2 * ctrl_num_ctrl_values)) + + if num_bit_flips is None: + num_bit_flips = (num_bitstrings * size_bitstring) // 2 + + single_ctrl_cost = cls.single_controlled_res_decomp( + num_bitstrings, + size_bitstring, + num_bit_flips, + select_swap_depth, + clean, + ) + + if ctrl_num_ctrl_wires == 1: + gate_cost.extend(single_ctrl_cost) + return gate_cost + + gate_cost.append(AllocWires(1)) + gate_cost.append( + GateCount(re.ResourceMultiControlledX.resource_rep(ctrl_num_ctrl_wires, 0)) + ) + gate_cost.extend(single_ctrl_cost) + gate_cost.append( + GateCount(re.ResourceMultiControlledX.resource_rep(ctrl_num_ctrl_wires, 0)) + ) + gate_cost.append(FreeWires(1)) + return gate_cost + + @property + def resource_params(self) -> Dict: + r"""Returns a dictionary containing the minimal information needed to compute the resources. + + Returns: + dict: A dictionary containing the resource parameters: + * num_bitstrings (int): the number of bitstrings that are to be encoded + * size_bitstring (int): the length of each bitstring + * num_bit_flips (int, optional): The total number of :math:`1`'s in the dataset. Defaults to :code:`(num_bitstrings * size_bitstring) // 2`, which is half the dataset. + * clean (bool, optional): Determine if allocated qubits should be reset after the computation (at the cost of higher gate counts). Defaults to :code`True`. + * select_swap_depth (Union[int, None], optional): A natural number that determines if data will be loaded in parallel by adding more rows following Figure 1.C of `Low et al. (2024) `_. Defaults to :code:`None`, which internally determines the optimal depth. + + """ + + return { + "num_bitstrings": self.num_bitstrings, + "size_bitstring": self.size_bitstring, + "num_bit_flips": self.num_bit_flips, + "select_swap_depth": self.select_swap_depth, + "clean": self.clean, + } + + @classmethod + def resource_rep( + cls, + num_bitstrings, + size_bitstring, + num_bit_flips=None, + clean=True, + select_swap_depth=None, + ) -> CompressedResourceOp: # pylint: disable=too-many-arguments + r"""Returns a compressed representation containing only the parameters of + the Operator that are needed to compute a resource estimation. + + Args: + num_bitstrings (int): the number of bitstrings that are to be encoded + size_bitstring (int): the length of each bitstring + num_bit_flips (int, optional): The total number of :math:`1`'s in the dataset. Defaults to + :code:`(num_bitstrings * size_bitstring) // 2`, which is half the dataset. + clean (bool, optional): Determine if allocated qubits should be reset after the computation + (at the cost of higher gate counts). Defaults to :code`True`. + select_swap_depth (Union[int, None], optional): A natural number that determines if data + will be loaded in parallel by adding more rows following Figure 1.C of `Low et al. (2024) `_. + Defaults to :code:`None`, which internally determines the optimal depth. + + Returns: + CompressedResourceOp: the operator in a compressed representation + """ + if num_bit_flips is None: + num_bit_flips = num_bitstrings * size_bitstring // 2 + + params = { + "num_bitstrings": num_bitstrings, + "num_bit_flips": num_bit_flips, + "size_bitstring": size_bitstring, + "select_swap_depth": select_swap_depth, + "clean": clean, + } + return CompressedResourceOp(cls, params) diff --git a/pennylane/labs/tests/resource_estimation/ops/qubit/test_qchem_ops.py b/pennylane/labs/tests/resource_estimation/ops/qubit/test_qchem_ops.py new file mode 100644 index 00000000000..e04319b229b --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/ops/qubit/test_qchem_ops.py @@ -0,0 +1,59 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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 qchem resource operators. +""" +import pytest + +import pennylane.labs.resource_estimation as plre + +# pylint: disable=no-self-use,too-many-arguments + + +class TestResourceSingleExcitation: + """Test the SingleExcitation class.""" + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resource_params(self, eps): + """Test that the resource params are correct.""" + if eps: + op = plre.ResourceSingleExcitation(eps=eps) + else: + op = plre.ResourceSingleExcitation() + + assert op.resource_params == {"eps": eps} + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resource_rep(self, eps): + """Test that the compressed representation is correct.""" + expected = plre.CompressedResourceOp(plre.ResourceSingleExcitation, {"eps": eps}) + assert plre.ResourceSingleExcitation.resource_rep(eps=eps) == expected + + @pytest.mark.parametrize("eps", (None, 1e-3)) + def test_resources(self, eps): + """Test that the resources are correct.""" + t_dag = plre.ResourceAdjoint.resource_rep(plre.resource_rep(plre.ResourceT)) + s_dag = plre.ResourceAdjoint.resource_rep(plre.resource_rep(plre.ResourceS)) + + expected = [ + plre.GateCount(t_dag, 2), + plre.GateCount(plre.resource_rep(plre.ResourceHadamard), 4), + plre.GateCount(plre.resource_rep(plre.ResourceS), 2), + plre.GateCount(s_dag, 2), + plre.GateCount(plre.resource_rep(plre.ResourceCNOT), 2), + plre.GateCount(plre.resource_rep(plre.ResourceRZ, {"eps": eps})), + plre.GateCount(plre.resource_rep(plre.ResourceRY, {"eps": eps})), + plre.GateCount(plre.resource_rep(plre.ResourceT), 2), + ] + assert plre.ResourceSingleExcitation.resource_decomp(eps=eps) == expected diff --git a/pennylane/labs/tests/resource_estimation/templates/test_subroutines.py b/pennylane/labs/tests/resource_estimation/templates/test_subroutines.py new file mode 100644 index 00000000000..e4c1c30fe73 --- /dev/null +++ b/pennylane/labs/tests/resource_estimation/templates/test_subroutines.py @@ -0,0 +1,466 @@ +# Copyright 2025 Xanadu Quantum Technologies Inc. + +# 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 quantum algorithmic subroutines resource operators. +""" +import pytest + +import pennylane.labs.resource_estimation as plre + +# pylint: disable=no-self-use,too-many-arguments + + +class TestResourceOutOfPlaceSquare: + """Test the OutOfPlaceSquare class.""" + + @pytest.mark.parametrize("register_size", (1, 2, 3)) + def test_resource_params(self, register_size): + """Test that the resource params are correct.""" + op = plre.ResourceOutOfPlaceSquare(register_size) + assert op.resource_params == {"register_size": register_size} + + @pytest.mark.parametrize("register_size", (1, 2, 3)) + def test_resource_rep(self, register_size): + """Test that the compressed representation is correct.""" + expected = plre.CompressedResourceOp( + plre.ResourceOutOfPlaceSquare, {"register_size": register_size} + ) + assert plre.ResourceOutOfPlaceSquare.resource_rep(register_size=register_size) == expected + + @pytest.mark.parametrize("register_size", (1, 2, 3)) + def test_resources(self, register_size): + """Test that the resources are correct.""" + expected = [ + plre.GateCount(plre.resource_rep(plre.ResourceToffoli), (register_size - 1) ** 2), + plre.GateCount(plre.resource_rep(plre.ResourceCNOT), register_size), + ] + assert ( + plre.ResourceOutOfPlaceSquare.resource_decomp(register_size=register_size) == expected + ) + + +class TestResourcePhaseGradient: + """Test the PhaseGradient class.""" + + @pytest.mark.parametrize("num_wires", (1, 2, 3, 4, 5)) + def test_resource_params(self, num_wires): + """Test that the resource params are correct.""" + op = plre.ResourcePhaseGradient(num_wires) + assert op.resource_params == {"num_wires": num_wires} + + @pytest.mark.parametrize("num_wires", (1, 2, 3, 4, 5)) + def test_resource_rep(self, num_wires): + """Test that the compressed representation is correct.""" + expected = plre.CompressedResourceOp(plre.ResourcePhaseGradient, {"num_wires": num_wires}) + assert plre.ResourcePhaseGradient.resource_rep(num_wires=num_wires) == expected + + @pytest.mark.parametrize( + "num_wires, expected_res", + ( + ( + 1, + [ + plre.GateCount(plre.ResourceHadamard.resource_rep()), + plre.GateCount(plre.ResourceZ.resource_rep()), + ], + ), + ( + 2, + [ + plre.GateCount(plre.ResourceHadamard.resource_rep(), 2), + plre.GateCount(plre.ResourceZ.resource_rep()), + plre.GateCount(plre.ResourceS.resource_rep()), + ], + ), + ( + 3, + [ + plre.GateCount(plre.ResourceHadamard.resource_rep(), 3), + plre.GateCount(plre.ResourceZ.resource_rep()), + plre.GateCount(plre.ResourceS.resource_rep()), + plre.GateCount(plre.ResourceT.resource_rep()), + ], + ), + ( + 5, + [ + plre.GateCount(plre.ResourceHadamard.resource_rep(), 5), + plre.GateCount(plre.ResourceZ.resource_rep()), + plre.GateCount(plre.ResourceS.resource_rep()), + plre.GateCount(plre.ResourceT.resource_rep()), + plre.GateCount(plre.ResourceRZ.resource_rep(), 2), + ], + ), + ), + ) + def test_resources(self, num_wires, expected_res): + """Test that the resources are correct.""" + assert plre.ResourcePhaseGradient.resource_decomp(num_wires=num_wires) == expected_res + + +class TestResourceOutMultiplier: + """Test the OutMultiplier class.""" + + @pytest.mark.parametrize("a_register_size", (1, 2, 3)) + @pytest.mark.parametrize("b_register_size", (4, 5, 6)) + def test_resource_params(self, a_register_size, b_register_size): + """Test that the resource params are correct.""" + op = plre.ResourceOutMultiplier(a_register_size, b_register_size) + assert op.resource_params == { + "a_num_qubits": a_register_size, + "b_num_qubits": b_register_size, + } + + @pytest.mark.parametrize("a_register_size", (1, 2, 3)) + @pytest.mark.parametrize("b_register_size", (4, 5, 6)) + def test_resource_rep(self, a_register_size, b_register_size): + """Test that the compressed representation is correct.""" + expected = plre.CompressedResourceOp( + plre.ResourceOutMultiplier, + {"a_num_qubits": a_register_size, "b_num_qubits": b_register_size}, + ) + assert plre.ResourceOutMultiplier.resource_rep(a_register_size, b_register_size) == expected + + def test_resources(self): + """Test that the resources are correct.""" + a_register_size = 5 + b_register_size = 3 + + toff = plre.resource_rep(plre.ResourceToffoli) + l_elbow = plre.resource_rep(plre.ResourceTempAND) + r_elbow = plre.resource_rep(plre.ResourceAdjoint, {"base_cmpr_op": l_elbow}) + + num_elbows = 12 + num_toff = 1 + + expected = [ + plre.GateCount(l_elbow, num_elbows), + plre.GateCount(r_elbow, num_elbows), + plre.GateCount(toff, num_toff), + ] + assert ( + plre.ResourceOutMultiplier.resource_decomp(a_register_size, b_register_size) == expected + ) + + +class TestResourceSemiAdder: + """Test the ResourceSemiAdder class.""" + + @pytest.mark.parametrize("register_size", (1, 2, 3, 4)) + def test_resource_params(self, register_size): + """Test that the resource params are correct.""" + op = plre.ResourceSemiAdder(register_size) + assert op.resource_params == {"max_register_size": register_size} + + @pytest.mark.parametrize("register_size", (1, 2, 3, 4)) + def test_resource_rep(self, register_size): + """Test that the compressed representation is correct.""" + expected = plre.CompressedResourceOp( + plre.ResourceSemiAdder, {"max_register_size": register_size} + ) + assert plre.ResourceSemiAdder.resource_rep(max_register_size=register_size) == expected + + @pytest.mark.parametrize( + "register_size, expected_res", + ( + ( + 1, + [plre.GateCount(plre.resource_rep(plre.ResourceCNOT))], + ), + ( + 2, + [ + plre.GateCount(plre.resource_rep(plre.ResourceCNOT), 2), + plre.GateCount(plre.resource_rep(plre.ResourceX), 2), + plre.GateCount(plre.resource_rep(plre.ResourceToffoli)), + ], + ), + ( + 3, + [ + plre.AllocWires(2), + plre.GateCount(plre.resource_rep(plre.ResourceCNOT), 9), + plre.GateCount(plre.resource_rep(plre.ResourceTempAND), 2), + plre.GateCount( + plre.resource_rep( + plre.ResourceAdjoint, + {"base_cmpr_op": plre.resource_rep(plre.ResourceTempAND)}, + ), + 2, + ), + plre.FreeWires(2), + ], + ), + ), + ) + def test_resources(self, register_size, expected_res): + """Test that the resources are correct.""" + assert plre.ResourceSemiAdder.resource_decomp(register_size) == expected_res + + def test_resources_controlled(self): + """Test that the special case controlled resources are correct.""" + op = plre.ResourceControlled( + plre.ResourceSemiAdder(max_register_size=5), + num_ctrl_wires=1, + num_ctrl_values=0, + ) + + expected_res = [ + plre.AllocWires(4), + plre.GateCount(plre.resource_rep(plre.ResourceCNOT), 24), + plre.GateCount(plre.resource_rep(plre.ResourceTempAND), 8), + plre.GateCount( + plre.resource_rep( + plre.ResourceAdjoint, {"base_cmpr_op": plre.resource_rep(plre.ResourceTempAND)} + ), + 8, + ), + plre.FreeWires(4), + ] + assert op.resource_decomp(**op.resource_params) == expected_res + + +class TestResourceBasisRotation: + """Test the BasisRotation class.""" + + @pytest.mark.parametrize("dim_n", (1, 2, 3)) + def test_resource_params(self, dim_n): + """Test that the resource params are correct.""" + op = plre.ResourceBasisRotation(dim_n) + assert op.resource_params == {"dim_N": dim_n} + + @pytest.mark.parametrize("dim_n", (1, 2, 3)) + def test_resource_rep(self, dim_n): + """Test that the compressed representation is correct.""" + expected = plre.CompressedResourceOp(plre.ResourceBasisRotation, {"dim_N": dim_n}) + assert plre.ResourceBasisRotation.resource_rep(dim_N=dim_n) == expected + + @pytest.mark.parametrize("dim_n", (1, 2, 3)) + def test_resources(self, dim_n): + """Test that the resources are correct.""" + expected = [ + plre.GateCount( + plre.resource_rep(plre.ResourcePhaseShift), dim_n + (dim_n * (dim_n - 1) // 2) + ), + plre.GateCount( + plre.resource_rep(plre.ResourceSingleExcitation), dim_n * (dim_n - 1) // 2 + ), + ] + assert plre.ResourceBasisRotation.resource_decomp(dim_n) == expected + + +class TestResourceSelect: + """Test the Select class.""" + + def test_resource_params(self): + """Test that the resource params are correct.""" + ops = [plre.ResourceRX(), plre.ResourceZ(), plre.ResourceCNOT()] + cmpr_ops = tuple(op.resource_rep_from_op() for op in ops) + + op = plre.ResourceSelect(ops) + assert op.resource_params == {"cmpr_ops": cmpr_ops} + + def test_resource_rep(self): + """Test that the compressed representation is correct.""" + ops = [plre.ResourceRX(), plre.ResourceZ(), plre.ResourceCNOT()] + cmpr_ops = tuple(op.resource_rep_from_op() for op in ops) + + expected = plre.CompressedResourceOp(plre.ResourceSelect, {"cmpr_ops": cmpr_ops}) + assert plre.ResourceSelect.resource_rep(cmpr_ops) == expected + + def test_resources(self): + """Test that the resources are correct.""" + ops = [plre.ResourceRX(), plre.ResourceZ(), plre.ResourceCNOT()] + cmpr_ops = tuple(op.resource_rep_from_op() for op in ops) + + expected = [ + plre.AllocWires(1), + plre.GateCount( + plre.ResourceControlled.resource_rep( + plre.ResourceRX.resource_rep(), + 1, + 0, + ) + ), + plre.GateCount( + plre.ResourceControlled.resource_rep( + plre.ResourceZ.resource_rep(), + 1, + 0, + ) + ), + plre.GateCount( + plre.ResourceControlled.resource_rep( + plre.ResourceCNOT.resource_rep(), + 1, + 0, + ) + ), + plre.GateCount(plre.ResourceX.resource_rep(), 4), + plre.GateCount(plre.ResourceCNOT.resource_rep(), 2), + plre.GateCount(plre.ResourceTempAND.resource_rep(), 2), + plre.GateCount( + plre.ResourceAdjoint.resource_rep( + plre.ResourceTempAND.resource_rep(), + ), + 2, + ), + plre.FreeWires(1), + ] + assert plre.ResourceSelect.resource_decomp(cmpr_ops) == expected + + +class TestResourceQROM: + """Test the ResourceQROM class.""" + + @pytest.mark.parametrize( + "num_data_points, size_data_points, num_bit_flips, depth, clean", + ( + (10, 3, 15, None, True), + (100, 5, 50, 2, False), + (12, 2, 5, 1, True), + ), + ) + def test_resource_params(self, num_data_points, size_data_points, num_bit_flips, depth, clean): + """Test that the resource params are correct.""" + if depth is None: + op = plre.ResourceQROM(num_data_points, size_data_points) + else: + op = plre.ResourceQROM(num_data_points, size_data_points, num_bit_flips, clean, depth) + + assert op.resource_params == { + "num_bitstrings": num_data_points, + "size_bitstring": size_data_points, + "num_bit_flips": num_bit_flips, + "select_swap_depth": depth, + "clean": clean, + } + + @pytest.mark.parametrize( + "num_data_points, size_data_points, num_bit_flips, depth, clean", + ( + (10, 3, 15, None, True), + (100, 5, 50, 2, False), + (12, 2, 5, 1, True), + ), + ) + def test_resource_rep(self, num_data_points, size_data_points, num_bit_flips, depth, clean): + """Test that the compressed representation is correct.""" + expected = plre.CompressedResourceOp( + plre.ResourceQROM, + { + "num_bitstrings": num_data_points, + "size_bitstring": size_data_points, + "num_bit_flips": num_bit_flips, + "select_swap_depth": depth, + "clean": clean, + }, + ) + assert ( + plre.ResourceQROM.resource_rep( + num_bitstrings=num_data_points, + size_bitstring=size_data_points, + num_bit_flips=num_bit_flips, + clean=clean, + select_swap_depth=depth, + ) + == expected + ) + + @pytest.mark.parametrize( + "num_data_points, size_data_points, num_bit_flips, depth, clean, expected_res", + ( + ( + 10, + 3, + 15, + None, + True, + [ + plre.AllocWires(5), + plre.GateCount(plre.ResourceHadamard.resource_rep(), 6), + plre.GateCount(plre.ResourceX.resource_rep(), 14), + plre.GateCount(plre.ResourceCNOT.resource_rep(), 36), + plre.GateCount(plre.ResourceTempAND.resource_rep(), 6), + plre.GateCount( + plre.ResourceAdjoint.resource_rep( + plre.ResourceTempAND.resource_rep(), + ), + 6, + ), + plre.FreeWires(2), + plre.GateCount(plre.ResourceCSWAP.resource_rep(), 12), + plre.FreeWires(3), + ], + ), + ( + 100, + 5, + 50, + 2, + False, + [ + plre.AllocWires(10), + plre.GateCount(plre.ResourceX.resource_rep(), 97), + plre.GateCount(plre.ResourceCNOT.resource_rep(), 98), + plre.GateCount(plre.ResourceTempAND.resource_rep(), 48), + plre.GateCount( + plre.ResourceAdjoint.resource_rep( + plre.ResourceTempAND.resource_rep(), + ), + 48, + ), + plre.FreeWires(5), + plre.GateCount(plre.ResourceCSWAP.resource_rep(), 5), + ], + ), + ( + 12, + 2, + 5, + 1, + True, # AllocWires(3), (4 x Hadamard), (42 x X), (30 x CNOT), (20 x TempAND), (20 x Adjoint(TempAND)), FreeWires(3), (0 x CSWAP), FreeWires(0) + [ + plre.AllocWires(3), + plre.GateCount(plre.ResourceHadamard.resource_rep(), 4), + plre.GateCount(plre.ResourceX.resource_rep(), 42), + plre.GateCount(plre.ResourceCNOT.resource_rep(), 30), + plre.GateCount(plre.ResourceTempAND.resource_rep(), 20), + plre.GateCount( + plre.ResourceAdjoint.resource_rep( + plre.ResourceTempAND.resource_rep(), + ), + 20, + ), + plre.FreeWires(3), + plre.GateCount(plre.ResourceCSWAP.resource_rep(), 0), + plre.FreeWires(0), + ], + ), + ), + ) + def test_resources( + self, num_data_points, size_data_points, num_bit_flips, depth, clean, expected_res + ): + """Test that the resources are correct.""" + assert ( + plre.ResourceQROM.resource_decomp( + num_bitstrings=num_data_points, + size_bitstring=size_data_points, + num_bit_flips=num_bit_flips, + clean=clean, + select_swap_depth=depth, + ) + == expected_res + ) From 14a173e47363062eb9102e98ff4687b345371b12 Mon Sep 17 00:00:00 2001 From: Jay Soni Date: Wed, 25 Jun 2025 17:13:15 -0400 Subject: [PATCH 18/18] undo yml fix --- .github/workflows/tests-labs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests-labs.yml b/.github/workflows/tests-labs.yml index b6cc6feb76c..2f8ce42405f 100644 --- a/.github/workflows/tests-labs.yml +++ b/.github/workflows/tests-labs.yml @@ -1,6 +1,8 @@ name: PennyLane Labs Unit-Tests on: pull_request: + branches: + - master concurrency: group: qml-labs-${{ github.workflow }}-${{ github.ref }}