diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py index 1784509f2fb..996c0dc4e45 100644 --- a/pennylane/labs/resource_estimation/__init__.py +++ b/pennylane/labs/resource_estimation/__init__.py @@ -105,6 +105,7 @@ ~ResourceAdjoint ~ResourceControlled ~ResourcePow + ~ResourceProd Templates ~~~~~~~~~ @@ -138,6 +139,7 @@ ~ResourcesNotDefined """ +from .compact_objects import CompactLCU, CompactState from .resource_operator import ResourceOperator, ResourcesNotDefined from .resource_tracking import DefaultGateSet, get_resources, resource_config @@ -183,6 +185,7 @@ ResourceOrbitalRotation, ResourcePauliRot, ResourcePow, + ResourceProd, ResourcePSWAP, ResourcePhaseShift, ResourceRot, diff --git a/pennylane/labs/resource_estimation/compact_objects.py b/pennylane/labs/resource_estimation/compact_objects.py new file mode 100644 index 00000000000..48ec0ba1648 --- /dev/null +++ b/pennylane/labs/resource_estimation/compact_objects.py @@ -0,0 +1,168 @@ +# 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. +"""This module contains the base classes for compact hamiltonians and states +to be used with the existing resource estimation pipeline""" +import pennylane.numpy as qnp +from pennylane.labs import resource_estimation as re + + +class CompactLCU: + """A class storing the meta data associated with an LCU decomposition of an operator.""" + + def __init__( + self, + num_wires, + lcu_type=None, + num_terms=None, + k_local=None, + cost_per_term=None, + cost_per_exp_term=None, + cost_per_ctrl_exp_term=None, + one_norm_error=None, + ) -> None: + """Store the meta info into the class attributes.""" + self.num_wires = num_wires + self.lcu_type = lcu_type + self.num_terms = num_terms + self.k_local = k_local + self.cost_per_term = cost_per_term + self.cost_per_exp_term = cost_per_exp_term + self.cost_per_ctrl_exp_term = cost_per_ctrl_exp_term + self.one_norm_error = one_norm_error + + def info(self, print_info=False): + """Return a dictionary of the metadata or display it on screen.""" + metadata_dict = self.__dict__ + + if print_info: + print(f"CompactLCU(num_wires={metadata_dict["num_wires"]}):") + for k, v in metadata_dict.items(): + if k == "num_wires": + continue + print(f"-> {k}: {v}") + + return metadata_dict + + def update(self): + """Update empty information after initializing the class.""" + if self.lcu_type == "pauli": + cost_per_term = {} + x, y, z = (re.ResourceX.resource_rep(), re.ResourceY.resource_rep(), re.ResourceZ.resource_rep()) + + freq = self.k_local // 3 + + cost_per_term[x] = freq + cost_per_term[z] = freq + cost_per_term[y] = self.k_local - 2*freq + + avg_pword = freq * "X" + freq * "Z" + (self.k_local - 2*freq) * "Y" + cost_per_exp_term = {re.ResourcePauliRot.resource_rep(avg_pword): 1} + + if self.cost_per_term is None: + self.cost_per_term = cost_per_term + + if self.cost_per_exp_term is None: + self.cost_per_exp_term = cost_per_exp_term + + if self.lcu_type == "cdf": + cost_per_term = {} + cost_per_exp_term = {} + cost_per_ctrl_exp_term = {} + + basis_rot = re.ResourceBasisRotation.resource_rep(self.num_wires) + adj_basis_rot = re.ResourceAdjoint.resource_rep( + re.ResourceBasisRotation, {"dim_N": self.num_wires} + ) + z = re.ResourceZ.resource_rep() + multi_z = re.ResourceMultiRZ.resource_rep(2) + ctrl_multi_z = re.ResourceControlled.resource_rep( + re.ResourceMultiRZ, {"num_wires": 2}, 1, 0, 0 + ) + + cost_per_term[basis_rot] = 2 + cost_per_term[adj_basis_rot] = 2 + cost_per_term[z] = 2 + + cost_per_exp_term[basis_rot] = 2 + cost_per_exp_term[adj_basis_rot] = 2 + cost_per_exp_term[multi_z] = 1 + + cost_per_ctrl_exp_term[basis_rot] = 2 + cost_per_ctrl_exp_term[adj_basis_rot] = 2 + cost_per_ctrl_exp_term[ctrl_multi_z] = 1 + + if self.cost_per_term is None: + self.cost_per_term = cost_per_term + + if self.cost_per_exp_term is None: + self.cost_per_exp_term = cost_per_exp_term + + if self.cost_per_ctrl_exp_term is None: + self.cost_per_ctrl_exp_term = cost_per_ctrl_exp_term + + return + + +class CompactState: + """A class storing the meta data associated with a quantum state.""" + + def __init__( + self, + num_wires, + data_size=None, + is_sparse=False, + is_bitstring=False, + precision=None, + num_aux_wires=None, + cost_per_prep=None, + cost_per_ctrl_prep=None, + ) -> None: + self.num_wires = num_wires + self.data_size = data_size + self.is_sparse = is_sparse + self.is_bitstring = is_bitstring + self.precision = precision + self.num_aux_wires = num_aux_wires + self.cost_per_prep = cost_per_prep + self.cost_per_ctrl_prep = cost_per_ctrl_prep + + def info(self, print_info=False): + """Return a dictionary of the metadata or display it on screen.""" + metadata_dict = self.__dict__ + + if print_info: + print(f"CompactState(num_wires={metadata_dict["num_wires"]}):") + for k, v in metadata_dict.items(): + if k == "num_wires": + continue + print(f"-> {k}: {v}") + + return metadata_dict + + def update(self): + """Update empty information after initializing the class.""" + if (eps := self.precision) is not None: + n = self.num_wires + n_ctrl = n+1 + t = re.ResourceT.resource_rep() + + if self.cost_per_prep is None: + self.cost_per_prep = {t: self._qrom_state_prep(n, eps)} + + if self.cost_per_ctrl_prep is None: + self.cost_per_ctrl_prep = {t: self._qrom_state_prep(n_ctrl, eps)} + + @staticmethod + def _qrom_state_prep(n, eps): + return qnp.round(qnp.sqrt((2**n)*qnp.log(n/eps)) * n * qnp.log(n/eps) * qnp.log(1/eps)) diff --git a/pennylane/labs/resource_estimation/ops/__init__.py b/pennylane/labs/resource_estimation/ops/__init__.py index f7a5a6fbea9..3dc11f9c0f6 100644 --- a/pennylane/labs/resource_estimation/ops/__init__.py +++ b/pennylane/labs/resource_estimation/ops/__init__.py @@ -66,4 +66,5 @@ ResourceControlled, ResourceControlledPhaseShift, ResourcePow, + ResourceProd, ) diff --git a/pennylane/labs/resource_estimation/ops/op_math/symbolic.py b/pennylane/labs/resource_estimation/ops/op_math/symbolic.py index 27b466826ec..2c5a4825262 100644 --- a/pennylane/labs/resource_estimation/ops/op_math/symbolic.py +++ b/pennylane/labs/resource_estimation/ops/op_math/symbolic.py @@ -23,6 +23,7 @@ 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.ops.op_math.sprod import SProd # pylint: disable=too-many-ancestors,arguments-differ,protected-access,too-many-arguments,too-many-positional-arguments @@ -277,6 +278,33 @@ def controlled_resource_decomp( raise re.ResourcesNotDefined +class ResourceProd(Prod, re.ResourceOperator): + """Resource class for Prod""" + + @classmethod + def _resource_decomp(cls, cmpr_reps, **kwargs) -> Dict[re.CompressedResourceOp, int]: + gate_types = defaultdict(int) + + for op in cmpr_reps: + gate_types[op] += 1 + + return gate_types + + def resource_params(self) -> dict: + ops = self.operands + cmpr_reps = tuple(op.resource_rep_from_op() for op in ops) + return {"cmpr_reps": cmpr_reps} + + @classmethod + def resource_rep(cls, cmpr_reps) -> re.CompressedResourceOp: + return re.CompressedResourceOp(cls, {"cmpr_reps": cmpr_reps}) + + @staticmethod + def tracking_name(cmpr_reps) -> str: + base_names = [(cmpr_rep.op_type).tracking_name(**cmpr_rep.params) for cmpr_rep in cmpr_reps] + return f"Prod({",".join(base_names)})" + + def _resources_from_pauli_word(pauli_word, num_wires): pauli_string = "".join((str(v) for v in pauli_word.values())) len_str = len(pauli_string) diff --git a/pennylane/labs/resource_estimation/resource_tracking.py b/pennylane/labs/resource_estimation/resource_tracking.py index cfc6c4cec8a..4c16f040102 100644 --- a/pennylane/labs/resource_estimation/resource_tracking.py +++ b/pennylane/labs/resource_estimation/resource_tracking.py @@ -18,7 +18,7 @@ from typing import Dict, Iterable, List, Set, Union import pennylane as qml -from pennylane.operation import Operation +from pennylane.operation import Operation, Operator from pennylane.queuing import AnnotatedQueue from pennylane.tape import QuantumScript from pennylane.wires import Wires @@ -155,7 +155,7 @@ def my_circuit(): @get_resources.register def resources_from_operation( - obj: Operation, gate_set: Set = DefaultGateSet, config: Dict = resource_config + obj: Operator, gate_set: Set = DefaultGateSet, config: Dict = resource_config ) -> Resources: """Get resources from an operation""" diff --git a/pennylane/labs/resource_estimation/templates/subroutines.py b/pennylane/labs/resource_estimation/templates/subroutines.py index 6c21f4b6bb7..5197b2b8df1 100644 --- a/pennylane/labs/resource_estimation/templates/subroutines.py +++ b/pennylane/labs/resource_estimation/templates/subroutines.py @@ -17,9 +17,12 @@ 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 +from pennylane.operation import StatePrepBase + # pylint: disable=arguments-differ @@ -112,8 +115,37 @@ class ResourceStatePrep(qml.StatePrep, ResourceOperator): TODO: add the resources here """ + def __init__( + self, + state, + wires=["NoListedWires"], + pad_with=None, + normalize=False, + id = None, + validate_norm = True, + ): + is_compact = isinstance(state, re.CompactState) + + if is_compact: + self.state = state + StatePrepBase.__init__(self, [1, 0], wires=wires) + return + + super().__init__( + state, + wires=wires, + pad_with=pad_with, + normalize=normalize, + id = id, + validate_norm = validate_norm, + ) + @staticmethod - def _resource_decomp(num_wires, **kwargs) -> Dict[CompressedResourceOp, int]: + def _resource_decomp(num_wires, state, **kwargs) -> Dict[CompressedResourceOp, int]: + if state: + if state.cost_per_prep: + return state.cost_per_prep + gate_types = {} rz = re.ResourceRZ.resource_rep() cnot = re.ResourceCNOT.resource_rep() @@ -129,11 +161,16 @@ def _resource_decomp(num_wires, **kwargs) -> Dict[CompressedResourceOp, int]: return gate_types def resource_params(self) -> dict: - return {"num_wires": len(self.wires)} + try: + state = self.state + except AttributeError: + state = None + + return {"num_wires": len(self.wires), "state": state} @classmethod - def resource_rep(cls, num_wires) -> CompressedResourceOp: - params = {"num_wires": num_wires} + def resource_rep(cls, num_wires, state=None) -> CompressedResourceOp: + params = {"num_wires": num_wires, "state": state} return CompressedResourceOp(cls, params) @@ -207,7 +244,7 @@ def _resource_decomp(cmpr_ops, **kwargs) -> Dict[CompressedResourceOp, int]: prep = ResourceStatePrep.resource_rep(num_wires) sel = ResourceSelect.resource_rep(cmpr_ops) - prep_dag = re.ResourceAdjoint.resource_rep(ResourceStatePrep, {"num_wires": num_wires}) + prep_dag = re.ResourceAdjoint.resource_rep(ResourceStatePrep, {"num_wires": num_wires, "state": None}) gate_types[prep] = 1 gate_types[sel] = 1 @@ -224,6 +261,49 @@ def resource_rep(cls, cmpr_ops) -> CompressedResourceOp: params = {"cmpr_ops": cmpr_ops} return CompressedResourceOp(cls, params) + @classmethod + def adjoint_resource_decomp(cls, cmpr_ops, **kwargs) -> Dict[CompressedResourceOp, int]: + """Returns a compressed representation of the adjoint of the operator""" + raise {cls.resource_rep(cmpr_ops): 1} + + @classmethod + def controlled_resource_decomp( + cls, num_ctrl_wires, num_ctrl_values, num_work_wires, cmpr_ops, **kwargs + ) -> Dict[CompressedResourceOp, int]: + """Returns a compressed representation of the controlled version of the operator""" + gate_types = {} + + num_ops = len(cmpr_ops) + num_wires = int(qnp.log2(num_ops)) + + prep = ResourceStatePrep.resource_rep(num_wires) + ctrl_sel = re.ResourceControlled.resource_rep( + ResourceSelect, {"cmpr_ops": cmpr_ops}, num_ctrl_wires, num_ctrl_values, num_work_wires + ) + prep_dag = re.ResourceAdjoint.resource_rep(ResourceStatePrep, {"num_wires": num_wires, "state": None}) + + gate_types[prep] = 1 + gate_types[ctrl_sel] = 1 + gate_types[prep_dag] = 1 + return gate_types + + @classmethod + def pow_resource_decomp(cls, z, cmpr_ops, **kwargs) -> Dict[CompressedResourceOp, int]: + """Returns a compressed representation of the operator raised to a power""" + gate_types = {} + + num_ops = len(cmpr_ops) + num_wires = int(qnp.log2(num_ops)) + + prep = ResourceStatePrep.resource_rep(num_wires) + pow_sel = re.ResourcePow.resource_rep(ResourceSelect, z, {"cmpr_ops": cmpr_ops}) + prep_dag = re.ResourceAdjoint.resource_rep(ResourceStatePrep, {"num_wires": num_wires, "state": None}) + + gate_types[prep] = 1 + gate_types[pow_sel] = 1 + gate_types[prep_dag] = 1 + return gate_types + class ResourceReflection(qml.Reflection, ResourceOperator): @@ -261,10 +341,11 @@ def resource_rep(cls, base, num_ref_wires) -> CompressedResourceOp: class ResourceQubitization(qml.Qubitization, ResourceOperator): + @staticmethod - def _resource_decomp(cmpr_ops, num_ctrl_wires, **kwargs) -> Dict[CompressedResourceOp, int]: + def _resource_decomp(cmpr_ops, num_c_wires, **kwargs) -> Dict[CompressedResourceOp, int]: gate_types = {} - ref = ResourceReflection.resource_rep(re.ResourceIdentity.resource_rep(), num_ctrl_wires) + ref = ResourceReflection.resource_rep(re.ResourceIdentity.resource_rep(), num_c_wires) psp = ResourcePrepSelPrep.resource_rep(cmpr_ops) gate_types[ref] = 1 @@ -276,10 +357,10 @@ def resource_params(self) -> dict: _, 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} + num_c_wires = len(self.hyperparameters["control"]) + return {"cmpr_ops": cmpr_ops, "num_c_wires": num_c_wires} @classmethod - def resource_rep(cls, cmpr_ops, num_ctrl_wires) -> CompressedResourceOp: - params = {"cmpr_ops": cmpr_ops, "num_ctrl_wires": num_ctrl_wires} + def resource_rep(cls, cmpr_ops, num_c_wires) -> CompressedResourceOp: + params = {"cmpr_ops": cmpr_ops, "num_c_wires": num_c_wires} return CompressedResourceOp(cls, params) diff --git a/pennylane/labs/resource_estimation/templates/trotter.py b/pennylane/labs/resource_estimation/templates/trotter.py index 8a0938ef431..fe3298ebe15 100644 --- a/pennylane/labs/resource_estimation/templates/trotter.py +++ b/pennylane/labs/resource_estimation/templates/trotter.py @@ -20,12 +20,15 @@ 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.labs.resource_estimation.resource_container import _scale_dict, _combine_dict + from pennylane.operation import Operation, Operator from pennylane.ops import Sum from pennylane.ops.op_math import SProd @@ -163,6 +166,7 @@ def __init__( # pylint: disable=too-many-arguments self, hamiltonian, time, n=1, order=1, check_hermitian=True, id=None ): r"""Initialize the TrotterProduct class""" + is_compact = isinstance(hamiltonian, re.CompactLCU) if order <= 0 or order != 1 and order % 2 != 0: raise ValueError( @@ -188,12 +192,12 @@ def __init__( # pylint: disable=too-many-arguments "There should be at least 2 terms in the Hamiltonian. Otherwise use `qml.exp`" ) - if not isinstance(hamiltonian, Sum): + if not isinstance(hamiltonian, Sum) and not is_compact: raise TypeError( f"The given operator must be a PennyLane ~.Sum or ~.SProd, got {hamiltonian}" ) - if check_hermitian: + if check_hermitian and not is_compact: for op in hamiltonian.operands: if not op.is_hermitian: raise ValueError( @@ -207,11 +211,26 @@ def __init__( # pylint: disable=too-many-arguments "check_hermitian": check_hermitian, } + if is_compact: + super().__init__(time, wires=["NoListedWires"], id=id) + return + super().__init__(*hamiltonian.data, time, wires=hamiltonian.wires, id=id) @staticmethod def _resource_decomp(base, time, n, order, **kwargs) -> Dict[CompressedResourceOp, int]: + is_compact = isinstance(base, re.CompactLCU) k = order // 2 + + if is_compact: + if base.cost_per_exp_term is not None: + gate_types = defaultdict(int, base.cost_per_exp_term) + total_terms = 2 * n * (5 ** (k - 1)) * base.num_terms + _scale_dict(gate_types, total_terms, in_place=True) + + return gate_types + raise re.ResourcesNotDefined + first_order_expansion = [ ResourceExp.resource_rep(op, (time / n) * 1j, num_steps=1) for op in base.operands ]