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
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
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)
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)
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)
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)