Source code for tensorcircuit.templates.measurements

"""
Shortcuts for measurement patterns on circuit
"""
# circuit in, scalar out

from typing import Any

from ..circuit import Circuit
from ..cons import backend, dtypestr
from ..quantum import QuOperator
from .. import gates as G

Tensor = Any
Graph = Any  # nx.graph


[docs]def any_measurements( c: Circuit, structures: Tensor, onehot: bool = False, reuse: bool = False ) -> Tensor: """ This measurements pattern is specifically suitable for vmap. Parameterize the Pauli string to be measured. :example: .. code-block:: python c = tc.Circuit(3) c.rx(0, theta=1.0) c.cnot(0, 1) c.cnot(1, 2) c.ry(2, theta=-1.0) z0x2 = c.expectation([tc.gates.z(), [0]], [tc.gates.x(), [2]]) z0x2p1 = tc.templates.measurements.parameterized_measurements( c, tc.array_to_tensor(np.array([3, 0, 1])), onehot=True ) z0x2p2 = tc.templates.measurements.parameterized_measurements( c, tc.array_to_tensor(np.array([[0, 0, 0, 1], [1, 0, 0, 0], [0, 1, 0, 0]])), onehot=False, ) np.testing.assert_allclose(z0x2, z0x2p1) np.testing.assert_allclose(z0x2, z0x2p2) :param c: The circuit to be measured :type c: Circuit :param structures: parameter tensors determines what Pauli string to be measured, shape is [nwires, 4] if ``onehot`` is False and [nwires] if ``onehot`` is True. :type structures: Tensor :param onehot: defaults to False. If set to be True, structures will first go through onehot procedure. :type onehot: bool, optional :param reuse: reuse the wavefunction when computing the expectations, defaults to be False :type reuse: bool, optional :return: The expectation value of given Pauli string by the tensor ``structures``. :rtype: Tensor """ if onehot is True: structuresc = backend.cast(structures, dtype="int32") structuresc = backend.onehot(structuresc, num=4) structuresc = backend.cast(structuresc, dtype=dtypestr) else: structuresc = structures nwires = c._nqubits obs = [] for i in range(nwires): obs.append( [ G.Gate( sum( [ structuresc[i, k] * g.tensor for k, g in enumerate(G.pauli_gates) ] ) ), (i,), ] ) loss = c.expectation(*obs, reuse=reuse) # type: ignore return backend.real(loss)
parameterized_measurements = any_measurements
[docs]def any_local_measurements( c: Circuit, structures: Tensor, onehot: bool = False, reuse: bool = True ) -> Tensor: """ This measurements pattern is specifically suitable for vmap. Parameterize the local Pauli string to be measured. :example: .. code-block:: python c = tc.Circuit(3) c.X(0) c.cnot(0, 1) c.H(-1) basis = tc.backend.convert_to_tensor(np.array([3, 3, 1])) z0, z1, x2 = tc.templates.measurements.parameterized_local_measurements( c, structures=basis, onehot=True ) # -1, -1, 1 :param c: The circuit to be measured :type c: Circuit :param structures: parameter tensors determines what Pauli string to be measured, shape is [nwires, 4] if ``onehot`` is False and [nwires] if ``onehot`` is True. :type structures: Tensor :param onehot: defaults to False. If set to be True, structures will first go through onehot procedure. :type onehot: bool, optional :param reuse: reuse the wavefunction when computing the expectations, defaults to be True :type reuse: bool, optional :return: The expectation value of given Pauli string by the tensor ``structures``. :rtype: Tensor """ if onehot is True: structuresc = backend.cast(structures, dtype="int32") structuresc = backend.onehot(structuresc, num=4) structuresc = backend.cast(structuresc, dtype=dtypestr) else: structuresc = structures nwires = c._nqubits loss = [] for i in range(nwires): loss.append( c.expectation( ( G.Gate( sum( [ structuresc[i, k] * g.tensor for k, g in enumerate(G.pauli_gates) ] ) ), [i], ), reuse=reuse, ) ) return backend.real(backend.stack(loss))
parameterized_local_measurements = any_local_measurements
[docs]def operator_expectation(c: Circuit, hamiltonian: Any) -> Tensor: """ Evaluate Hamiltonian expectation where ``hamiltonian`` can be dense matrix, sparse matrix or MPO. :param c: The circuit whose output state is used to evaluate the expectation :type c: Circuit :param hamiltonian: Hamiltonian matrix in COO_sparse_matrix form :type hamiltonian: Tensor :return: a real and scalar tensor of shape [] as the expectation value :rtype: Tensor """ if isinstance(hamiltonian, QuOperator): return mpo_expectation(c, hamiltonian) elif backend.is_sparse(hamiltonian): return sparse_expectation(c, hamiltonian) else: w = c.state(form="ket") e = (backend.adjoint(w) @ hamiltonian @ w)[0, 0] return backend.real(e)
[docs]def sparse_expectation(c: Circuit, hamiltonian: Tensor) -> Tensor: """ Evaluate Hamiltonian expectation where ``hamiltonian`` is kept in sparse matrix form to save space :param c: The circuit whose output state is used to evaluate the expectation :type c: Circuit :param hamiltonian: Hamiltonian matrix in COO_sparse_matrix form :type hamiltonian: Tensor :return: a real and scalar tensor of shape [] as the expectation value :rtype: Tensor """ state = c.wavefunction(form="ket") tmp = backend.sparse_dense_matmul(hamiltonian, state) expt = backend.adjoint(state) @ tmp return backend.real(expt)[0, 0]
[docs]def mpo_expectation(c: Circuit, mpo: QuOperator) -> Tensor: """ Evaluate expectation of operator ``mpo`` defined in ``QuOperator`` MPO format with the output quantum state from circuit ``c``. :param c: The circuit for the output state :type c: Circuit :param mpo: MPO operator :type mpo: QuOperator :return: a real and scalar tensor of shape [] as the expectation value :rtype: Tensor """ mps = c.get_quvector() e = (mps.adjoint() @ mpo @ mps).eval_matrix() return backend.real(e)[0, 0]
[docs]def heisenberg_measurements( c: Circuit, g: Graph, hzz: float = 1.0, hxx: float = 1.0, hyy: float = 1.0, hz: float = 0.0, hx: float = 0.0, hy: float = 0.0, reuse: bool = True, ) -> Tensor: r""" Evaluate Heisenberg energy expectation, whose Hamiltonian is defined on the lattice graph ``g`` as follows: (e are edges in graph ``g`` where e1 and e2 are two nodes for edge e and v are nodes in graph ``g``) .. math:: H = \sum_{e\in g} w_e (h_{xx} X_{e1}X_{e2} + h_{yy} Y_{e1}Y_{e2} + h_{zz} Z_{e1}Z_{e2}) + \sum_{v\in g} (h_x X_v + h_y Y_v + h_z Z_v) :example: .. code-block:: python g = tc.templates.graphs.Line1D(n=5) c = tc.Circuit(5) c.X(0) energy = tc.templates.measurements.heisenberg_measurements(c, g) # 1 :param c: Circuit to be measured :type c: Circuit :param g: Lattice graph defining Heisenberg Hamiltonian :type g: Graph :param hzz: [description], defaults to 1.0 :type hzz: float, optional :param hxx: [description], defaults to 1.0 :type hxx: float, optional :param hyy: [description], defaults to 1.0 :type hyy: float, optional :param hz: [description], defaults to 0.0 :type hz: float, optional :param hx: [description], defaults to 0.0 :type hx: float, optional :param hy: [description], defaults to 0.0 :type hy: float, optional :param reuse: [description], defaults to True :type reuse: bool, optional :return: Value of Heisenberg energy :rtype: Tensor """ loss = 0.0 for e in g.edges: loss += ( g[e[0]][e[1]]["weight"] * hzz * c.expectation((G.z(), [e[0]]), (G.z(), [e[1]]), reuse=reuse) # type: ignore ) loss += ( g[e[0]][e[1]]["weight"] * hyy * c.expectation((G.y(), [e[0]]), (G.y(), [e[1]]), reuse=reuse) # type: ignore ) loss += ( g[e[0]][e[1]]["weight"] * hxx * c.expectation((G.x(), [e[0]]), (G.x(), [e[1]]), reuse=reuse) # type: ignore ) if hx != 0: for i in range(len(g.nodes)): loss += hx * c.expectation((G.x(), [i]), reuse=reuse) # type: ignore if hy != 0: for i in range(len(g.nodes)): loss += hy * c.expectation((G.y(), [i]), reuse=reuse) # type: ignore if hz != 0: for i in range(len(g.nodes)): loss += hz * c.expectation((G.z(), [i]), reuse=reuse) # type: ignore return backend.real(loss)
[docs]def spin_glass_measurements(c: Circuit, g: Graph, reuse: bool = True) -> Tensor: r""" Compute spin glass energy defined on graph ``g`` expectation for output state of the circuit ``c``. The Hamiltonian to be evaluated is defined as (first term is determined by node weights while the second term is determined by edge weights of the graph): .. math:: H = \sum_{v\in g} w_v Z_v + \sum_{e\in g} w_e Z_{e1} Z_{e2} :example: .. code-block:: python import networkx as nx # building the lattice graph for spin glass Hamiltonian g = nx.Graph() g.add_node(0, weight=1) g.add_node(1, weight=-1) g.add_node(2, weight=1) g.add_edge(0, 1, weight=-1) g.add_edge(1, 2, weight=-1) c = tc.Circuit(3) c.X(1) energy = tc.templates.measurements.spin_glass_measurements(c, g) print(energy) # 5.0 :param c: The quantum circuit :type c: Circuit :param g: The graph for spin glass Hamiltonian definition :type g: Graph :param reuse: Whether measure the circuit with reusing the wavefunction, defaults to True :type reuse: bool, optional :return: The spin glass energy expectation value :rtype: Tensor """ loss = 0 for e1, e2 in g.edges: loss += g[e1][e2].get("weight", 1.0) * c.expectation( (G.z(), [e1]), (G.z(), [e2]), reuse=reuse # type: ignore ) for n in g.nodes: loss += g.nodes[n].get("weight", 0.0) * c.expectation((G.z(), [n]), reuse=reuse) # type: ignore return backend.real(loss)