def tensor_product(self, other: "QuOperator") -> "QuOperator": """Tensor product with another operator. Given two operators `A` and `B`, produces a new operator `AB` representing `A` ⊗ `B`. The `out_edges` (`in_edges`) of `AB` is simply the concatenation of the `out_edges` (`in_edges`) of `A.copy()` with that of `B.copy()`: `new_out_edges = [*out_edges_A_copy, *out_edges_B_copy]` `new_in_edges = [*in_edges_A_copy, *in_edges_B_copy]` Args: other: The other operator (`B`). Returns: The result (`AB`). """ nodes_dict1, edges_dict1 = copy(self.nodes, False) nodes_dict2, edges_dict2 = copy(other.nodes, False) in_edges = ([edges_dict1[e] for e in self.in_edges] + [edges_dict2[e] for e in other.in_edges]) out_edges = ([edges_dict1[e] for e in self.out_edges] + [edges_dict2[e] for e in other.out_edges]) ref_nodes = ([n for _, n in nodes_dict1.items()] + [n for _, n in nodes_dict2.items()]) ignore_edges = ([edges_dict1[e] for e in self.ignore_edges] + [edges_dict2[e] for e in other.ignore_edges]) return quantum_constructor(out_edges, in_edges, ref_nodes, ignore_edges)
def __matmul__(self, other: "QuOperator") -> "QuOperator": """The action of this operator on another. Given `QuOperator`s `A` and `B`, produces a new `QuOperator` for `A @ B`, where `A @ B` means: "the action of A, as a linear operator, on B". Under the hood, this produces copies of the tensor networks defining `A` and `B` and then connects the copies by hooking up the `in_edges` of `A.copy()` to the `out_edges` of `B.copy()`. """ check_spaces(self.in_edges, other.out_edges) # Copy all nodes involved in the two operators. # We must do this separately for self and other, in case self and other # are defined via the same network components (e.g. if self === other). nodes_dict1, edges_dict1 = copy(self.nodes, False) nodes_dict2, edges_dict2 = copy(other.nodes, False) # connect edges to create network for the result for (e1, e2) in zip(self.in_edges, other.out_edges): _ = edges_dict1[e1] ^ edges_dict2[e2] in_edges = [edges_dict2[e] for e in other.in_edges] out_edges = [edges_dict1[e] for e in self.out_edges] ref_nodes = ([n for _, n in nodes_dict1.items()] + [n for _, n in nodes_dict2.items()]) ignore_edges = ([edges_dict1[e] for e in self.ignore_edges] + [edges_dict2[e] for e in other.ignore_edges]) return quantum_constructor(out_edges, in_edges, ref_nodes, ignore_edges)
def adjoint(self) -> "QuOperator": """The adjoint of the operator. This creates a new `QuOperator` with complex-conjugate copies of all tensors in the network and with the input and output edges switched. """ nodes_dict, edge_dict = copy(self.nodes, True) out_edges = [edge_dict[e] for e in self.in_edges] in_edges = [edge_dict[e] for e in self.out_edges] ref_nodes = [nodes_dict[n] for n in self.ref_nodes] ignore_edges = [edge_dict[e] for e in self.ignore_edges] return quantum_constructor(out_edges, in_edges, ref_nodes, ignore_edges)
def partial_trace( self, subsystems_to_trace_out: Collection[int]) -> "QuOperator": """The partial trace of the operator. Subsystems to trace out are supplied as indices, so that dangling edges are connected to eachother as: `out_edges[i] ^ in_edges[i] for i in subsystems_to_trace_out` This does not modify the original network. The original ordering of the remaining subsystems is maintained. Args: subsystems_to_trace_out: Indices of subsystems to trace out. Returns: A new QuOperator or QuScalar representing the result. """ out_edges_trace = [self.out_edges[i] for i in subsystems_to_trace_out] in_edges_trace = [self.in_edges[i] for i in subsystems_to_trace_out] check_spaces(in_edges_trace, out_edges_trace) nodes_dict, edge_dict = copy(self.nodes, False) for (e1, e2) in zip(out_edges_trace, in_edges_trace): edge_dict[e1] = edge_dict[e1] ^ edge_dict[e2] # get leftover edges in the original order out_edges_trace = set(out_edges_trace) in_edges_trace = set(in_edges_trace) out_edges = [ edge_dict[e] for e in self.out_edges if e not in out_edges_trace ] in_edges = [ edge_dict[e] for e in self.in_edges if e not in in_edges_trace ] ref_nodes = [n for _, n in nodes_dict.items()] ignore_edges = [edge_dict[e] for e in self.ignore_edges] return quantum_constructor(out_edges, in_edges, ref_nodes, ignore_edges)