diff --git a/zne/noise_amplification/folding_amplifier/glorious_folding_amplifier.py b/zne/noise_amplification/folding_amplifier/glorious_folding_amplifier.py new file mode 100644 index 0000000..927a812 --- /dev/null +++ b/zne/noise_amplification/folding_amplifier/glorious_folding_amplifier.py @@ -0,0 +1,227 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022-2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Glorious Global Folding Noise Amplfiier (Temporary)""" + +from collections import namedtuple +from collections.abc import Set +from math import ceil, floor +from typing import Tuple, Union + +from numpy import isclose +from qiskit.circuit import Operation +from qiskit.circuit.library import Barrier +from qiskit.dagcircuit import DAGCircuit, DAGOpNode + +from ..noise_amplifier import DAGNoiseAmplifier + +Folding = namedtuple("Folding", ("full", "partial", "effective_noise_factor")) +# noise_factor = approximate noise_factor + + +class GloriousFoldingAmplifier(DAGNoiseAmplifier): + """Interface for folding amplifier strategies.""" + + def __init__(self, barriers: bool = True, tolerance: float = 0.05) -> None: + self._set_barriers(barriers) + self._set_tolerance(tolerance) + + ################################################################################ + ## PROPERTIES + ################################################################################ + @property + def barriers(self) -> bool: + """Barriers setter""" + return self._barriers + + def _set_barriers(self, barriers: bool) -> None: + """Set barriers property""" + self._barriers = bool(barriers) + + @property + def tolerance(self) -> float: + """Tolerance setter""" + return self._tolerance + + def _set_tolerance(self, tolerance: float) -> None: + """Set Tolerance property""" + self._tolerance = float(tolerance) + + ################################################################################ + # AUXILIARY + ################################################################################ + def _compute_folding_nums(self, noise_factor: float, num_nodes: int) -> Folding: + """Compute number of foldings. + + Args: + noise_factor: The original noise_factor input. + num_nodes: total number of foldable nodes for input DAG + + Returns: + Folding: named tuple containing full foldings, number of gates to partially fold and + effective noise factor of the operation + """ + + foldings = (noise_factor - 1) / 2 + full_foldings = int(foldings) + partial_foldings = foldings - full_foldings + + gates_to_partial_fold = self._compute_best_estimated_gates(num_nodes, partial_foldings) + + effective_foldings = full_foldings + gates_to_partial_fold / num_nodes + effective_noise_factor = 2 * effective_foldings + 1 + + if isclose(effective_noise_factor, noise_factor, atol=self.tolerance, equal_nan=False): + print("Warning!!!") + print( + "Folding with effective noise factor of", + effective_noise_factor, + "instead of", + noise_factor, + ". Effective folding accuracy:", + effective_noise_factor / noise_factor * 100, + ) + return Folding(full_foldings, gates_to_partial_fold, effective_noise_factor) + + def _compute_best_estimated_gates(self, num_nodes: float, partial_foldings: float) -> float: + """Computes best estimates from possible candidates for number of partial folded gates + + Args: + num_nodes: total number of foldable nodes for input DAG + partial_foldings: Extra foldings required + + Returns: + float: returns closest estimated number of gates required to be partially folded + to achieve the user expected noise_factor + """ + lower_estimate = floor(num_nodes * partial_foldings) + lower_diff = abs(lower_estimate - partial_foldings) + upper_estimate = ceil(num_nodes * partial_foldings) + upper_diff = abs(upper_estimate - partial_foldings) + if lower_diff < upper_diff: + return lower_estimate + return upper_estimate + + def _compute_folding_mask( + self, + folding_nums: Folding, + dag: DAGCircuit, + gates_to_fold: Set[Union[str, int] | None] = None, + ) -> list: + """Computes folding mask based on folding_nums and gates_to_fold + + Args: + folding_nums: namedTuple with full foldings, partial gates and effective_noise_factor + dag: The original dag circuit without foldings. + gates_to_fold: Set of gates to fold supplied by user + + Returns: + list: list mask for applying folding operation. + """ + partial_folding_mask = [] + counter = folding_nums.partial + if gates_to_fold is None: # For global folding + for node in dag.topological_op_nodes(): + if counter != 0: + partial_folding_mask.append(1) + counter -= 1 + else: + partial_folding_mask.append(0) + return partial_folding_mask + for node in dag.topological_op_nodes(): # For local folding + if node.name in gates_to_fold: + if counter != 0: + partial_folding_mask.append(folding_nums.full + 1) + counter -= 1 + else: + partial_folding_mask.append(folding_nums.full) + elif node.op.num_qubits in gates_to_fold and counter != 0: + if counter != 0: + partial_folding_mask.append(folding_nums.full + 1) + counter -= 1 + else: + partial_folding_mask.append(folding_nums.full) + else: + partial_folding_mask.append(0) + return partial_folding_mask + + def _apply_folded_operation_back( + self, + dag: DAGCircuit, + node: DAGOpNode, + num_foldings: int, + ) -> DAGCircuit: + """Folds each gate of original DAG circuit a number of ``num_foldings`` times. + + Args: + dag: The original dag circuit without foldings. + node: The DAGOpNode to apply folded. + num_foldings: Number of times the circuit should be folded. + + Returns: + DAGCircuit: The noise amplified DAG circuit. + """ + if num_foldings == 0: + dag.apply_operation_back(node.op) + return dag + original_op = node.op + inverted_op = original_op.inverse() + if self.barriers: + barrier = Barrier(original_op.num_qubits) + dag.apply_operation_back(barrier, qargs=node.qargs) + self._apply_operation_back(dag, original_op, node.qargs, node.cargs) + for _ in range(num_foldings): + self._apply_operation_back(dag, inverted_op, node.qargs, node.cargs) + self._apply_operation_back(dag, original_op, node.qargs, node.cargs) + return dag + + def _apply_operation_back( + self, + dag: DAGCircuit, + dag_op: Operation, + qargs: Tuple = (), + cargs: Tuple = (), + ) -> DAGCircuit: + dag.apply_operation_back(dag_op, qargs, cargs) + if self.barriers: + barrier = Barrier(dag_op.num_qubits) + dag.apply_operation_back(barrier, qargs) + return dag + + def _validate_noise_factor(self, noise_factor: float) -> float: + """Normalizes and validates noise factor. + + Args: + noise_factor: The original noisefactor input. + + Returns: + float: Normalised noise_factor input. + + Raises: + ValueError: If input noise_factor value is not of type float. + TypeError: If input noise_factor value is not of type float. + """ + try: + noise_factor = float(noise_factor) + except ValueError: + raise ValueError( # pylint: disable=raise-missing-from + f"Expected positive float value, received {noise_factor} instead." + ) + except TypeError: + raise TypeError( # pylint: disable=raise-missing-from + f"Expected positive float, received {type(noise_factor)} instead." + ) + if noise_factor < 1: + raise ValueError( + f"Expected positive float noise_factor >= 1, received {noise_factor} instead." + ) + return noise_factor diff --git a/zne/noise_amplification/folding_amplifier/glorious_global_folding_amplifier.py b/zne/noise_amplification/folding_amplifier/glorious_global_folding_amplifier.py new file mode 100644 index 0000000..e28b813 --- /dev/null +++ b/zne/noise_amplification/folding_amplifier/glorious_global_folding_amplifier.py @@ -0,0 +1,100 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022-2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + + +"""Glorious Global DAG Folding Noise Amplification (Temporary)""" + +from typing import Tuple + +from qiskit.circuit.library import Barrier +from qiskit.dagcircuit import DAGCircuit + +from .glorious_folding_amplifier import Folding, GloriousFoldingAmplifier + + +class GloriousGlobalFoldingAmplifier(GloriousFoldingAmplifier): + """Alternatingly composes the circuit and its inverse as many times as indicated + by the ``noise_factor``. + + References: + [1] T. Giurgica-Tiron et al. (2020). + Digital zero noise extrapolation for quantum error mitigation. + `` + """ + + def __init__(self) -> None: + super().__init__() + self.gates_to_fold = None + + ################################################################################ + ## INTERFACE IMPLEMENTATION + ################################################################################ + def amplify_dag_noise(self, dag: DAGCircuit, noise_factor: float) -> DAGCircuit: + """Applies global folding to input DAGCircuit and returns amplified circuit""" + noise_factor = self._validate_noise_factor(noise_factor) + num_nodes = dag.size() + folding_nums = self._compute_folding_nums(noise_factor, num_nodes) + num_foldings = self._compute_folding_mask(folding_nums, dag, self.gates_to_fold) + return self._apply_full_folding(dag, folding_nums, num_foldings) + + ################################################################################ + ## AUXILIARY + ################################################################################ + def _apply_full_folding( + self, + dag: DAGCircuit, + folding_nums: Folding, + num_foldings: list, + ) -> DAGCircuit: + """Fully folds the original DAG circuit a number of ``num_foldings`` times. + + Args: + dag: The original dag circuit without foldings. + num_foldings: Number of times the circuit should be folded. + + Returns: + DAGCircuit: The noise amplified DAG circuit. + """ + noisy_dag = dag.copy_empty_like() + inverse_dag = self._invert_dag(dag) + if self.barriers: + barrier = Barrier(noisy_dag.num_qubits()) + noisy_dag.apply_operation_back(barrier, noisy_dag.qubits) + self._compose(noisy_dag, dag, noisy_dag.qubits) + for _ in range(folding_nums.full): + self._compose(noisy_dag, inverse_dag, noisy_dag.qubits) + self._compose(noisy_dag, dag, noisy_dag.qubits) + for node, num in zip(dag.topological_op_nodes(), num_foldings): + noisy_dag = self._apply_folded_operation_back(noisy_dag, node, num) + return noisy_dag + + def _compose( + self, dag: DAGCircuit, dag_to_compose: DAGCircuit, qargs: Tuple = () + ) -> DAGCircuit: + dag.compose(dag_to_compose, inplace=True) + if self.barriers: + barrier = Barrier(dag.num_qubits()) + dag.apply_operation_back(barrier, qargs) + + def _invert_dag(self, dag_to_inverse: DAGCircuit) -> DAGCircuit: + """Inverts an input dag circuit. + + Args: + dag_to_inverse: The original dag circuit to invert. + + Returns: + DAGCircuit: The inverted DAG circuit. + """ + inverse_dag = dag_to_inverse.copy_empty_like() + for node in dag_to_inverse.topological_op_nodes(): + inverse_dag.apply_operation_front(node.op.inverse(), qargs=node.qargs, cargs=node.cargs) + return inverse_dag diff --git a/zne/noise_amplification/folding_amplifier/glorious_local_folding_amplifier.py b/zne/noise_amplification/folding_amplifier/glorious_local_folding_amplifier.py new file mode 100644 index 0000000..c8388d9 --- /dev/null +++ b/zne/noise_amplification/folding_amplifier/glorious_local_folding_amplifier.py @@ -0,0 +1,131 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022-2023. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Glorious Local DAG Folding Noise Amplification (Temporary)""" + +from collections.abc import Set +from typing import Union + +from qiskit.circuit.library import standard_gates +from qiskit.dagcircuit import DAGCircuit + +from .glorious_folding_amplifier import GloriousFoldingAmplifier + + +class GloriousLocalFoldingAmplifier(GloriousFoldingAmplifier): + """Amplifies noise in the circuit by said gate and its inverse alternatingly as many times as + indicated by noise_factor. The gates that should be folded can be specified by + ``gates_to_fold``. By default, all gates of the circuit are folded. + + References: + [1] T. Giurgica-Tiron et al. (2020). + Digital zero noise extrapolation for quantum error mitigation. + `` + """ + + def __init__( + self, + gates_to_fold: Set[Union[str, int] | None], + custom_gates: bool = True, + ) -> None: + super().__init__() + self._set_custom_gates(custom_gates) + self._set_gates_to_fold(gates_to_fold) + + ################################################################################ + ## PROPERTIES + ################################################################################ + @property + def gates_to_fold(self) -> Set[Union[str, int]]: + """Gates_to_fold setter""" + return self._gates_to_fold + + def _set_gates_to_fold(self, gates_to_fold: Set[Union[str, int]]) -> None: + """Set gates_to_fold property""" + self._gates_to_fold = self._validate_gates_to_fold(gates_to_fold) + + @property + def custom_gates(self) -> bool: + """Custom_gates flag setter""" + return self._custom_gates + + def _set_custom_gates(self, custom_gates: bool) -> None: + """Set gates_to_fold property""" + self._custom_gates = bool(custom_gates) + + ################################################################################ + ## INTERFACE IMPLEMENTATION + ################################################################################ + def amplify_dag_noise( # pylint: disable=arguments-differ + self, dag: DAGCircuit, noise_factor: float + ) -> DAGCircuit: + noise_factor = self._validate_noise_factor(noise_factor) + if not self.gates_to_fold: + return dag + # TODO: find number of nodes + num_foldable_nodes = self._compute_foldable_nodes(dag, self.gates_to_fold) + folding_nums = self._compute_folding_nums(noise_factor, num_foldable_nodes) + num_foldings = self._compute_folding_mask(folding_nums, dag, self.gates_to_fold) + noisy_dag = dag.copy() + for node, num in zip(dag.topological_op_nodes(), num_foldings): + noisy_dag = self._apply_folded_operation_back(noisy_dag, node, num) + return noisy_dag + + ################################################################################ + # AUXILIARY + ################################################################################ + + def _compute_foldable_nodes(self, dag: DAGCircuit, gates_to_fold: Set[Union[str, int]]) -> int: + """Computes number of foldable gates from gates_to_fold supplied by user + + Args: + dag: The original dag circuit without foldings. + gates_to_fold: Set of gates to fold supplied by user + + Returns: + int: Number of effective gates to fold + """ + num_foldable_nodes = 0 + for node in dag.topological_op_nodes(): + if node.op.num_qubits in gates_to_fold: + num_foldable_nodes += 1 + if node.name in gates_to_fold: + num_foldable_nodes += 1 + return num_foldable_nodes + + ################################################################################ + # VALIDATION + ################################################################################ + def _validate_gates_to_fold(self, gates_to_fold: Set[Union[str, int]]) -> set[int | str]: + """Validates if gates_to_fold is valid. + + Args: + gates_to_fold: Original gates_to_fold input. + """ + if gates_to_fold is None: + return set() + + if isinstance(gates_to_fold, (int, str)): + gates_to_fold = {gates_to_fold} + VALID_GATES = set( # pylint: disable=invalid-name + standard_gates.get_standard_gate_name_mapping().keys() + ) + if self.custom_gates: + pass + # TODO: Append valid custom_gate_names to VALID_GATES + + for value in gates_to_fold: + bad_int = isinstance(value, int) and value <= 0 + bad_str = isinstance(value, str) and value not in VALID_GATES + if bad_int or bad_str: + raise ValueError(f"{value!r} not a valid gate to fold option.") + return set(gates_to_fold)