quimb.tensor.circuit.peps ========================= .. py:module:: quimb.tensor.circuit.peps .. autoapi-nested-parse:: PEPS simple-update circuit simulator. Classes ------- .. autoapisummary:: quimb.tensor.circuit.peps.CircuitPEPSSimpleUpdate Module Contents --------------- .. py:class:: CircuitPEPSSimpleUpdate(N=None, *, edges=None, gates=None, psi0=None, max_bond=None, cutoff=1e-10, renorm=False, gauge_smudge=1e-12, equilibrate_every=None, equilibrate_opts=None, gate_opts=None, dtype=None, to_backend=None, convert_eager=True, **circuit_opts) Bases: :py:obj:`quimb.tensor.circuit.exact.Circuit` Quantum circuit simulation keeping the state as a generic tensor network (a "PEPS" defined by an arbitrary graph of ``edges``) and applying gates with the simple update rule. The state always keeps a single tensor per site, with bonds only along the supplied edges; two-qubit gates are only supported on those edges. Bond singular values are tracked as Vidal-style gauges, which makes gate application and the computation of local expectations cheap and approximate. This is useful for circuits on lattices that build up more than 1D worth of entanglement, where an exact or MPS simulation is intractable but a truncated, gauged tensor network state is a good approximation. :param N: The number of qubits in the circuit. If not given it is inferred from the geometry. Supply it to pad the geometry up to ``N`` sites, including any that have no edges. :type N: int, optional :param edges: The edges defining the geometry of the PEPS. A bond is placed between each pair of sites, and two-qubit gates are only supported on these edges. Every site appearing in ``edges`` is included. If not given the geometry is taken from ``gates`` or ``psi0`` instead. :type edges: sequence[tuple[int, int]], optional :param gates: If ``edges`` is not given, infer the geometry from the two-qubit gates in this sequence. The gates are only inspected here, not applied, so you still pass them to :meth:`apply_gates` afterwards. :type gates: sequence, optional :param psi0: Supply the initial state directly instead of starting from the ``|00...0>`` product state. If ``edges`` is not given the geometry is read from the bonds of this state, and the bond gauges are seeded from it. Only a single seeding sweep is performed; unlike imaginary time simple update the gauge matters immediately, so for an arbitrary ``psi0`` you may want to call :meth:`equilibrate` once before applying gates. :type psi0: TensorNetworkGenVector, optional :param max_bond: The maximum bond dimension to truncate to when applying gates. :type max_bond: int, optional :param cutoff: The singular value cutoff to use when truncating after applying gates. :type cutoff: float, optional :param renorm: Whether to renormalize the singular values of a bond after each gate. The default ``False`` tracks the norm of the state rather than forcing it to one, which is the sensible choice for real time and general circuit dynamics. Set ``True`` to instead keep the state normalized after every gate, e.g. for the near-identity gates of imaginary time evolution. :type renorm: bool, optional :param gauge_smudge: Small value added to the gauges before they are multiplied in and inverted, for numerical stability with very small singular values. :type gauge_smudge: float, optional :param equilibrate_every: If given, automatically call :meth:`equilibrate` after every this many gates have been applied. :type equilibrate_every: int, optional :param equilibrate_opts: Default options forwarded to :meth:`equilibrate`. :type equilibrate_opts: dict, optional :param gate_opts: Default options to pass to ``gate_simple_`` such as ``max_bond`` and ``cutoff``. :type gate_opts: dict, optional :param dtype: If given, ensure the state tensors are cast to this data type. :type dtype: str, optional :param to_backend: If given, apply this function to the state tensors to convert them to a particular array backend. :type to_backend: callable, optional :param convert_eager: Whether to apply the ``dtype`` and ``to_backend`` conversions eagerly as each gate is applied. The default ``True`` matches the other running simulators (e.g. :class:`CircuitMPS`), since the simple update rule contracts each gate into the state immediately rather than building a lazy network to contract later. :type convert_eager: bool, optional .. attribute:: edges The unique edges defining the PEPS geometry. :type: tuple[tuple[hashable, hashable]] .. attribute:: sites The sites (qubit labels) of the PEPS. :type: tuple[hashable] .. attribute:: gauges The current Vidal-style bond gauges (singular values), keyed by bond index, updated in place as gates are applied. :type: dict[str, array] .. rubric:: Notes The gates applied must address qubits using the same labels that appear in ``edges``. Two-qubit gates are only supported along an existing edge. .. rubric:: Examples >>> import quimb.tensor as qtn >>> edges = [(0, 1), (1, 2), (0, 3), (1, 4), (2, 5), (3, 4), (4, 5)] >>> circ = qtn.CircuitPEPSSimpleUpdate(edges=edges, max_bond=8) >>> circ.apply_gates(gates) >>> peps = circ.psi .. seealso:: :py:obj:`CircuitMPS`, :py:obj:`CircuitDense` .. py:attribute:: _edges .. py:attribute:: _sites .. py:attribute:: _site_set .. py:attribute:: _edge_set .. py:attribute:: gauges .. py:attribute:: _equilibrate_every :value: None .. py:attribute:: _equilibrate_opts .. py:method:: copy() Copy the circuit, including its state, gauges and geometry. The base :class:`Circuit` copy does not know about the extra simple update attributes, so they are carried over here (the gauges are copied so the two circuits can be evolved independently). .. py:property:: edges The unique edges defining the PEPS geometry. .. py:property:: sites The sites (qubit labels) of the PEPS. .. py:method:: _init_state(N, dtype='complex128') .. py:method:: _apply_gate(gate, tags=None, **gate_opts) Apply a ``Gate`` to this ``Circuit``. This is the main method that all calls to apply a gate should go through. :param gate: The gate to apply. :type gate: Gate :param tags: Tags to add to the gate tensor(s). :type tags: str or sequence of str, optional .. py:method:: apply_gates(gates, progbar=False, **gate_opts) Apply a sequence of gates to this tensor network quantum circuit. :param gates: The sequence of gates to apply. :type gates: Sequence[Gate] or Sequence[Tuple] :param gate_opts: Supplied to :meth:`~quimb.tensor.circuit.Circuit.apply_gate`. .. py:method:: equilibrate(**gauge_opts) Re-gauge the whole state with the simple update rule, improving the consistency of the tracked bond gauges. This does not change the state represented, only the gauge, and can be called periodically between rounds of gates to keep the simple update approximation well behaved. The default options given at construction via ``equilibrate_opts`` are applied first, with any keyword arguments here taking precedence. :param gauge_opts: Supplied to :meth:`~quimb.tensor.tensor_core.TensorNetwork.gauge_all_simple_`, for example ``max_iterations`` and ``tol``. .. py:method:: local_expectation(G, where, *, max_distance=0, normalized=True, **contract_opts) Compute the local expectation value of operator ``G`` at the site(s) ``where``, using the simple update bond gauges to approximate the environment beyond ``max_distance``. :param G: The local operator. :type G: array_like :param where: The site or sites to compute the expectation at. A single site label (which may itself be a tuple, e.g. a 2D coordinate) is detected by membership in the set of sites. :type where: hashable or sequence[hashable] :param max_distance: How many graph hops of neighboring tensors to include in the local cluster used to approximate the reduced density matrix. The default ``0`` uses only the target site(s) and their gauges, matching :meth:`~quimb.tensor.tnag.core.TensorNetworkGenVector.compute_local_expectation_cluster`. :type max_distance: int, optional :param normalized: Whether to normalize by the local norm. :type normalized: bool, optional :param contract_opts: Supplied to :meth:`~quimb.tensor.tnag.core.TensorNetworkGenVector.compute_local_expectation_cluster`. :rtype: float .. py:method:: get_state(absorb_gauges=True) Return the current PEPS state, optionally absorbing the bond gauges. :param absorb_gauges: How to handle the tracked Vidal-style bond gauges. If ``True`` (the default) the gauges are absorbed, so the returned tensor network is the actual wavefunction (up to the simple update approximation). If ``False`` the gauges are added to the network as uncontracted diagonal tensors. If ``"return"`` the raw gauged network and a copy of the gauges are returned separately. The internal state is left untouched in every case. :type absorb_gauges: bool or "return", optional :returns: * **psi** (*TensorNetwork*) -- The current state. * **gauges** (*dict*) -- The current gauges, only if ``absorb_gauges == "return"``. .. py:property:: psi The PEPS tensor network state, with the simple update bond gauges absorbed back in so that it represents the actual wavefunction (a proper contraction of ``psi`` gives the state, up to the simple update approximation). The internal gauged form is left untouched. Shorthand for ``get_state(absorb_gauges=True)``. .. py:method:: calc_qubit_ordering(qubits=None) Get a order to measure ``qubits`` in, by greedily choosing whichever has the smallest reverse lightcone followed by whichever expands this lightcone *least*. :param qubits: The qubits to generate a lightcone ordering for, if ``None``, assume all qubits. :type qubits: None or sequence of int :returns: The order to 'measure' qubits in. :rtype: tuple[int] .. py:method:: _unsupported_exact(name) :abstractmethod: .. py:method:: to_dense(*args, **kwargs) Contract the gauged PEPS into a dense wavefunction, a column-vector ``qarray`` of length ``2**N`` ordered like :attr:`sites`, matching the output of :meth:`Circuit.to_dense`. This is the actual (approximate) state, so the cost grows exponentially with the number of qubits. Arguments are forwarded to :meth:`~quimb.tensor.tnag.core.TensorNetworkGenVector.to_dense`. .. py:method:: amplitude(*args, **kwargs) Get the amplitude coefficient of bitstring ``b``. .. math:: c_b = \langle b | \psi \rangle :param b: The bitstring to compute the transition amplitude for. :type b: str or sequence of int :param optimize: Contraction path optimizer to use for the amplitude, can be a non-reusable path optimizer as only called once (though path won't be cached for later use in that case). :type optimize: str, optional :param simplify_sequence: Which local tensor network simplifications to perform and in which order, see :meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`. :type simplify_sequence: str, optional :param simplify_atol: The tolerance with which to compare to zero when applying :meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`. :type simplify_atol: float, optional :param simplify_equalize_norms: Actively renormalize tensor norms during simplification. :type simplify_equalize_norms: bool, optional :param backend: Backend to perform the contraction with, e.g. ``'numpy'``, ``'cupy'`` or ``'jax'``. Passed to ``cotengra``. :type backend: str, optional :param dtype: Data type to cast the TN to before contraction. :type dtype: str, optional :param rehearse: If ``True``, generate and cache the simplified tensor network and contraction tree but don't actually perform the contraction. Returns a dict with keys ``"tn"`` and ``'tree'`` with the tensor network that will be contracted and the corresponding contraction tree if so. :type rehearse: bool or "tn", optional .. py:method:: sample(*args, **kwargs) Sample the circuit given by ``gates``, ``C`` times, using lightcone cancelling and caching marginal distribution results. This is a generator. This proceeds as a chain of marginal computations. Assuming we have ``group_size=1``, and some ordering of the qubits, :math:`\{q_0, q_1, q_2, q_3, \ldots\}` we first compute: .. math:: p(q_0) = \mathrm{diag} \mathrm{Tr}_{1, 2, 3,\ldots} | \psi_{0} \rangle \langle \psi_{0} | I.e. simply the probability distribution on a single qubit, conditioned on nothing. The subscript on :math:`\psi` refers to the fact that we only need gates from the causal cone of qubit 0. From this we can sample an outcome, either 0 or 1, if we call this :math:`r_0` we can then move on to the next marginal: .. math:: p(q_1 | r_0) = \mathrm{diag} \mathrm{Tr}_{2, 3,\ldots} \langle r_0 | \psi_{0, 1} \rangle \langle \psi_{0, 1} | r_0 \rangle I.e. the probability distribution of the next qubit, given our prior result. We can sample from this to get :math:`r_1`. Then we compute: .. math:: p(q_2 | r_0 r_1) = \mathrm{diag} \mathrm{Tr}_{3,\ldots} \langle r_0 r_1 | \psi_{0, 1, 2} \rangle \langle \psi_{0, 1, 2} | r_0 r_1 \rangle Eventually we will reach the 'final marginal', which we can compute as .. math:: |\langle r_0 r_1 r_2 r_3 \ldots | \psi \rangle|^2 since there is nothing left to trace out. :param C: The number of times to sample. :type C: int :param qubits: Which qubits to measure, defaults (``None``) to all qubits. :type qubits: None or sequence of int, optional :param order: Which order to measure the qubits in, defaults (``None``) to an order based on greedily expanding the smallest reverse lightcone. If specified it should be a permutation of ``qubits``. :type order: None or sequence of int, optional :param group_size: How many qubits to group together into marginals, the larger this is the fewer marginals need to be computed, which can be faster at the cost of higher memory. The marginal themselves will each be of size ``2**group_size``. :type group_size: int, optional :param max_marginal_storage: The total cumulative number of marginal probabilites to cache, once this is exceeded caching will be turned off. :type max_marginal_storage: int, optional :param seed: A random seed, passed to ``numpy.random.seed`` if given. :type seed: None or int, optional :param optimize: Contraction path optimizer to use for the marginals, shouldn't be a non-reusable path optimizer as called on many different TNs. Passed to :func:`cotengra.array_contract_tree`. :type optimize: str, optional :param backend: Backend to perform the marginal contraction with, e.g. ``'numpy'``, ``'cupy'`` or ``'jax'``. Passed to ``cotengra``. :type backend: str, optional :param dtype: Data type to cast the TN to before contraction. :type dtype: str, optional :param simplify_sequence: Which local tensor network simplifications to perform and in which order, see :meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`. :type simplify_sequence: str, optional :param simplify_atol: The tolerance with which to compare to zero when applying :meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`. :type simplify_atol: float, optional :param simplify_equalize_norms: Actively renormalize tensor norms during simplification. :type simplify_equalize_norms: bool, optional :Yields: **bitstrings** (*sequence of str*) .. py:method:: sample_chaotic(*args, **kwargs) Sample from this circuit, *assuming* it to be chaotic. Which is to say, only compute and sample correctly from the final marginal, assuming that the distribution on the other qubits is uniform. Given ``marginal_qubits=5`` for instance, for each sample a random bit-string :math:`r_0 r_1 r_2 \ldots r_{N - 6}` for the remaining :math:`N - 5` qubits will be chosen, then the final marginal will be computed as .. math:: p(q_{N-5}q_{N-4}q_{N-3}q_{N-2}q_{N-1} | r_0 r_1 r_2 \ldots r_{N-6}) = |\langle r_0 r_1 r_2 \ldots r_{N - 6} | \psi \rangle|^2 and then sampled from. Note the expression on the right hand side has 5 open indices here and so is a tensor, however if ``marginal_qubits`` is not too big then the cost of contracting this is very similar to a single amplitude. .. note:: This method *assumes* the circuit is chaotic, if its not, then the samples produced will not be an accurate representation of the probability distribution. :param C: The number of times to sample. :type C: int :param marginal_qubits: The number of qubits to treat as marginal, or the actual qubits. If an int is given then the qubits treated as marginal will be ``circuit.calc_qubit_ordering()[:marginal_qubits]``. :type marginal_qubits: int or sequence of int :param fix: Measurement results on other qubits to fix. These will be randomly sampled if ``fix`` is not given or a qubit is missing. :type fix: None or dict[int, str], optional :param seed: A random seed, passed to ``numpy.random.seed`` if given. :type seed: None or int, optional :param optimize: Contraction path optimizer to use for the marginal, can be a non-reusable path optimizer as only called once (though path won't be cached for later use in that case). :type optimize: str, optional :param backend: Backend to perform the marginal contraction with, e.g. ``'numpy'``, ``'cupy'`` or ``'jax'``. Passed to ``cotengra``. :type backend: str, optional :param dtype: Data type to cast the TN to before contraction. :type dtype: str, optional :param simplify_sequence: Which local tensor network simplifications to perform and in which order, see :meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`. :type simplify_sequence: str, optional :param simplify_atol: The tolerance with which to compare to zero when applying :meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`. :type simplify_atol: float, optional :param simplify_equalize_norms: Actively renormalize tensor norms during simplification. :type simplify_equalize_norms: bool, optional :Yields: *str* .. py:method:: sample_chaotic_rehearse(*args, **kwargs) Rehearse chaotic sampling (perform just the TN simplifications and contraction tree finding). :param marginal_qubits: The number of qubits to treat as marginal, or the actual qubits. If an int is given then the qubits treated as marginal will be ``circuit.calc_qubit_ordering()[:marginal_qubits]``. :type marginal_qubits: int or sequence of int :param result: Explicitly check the computational cost of this result, assumed to be all zeros if not given. :type result: None or dict[int, str], optional :param optimize: Contraction path optimizer to use for the marginal, can be a non-reusable path optimizer as only called once (though path won't be cached for later use in that case). :type optimize: str, optional :param simplify_sequence: Which local tensor network simplifications to perform and in which order, see :meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`. :type simplify_sequence: str, optional :param simplify_atol: The tolerance with which to compare to zero when applying :meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`. :type simplify_atol: float, optional :param simplify_equalize_norms: Actively renormalize tensor norms during simplification. :type simplify_equalize_norms: bool, optional :param dtype: Data type to cast the TN to before contraction. :type dtype: str, optional :returns: The contraction path information for the main computation, the key is the qubits that formed the final marginal. The value is itself a dict with keys ``'tn'`` - a representative tensor network - and ``'tree'`` - the contraction tree. :rtype: dict[tuple[int], dict] .. py:method:: partial_trace(*args, **kwargs) Perform the partial trace on the circuit wavefunction, retaining only qubits in ``keep``, and making use of reverse lightcone cancellation: .. math:: \rho_{\bar{q}} = Tr_{\bar{p}} |\psi_{\bar{q}} \rangle \langle \psi_{\bar{q}}| Where :math:`\bar{q}` is the set of qubits to keep, :math:`\psi_{\bar{q}}` is the circuit wavefunction only with gates in the causal cone of this set, and :math:`\bar{p}` is the remaining qubits. :param keep: The qubit(s) to keep as we trace out the rest. :type keep: int or sequence of int :param optimize: Contraction path optimizer to use for the reduced density matrix, can be a non-reusable path optimizer as only called once (though path won't be cached for later use in that case). :type optimize: str, optional :param simplify_sequence: Which local tensor network simplifications to perform and in which order, see :meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`. :type simplify_sequence: str, optional :param simplify_atol: The tolerance with which to compare to zero when applying :meth:`~quimb.tensor.tensor_core.TensorNetwork.full_simplify`. :type simplify_atol: float, optional :param simplify_equalize_norms: Actively renormalize tensor norms during simplification. :type simplify_equalize_norms: bool, optional :param backend: Backend to perform the marginal contraction with, e.g. ``'numpy'``, ``'cupy'`` or ``'jax'``. Passed to ``cotengra``. :type backend: str, optional :param dtype: Data type to cast the TN to before contraction. :type dtype: str, optional :param rehearse: If ``True``, generate and cache the simplified tensor network and contraction tree but don't actually perform the contraction. Returns a dict with keys ``"tn"`` and ``'tree'`` with the tensor network that will be contracted and the corresponding contraction tree if so. :type rehearse: bool or "tn", optional :rtype: array or dict .. py:property:: uni :abstractmethod: .. py:method:: get_psi_reverse_lightcone(where, keep_psi0=False) Get just the bit of the wavefunction in the reverse lightcone of sites in ``where`` - i.e. causally linked. :param where: The sites to propagate the the lightcone back from, supplied to :meth:`~quimb.tensor.circuit.Circuit.get_reverse_lightcone_tags`. :type where: int, or sequence of int :param keep_psi0: Keep the tensors corresponding to the initial wavefunction regardless of whether they are outside of the lightcone. :type keep_psi0: bool, optional :returns: **psi_lc** :rtype: TensorNetwork1DVector