def __init__( self, qubit_map: Dict['cirq.Qid', int], rsum2_cutoff: float, sum_prob_atol: float, grouping: Optional[Dict['cirq.Qid', int]] = None, initial_state: int = 0, ): """Creates and MPSState Args: qubit_map: A map from Qid to an integer that uniquely identifies it. rsum2_cutoff: We drop singular values so that the sum of the square of the dropped singular values divided by the sum of the square of all the singular values is less than rsum2_cutoff. This is related to the fidelity of the computation. If we have N 2D gates, then the estimated fidelity is (1 - rsum2_cutoff) ** N. sum_prob_atol: Because the computation is approximate, the sum of the probabilities is not 1.0. This parameter is the absolute deviation from 1.0 that is allowed. grouping: How to group qubits together, if None all are individual. initial_state: An integer representing the initial state. """ self.qubit_map = qubit_map self.grouping = qubit_map if grouping is None else grouping if self.grouping.keys() != self.qubit_map.keys(): raise ValueError('Grouping must cover exactly the qubits.') self.M = [] for _ in range(max(self.grouping.values()) + 1): self.M.append(qtn.Tensor()) # The order of the qubits matters, because the state |01> is different from |10>. Since # Quimb uses strings to name tensor indices, we want to be able to sort them too. If we are # working with, say, 123 qubits then we want qubit 3 to come before qubit 100, but then # we want write the string '003' which comes before '100' in lexicographic order. The code # below is just simple string formatting. max_num_digits = len('{}'.format(max(qubit_map.values()))) self.format_i = 'i_{{:0{}}}'.format(max_num_digits) self.format_mu = 'mu_{}_{}' # TODO(tonybruguier): Instead of relying on sortable indices could you keep a parallel # mapping of e.g. qubit to string-index and do all "logic" on the qubits themselves and # only translate to string-indices when calling a quimb API. # TODO(tonybruguier): Refactor out so that the code below can also be used by # circuit_to_tensors in cirq.contrib.quimb.state_vector. for qubit in reversed(list(qubit_map.keys())): d = qubit.dimension x = np.zeros(d) x[initial_state % d] = 1.0 i = qubit_map[qubit] n = self.grouping[qubit] self.M[n] @= qtn.Tensor(x, inds=(self.i_str(i), )) initial_state = initial_state // d self.rsum2_cutoff = rsum2_cutoff self.sum_prob_atol = sum_prob_atol self.num_svd_splits = 0
def __init__( self, qubit_map: Dict['cirq.Qid', int], prng: np.random.RandomState, simulation_options: MPSOptions = MPSOptions(), grouping: Optional[Dict['cirq.Qid', int]] = None, initial_state: int = 0, axes: Iterable[int] = None, log_of_measurement_results: Dict[str, Any] = None, ): """Creates and MPSState Args: qubit_map: A map from Qid to an integer that uniquely identifies it. prng: A random number generator, used to simulate measurements. simulation_options: Numerical options for the simulation. grouping: How to group qubits together, if None all are individual. initial_state: An integer representing the initial state. axes: The indices of axes corresponding to the qubits that the operation is supposed to act upon. log_of_measurement_results: A mutable object that measurements are being recorded into. """ super().__init__(prng, axes, log_of_measurement_results) self.qubit_map = qubit_map self.grouping = qubit_map if grouping is None else grouping if self.grouping.keys() != self.qubit_map.keys(): raise ValueError('Grouping must cover exactly the qubits.') self.M = [] for _ in range(max(self.grouping.values()) + 1): self.M.append(qtn.Tensor()) # The order of the qubits matters, because the state |01> is different from |10>. Since # Quimb uses strings to name tensor indices, we want to be able to sort them too. If we are # working with, say, 123 qubits then we want qubit 3 to come before qubit 100, but then # we want write the string '003' which comes before '100' in lexicographic order. The code # below is just simple string formatting. max_num_digits = len(f'{max(qubit_map.values())}') self.format_i = f'i_{{:0{max_num_digits}}}' self.format_mu = 'mu_{}_{}' # TODO(tonybruguier): Instead of relying on sortable indices could you keep a parallel # mapping of e.g. qubit to string-index and do all "logic" on the qubits themselves and # only translate to string-indices when calling a quimb API. # TODO(tonybruguier): Refactor out so that the code below can also be used by # circuit_to_tensors in cirq.contrib.quimb.state_vector. for qubit in reversed(list(qubit_map.keys())): d = qubit.dimension x = np.zeros(d) x[initial_state % d] = 1.0 i = qubit_map[qubit] n = self.grouping[qubit] self.M[n] @= qtn.Tensor(x, inds=(self.i_str(i), )) initial_state = initial_state // d self.simulation_options = simulation_options self.estimated_gate_error_list: List[float] = []
def to_quimb_tensor(g: BaseGraph) -> 'qtn.TensorNetwork': """Converts tensor network representing the given :func:`pyzx.graph.Graph`. Pretty printing: to_tensor(g).draw(color = ['V', 'H']) Args: g: graph to be converted.""" if qu is None: raise ImportError("quimb must be installed to use this function.") # copying a graph guarantees consecutive indices, which are needed for the tensor net g = g.copy() # only Z spiders are handled below to_gh(g) tensors = [] # Here we have phase tensors corresponding to Z-spiders with only one output and no input. for v in g.vertices(): if g.type(v) == VertexType.Z and g.phase(v) != 0: tensors.append( qtn.Tensor(data=[1, np.exp(1j * np.pi * g.phase(v))], inds=(f'{v}', ), tags=("V", ))) # Hadamard or Kronecker tensors, one for each edge of the diagram. for i, edge in enumerate(g.edges()): x, y = edge isHadamard = g.edge_type(edge) == EdgeType.HADAMARD t = qtn.Tensor(data=qu.hadamard() if isHadamard else np.array([1, 0, 0, 1]).reshape(2, 2), inds=(f'{x}', f'{y}'), tags=("H", ) if isHadamard else ("N", )) tensors.append(t) # TODO: This is not taking care of all the stuff that can be in g.scalar # In particular, it doesn't check g.scalar.phasenodes # TODO: This will give the wrong tensor when g.scalar.is_zero == True. # Grab the float factor and exponent from the scalar scalar_float = np.exp(1j * np.pi * g.scalar.phase) * g.scalar.floatfactor for node in g.scalar.phasenodes: # Each node is a Fraction scalar_float *= 1 + np.exp(1j * np.pi * node) scalar_exp = math.log10(math.sqrt(2)) * g.scalar.power2 # If the TN is empty, create a single 0-tensor with scalar factor, otherwise # multiply the scalar into one of the tensors. if len(tensors) == 0: tensors.append(qtn.Tensor(data=scalar_float, inds=(), tags=("S", ))) else: tensors[0].modify(data=tensors[0].data * scalar_float) network = qtn.TensorNetwork(tensors) # the exponent can be very large, so distribute it evenly through the TN network.exponent = scalar_exp network.distribute_exponent() return network
def circuit_to_tensors( circuit: cirq.Circuit, qubits: Optional[Sequence[cirq.Qid]] = None, initial_state: Union[int, None] = 0, ) -> Tuple[List[qtn.Tensor], Dict['cirq.Qid', int], None]: """Given a circuit, construct a tensor network representation. Indices are named "i{i}_q{x}" where i is a time index and x is a qubit index. Args: circuit: The circuit containing operations that implement the cirq.unitary() protocol. qubits: A list of qubits in the circuit. initial_state: Either `0` corresponding to the |0..0> state, in which case the tensor network will represent the final state vector; or `None` in which case the starting indices will be left open and the tensor network will represent the circuit unitary. Returns: tensors: A list of quimb Tensor objects qubit_frontier: A mapping from qubit to time index at the end of the circuit. This can be used to deduce the names of the free tensor indices. positions: Currently None. May be changed in the future to return a suitable mapping for tn.graph()'s `fix` argument. Currently, `fix=None` will draw the resulting tensor network using a spring layout. """ if qubits is None: qubits = sorted(circuit.all_qubits()) # coverage: ignore qubit_frontier = {q: 0 for q in qubits} positions = None tensors: List[qtn.Tensor] = [] if initial_state == 0: for q in qubits: tensors += [qtn.Tensor(data=quimb.up().squeeze(), inds=(f'i0_q{q}',), tags={'Q0'})] elif initial_state is None: # no input tensors, return a network representing the unitary pass else: raise ValueError("Right now, only |0> or `None` " "initial states are supported.") for moment in circuit.moments: for op in moment.operations: assert op.gate._has_unitary_() start_inds = [f'i{qubit_frontier[q]}_q{q}' for q in op.qubits] for q in op.qubits: qubit_frontier[q] += 1 end_inds = [f'i{qubit_frontier[q]}_q{q}' for q in op.qubits] U = cirq.unitary(op).reshape((2,) * 2 * len(op.qubits)) t = qtn.Tensor(data=U, inds=end_inds + start_inds, tags={f'Q{len(op.qubits)}'}) tensors.append(t) return tensors, qubit_frontier, positions
def create( cls, *, qid_shape: Tuple[int, ...], grouping: Dict[int, int], initial_state: int = 0, simulation_options: MPSOptions = MPSOptions(), ): """Creates an MPSQuantumState Args: qid_shape: Dimensions of the qubits represented. grouping: How to group qubits together, if None all are individual. initial_state: The initial computational basis state. simulation_options: Numerical options for the simulation. Raises: ValueError: If the grouping does not cover the qubits. """ M = [] for _ in range(max(grouping.values()) + 1): M.append(qtn.Tensor()) # The order of the qubits matters, because the state |01> is different from |10>. Since # Quimb uses strings to name tensor indices, we want to be able to sort them too. If we are # working with, say, 123 qubits then we want qubit 3 to come before qubit 100, but then # we want write the string '003' which comes before '100' in lexicographic order. The code # below is just simple string formatting. max_num_digits = len(f'{max(grouping.values())}') format_i = f'i_{{:0{max_num_digits}}}' # TODO(tonybruguier): Instead of relying on sortable indices could you keep a parallel # mapping of e.g. qubit to string-index and do all "logic" on the qubits themselves and # only translate to string-indices when calling a quimb API. # TODO(tonybruguier): Refactor out so that the code below can also be used by # circuit_to_tensors in cirq.contrib.quimb.state_vector. for axis in reversed(range(len(qid_shape))): d = qid_shape[axis] x = np.zeros(d) x[initial_state % d] = 1.0 n = grouping[axis] M[n] @= qtn.Tensor(x, inds=(format_i.format(axis), )) initial_state = initial_state // d return _MPSHandler( qid_shape=qid_shape, grouping=grouping, M=M, format_i=format_i, estimated_gate_error_list=[], simulation_options=simulation_options, )
def test_id_tensor(self): g = Graph() x = g.add_vertex(VertexType.BOUNDARY) y = g.add_vertex(VertexType.BOUNDARY) g.add_edge(g.edge(x, y), edgetype = EdgeType.SIMPLE) tn = to_quimb_tensor(g) self.assertTrue((tn & qtn.Tensor(data = [0, 1], inds = ("0",)) & qtn.Tensor(data = [0, 1], inds = ("1",))) .contract(output_inds = ()) == 1) self.assertTrue((tn & qtn.Tensor(data = [1, 0], inds = ("0",)) & qtn.Tensor(data = [1, 0], inds = ("1",))) .contract(output_inds = ()) == 1)
def test_hadamard_tensor(self): g = Graph() x = g.add_vertex(VertexType.BOUNDARY) y = g.add_vertex(VertexType.BOUNDARY) g.add_edge(g.edge(x, y), edgetype = EdgeType.HADAMARD) tn = to_quimb_tensor(g) self.assertTrue(abs((tn & qtn.Tensor(data = [1, 0], inds = ("0",)) & qtn.Tensor(data = [1 / np.sqrt(2), 1 / np.sqrt(2)], inds = ("1",))) .contract(output_inds = ()) - 1) < 1e-9) self.assertTrue(abs((tn & qtn.Tensor(data = [0, 1], inds = ("0",)) & qtn.Tensor(data = [1 / np.sqrt(2), -1 / np.sqrt(2)], inds = ("1",))) .contract(output_inds = ()) - 1) < 1e-9)
def tensor_expectation_value(circuit: cirq.Circuit, pauli_string: cirq.PauliString, max_ram_gb=16, tol=1e-6) -> float: """Compute an expectation value for an operator and a circuit via tensor contraction. This will give up if it looks like the computation will take too much RAM. """ circuit_sand = circuit_for_expectation_value( circuit, pauli_string / pauli_string.coefficient) qubits = sorted(circuit_sand.all_qubits()) tensors, qubit_frontier, _ = circuit_to_tensors(circuit=circuit_sand, qubits=qubits) end_bras = [ qtn.Tensor(data=quimb.up().squeeze(), inds=(f'i{qubit_frontier[q]}_q{q}', ), tags={'Q0', 'bra0'}) for q in qubits ] tn = qtn.TensorNetwork(tensors + end_bras) tn.rank_simplify(inplace=True) path_info = tn.contract(get='path-info') ram_gb = path_info.largest_intermediate * 128 / 8 / 1024 / 1024 / 1024 if ram_gb > max_ram_gb: raise MemoryError("We estimate that this contraction " "will take too much RAM! {} GB".format(ram_gb)) e_val = tn.contract(inplace=True) assert e_val.imag < tol assert pauli_string.coefficient.imag < tol return e_val.real * pauli_string.coefficient
def main_test(): psi = beeky.QubitEncodeVector.rand(3, 3) X, Y, Z = (qu.pauli(i) for i in 'xyz') where = (3, 4, 9) #which qubits to act on numsites = len(where) dp = 2 #phys ind dimension gate = X & X & X ## take over from here ## g_xx = qtn.tensor_1d.maybe_factor_gate_into_tensor( gate, dp, numsites, where) #shape (2,2,2,2) or (2,2,2,2,2,2) site_inds = [psi.phys_ind_id.format(q) for q in where] bond_inds = [qtn.rand_uuid() for _ in range(numsites)] reindex_map = dict(zip(site_inds, bond_inds)) TG = qtn.Tensor(g_xx, inds=site_inds + bond_inds, left_inds=bond_inds, tags=['GATE']) original_ts = [psi[q] for q in where] bonds_along = [ next(iter(qtn.bonds(t1, t2))) for t1, t2 in qu.utils.pairwise(original_ts) ] triangle_gate_absorb(TG=TG, reindex_map=reindex_map, vertex_tensors=(psi[where[0]], psi[where[1]]), face_tensor=psi[where[2]], phys_inds=site_inds)
def TNReducer(TN, sobits, nq, nr, shave): counter = 0 for rep in range(0, nr): for b in range(0, nq - 1): if counter < shave: bit = sobits[rep * (nq - 1) + b] indb = ('sf{:d}r{:d}'.format(b, rep), ) tagb = ('SF{:d}r{:d}BIT{:d}'.format(b, rep, bit)) if bit == 0: TN = TN & qtn.Tensor(np.array([1, 0]), indb, tagb) elif bit == 1: TN = TN & qtn.Tensor(np.array([0, 1]), indb, tagb) else: print('BAD') counter = counter + 1 TNC = TN.contract(all, optimize='auto-hq') out = TNC.data return out
def __init__(self, d=2, L_y=4, L_x=10, dist_type='uniform', data_type='float64', seed_0=10): self.L_x = L_x self.d = d self.L_y = L_y self.type = data_type self.dist = dist_type peps = qtn.PEPS.rand(Lx=L_x, Ly=L_y, bond_dim=D, phys_dim=d, seed=4) rotate_ten_list = [] for j in range(L_y - 1): for i in range(L_x): A = qtn.Tensor(qu.rand(2, seed=seed_0 + i + j, dist='uniform', dtype=data_type).reshape(2), inds={f"k{i},{j}"}, tags={}) rotate_ten_list.append(A) R_l = qtn.TensorNetwork(rotate_ten_list) self.tn = peps & R_l for j in range(L_y - 1): for i in range(L_x): self.tn.contract_ind(f"k{i},{j}", optimize='auto-hq') #TN_l.graph(color=peps.site_tags, show_tags=True, figsize=(10, 10)) for j in range(L_y): for i in range(L_x): t = list((self.tn[{f'I{i},{j}', f'ROW{i}', f'COL{j}'}].inds)) for m in range(len(t)): if t[m] == f'k{i},{j}': t[m] = f'k{i}' self.tn[{f'I{i},{j}', f'ROW{i}', f'COL{j}'}].modify(inds=t) # for j in range(L_y): # for i in range(L_x): # dim_tuple=self.tn[{ f'I{i},{j}', f'ROW{i}', f'COL{j}' }].shape # lis_a=list(dim_tuple) # dim=1 # for i_val in range(len(lis_a)): # dim*=lis_a[i_val] # rand_tn=qu.rand(dim, dist='uniform', seed=seed_0, dtype=data_type).reshape(*dim_tuple) # rand_tn=rand_tn*LA.norm(rand_tn)**(-1.0) # self.tn[{f'COL{j}', f'I{i},{j}', f'ROW{i}' }].modify(data=rand_tn) self.tn.balance_bonds_() self.tn.equalize_norms_(2.0)
def test_xor_tensor(self): g = Graph() x = g.add_vertex(VertexType.BOUNDARY) y = g.add_vertex(VertexType.BOUNDARY) v = g.add_vertex(VertexType.Z) z = g.add_vertex(VertexType.BOUNDARY) g.add_edge(g.edge(x, v), edgetype = EdgeType.HADAMARD) g.add_edge(g.edge(y, v), edgetype = EdgeType.HADAMARD) g.add_edge(g.edge(v, z), edgetype = EdgeType.HADAMARD) tn = to_quimb_tensor(g) for x in range(2): for y in range(2): for z in range(2): self.assertTrue(abs((tn & qtn.Tensor(data = [1 - x, x], inds = ("0",)) & qtn.Tensor(data = [1 - y, y], inds = ("1",)) & qtn.Tensor(data = [1 - z, z], inds = ("3",))).contract(output_inds = ()) - ((x ^ y) == z) / np.sqrt(2)) < 1e-9)
def perform_measurement(self, qubits: Sequence['cirq.Qid'], prng: np.random.RandomState, collapse_state_vector=True) -> List[int]: """Performs a measurement over one or more qubits. Args: qubits: The sequence of qids to measure, in that order. prng: A random number generator, used to simulate measurements. collapse_state_vector: A Boolean specifying whether we should mutate the state after the measurement. Raises: ValueError: If the probabilities for the measurements differ too much from one for the tolerance specified in simulation options. """ results: List[int] = [] if collapse_state_vector: state = self else: state = self.copy() for qubit in qubits: n = state.qubit_map[qubit] # Trace out other qubits M = state.partial_trace(keep_qubits={qubit}) probs = np.diag(M).real sum_probs = sum(probs) # Because the computation is approximate, the probabilities do not # necessarily add up to 1.0, and thus we re-normalize them. if abs(sum_probs - 1.0) > self.simulation_options.sum_prob_atol: raise ValueError( f'Sum of probabilities exceeds tolerance: {sum_probs}') norm_probs = [x / sum_probs for x in probs] d = qubit.dimension result: int = int(prng.choice(d, p=norm_probs)) collapser = np.zeros((d, d)) collapser[result][result] = 1.0 / math.sqrt(probs[result]) old_n = state.i_str(n) new_n = 'new_' + old_n collapser = qtn.Tensor(collapser, inds=(new_n, old_n)) state.M[n] = (collapser @ state.M[n]).reindex({new_n: old_n}) results.append(result) return results
def test_phases_tensor(self): # This diagram represents a 1-input 1-output Z-spider of phase pi/2, # but written using two Z-spiders of phases pi/6 and pi/3 that are # connected by a simple edge. g = Graph() x = g.add_vertex(VertexType.BOUNDARY) v = g.add_vertex(VertexType.Z, phase = 1. / 6.) w = g.add_vertex(VertexType.Z, phase = 1. / 3.) y = g.add_vertex(VertexType.BOUNDARY) g.add_edge(g.edge(x, v), edgetype = EdgeType.SIMPLE) g.add_edge(g.edge(v, w), edgetype = EdgeType.SIMPLE) g.add_edge(g.edge(w, y), edgetype = EdgeType.SIMPLE) tn = to_quimb_tensor(g) self.assertTrue(abs((tn & qtn.Tensor(data = [1, 0], inds = ("0",)) & qtn.Tensor(data = [1, 0], inds = ("3",))) .contract(output_inds = ()) - 1) < 1e-9) self.assertTrue(abs((tn & qtn.Tensor(data = [0, 1], inds = ("0",)) & qtn.Tensor(data = [0, 1j], inds = ("3",))) .contract(output_inds = ()) + 1) < 1e-9)
def absorb_three_body_gate(self, G, coos, gate_tags=('GATE', ), restore_dummies=True, inplace=False, **compress_opts): '''Converts the raw gate ``G`` into a tensor and passes it to ``self.absorb_three_body_tensor``. G: raw qarray The gate to apply coos: sequence of tuple[int] The 3 coos to act on, e.g. ((0,0),(0,2),(1,1)) restore_dummies: bool, optional Whether to "restore" dummy identities and keep square lattice structure or "triangles" in the lattice. ''' gate_tags = qtn.tensor_2d.tags_to_oset(gate_tags) # assuming physical dimension = 2 G = qtn.tensor_1d.maybe_factor_gate_into_tensor(G, dp=2, ng=3, where=coos) # new physical indices "k{x},{y}" phys_inds = [self._site_ind_id.format(*c) for c in coos] # old physical indices joined to new gate bond_inds = [qtn.rand_uuid() for _ in range(3)] # replace physical inds with gate bonds reindex_map = dict(zip(phys_inds, bond_inds)) TG = qtn.Tensor(G, inds=phys_inds + bond_inds, left_inds=bond_inds, tags=gate_tags) return self.absorb_three_body_tensor(TG, coos, reindex_map, phys_inds, gate_tags, restore_dummies=restore_dummies, inplace=inplace, **compress_opts)
def _measure(self, axes: Sequence[int], prng: np.random.RandomState, collapse_state_vector=True) -> List[int]: results: List[int] = [] if collapse_state_vector: state = self else: state = self.copy() for axis in axes: # Trace out other qubits M = state.partial_trace(keep_axes={axis}) probs = np.diag(M).real sum_probs = sum(probs) # Because the computation is approximate, the probabilities do not # necessarily add up to 1.0, and thus we re-normalize them. if abs(sum_probs - 1.0) > self._simulation_options.sum_prob_atol: raise ValueError( f'Sum of probabilities exceeds tolerance: {sum_probs}') norm_probs = [x / sum_probs for x in probs] d = self._qid_shape[axis] result: int = int(prng.choice(d, p=norm_probs)) collapser = np.zeros((d, d)) collapser[result][result] = 1.0 / math.sqrt(probs[result]) old_n = state.i_str(axis) new_n = 'new_' + old_n collapser = qtn.Tensor(collapser, inds=(new_n, old_n)) state._M[axis] = (collapser @ state._M[axis]).reindex( {new_n: old_n}) results.append(result) return results
def _get_node(t, gate): # Get matrix U = np.reshape(gate.matrix().astype(complex_type), [2] * (2 * len(gate.qubits))) # Get indexes inds = [f'{leaves_prefix}_{qubits_map[q]}_{t}' for q in gate.qubits] + [ f'{leaves_prefix}_{qubits_map[q]}_{last_tag[q]}' for q in gate.qubits ] # Update last_tag for q in gate.qubits: last_tag[q] = t # Return node return tn.Tensor( U.astype(complex_type), inds=inds, tags=[f'{leaves_prefix}_{qubits_map[q]}' for q in gate.qubits] + [f'gate-idx_{t}'])
def tensor_expectation_value(circuit: cirq.Circuit, pauli_string: cirq.PauliString, max_ram_gb=16, tol=1e-6) -> float: """Compute an expectation value for an operator and a circuit via tensor contraction. This will give up if it looks like the computation will take too much RAM. """ circuit_sand = circuit_for_expectation_value( circuit, pauli_string / pauli_string.coefficient) qubits = sorted(circuit_sand.all_qubits()) tensors, qubit_frontier, _ = circuit_to_tensors(circuit=circuit_sand, qubits=qubits) end_bras = [ qtn.Tensor(data=quimb.up().squeeze(), inds=(f'i{qubit_frontier[q]}_q{q}', ), tags={'Q0', 'bra0'}) for q in qubits ] tn = qtn.TensorNetwork(tensors + end_bras) if QUIMB_VERSION[0] < (1, 3): # coverage: ignore warnings.warn(f'quimb version {QUIMB_VERSION[1]} detected. Please use ' f'quimb>=1.3 for optimal performance in ' '`tensor_expectation_value`. ' 'See https://github.com/quantumlib/Cirq/issues/3263') else: tn.rank_simplify(inplace=True) path_info = tn.contract(get='path-info') ram_gb = path_info.largest_intermediate * 128 / 8 / 1024 / 1024 / 1024 if ram_gb > max_ram_gb: raise MemoryError( f"We estimate that this contraction will take too much RAM! {ram_gb} GB" ) e_val = tn.contract(inplace=True) assert e_val.imag < tol assert pauli_string.coefficient.imag < tol return e_val.real * pauli_string.coefficient
def gate( self, G, coos, contract='auto_split', tags=('GATE', ), inplace=False, info=None, **compress_opts, ): ''' contract: {False, 'reduce_split', 'triangle_absorb', 'reduce_split_lr'} -False: leave gate uncontracted at sites [For 2-body ops:] -reduce_split: Absorb dummy, apply gate with `qtn.tensor_2d.reduce_split`, then reinsert dummy. (NOTE: this one seems very slow) -reduce_split_lr: leave dummy as-is, treat gate as a LR interaction. The final bonds are much smaller this way! [For 3-body ops:] -triangle_absorb: use `three_body_op.triangle_gate_absorb` to apply the 3-body gate. Assumes `coos` is ordered like ~ (vertex, vertex, face)! [For any n-body:] -auto_split: will automatically choose depending on n. n=1 -> contract = True n=2 -> contract = 'reduce_split_lr' n=3 -> contract = 'triangle_absorb' ''' check_opt("contract", contract, (False, True, 'reduce_split', 'triangle_absorb', 'reduce_split_lr', 'auto_split')) psi = self if inplace else self.copy() if is_lone_coo(coos): coos = (coos, ) else: coos = tuple(coos) numsites = len(coos) #num qubits acted on if contract == 'auto_split': contract = { 1: True, 2: 'reduce_split_lr', 3: 'triangle_absorb' }[numsites] #inds like 'k{x},{y}' site_inds = [self._site_ind_id.format(*c) for c in coos] # physical dimension, d=2 for qubits dp = self.ind_size(site_inds[0]) gate_tags = tags_to_oset(tags) G = qtn.tensor_1d.maybe_factor_gate_into_tensor(G, dp, numsites, coos) #old physical indices joined to new gate bond_inds = [qtn.rand_uuid() for _ in range(numsites)] reindex_map = dict(zip(site_inds, bond_inds)) TG = qtn.Tensor(G, inds=site_inds + bond_inds, left_inds=bond_inds, tags=gate_tags) if contract is False: #attach gates without contracting any bonds # # 'qA' 'qB' # │ │ <- site_inds # GGGGG # │╱ │╱ <- bond_inds # ──●───●── # ╱ ╱ # psi.reindex_(reindex_map) psi |= TG return psi elif (contract is True) or (numsites == 1): # # │╱ │╱ # ──GGGGG── # ╱ ╱ # psi.reindex_(reindex_map) # get the sites that used to have the physical indices site_tids = psi._get_tids_from_inds(bond_inds, which='any') # pop the sites, contract, then re-add pts = [psi._pop_tensor(tid) for tid in site_tids] psi |= qtn.tensor_contract(*pts, TG) return psi elif contract == 'triangle_absorb' and numsites == 3: # absorbs 3-body gate while preserving lattice structure. psi.absorb_three_body_tensor_(TG=TG, coos=coos, reindex_map=reindex_map, phys_inds=site_inds, gate_tags=gate_tags, **compress_opts) return psi # NOTE: this one seems very inefficient for # "next-nearest" neighbor interactions. elif contract == 'reduce_split' and numsites == 2: # First absorb identity into a site, then # restore after gate has been applied. # # 1. Absorb identity step: # # │ │ Absorb │ │ # GGGGGGG ident. GGGGGGG # │╱ ╱ │╱ ==> │╱ │╱╱ # ──●──I──●── ──●─────●─ # a ╱ ╱ ╱ b ╱ ╱╱ # # 2. Gate 'reduce_split' step: # # │ │ │ │ # GGGGG GGG │ │ # │╱ │╱ ==> ╱│ │ ╱ ==> ╱│ │ ╱ │╱ │╱ # ──●───●── ──>─●─●─<── ──>─GGG─<── ==> ──G┄┄┄G── # ╱ ╱ ╱ ╱ ╱ ╱ ╱ ╱ # <QR> <LQ> <SVD> # # 3. Reinsert identity: # # │╱ │╱╱ │╱ ╱ │╱ # ──G┄┄┄┄┄G── ==> ──G┄┄I┄┄G── # ╱ ╱╱ ╱ ╱ ╱ # (x1, y1), (x2, y2) = coos mid_coo = (int((x1 + x2) / 2), int((y1 + y2) / 2)) dummy_coo_tag = psi.site_tag_id.format(*mid_coo) # keep track of dummy identity's tags and neighbors prev_dummy_info = { 'tags': psi[dummy_coo_tag].tags, 'neighbor_tags': tuple(t.tags for t in psi.select_neighbors(dummy_coo_tag)) } which_bond = int( psi.bond_size(coos[0], mid_coo) >= psi.bond_size( coos[1], mid_coo)) if which_bond == 0: # (vertex_0 ── identity) bond is larger vertex_tag = psi.site_tag_id.format(*coos[0]) else: # (vertex_1 -- identity) bond larger vertex_tag = psi.site_tag_id.format(*coos[1]) tids = psi._get_tids_from_tags(tags=(vertex_tag, dummy_coo_tag), which='any') # pop and reattach the (vertex & identity) tensor pts = [psi._pop_tensor(tid) for tid in tids] new_vertex = qtn.tensor_contract(*pts) # new_vertex.drop_tags(prev_dummy_info['tags'] - ) new_vertex.drop_tags(pts[1].tags - pts[0].tags) psi |= new_vertex # reattach [vertex & identity] # insert 2-body gate! qtn.tensor_2d.gate_string_reduce_split_( TG=TG, where=coos, string=coos, original_ts=[psi[c] for c in coos], bonds_along=(psi.bond(*coos), ), reindex_map=reindex_map, site_ix=site_inds, info=info, **compress_opts) # now restore the dummy identity between vertices vtensor = psi[ coos[which_bond]] # the vertex we absorbed dummy into ts_to_connect = set( psi[tags] for tags in prev_dummy_info['neighbor_tags']) - set([vtensor]) for T2 in ts_to_connect: # restore previous dummy bonds psi |= qubit_networks.insert_identity_between_tensors( T1=vtensor, T2=T2, add_tags='TEMP') # contract new dummies into a single identity psi ^= 'TEMP' for t in prev_dummy_info['tags']: psi['TEMP'].add_tag(t) # restore previous dummy tags psi.drop_tags('TEMP') return psi.fuse_multibonds_() elif contract == 'reduce_split_lr' and numsites == 2: # There will be a 'dummy' identity tensor between the # sites, so the 2-body operator will look "long-range" # # │ │ # GGGGGGG # │╱ ╱ │╱ │╱ ╱ │╱ # ──●──I──●── ==> ──G┄┄I┄┄G── # ╱ ╱ ╱ ╱ ╱ ╱ # (x1, y1), (x2, y2) = coos mid_coo = (int((x1 + x2) / 2), int((y1 + y2) / 2)) dummy_coo_tag = psi.site_tag_id.format(*mid_coo) string = (coos[0], mid_coo, coos[1]) original_ts = [psi[coo] for coo in string] bonds_along = [ next(iter(qtn.bonds(t1, t2))) for t1, t2 in qu.utils.pairwise(original_ts) ] qtn.tensor_2d.gate_string_reduce_split_(TG=TG, where=coos, string=string, original_ts=original_ts, bonds_along=bonds_along, reindex_map=reindex_map, site_ix=site_inds, info=info, **compress_opts) return psi
def CLTNRepHWQC(nq, nr, p): px = 0.004 kappa2 = 10**7 alpha2 = 8 pi1 = [1 - 10 * p, 10 * p] pi2 = [1 - 0.299 * (p**0.5), 0.299 * (p**0.5)] p3 = p * kappa2 * alpha2 * (350 * (10**(-9)) + (10 / (kappa2 * alpha2))) pi3 = [1 - p3, p3] pz1 = 0.845 * (p**0.5) pz2 = 0.133 * (p**0.5) pz1z2 = 0.133 * (p**0.5) pm = [1 - px, px] pp = [1 - 15 * p / 2, 15 * p / 2] perf = [1, 0] ptq = [1 - pz1 - pz2 - pz1z2, pz1, pz2, pz1z2] #print(psq) #print(ptq) #print(pm) #print(pp) gh0 = format(0, 'b').zfill(nq + 1) gh1 = format(2**(nq + 1) - 1, 'b').zfill(nq + 1) ghz = (qtn.MPS_computational_state(gh0) + qtn.MPS_computational_state(gh1)) ghz = ghz.contract(all) ghzd = ghz.data for r in range(0, nr): if r == 0: tagsD = ('D{:d}T0'.format(nq - 1)) indsD = ('d{:d}g0'.format(nq - 1), ) TN = qtn.Tensor(DTermWait(pi1), indsD, tagsD) for dq in range(0, nq - 1): tagsD = ('D{:d}T0'.format(dq)) indsD = ('d{:d}g0'.format(dq), ) TN = TN & qtn.Tensor(DTermWait(pi1), indsD, tagsD) else: for dq in range(0, nq): tagsD = ('D{:d}T{:d}'.format(dq, 4 * r)) indsD = ('d{:d}g{:d}'.format(dq, 4 * r - 1), 'd{:d}g{:d}'.format(dq, 4 * r)) TN = TN & qtn.Tensor(IdTensor(perf), indsD, tagsD) for aq in range(0, nq - 1): tagsA = ('A{:d}T{:d}'.format(aq, 4 * r)) indsA = ('sf{:d}r{:d}'.format(aq, r), 'a{:d}g{:d}'.format(aq, 4 * r)) TN = TN & qtn.Tensor(APrepTensor1(pp), indsA, tagsA) #first round of CNOTs for aq in range(0, nq - 1): tagsD = ('D{:d}A{:d}T{:d}'.format(aq, aq, 4 * r + 1)) indsD = ('d{:d}g{:d}'.format(aq, 4 * r), 'a{:d}g{:d}'.format( aq, 4 * r), 'd{:d}g{:d}'.format(aq, 4 * r + 1), 'a{:d}g{:d}'.format(aq, 4 * r + 1)) TN = TN & qtn.Tensor(RepCNOTTensorHW(ptq), indsD, tagsD) tagsD = ('D{:d}T{:d}'.format(nq - 1, 4 * r + 1)) indsD = ('d{:d}g{:d}'.format(nq - 1, 4 * r), 'd{:d}g{:d}'.format(nq - 1, 4 * r + 1)) TN = TN & qtn.Tensor(IdTensor(pi2), indsD, tagsD) #second round of CNOTs for aq in range(0, nq - 1): tagsD = ('D{:d}A{:d}T{:d}'.format(aq + 1, aq, 4 * r + 2)) indsD = ('d{:d}g{:d}'.format(aq + 1, 4 * r + 1), 'a{:d}g{:d}'.format(aq, 4 * r + 1), 'd{:d}g{:d}'.format( aq + 1, 4 * r + 2), 'a{:d}g{:d}'.format(aq, 4 * r + 2)) TN = TN & qtn.Tensor(RepCNOTTensorHW(ptq), indsD, tagsD) tagsD = ('D{:d}T{:d}'.format(0, 4 * r + 2)) indsD = ('d{:d}g{:d}'.format(0, 4 * r + 1), 'd{:d}g{:d}'.format(0, 4 * r + 2)) TN = TN & qtn.Tensor(IdTensor(pi2), indsD, tagsD) #measurement round/wait locations #first do the terminal data wait locations if r == nr - 1: for dq in range(0, nq): tagsD = ('D{:d}T{:d}'.format(dq, 4 * r + 3)) indsD = ('d{:d}g{:d}'.format(dq, 4 * r + 2), 'd{:d}g{:d}'.format(dq, 4 * r + 3)) TN = TN & qtn.Tensor(IdTensor(pm), indsD, tagsD) for dq in range(0, (nq - 1) // 2): tagsD = ('D{:d}T{:d}'.format(dq, 4 * r + 4)) indsD = ('d{:d}g{:d}'.format(dq, 4 * r + 3), 'd{:d}lz'.format(dq), 'd{:d}p'.format(dq)) TN = TN & qtn.Tensor(DatTermTensor(pi3), indsD, tagsD) tagsD = ('D{:d}T{:d}'.format((nq - 1) // 2, 4 * r + 4)) indsD = ('d{:d}g{:d}'.format( (nq - 1) // 2, 4 * r + 3), 'd{:d}lz'.format((nq - 1) // 2)) TN = TN & qtn.Tensor(IdTensor(pi3), indsD, tagsD) for dq in range(((nq - 1) // 2) + 1, nq): tagsD = ('D{:d}T{:d}'.format(dq, 4 * r + 4)) indsD = ('d{:d}g{:d}'.format(dq, 4 * r + 3), 'd{:d}lz'.format(dq), 'd{:d}p'.format(dq - 1)) TN = TN & qtn.Tensor(DatTermTensor(pi3), indsD, tagsD) else: for dq in range(0, (nq - 1) // 2): tagsD = ('D{:d}T{:d}'.format(dq, 4 * r + 3)) indsD = ('d{:d}g{:d}'.format(dq, 4 * r + 2), 'd{:d}g{:d}'.format(dq, 4 * r + 3)) TN = TN & qtn.Tensor(IdTensor(pi3), indsD, tagsD) tagsD = ('D{:d}T{:d}'.format((nq - 1) // 2, 4 * r + 3)) indsD = ('d{:d}g{:d}'.format( (nq - 1) // 2, 4 * r + 2), 'd{:d}g{:d}'.format((nq - 1) // 2, 4 * r + 3)) TN = TN & qtn.Tensor(IdTensor(pi3), indsD, tagsD) for dq in range(((nq - 1) // 2) + 1, nq): tagsD = ('D{:d}T{:d}'.format(dq, 4 * r + 3)) indsD = ('d{:d}g{:d}'.format(dq, 4 * r + 2), 'd{:d}g{:d}'.format(dq, 4 * r + 3)) TN = TN & qtn.Tensor(IdTensor(pi3), indsD, tagsD) #now do the ancilla measurement failure locations for aq in range(0, nq - 1): tagsA = ('A{:d}T{:d}'.format(aq, 4 * r + 3)) indsA = ('a{:d}g{:d}'.format(aq, 4 * r + 2), ) TN = TN & qtn.Tensor(AMeasTensor1(pm), indsA, tagsA) #finally, put in the logical control tensor tagsL = ('LZ') indsL = ('lz', ) for dq in range(0, nq): indsL = indsL + ('d{:d}lz'.format(dq), ) TN = TN & qtn.Tensor(ghzd, indsL, tagsL) return TN
def CLTNRepHWTraceMeas(nq, nr, p): #First initialize all HW noise parameters. Note that p=k1/k2 pxm = 0.004 #X readout failure probability kappa2 = 10**7 alpha2 = 8 #photon number p1 = 10 * p #fail prob for data qubit wait location during very first ancilla initialization p2 = 0.299 * ( p**0.5 ) #fail prob for data qubit wait location during execution of a CNOT on other qubits p3 = p * kappa2 * alpha2 * ( 350 * (10**(-9)) + (10 / (kappa2 * alpha2)) ) #fail prob for data qubit wait location during readout of ancilla pz1 = 0.845 * ( p**0.5 ) #prob of z1 otimes I failure after CNOT (qubit 1 is control, 2 is target) pz2 = 0.133 * ( p**0.5 ) #prob of I otimes z2 failure after CNOT (qubit 1 is control, 2 is target) pz1z2 = 0.133 * (p**0.5) #prob of z1 otimes z2 failure after CNOT #create probability distributions to be passed to local tensor constructors which model different locations pi1 = [1 - p1, p1] #data qubit wait during first ancilla init pi2 = [1 - p2, p2] #data qubit wait during CNOT pi3 = [1 - p3, p3] #data qubit wait during ancilla measurement and re-init pm = [1 - pxm, pxm] #X measurement location pp = [1 - 15 * p / 2, 15 * p / 2] #ancilla initialization perf = [1, 0] #perfect wait location ptq = [1 - pz1 - pz2 - pz1z2, pz1, pz2, pz1z2] #CNOT location gh0 = format(0, 'b').zfill(nq + 1) gh1 = format(2**(nq + 1) - 1, 'b').zfill(nq + 1) ghz = (qtn.MPS_computational_state(gh0) + qtn.MPS_computational_state(gh1)) ghz = ghz.contract(all) ghzd = ghz.data for r in range(0, nr): if r == 0: tagsD = ('D{:d}T0'.format(nq - 1)) indsD = ('d{:d}g0'.format(nq - 1), ) TN = qtn.Tensor(DTermWait(pi1), indsD, tagsD) for dq in range(0, nq - 1): tagsD = ('D{:d}T0'.format(dq)) indsD = ('d{:d}g0'.format(dq), ) TN = TN & qtn.Tensor(DTermWait(pi1), indsD, tagsD) else: for dq in range(0, nq): tagsD = ('D{:d}T{:d}'.format(dq, 4 * r)) indsD = ('d{:d}g{:d}'.format(dq, 4 * r - 1), 'd{:d}g{:d}'.format(dq, 4 * r)) TN = TN & qtn.Tensor(IdTensor(perf), indsD, tagsD) for aq in range(0, nq - 1): tagsA = ('A{:d}T{:d}'.format(aq, 4 * r)) indsA = ('sf{:d}r{:d}'.format(aq, r), 'a{:d}g{:d}'.format(aq, 4 * r)) TN = TN & qtn.Tensor(APrepTensor1(pp), indsA, tagsA) tagsST = ('A{:d}T{:d}TR'.format(aq, 4 * r)) indsST = ('sf{:d}r{:d}'.format(aq, r), ) TN = TN & qtn.Tensor(np.ones(2), indsST, tagsST) #first round of CNOTs for aq in range(0, nq - 1): tagsD = ('D{:d}A{:d}T{:d}'.format(aq, aq, 4 * r + 1)) indsD = ('d{:d}g{:d}'.format(aq, 4 * r), 'a{:d}g{:d}'.format( aq, 4 * r), 'd{:d}g{:d}'.format(aq, 4 * r + 1), 'a{:d}g{:d}'.format(aq, 4 * r + 1)) TN = TN & qtn.Tensor(RepCNOTTensorHW(ptq), indsD, tagsD) tagsD = ('D{:d}T{:d}'.format(nq - 1, 4 * r + 1)) indsD = ('d{:d}g{:d}'.format(nq - 1, 4 * r), 'd{:d}g{:d}'.format(nq - 1, 4 * r + 1)) TN = TN & qtn.Tensor(IdTensor(pi2), indsD, tagsD) #second round of CNOTs for aq in range(0, nq - 1): tagsD = ('D{:d}A{:d}T{:d}'.format(aq + 1, aq, 4 * r + 2)) indsD = ('d{:d}g{:d}'.format(aq + 1, 4 * r + 1), 'a{:d}g{:d}'.format(aq, 4 * r + 1), 'd{:d}g{:d}'.format( aq + 1, 4 * r + 2), 'a{:d}g{:d}'.format(aq, 4 * r + 2)) TN = TN & qtn.Tensor(RepCNOTTensorHW(ptq), indsD, tagsD) tagsD = ('D{:d}T{:d}'.format(0, 4 * r + 2)) indsD = ('d{:d}g{:d}'.format(0, 4 * r + 1), 'd{:d}g{:d}'.format(0, 4 * r + 2)) TN = TN & qtn.Tensor(IdTensor(pi2), indsD, tagsD) #measurement round/wait locations #first do the terminal data wait locations if r == nr - 1: for dq in range(0, (nq - 1) // 2): tagsD = ('D{:d}T{:d}'.format(dq, 4 * r + 3)) indsD = ('d{:d}g{:d}'.format(dq, 4 * r + 2), 'd{:d}lz'.format(dq), 'd{:d}p'.format(dq)) TN = TN & qtn.Tensor(DatTermTensor(pi3), indsD, tagsD) tagsD = ('D{:d}T{:d}'.format((nq - 1) // 2, 4 * r + 3)) indsD = ('d{:d}g{:d}'.format( (nq - 1) // 2, 4 * r + 2), 'd{:d}lz'.format((nq - 1) // 2)) TN = TN & qtn.Tensor(IdTensor(pi3), indsD, tagsD) for dq in range(((nq - 1) // 2) + 1, nq): tagsD = ('D{:d}T{:d}'.format(dq, 4 * r + 3)) indsD = ('d{:d}g{:d}'.format(dq, 4 * r + 2), 'd{:d}lz'.format(dq), 'd{:d}p'.format(dq - 1)) TN = TN & qtn.Tensor(DatTermTensor(pi3), indsD, tagsD) else: for dq in range(0, (nq - 1) // 2): tagsD = ('D{:d}T{:d}'.format(dq, 4 * r + 3)) indsD = ('d{:d}g{:d}'.format(dq, 4 * r + 2), 'd{:d}g{:d}'.format(dq, 4 * r + 3)) TN = TN & qtn.Tensor(IdTensor(pi3), indsD, tagsD) tagsD = ('D{:d}T{:d}'.format((nq - 1) // 2, 4 * r + 3)) indsD = ('d{:d}g{:d}'.format( (nq - 1) // 2, 4 * r + 2), 'd{:d}g{:d}'.format((nq - 1) // 2, 4 * r + 3)) TN = TN & qtn.Tensor(IdTensor(pi3), indsD, tagsD) for dq in range(((nq - 1) // 2) + 1, nq): tagsD = ('D{:d}T{:d}'.format(dq, 4 * r + 3)) indsD = ('d{:d}g{:d}'.format(dq, 4 * r + 2), 'd{:d}g{:d}'.format(dq, 4 * r + 3)) TN = TN & qtn.Tensor(IdTensor(pi3), indsD, tagsD) #now do the ancilla measurement failure locations for aq in range(0, nq - 1): tagsA = ('A{:d}T{:d}'.format(aq, 4 * r + 3)) indsA = ('a{:d}g{:d}'.format(aq, 4 * r + 2), ) TN = TN & qtn.Tensor(AMeasTensor1(pm), indsA, tagsA) #finally, put in the logical control tensor tagsL = ('LZ') indsL = ('lz', ) for dq in range(0, nq): indsL = indsL + ('d{:d}lz'.format(dq), ) TN = TN & qtn.Tensor(ghzd, indsL, tagsL) return TN
def apply_op(self, op: 'cirq.Operation', prng: np.random.RandomState): """Applies a unitary operation, mutating the object to represent the new state. op: The operation that mutates the object. Note that currently, only 1- and 2- qubit operations are currently supported. """ old_inds = tuple( [self.i_str(self.qubit_map[qubit]) for qubit in op.qubits]) new_inds = tuple(['new_' + old_ind for old_ind in old_inds]) if protocols.has_unitary(op): U = protocols.unitary(op) else: mixtures = protocols.mixture(op) mixture_idx = int( prng.choice(len(mixtures), p=[mixture[0] for mixture in mixtures])) U = mixtures[mixture_idx][1] U = qtn.Tensor(U.reshape([qubit.dimension for qubit in op.qubits] * 2), inds=(new_inds + old_inds)) # TODO(tonybruguier): Explore using the Quimb's tensor network natively. if len(op.qubits) == 1: n = self.grouping[op.qubits[0]] self.M[n] = (U @ self.M[n]).reindex({new_inds[0]: old_inds[0]}) elif len(op.qubits) == 2: n, p = [self.grouping[qubit] for qubit in op.qubits] if n == p: self.M[n] = (U @ self.M[n]).reindex({ new_inds[0]: old_inds[0], new_inds[1]: old_inds[1] }) else: # This is the index on which we do the contraction. We need to add it iff it's # the first time that we do the joining for that specific pair. mu_ind = self.mu_str(n, p) if mu_ind not in self.M[n].inds: self.M[n].new_ind(mu_ind) if mu_ind not in self.M[p].inds: self.M[p].new_ind(mu_ind) T = U @ self.M[n] @ self.M[p] left_inds = tuple(set(T.inds) & set(self.M[n].inds)) + (new_inds[0], ) X, Y = T.split( left_inds, method=self.simulation_options.method, max_bond=self.simulation_options.max_bond, cutoff=self.simulation_options.cutoff, cutoff_mode=self.simulation_options.cutoff_mode, get='tensors', absorb='both', bond_ind=mu_ind, ) # Equations (13), (14), and (15): # TODO(tonybruguier): When Quimb 2.0.0 is released, the split() # function should have a 'renorm' that, when set to None, will # allow to compute e_n exactly as: # np.sum(abs((X @ Y).data) ** 2).real / np.sum(abs(T) ** 2).real # # The renormalization would then have to be done manually. # # However, for now, e_n are just the estimated value. e_n = self.simulation_options.cutoff self.estimated_gate_error_list.append(e_n) self.M[n] = X.reindex({new_inds[0]: old_inds[0]}) self.M[p] = Y.reindex({new_inds[1]: old_inds[1]}) else: # NOTE(tonybruguier): There could be a way to handle higher orders. I think this could # involve HOSVDs: # https://en.wikipedia.org/wiki/Higher-order_singular_value_decomposition # # TODO(tonybruguier): Evaluate whether it's even useful to implement and learn more # about HOSVDs. raise ValueError('Can only handle 1 and 2 qubit operations')
def _simulate_tn_mpi(circuit: Circuit, initial_state: any, final_state: any, optimize: any, backend: any, complex_type: any, tensor_only: bool, verbose: bool, **kwargs): import quimb.tensor as tn import cotengra as ctg # Get MPI _mpi_comm = MPI.COMM_WORLD _mpi_size = _mpi_comm.Get_size() _mpi_rank = _mpi_comm.Get_rank() # Set default parameters kwargs.setdefault('compress', 2) kwargs.setdefault('simplify_tn', 'RC') kwargs.setdefault('max_iterations', 1) kwargs.setdefault('methods', ['kahypar', 'greedy']) kwargs.setdefault('max_time', 120) kwargs.setdefault('max_repeats', 16) kwargs.setdefault('minimize', 'combo') kwargs.setdefault('target_largest_intermediate', 0) kwargs.setdefault('max_largest_intermediate', 2**26) kwargs.setdefault('temperatures', [1.0, 0.1, 0.01]) kwargs.setdefault('parallel', None) kwargs.setdefault('cotengra', {}) kwargs.setdefault('max_n_slices', None) kwargs.setdefault('return_info', False) # Get random leaves_prefix leaves_prefix = ''.join( np.random.choice(list('abcdefghijklmnopqrstuvwxyz'), size=20)) # Initialize info _sim_info = {} # Alias for tn if optimize == 'tn': optimize = 'cotengra' if isinstance(circuit, Circuit): if not kwargs['parallel']: kwargs['parallel'] = 1 else: # If number of threads not provided, just use half of the number of available cpus if isinstance(kwargs['parallel'], bool) and kwargs['parallel'] == True: kwargs['parallel'] = cpu_count() // 2 if optimize is not None and kwargs['parallel'] and kwargs[ 'max_iterations'] == 1: warn("Parallelization for MPI works for multiple iterations only. " "For a better performance, use: 'max_iterations' > 1") # Get number of qubits qubits = circuit.all_qubits() n_qubits = len(qubits) # If initial/final state is None, set to all .'s initial_state = '.' * n_qubits if initial_state is None else initial_state final_state = '.' * n_qubits if final_state is None else final_state # Initial and final states must be valid strings for state, sname in [(initial_state, 'initial_state'), (final_state, 'final_state')]: # Get alphabet from string import ascii_letters # Check if string if not isinstance(state, str): raise ValueError(f"'{sname}' must be a valid string.") # Deprecated error if any(x in 'xX' for x in state): from warnings import warn # Define new DeprecationWarning (to always print the warning # signal) class DeprecationWarning(Warning): pass # Warn the user that '.' is used to represent open qubits warn( "Since '0.6.3', letters in the alphabet are used to " "trace selected qubits (including 'x' and 'X'). " "Instead, '.' is used to represent an open qubit.", DeprecationWarning) # Check only valid symbols are present if set(state).difference('01+-.' + ascii_letters): raise ValueError(f"'{sname}' contains invalid symbols.") # Check number of qubits if len(state) != n_qubits: raise ValueError(f"'{sname}' has the wrong number of qubits " f"(expected {n_qubits}, got {len(state)})") # Check memory if 2**(initial_state.count('.') + final_state.count('.')) > kwargs['max_largest_intermediate']: raise MemoryError("Memory for the given number of open qubits " "exceeds the 'max_largest_intermediate'.") # Compress circuit if kwargs['compress']: if verbose: print( f"Compress circuit (max_n_qubits={kwargs['compress']}): ", end='', file=stderr) _time = time() circuit = utils.compress( circuit, kwargs['compress']['max_n_qubits'] if isinstance( kwargs['compress'], dict) else kwargs['compress'], verbose=verbose, **({ k: v for k, v in kwargs['compress'].items() if k != 'max_n_qubits' } if isinstance(kwargs['compress'], dict) else {})) circuit = Circuit( utils.to_matrix_gate(c, complex_type=complex_type) for c in circuit) if verbose: print(f"Done! ({time()-_time:1.2f}s)", file=stderr) # Get tensor network representation of circuit tensor, tn_qubits_map = utils.to_tn(circuit, return_qubits_map=True, leaves_prefix=leaves_prefix) # Define basic MPS _mps = { '0': np.array([1, 0]), '1': np.array([0, 1]), '+': np.array([1, 1]) / np.sqrt(2), '-': np.array([1, -1]) / np.sqrt(2) } # Attach initial/final state for state, ext in [(initial_state, 'i'), (final_state, 'f')]: for s, q in ((s, q) for s, q in zip(state, qubits) if s in _mps): inds = [f'{leaves_prefix}_{tn_qubits_map[q]}_{ext}'] tensor &= tn.Tensor(_mps[s], inds=inds, tags=inds) # For each unique letter, apply trace for x in set(initial_state + final_state).difference(''.join(_mps) + '.'): # Get indexes inds = [ f'{leaves_prefix}_{tn_qubits_map[q]}_i' for s, q in zip(initial_state, qubits) if s == x ] inds += [ f'{leaves_prefix}_{tn_qubits_map[q]}_f' for s, q in zip(final_state, qubits) if s == x ] # Apply trace tensor &= tn.Tensor(np.reshape([1] + [0] * (2**len(inds) - 2) + [1], (2, ) * len(inds)), inds=inds) # Simplify if requested if kwargs['simplify_tn']: tensor.full_simplify_(kwargs['simplify_tn']).astype_(complex_type) else: # Otherwise, just convert to the given complex_type tensor.astype_(complex_type) # Get contraction from heuristic if optimize == 'cotengra' and kwargs['max_iterations'] > 0: # Set cotengra parameters def cotengra_params(): # Get HyperOptimizer q = ctg.HyperOptimizer(methods=kwargs['methods'], max_time=kwargs['max_time'], max_repeats=kwargs['max_repeats'], minimize=kwargs['minimize'], progbar=False, parallel=False, **kwargs['cotengra']) # For some optlib, HyperOptimizer._retrieve_params is not # pickeable. Let's fix the problem by hand. q._retrieve_params = __FunctionWrap(q._retrieve_params) # Return HyperOptimizer return q # Get target size tli = kwargs['target_largest_intermediate'] with Pool(kwargs['parallel']) as pool: # Sumbit jobs _opts = [ cotengra_params() for _ in range(kwargs['max_iterations']) ] _map = [ pool.apply_async(tensor.contract, (all, ), dict(optimize=_opt, get='path-info')) for _opt in _opts ] with tqdm(total=len(_map), disable=not verbose, desc='Collecting contractions') as pbar: _old_completed = 0 while 1: # Count number of completed _completed = 0 for _w in _map: _completed += _w.ready() if _w.ready() and not _w.successful(): _w.get() # Update pbar pbar.update(_completed - _old_completed) _old_completed = _completed if _completed == len(_map): break # Wait sleep(1) # Collect results _infos = [_w.get() for _w in _map] if kwargs['minimize'] == 'size': opt, info = sort( zip(_opts, _infos), key=lambda w: (w[1].largest_intermediate, w[0].best['flops']))[0] else: opt, info = sort( zip(_opts, _infos), key=lambda w: (w[0].best['flops'], w[1].largest_intermediate))[0] if optimize == 'cotengra': # Gather best contractions _cost = _mpi_comm.gather( (info.largest_intermediate, info.opt_cost, _mpi_rank), root=0) if _mpi_rank == 0: if kwargs['minimize'] == 'size': _best_rank = sort(_cost, key=lambda x: (x[0], x[1]))[0][-1] else: _best_rank = sort(_cost, key=lambda x: (x[1], x[0]))[0][-1] else: _best_rank = None _best_rank = _mpi_comm.bcast(_best_rank, root=0) if hasattr(opt, '_pool'): del (opt._pool) # Distribute opt/info tensor, info, opt = _mpi_comm.bcast((tensor, info, opt), root=_best_rank) # Just return tensor if required if tensor_only: if optimize == 'cotengra' and kwargs['max_iterations'] > 0: return tensor, (info, opt) else: return tensor else: # Set tensor tensor = circuit if len(optimize) == 2 and isinstance( optimize[0], PathInfo) and isinstance( optimize[1], ctg.hyper.HyperOptimizer): # Get info and opt from optimize info, opt = optimize # Set optimization optimize = 'cotengra' else: # Get tensor and path tensor = circuit # Print some info if verbose and _mpi_rank == 0: print( f'Largest Intermediate: 2^{np.log2(float(info.largest_intermediate)):1.2f}', file=stderr) print( f'Max Largest Intermediate: 2^{np.log2(float(kwargs["max_largest_intermediate"])):1.2f}', file=stderr) print(f'Flops: 2^{np.log2(float(info.opt_cost)):1.2f}', file=stderr) if optimize == 'cotengra': if _mpi_rank == 0: # Get indexes _inds = tensor.outer_inds() # Get input indexes and output indexes _i_inds = sort([x for x in _inds if x[-2:] == '_i'], key=lambda x: int(x.split('_')[1])) _f_inds = sort([x for x in _inds if x[-2:] == '_f'], key=lambda x: int(x.split('_')[1])) # Get order _inds = [_inds.index(x) for x in _i_inds + _f_inds] # Get slice finder sf = ctg.SliceFinder( info, target_size=kwargs['max_largest_intermediate'], allow_outer=False) # Find slices with tqdm(kwargs['temperatures'], disable=not verbose, leave=False) as pbar: for _temp in pbar: pbar.set_description(f'Find slices (T={_temp})') ix_sl, cost_sl = sf.search(temperature=_temp) # Get slice contractor sc = sf.SlicedContractor([t.data for t in tensor]) # Make sure that no open qubits are sliced assert (not { ix: i for i, ix in enumerate(sc.output) if ix in sc.sliced }) # Print some infos if verbose: print( f'Number of slices: 2^{np.log2(float(cost_sl.nslices)):1.2f}', file=stderr) print( f'Flops+Cuts: 2^{np.log2(float(cost_sl.total_flops)):1.2f}', file=stderr) # Update infos _sim_info.update({ 'flops': info.opt_cost, 'largest_intermediate': info.largest_intermediate, 'n_slices': cost_sl.nslices, 'total_flops': cost_sl.total_flops }) # Get slices slices = list(range(cost_sl.nslices + 1)) + [None] * ( _mpi_size - cost_sl.nslices) if cost_sl.nslices < _mpi_size else [ cost_sl.nslices / _mpi_size * i for i in range(_mpi_size) ] + [cost_sl.nslices] if not np.alltrue( [int(x) == x for x in slices if x is not None]) or not np.alltrue([ slices[i] < slices[i + 1] for i in range(_mpi_size) if slices[i] is not None and slices[i + 1] is not None ]): raise RuntimeError('Something went wrong') # Convert all to integers slices = [int(x) if x is not None else None for x in slices] else: sc = slices = None # Distribute slicer and slices sc, slices = _mpi_comm.bcast((sc, slices), root=0) _n_slices = max(x for x in slices if x) if kwargs['max_n_slices'] and _n_slices > kwargs['max_n_slices']: raise RuntimeError( f'Too many slices ({_n_slices} > {kwargs["max_n_slices"]})') # Contract slices _tensor = None if slices[_mpi_rank] is not None and slices[_mpi_rank + 1] is not None: for i in tqdm(range(slices[_mpi_rank], slices[_mpi_rank + 1]), desc='Contracting slices', disable=not verbose, leave=False): if _tensor is None: _tensor = np.copy(sc.contract_slice(i, backend=backend)) else: _tensor += sc.contract_slice(i, backend=backend) # Gather tensors if _mpi_rank != 0: _mpi_comm.send(_tensor, dest=0, tag=11) elif _mpi_rank == 0: for i in tqdm(range(1, _mpi_size), desc='Collecting tensors', disable=not verbose): _p_tensor = _mpi_comm.recv(source=i, tag=11) if _p_tensor is not None: _tensor += _p_tensor if _mpi_rank == 0: # Create map _map = ''.join([get_symbol(x) for x in range(len(_inds))]) _map += '->' _map += ''.join([get_symbol(x) for x in _inds]) # Reorder tensor tensor = contract(_map, _tensor) # Deprecated ## Reshape tensor #if _inds: # if _i_inds and _f_inds: # tensor = np.reshape(tensor, # (2**len(_i_inds), 2**len(_f_inds))) # else: # tensor = np.reshape(tensor, # (2**max(len(_i_inds), len(_f_inds)),)) else: tensor = None else: if _mpi_rank == 0: # Contract tensor tensor = tensor.contract(optimize=optimize, backend=backend) if hasattr(tensor, 'inds'): # Get input indexes and output indexes _i_inds = sort([x for x in tensor.inds if x[-2:] == '_i'], key=lambda x: int(x.split('_')[1])) _f_inds = sort([x for x in tensor.inds if x[-2:] == '_f'], key=lambda x: int(x.split('_')[1])) # Transpose tensor tensor.transpose(*(_i_inds + _f_inds), inplace=True) # Deprecated ## Reshape tensor #if _i_inds and _f_inds: # tensor = np.reshape(tensor, # (2**len(_i_inds), 2**len(_f_inds))) #else: # tensor = np.reshape(tensor, # (2**max(len(_i_inds), len(_f_inds)),)) else: tensor = None if kwargs['return_info']: return tensor, _sim_info else: return tensor
tags={'KET' }) #Build a random starting tensor ame_trial = qtn.TensorNetwork([ normalize_state(ket) ]) # Take this tensor to be its own tensor network else: # Or choose to split it up into a network of smaller tensors data_tensors = [] for i in range(phys_inds): data_tensors.append( np.random.rand(phys_dim, virt_dim, virt_dim)) # Make random ndarrays for each tensor kets = [] # Make tensors naming the physical and virtual indices so they match up kets.append( qtn.Tensor(data_tensors[0], inds=(f'k{0}', f'v{phys_inds-1}', f'v{0}'), tags={'KET'})) for i in range(1, phys_inds): kets.append( qtn.Tensor(data_tensors[i], inds=(f'k{i}', f'v{i-1}', f'v{i}'), tags={'KET'})) # Combine the tensors in to a tensor network with the & sign ame_trial = kets[0] for i in range(phys_inds - 1): ame_trial &= kets[i + 1] # If i split it up then normalize the full network as a ket ame_trial = normalize_state(ame_trial) #list of all partitions half_partitions = list(combinations(range(phys_inds), int(phys_inds / 2)))
def _simulate_tn(circuit: any, initial_state: any, final_state: any, optimize: any, backend: any, complex_type: any, tensor_only: bool, verbose: bool, **kwargs): import quimb.tensor as tn import cotengra as ctg # Get random leaves_prefix leaves_prefix = ''.join( np.random.choice(list('abcdefghijklmnopqrstuvwxyz'), size=20)) # Initialize info _sim_info = {} # Alias for tn if optimize == 'tn': optimize = 'cotengra' if isinstance(circuit, Circuit): # Get number of qubits qubits = circuit.all_qubits() n_qubits = len(qubits) # If initial/final state is None, set to all .'s initial_state = '.' * n_qubits if initial_state is None else initial_state final_state = '.' * n_qubits if final_state is None else final_state # Initial and final states must be valid strings for state, sname in [(initial_state, 'initial_state'), (final_state, 'final_state')]: # Get alphabet from string import ascii_letters # Check if string if not isinstance(state, str): raise ValueError(f"'{sname}' must be a valid string.") # Deprecated error if any(x in 'xX' for x in state): from hybridq.utils import DeprecationWarning from warnings import warn # Warn the user that '.' is used to represent open qubits warn( "Since '0.6.3', letters in the alphabet are used to " "trace selected qubits (including 'x' and 'X'). " "Instead, '.' is used to represent an open qubit.", DeprecationWarning) # Check only valid symbols are present if set(state).difference('01+-.' + ascii_letters): raise ValueError(f"'{sname}' contains invalid symbols.") # Check number of qubits if len(state) != n_qubits: raise ValueError(f"'{sname}' has the wrong number of qubits " f"(expected {n_qubits}, got {len(state)})") # Check memory if 2**(initial_state.count('.') + final_state.count('.')) > kwargs['max_largest_intermediate']: raise MemoryError("Memory for the given number of open qubits " "exceeds the 'max_largest_intermediate'.") # Compress circuit if kwargs['compress']: if verbose: print( f"Compress circuit (max_n_qubits={kwargs['compress']}): ", end='', file=stderr) _time = time() circuit = utils.compress( circuit, kwargs['compress']['max_n_qubits'] if isinstance( kwargs['compress'], dict) else kwargs['compress'], verbose=verbose, **({ k: v for k, v in kwargs['compress'].items() if k != 'max_n_qubits' } if isinstance(kwargs['compress'], dict) else {})) circuit = Circuit( utils.to_matrix_gate(c, complex_type=complex_type) for c in circuit) if verbose: print(f"Done! ({time()-_time:1.2f}s)", file=stderr) # Get tensor network representation of circuit tensor, tn_qubits_map = utils.to_tn(circuit, return_qubits_map=True, leaves_prefix=leaves_prefix) # Define basic MPS _mps = { '0': np.array([1, 0]), '1': np.array([0, 1]), '+': np.array([1, 1]) / np.sqrt(2), '-': np.array([1, -1]) / np.sqrt(2) } # Attach initial/final state for state, ext in [(initial_state, 'i'), (final_state, 'f')]: for s, q in ((s, q) for s, q in zip(state, qubits) if s in _mps): inds = [f'{leaves_prefix}_{tn_qubits_map[q]}_{ext}'] tensor &= tn.Tensor(_mps[s], inds=inds, tags=inds) # For each unique letter, apply trace for x in set(initial_state + final_state).difference(''.join(_mps) + '.'): # Get indexes inds = [ f'{leaves_prefix}_{tn_qubits_map[q]}_i' for s, q in zip(initial_state, qubits) if s == x ] inds += [ f'{leaves_prefix}_{tn_qubits_map[q]}_f' for s, q in zip(final_state, qubits) if s == x ] # Apply trace tensor &= tn.Tensor(np.reshape([1] + [0] * (2**len(inds) - 2) + [1], (2, ) * len(inds)), inds=inds) # Simplify if requested if kwargs['simplify_tn']: tensor.full_simplify_(kwargs['simplify_tn']).astype_(complex_type) else: # Otherwise, just convert to the given complex_type tensor.astype_(complex_type) # Get contraction from heuristic if optimize == 'cotengra' and kwargs['max_iterations'] > 0: # Create local client if MPI has been detected (not compatible with Dask at the moment) if _mpi_env and kwargs['parallel']: from distributed import Client, LocalCluster _client = Client(LocalCluster(processes=False)) else: _client = None # Set cotengra parameters cotengra_params = lambda: ctg.HyperOptimizer( methods=kwargs['methods'], max_time=kwargs['max_time'], max_repeats=kwargs['max_repeats'], minimize=kwargs['minimize'], progbar=verbose, parallel=kwargs['parallel'], **kwargs['cotengra']) # Get optimized path opt = cotengra_params() info = tensor.contract(all, optimize=opt, get='path-info') # Get target size tli = kwargs['target_largest_intermediate'] # Repeat for the requested number of iterations for _ in range(1, kwargs['max_iterations']): # Break if largest intermediate is equal or smaller than target if info.largest_intermediate <= tli: break # Otherwise, restart _opt = cotengra_params() _info = tensor.contract(all, optimize=_opt, get='path-info') # Store the best if kwargs['minimize'] == 'size': if _info.largest_intermediate < info.largest_intermediate or ( _info.largest_intermediate == info.largest_intermediate and _opt.best['flops'] < opt.best['flops']): info = _info opt = _opt else: if _opt.best['flops'] < opt.best['flops'] or ( _opt.best['flops'] == opt.best['flops'] and _info.largest_intermediate < info.largest_intermediate): info = _info opt = _opt # Close client if exists if _client: _client.shutdown() _client.close() # Just return tensor if required if tensor_only: if optimize == 'cotengra' and kwargs['max_iterations'] > 0: return tensor, (info, opt) else: return tensor else: # Set tensor tensor = circuit if len(optimize) == 2 and isinstance( optimize[0], PathInfo) and isinstance( optimize[1], ctg.hyper.HyperOptimizer): # Get info and opt from optimize info, opt = optimize # Set optimization optimize = 'cotengra' else: # Get tensor and path tensor = circuit # Print some info if verbose: print( f'Largest Intermediate: 2^{np.log2(float(info.largest_intermediate)):1.2f}', file=stderr) print( f'Max Largest Intermediate: 2^{np.log2(float(kwargs["max_largest_intermediate"])):1.2f}', file=stderr) print(f'Flops: 2^{np.log2(float(info.opt_cost)):1.2f}', file=stderr) if optimize == 'cotengra': # Get indexes _inds = tensor.outer_inds() # Get input indexes and output indexes _i_inds = sort([x for x in _inds if x[-2:] == '_i'], key=lambda x: int(x.split('_')[1])) _f_inds = sort([x for x in _inds if x[-2:] == '_f'], key=lambda x: int(x.split('_')[1])) # Get order _inds = [_inds.index(x) for x in _i_inds + _f_inds] # Get slice finder sf = ctg.SliceFinder(info, target_size=kwargs['max_largest_intermediate']) # Find slices with tqdm(kwargs['temperatures'], disable=not verbose, leave=False) as pbar: for _temp in pbar: pbar.set_description(f'Find slices (T={_temp})') ix_sl, cost_sl = sf.search(temperature=_temp) # Get slice contractor sc = sf.SlicedContractor([t.data for t in tensor]) # Update infos _sim_info.update({ 'flops': info.opt_cost, 'largest_intermediate': info.largest_intermediate, 'n_slices': cost_sl.nslices, 'total_flops': cost_sl.total_flops }) # Print some infos if verbose: print( f'Number of slices: 2^{np.log2(float(cost_sl.nslices)):1.2f}', file=stderr) print(f'Flops+Cuts: 2^{np.log2(float(cost_sl.total_flops)):1.2f}', file=stderr) if kwargs['max_n_slices'] and sc.nslices > kwargs['max_n_slices']: raise RuntimeError( f'Too many slices ({sc.nslices} > {kwargs["max_n_slices"]})') # Contract tensor _li = np.log2(float(info.largest_intermediate)) _mli = np.log2(float(kwargs["max_largest_intermediate"])) _tensor = sc.gather_slices((sc.contract_slice( i, backend=backend ) for i in tqdm( range(sc.nslices), desc=f'Contracting tensor (li=2^{_li:1.0f}, mli=2^{_mli:1.1f})', leave=False))) # Create map _map = ''.join([get_symbol(x) for x in range(len(_inds))]) _map += '->' _map += ''.join([get_symbol(x) for x in _inds]) # Reorder tensor tensor = contract(_map, _tensor) # Deprecated ## Reshape tensor #if _inds: # if _i_inds and _f_inds: # tensor = np.reshape(tensor, (2**len(_i_inds), 2**len(_f_inds))) # else: # tensor = np.reshape(tensor, # (2**max(len(_i_inds), len(_f_inds)),)) else: # Contract tensor tensor = tensor.contract(optimize=optimize, backend=backend) if hasattr(tensor, 'inds'): # Get input indexes and output indexes _i_inds = sort([x for x in tensor.inds if x[-2:] == '_i'], key=lambda x: int(x.split('_')[1])) _f_inds = sort([x for x in tensor.inds if x[-2:] == '_f'], key=lambda x: int(x.split('_')[1])) # Transpose tensor tensor.transpose(*(_i_inds + _f_inds), inplace=True) # Deprecated ## Reshape tensor #if _i_inds and _f_inds: # tensor = np.reshape(tensor, (2**len(_i_inds), 2**len(_f_inds))) #else: # tensor = np.reshape(tensor, # (2**max(len(_i_inds), len(_f_inds)),)) if kwargs['return_info']: return tensor, _sim_info else: return tensor
def apply_unitary(self, op: 'cirq.Operation'): """Applies a unitary operation, mutating the object to represent the new state. op: The operation that mutates the object. Note that currently, only 1- and 2- qubit operations are currently supported. """ U = protocols.unitary(op).reshape( [qubit.dimension for qubit in op.qubits] * 2) # TODO(tonybruguier): Explore using the Quimb's tensor network natively. if len(op.qubits) == 1: n = self.qubit_map[op.qubits[0]] old_n = self.i_str(n) new_n = 'new_' + old_n U = qtn.Tensor(U, inds=(new_n, old_n)) self.M[n] = (U @ self.M[n]).reindex({new_n: old_n}) elif len(op.qubits) == 2: self.num_svd_splits += 1 n, p = [self.qubit_map[qubit] for qubit in op.qubits] old_n = self.i_str(n) old_p = self.i_str(p) new_n = 'new_' + old_n new_p = 'new_' + old_p U = qtn.Tensor(U, inds=(new_n, new_p, old_n, old_p)) # This is the index on which we do the contraction. We need to add it iff it's the first # time that we do the joining for that specific pair. mu_ind = self.mu_str(n, p) if mu_ind not in self.M[n].inds: self.M[n].new_ind(mu_ind) if mu_ind not in self.M[p].inds: self.M[p].new_ind(mu_ind) T = U @ self.M[n] @ self.M[p] left_inds = tuple(set(T.inds) & set(self.M[n].inds)) + (new_n, ) X, Y = T.split( left_inds, cutoff=self.rsum2_cutoff, cutoff_mode='rsum2', get='tensors', absorb='both', bond_ind=mu_ind, ) self.M[n] = X.reindex({new_n: old_n}) self.M[p] = Y.reindex({new_p: old_p}) else: # NOTE(tonybruguier): There could be a way to handle higher orders. I think this could # involve HOSVDs: # https://en.wikipedia.org/wiki/Higher-order_singular_value_decomposition # # TODO(tonybruguier): Evaluate whether it's even useful to implement and learn more # about HOSVDs. raise ValueError('Can only handle 1 and 2 qubit operations')
list_ids=[] list_alphabet=list(string.ascii_lowercase) for i in range(N_lay): list_ids.append(f"__ind_{list_alphabet[i]}{{}}__") list_tn=align_TN_1D(*list_mpo, ind_ids={*list_ids}, inplace=False) TN_l=qtn.TensorNetwork(list_tn) rotate_ten_list=[] for i in range(L): rotate_ten_list.append(qtn.Tensor(qu.rand(D*d).reshape(D, d), inds=(f'b{i}',f'k{i}'), tags={"R"})) R_l=qtn.TensorNetwork(rotate_ten_list) TN_l=TN_l & R_l for i in range(L): TN_l.contract_ind(f'b{i}', optimize='auto-hq') list_tags=[] for i in range(N_lay): list_tags.append(f"G{i}") #print (TN_l.graph(color=list_tags,show_inds=all, show_tags=False, iterations=4800, k=None, fix=None, figsize=(30, 30),node_size=200) )
def circuit_to_density_matrix_tensors( circuit: cirq.Circuit, qubits: Optional[Sequence[cirq.Qid]] = None ) -> Tuple[List[qtn.Tensor], Dict['cirq.Qid', int], Dict[Tuple[str, str], Tuple[float, float]]]: """Given a circuit with mixtures or channels, construct a tensor network representation of the density matrix. This assumes you start in the |0..0><0..0| state. Indices are named "nf{i}_q{x}" and "nb{i}_q{x}" where i is a time index and x is a qubit index. nf- and nb- refer to the "forwards" and "backwards" copies of the circuit. Kraus indices are named "k{j}" where j is an independent "kraus" internal index which you probably never need to access. Args: circuit: The circuit containing operations that support the cirq.unitary() or cirq.kraus() protocols. qubits: The qubits in the circuit. The `positions` return argument will position qubits according to their index in this list. Returns: tensors: A list of Quimb Tensor objects qubit_frontier: A mapping from qubit to time index at the end of the circuit. This can be used to deduce the names of the free tensor indices. positions: A positions dictionary suitable for passing to tn.graph()'s `fix` argument to draw the resulting tensor network similar to a quantum circuit. Raises: ValueError: If an op is encountered that cannot be converted. """ if qubits is None: # coverage: ignore qubits = sorted(circuit.all_qubits()) qubits = tuple(qubits) qubit_frontier: Dict[cirq.Qid, int] = {q: 0 for q in qubits} kraus_frontier = 0 positions: Dict[Tuple[str, str], Tuple[float, float]] = {} tensors: List[qtn.Tensor] = [] x_scale = 2 y_scale = 3 x_nudge = 0.3 n_qubits = len(qubits) yb_offset = (n_qubits + 0.5) * y_scale def _positions(_mi, _these_qubits): return _add_to_positions( positions, _mi, _these_qubits, all_qubits=qubits, x_scale=x_scale, y_scale=y_scale, x_nudge=x_nudge, yb_offset=yb_offset, ) # Initialize forwards and backwards qubits into the 0 state, i.e. prepare # rho_0 = |0><0|. for q in qubits: tensors += [ qtn.Tensor(data=quimb.up().squeeze(), inds=(f'nf0_q{q}', ), tags={'Q0', 'i0f', _qpos_tag(q)}), qtn.Tensor(data=quimb.up().squeeze(), inds=(f'nb0_q{q}', ), tags={'Q0', 'i0b', _qpos_tag(q)}), ] _positions(0, q) for mi, moment in enumerate(circuit.moments): for op in moment.operations: start_inds_f = [f'nf{qubit_frontier[q]}_q{q}' for q in op.qubits] start_inds_b = [f'nb{qubit_frontier[q]}_q{q}' for q in op.qubits] for q in op.qubits: qubit_frontier[q] += 1 end_inds_f = [f'nf{qubit_frontier[q]}_q{q}' for q in op.qubits] end_inds_b = [f'nb{qubit_frontier[q]}_q{q}' for q in op.qubits] if cirq.has_unitary(op): U = cirq.unitary(op).reshape( (2, ) * 2 * len(op.qubits)).astype(np.complex128) tensors.append( qtn.Tensor( data=U, inds=end_inds_f + start_inds_f, tags={ f'Q{len(op.qubits)}', f'i{mi + 1}f', _qpos_tag(op.qubits) }, )) tensors.append( qtn.Tensor( data=np.conj(U), inds=end_inds_b + start_inds_b, tags={ f'Q{len(op.qubits)}', f'i{mi + 1}b', _qpos_tag(op.qubits) }, )) elif cirq.has_kraus(op): K = np.asarray(cirq.kraus(op), dtype=np.complex128) kraus_inds = [f'k{kraus_frontier}'] tensors.append( qtn.Tensor( data=K, inds=kraus_inds + end_inds_f + start_inds_f, tags={ f'kQ{len(op.qubits)}', f'i{mi + 1}f', _qpos_tag(op.qubits) }, )) tensors.append( qtn.Tensor( data=np.conj(K), inds=kraus_inds + end_inds_b + start_inds_b, tags={ f'kQ{len(op.qubits)}', f'i{mi + 1}b', _qpos_tag(op.qubits) }, )) kraus_frontier += 1 else: raise ValueError(repr(op)) # coverage: ignore _positions(mi + 1, op.qubits) return tensors, qubit_frontier, positions