def taper(self, operator: PauliSumOp) -> OperatorBase:
        """
        Taper an operator based on the z2_symmetries info and sector defined by `tapering_values`.
        The `tapering_values` will be stored into the resulted operator for a record.

        Args:
            operator: the to-be-tapered operator.

        Returns:
            If tapering_values is None: [:class`PauliSumOp`]; otherwise, :class:`PauliSumOp`
        Raises:
            OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty
        """
        if not self._symmetries or not self._sq_paulis or not self._sq_list:
            raise OpflowError(
                "Z2 symmetries, single qubit pauli and single qubit list cannot be empty."
            )

        # If the operator is zero then we can skip the following. We still need to taper the
        # operator to reduce its size i.e. the number of qubits so for example 0*"IIII" could
        # taper to 0*"II" when symmetries remove two qubits.
        if not operator.is_zero():
            for clifford in self.cliffords:
                operator = cast(PauliSumOp, clifford @ operator @ clifford)

        if self._tapering_values is None:
            tapered_ops_list = [
                self._taper(operator, list(coeff))
                for coeff in itertools.product([1, -1], repeat=len(self._sq_list))
            ]
            tapered_ops: OperatorBase = ListOp(tapered_ops_list)
        else:
            tapered_ops = self._taper(operator, self._tapering_values)

        return tapered_ops
예제 #2
0
    def taper(self, operator: PauliSumOp) -> OperatorBase:
        """
        Taper an operator based on the z2_symmetries info and sector defined by `tapering_values`.
        The `tapering_values` will be stored into the resulted operator for a record.

        Args:
            operator: the to-be-tapered operator.

        Returns:
            If tapering_values is None: [:class`PauliSumOp`]; otherwise, :class:`PauliSumOp`
        Raises:
            OpflowError: Z2 symmetries, single qubit pauli and single qubit list cannot be empty
        """
        if not self._symmetries or not self._sq_paulis or not self._sq_list:
            raise OpflowError(
                "Z2 symmetries, single qubit pauli and " "single qubit list cannot be empty."
            )

        if operator.is_zero():
            logger.warning("The operator is empty, return the empty operator directly.")
            return operator

        for clifford in self.cliffords:
            operator = cast(PauliSumOp, clifford @ operator @ clifford)

        if self._tapering_values is None:
            tapered_ops_list = [
                self._taper(operator, list(coeff))
                for coeff in itertools.product([1, -1], repeat=len(self._sq_list))
            ]
            tapered_ops: OperatorBase = ListOp(tapered_ops_list)
        else:
            tapered_ops = self._taper(operator, self._tapering_values)

        return tapered_ops
예제 #3
0
    def group_subops(cls, list_op: Union[ListOp, PauliSumOp]) -> ListOp:
        """Given a ListOp, attempt to group into Abelian ListOps of the same type.

        Args:
            list_op: The Operator to group into Abelian groups

        Returns:
            The grouped Operator.

        Raises:
            OpflowError: If any of list_op's sub-ops is not ``PauliOp``.
        """
        if isinstance(list_op, ListOp):
            for op in list_op.oplist:
                if not isinstance(op, PauliOp):
                    raise OpflowError(
                        "Cannot determine Abelian groups if any Operator in list_op is not "
                        f"`PauliOp`. E.g., {op} ({type(op)})")

        edges = cls._anti_commutation_graph(list_op)
        nodes = range(len(list_op))

        graph = rx.PyGraph()
        graph.add_nodes_from(nodes)
        graph.add_edges_from_no_data(edges)
        # Keys in coloring_dict are nodes, values are colors
        coloring_dict = rx.graph_greedy_color(graph)
        groups = defaultdict(list)
        for idx, color in coloring_dict.items():
            groups[color].append(idx)

        if isinstance(list_op, PauliSumOp):
            primitive = list_op.primitive
            return SummedOp(
                [
                    PauliSumOp(primitive[group], grouping_type="TPB")
                    for group in groups.values()
                ],
                coeff=list_op.coeff,
            )

        group_ops: List[ListOp] = [
            list_op.__class__([list_op[idx] for idx in group], abelian=True)
            for group in groups.values()
        ]
        if len(group_ops) == 1:
            return group_ops[0].mul(list_op.coeff)
        return list_op.__class__(group_ops, coeff=list_op.coeff)
예제 #4
0
    def convert(self, operator: OperatorBase) -> OperatorBase:
        """
        Converts the Operator to tapered one by Z2 symmetries.

        Args:
            operator: the operator
        Returns:
            A new operator whose qubit number is reduced by 2.
        """
        if not isinstance(operator, PauliSumOp):
            return operator

        operator = cast(PauliSumOp, operator)

        if operator.is_zero():
            logger.info("Operator is empty, can not do two qubit reduction. "
                        "Return the empty operator back.")
            return PauliSumOp.from_list([("I" * (operator.num_qubits - 2), 0)])

        num_qubits = operator.num_qubits
        last_idx = num_qubits - 1
        mid_idx = num_qubits // 2 - 1
        sq_list = [mid_idx, last_idx]

        # build symmetries, sq_paulis:
        symmetries, sq_paulis = [], []
        for idx in sq_list:
            pauli_str = ["I"] * num_qubits

            pauli_str[idx] = "Z"
            z_sym = Pauli("".join(pauli_str)[::-1])
            symmetries.append(z_sym)

            pauli_str[idx] = "X"
            sq_pauli = Pauli("".join(pauli_str)[::-1])
            sq_paulis.append(sq_pauli)

        z2_symmetries = Z2Symmetries(symmetries, sq_paulis, sq_list,
                                     self._tapering_values)
        return z2_symmetries.taper(operator)
예제 #5
0
    def find_Z2_symmetries(cls, operator: PauliSumOp) -> "Z2Symmetries":
        """
        Finds Z2 Pauli-type symmetries of an Operator.

        Returns:
            a z2_symmetries object contains symmetries, single-qubit X, single-qubit list.
        """
        pauli_symmetries = []
        sq_paulis = []
        sq_list = []

        stacked_paulis = []

        if operator.is_zero():
            logger.info("Operator is empty.")
            return cls([], [], [], None)

        for pauli in operator:
            stacked_paulis.append(
                np.concatenate(
                    (pauli.primitive.table.X[0], pauli.primitive.table.Z[0]), axis=0
                ).astype(int)
            )

        stacked_matrix = np.array(np.stack(stacked_paulis))
        symmetries = _kernel_F2(stacked_matrix)

        if not symmetries:
            logger.info("No symmetry is found.")
            return cls([], [], [], None)

        stacked_symmetries = np.stack(symmetries)
        symm_shape = stacked_symmetries.shape

        for row in range(symm_shape[0]):

            pauli_symmetries.append(
                Pauli(
                    (
                        stacked_symmetries[row, : symm_shape[1] // 2],
                        stacked_symmetries[row, symm_shape[1] // 2:],
                    )
                )
            )

            stacked_symm_del = np.delete(stacked_symmetries, row, axis=0)
            for col in range(symm_shape[1] // 2):
                # case symmetries other than one at (row) have Z or I on col qubit
                Z_or_I = True
                for symm_idx in range(symm_shape[0] - 1):
                    if not (
                        stacked_symm_del[symm_idx, col] == 0
                        and stacked_symm_del[symm_idx, col + symm_shape[1] // 2] in (0, 1)
                    ):
                        Z_or_I = False
                if Z_or_I:
                    if (
                        stacked_symmetries[row, col] == 1
                        and stacked_symmetries[row, col + symm_shape[1] // 2] == 0
                    ) or (
                        stacked_symmetries[row, col] == 1
                        and stacked_symmetries[row, col + symm_shape[1] // 2] == 1
                    ):
                        sq_paulis.append(
                            Pauli((np.zeros(symm_shape[1] // 2), np.zeros(symm_shape[1] // 2)))
                        )
                        sq_paulis[row].z[col] = False
                        sq_paulis[row].x[col] = True
                        sq_list.append(col)
                        break

                # case symmetries other than one at (row) have X or I on col qubit
                X_or_I = True
                for symm_idx in range(symm_shape[0] - 1):
                    if not (
                        stacked_symm_del[symm_idx, col] in (0, 1)
                        and stacked_symm_del[symm_idx, col + symm_shape[1] // 2] == 0
                    ):
                        X_or_I = False
                if X_or_I:
                    if (
                        stacked_symmetries[row, col] == 0
                        and stacked_symmetries[row, col + symm_shape[1] // 2] == 1
                    ) or (
                        stacked_symmetries[row, col] == 1
                        and stacked_symmetries[row, col + symm_shape[1] // 2] == 1
                    ):
                        sq_paulis.append(
                            Pauli(np.zeros(symm_shape[1] // 2), np.zeros(symm_shape[1] // 2))
                        )
                        sq_paulis[row].z[col] = True
                        sq_paulis[row].x[col] = False
                        sq_list.append(col)
                        break

                # case symmetries other than one at (row)  have Y or I on col qubit
                Y_or_I = True
                for symm_idx in range(symm_shape[0] - 1):
                    if not (
                        (
                            stacked_symm_del[symm_idx, col] == 1
                            and stacked_symm_del[symm_idx, col + symm_shape[1] // 2] == 1
                        )
                        or (
                            stacked_symm_del[symm_idx, col] == 0
                            and stacked_symm_del[symm_idx, col + symm_shape[1] // 2] == 0
                        )
                    ):
                        Y_or_I = False
                if Y_or_I:
                    if (
                        stacked_symmetries[row, col] == 0
                        and stacked_symmetries[row, col + symm_shape[1] // 2] == 1
                    ) or (
                        stacked_symmetries[row, col] == 1
                        and stacked_symmetries[row, col + symm_shape[1] // 2] == 0
                    ):
                        sq_paulis.append(
                            Pauli(np.zeros(symm_shape[1] // 2), np.zeros(symm_shape[1] // 2))
                        )
                        sq_paulis[row].z[col] = True
                        sq_paulis[row].x[col] = True
                        sq_list.append(col)
                        break

        return cls(pauli_symmetries, sq_paulis, sq_list, None)
 def assign_parameters(self, param_dict: dict) -> OperatorBase:
     pauli_sum = PauliSumOp(self.primitive, self.coeff)
     return pauli_sum.assign_parameters(param_dict)
예제 #7
0
    def convert(self, operator: OperatorBase) -> OperatorBase:
        r"""
        Given a ``PauliOp``, or an Operator containing ``PauliOps`` if ``_traverse`` is True,
        converts each Pauli into the basis specified by self._destination and a
        basis-change-circuit, calls ``replacement_fn`` with these two Operators, and replaces
        the ``PauliOps`` with the output of ``replacement_fn``. For example, for the built-in
        ``operator_replacement_fn`` below, each PauliOp p will be replaced by the composition
        of the basis-change Clifford ``CircuitOp`` c with the destination PauliOp d and c†,
        such that p = c·d·c†, up to global phase.

        Args:
            operator: The Operator to convert.

        Returns:
            The converted Operator.

        """
        if (isinstance(operator, OperatorStateFn)
                and isinstance(operator.primitive, PauliSumOp)
                and operator.primitive.grouping_type == "TPB"):
            primitive = operator.primitive.primitive.copy()
            origin_x = reduce(np.logical_or, primitive.table.X)
            origin_z = reduce(np.logical_or, primitive.table.Z)
            origin_pauli = Pauli((origin_z, origin_x))
            cob_instr_op, _ = self.get_cob_circuit(origin_pauli)
            primitive.table.Z = np.logical_or(primitive.table.X,
                                              primitive.table.Z)
            primitive.table.X = False
            dest_pauli_sum_op = PauliSumOp(primitive,
                                           coeff=operator.coeff,
                                           grouping_type="TPB")
            return self._replacement_fn(cob_instr_op, dest_pauli_sum_op)

        if (isinstance(operator, OperatorStateFn)
                and isinstance(operator.primitive, SummedOp) and all(
                    isinstance(op, PauliSumOp) and op.grouping_type == "TPB"
                    for op in operator.primitive.oplist)):
            sf_list: List[OperatorBase] = [
                StateFn(op, is_measurement=operator.is_measurement)
                for op in operator.primitive.oplist
            ]
            listop_of_statefns = SummedOp(oplist=sf_list, coeff=operator.coeff)
            return listop_of_statefns.traverse(self.convert)

        if isinstance(operator, OperatorStateFn) and isinstance(
                operator.primitive, PauliSumOp):
            operator = OperatorStateFn(
                operator.primitive.to_pauli_op(),
                coeff=operator.coeff,
                is_measurement=operator.is_measurement,
            )

        if isinstance(operator, PauliSumOp):
            operator = operator.to_pauli_op()

        if isinstance(operator, (Pauli, PauliOp)):
            cob_instr_op, dest_pauli_op = self.get_cob_circuit(operator)
            return self._replacement_fn(cob_instr_op, dest_pauli_op)
        if isinstance(operator,
                      StateFn) and "Pauli" in operator.primitive_strings():
            # If the StateFn/Meas only contains a Pauli, use it directly.
            if isinstance(operator.primitive, PauliOp):
                cob_instr_op, dest_pauli_op = self.get_cob_circuit(
                    operator.primitive)
                return self._replacement_fn(cob_instr_op,
                                            dest_pauli_op * operator.coeff)
            # TODO make a canonical "distribute" or graph swap as method in ListOp?
            elif operator.primitive.distributive:
                if operator.primitive.abelian:
                    origin_pauli = self.get_tpb_pauli(operator.primitive)
                    cob_instr_op, _ = self.get_cob_circuit(origin_pauli)
                    diag_ops: List[OperatorBase] = [
                        self.get_diagonal_pauli_op(op)
                        for op in operator.primitive.oplist
                    ]
                    dest_pauli_op = operator.primitive.__class__(
                        diag_ops, coeff=operator.coeff, abelian=True)
                    return self._replacement_fn(cob_instr_op, dest_pauli_op)
                else:
                    sf_list = [
                        StateFn(op, is_measurement=operator.is_measurement)
                        for op in operator.primitive.oplist
                    ]
                    listop_of_statefns = operator.primitive.__class__(
                        oplist=sf_list, coeff=operator.coeff)
                    return listop_of_statefns.traverse(self.convert)

        elif (isinstance(operator, ListOp) and self._traverse
              and "Pauli" in operator.primitive_strings()):
            # If ListOp is abelian we can find a single post-rotation circuit
            # for the whole set. For now,
            # assume operator can only be abelian if all elements are
            # Paulis (enforced in AbelianGrouper).
            if operator.abelian:
                origin_pauli = self.get_tpb_pauli(operator)
                cob_instr_op, _ = self.get_cob_circuit(origin_pauli)
                oplist = cast(List[PauliOp], operator.oplist)
                diag_ops = [self.get_diagonal_pauli_op(op) for op in oplist]
                dest_list_op = operator.__class__(diag_ops,
                                                  coeff=operator.coeff,
                                                  abelian=True)
                return self._replacement_fn(cob_instr_op, dest_list_op)
            else:
                return operator.traverse(self.convert)

        return operator
 def assign_parameters(self, param_dict: dict) -> OperatorBase:
     pauli_sum = PauliSumOp(self.primitive, self.coeff)  # pylint: disable=no-member
     return pauli_sum.assign_parameters(param_dict)