Source code for hiperwalk.quantum_walk.coined

import numpy as np
import scipy
import scipy.sparse
import networkx as nx
from .quantum_walk import QuantumWalk
from ..graph import SDMultigraph
from scipy.linalg import hadamard, dft
from . import _pyneblina_interface as nbl

[docs] class Coined(QuantumWalk): r""" Manage instances of coined quantum walks on arbitrary graphs. The class provides methods to handle and generate operators in the coined quantum walk model. It also facilitates the simulation of coined quantum walks on graphs. For additional details about coined quantum walks, refer to the Notes Section. Parameters ---------- graph Graph on which the quantum walk takes place. Two types of entries are accepted: * Simple graph (:class:`hiperwalk.Graph`); * Multigraph (:class:`hiperwalk.Multigraph`); A symmetric directed multigraph is created based on the input. **kwargs : optional Optional arguments for setting the non-default evolution operator. See :meth:`set_evolution`. Raises ------ TypeError if ``adj_matrix`` is not an instance of :class:`scipy.sparse.csr_array`. See Also -------- set_evolution set_shift set_coin Notes ----- The coined quantum walk model is a quantum analog of classical random walks, incorporating an additional quantum coin-toss mechanism. It uses an extra quantum internal degree of freedom, represented by the coin state, to determine the direction of the walker's movement on a graph. In the coined model, the graph is interpreted as a directed graph as follows: Each edge in :math:`G(V, E)` connecting two distinct vertices translates into a pair of arcs in the directed graph :math:`\vec{G}(V, \mathcal{A})`, where .. math:: \begin{align*} \mathcal{A} = \bigcup_{v_k v_\ell\, \in E} \{(v_k, v_\ell), (v_\ell, v_k)\}. \end{align*} .. note:: The order of arcs depends on the order of neighbors. Arcs are represented using either the (tail,head) notation or numerical labels. In the :obj:`Graph` class, the arc labels are ordered such that for two arcs, :math:`(v_i, v_j)` and :math:`(v_k, v_\ell)`, with labels :math:`a_1` and :math:`a_2` respectively, :math:`a_1 < a_2` if and only if :math:`i < k` or (:math:`i = k` and :math:`j < \ell`). Loops are depicted as single arcs, affecting the dimension of the associated Hilbert space. In coined quantum walks, the weights of arcs do not influence the dynamics. The computational basis is composed of the graph's arc set. For simple graphs, the cardinality of the computational basis is :math:`2|E|`, where :math:`E` represents the graph's edge set. When a loop is added to the graph, the cardinality of the computational basis increases by one for each loop. The arcs are arranged within the computational basis to ensure that the coin operator adopts a block-diagonal matrix form. For additional information on the arc ordering, please consult the respective graph descriptions. For a more detailed understanding of coined quantum walks, refer to Section 7.2: Coined Walks on Arbitrary Graphs, found in the book 'Quantum Walks and Search Algorithms' [1]_. For example, the graph :math:`G(V, E)` shown in Figure 1 has an adjacency matrix ``adj_matrix``. .. testsetup:: import numpy as np >>> adj_matrix = np.array([ ... [0, 1, 0, 0], ... [1, 0, 1, 1], ... [0, 1, 0, 1], ... [0, 1, 1, 0]]) >>> adj_matrix array([[0, 1, 0, 0], [1, 0, 1, 1], [0, 1, 0, 1], [0, 1, 1, 0]]) .. graphviz:: ../../graphviz/graph-example.dot :align: center :layout: neato :caption: Figure 1 Suppose that, for this graph, the neighbors are given in ascending order. Then, the arcs of the associated digraph in the arc notation are >>> arcs = [(i, j) for i in range(4) ... for j in range(4) if adj_matrix[i,j] == 1] >>> arcs [(0, 1), (1, 0), (1, 2), (1, 3), (2, 1), (2, 3), (3, 1), (3, 2)] Note that ``arcs`` is already sorted, hence the associated numeric labels are >>> arcs_labels = {arcs[i]: i for i in range(len(arcs))} >>> arcs_labels {(0, 1): 0, (1, 0): 1, (1, 2): 2, (1, 3): 3, (2, 1): 4, (2, 3): 5, (3, 1): 6, (3, 2): 7} The numeric labels are depicted in Figure 2. .. graphviz:: ../../graphviz/graph-arcs.dot :align: center :layout: neato :caption: Figure 2 If we insert the labels of the arcs into the adjacency matrix, we obtain matrix ``adj_labels`` as follows: >>> adj_labels = [[arcs_labels[(i,j)] if (i,j) in arcs_labels ... else '' for j in range(4)] ... for i in range(4)] >>> adj_labels = np.matrix(adj_labels) >>> adj_labels matrix([['', '0', '', ''], ['1', '', '2', '3'], ['', '4', '', '5'], ['', '6', '7', '']], dtype='<U21') Note that, intuitively, the arcs are labeled in left-to-right and top-to-bottom fashion. References ---------- .. [1] R. Portugal. "Quantum walks and search algorithms", 2nd edition, Springer, 2018. """ # static attributes. # The class must be instantiated once, otherwise are dicts are empty. # The class must be instantiated so the interpreter knows # the memory location for the function pointers. _coin_funcs = dict() _valid_kwargs = dict()
[docs] def __init__(self, graph=None, **kwargs): # create symmetric directed multigraph from input sdmg = SDMultigraph(graph) super().__init__(graph=sdmg) self.hilb_dim = self._graph.number_of_arcs() # Specific coined quantum walk attributes self._shift = None self._coin = None self._oracle_coin = [] # create static dicts if not bool(Coined._valid_kwargs): Coined._valid_kwargs = { 'shift': Coined._get_valid_kwargs(self._set_shift), 'coin': Coined._get_valid_kwargs(self._set_coin), 'marked': Coined._get_valid_kwargs(self._set_marked), 'evolution': Coined._get_valid_kwargs( self._set_evolution) } # dict with valid coins as keys and the respective # function pointers. if not bool(Coined._coin_funcs): # assign static attribute Coined._coin_funcs = { 'fourier': Coined._fourier_coin, 'grover': Coined._grover_coin, 'hadamard': Coined._hadamard_coin, 'identity': Coined._identity_coin, 'minus_fourier': Coined._minus_fourier_coin, 'minus_grover': Coined._minus_grover_coin, 'minus_hadamard': Coined._minus_hadamard_coin, 'minus_identity': Coined._minus_identity_coin } self.set_evolution(**kwargs)
def _set_flipflop_shift(self): r""" Creates the flipflop shift operator (:math:`S`) based on the ``_graph`` attribute. The operator is configured for future use. If an evolution operator was set earlier, it will be unset to maintain coherence. """ g = self._graph num_vert = g.number_of_vertices() num_arcs = g.number_of_arcs() if g.is_underlying_simple(): S_cols = [g.arc_number((j, i)) for i in range(num_vert) for j in g.neighbors(i)] else: S_cols = [g.arc_number((j, i, e)) for i in range(num_vert) for j in g.neighbors(i) for e in range(g.number_of_edges(j, i))] # Using csr_array((data, indices, indptr), shape) # Note that there is only one entry per row and column S = scipy.sparse.csr_array( ( np.ones(num_arcs, dtype=np.int8), S_cols, np.arange(num_arcs+1) ), shape=(num_arcs, num_arcs) ) self._shift = S
[docs] def has_persistent_shift(self): r""" Check whether the persistent shift operator is defined for the current graph. Returns ------- bool Notes ----- The persistent shift is sometimes called *moving shift*. The persistent shift operator can only be defined in a meaningful way for certain specific graphs. For instance, for graphs that can be embedded onto a plane so that directions such as left, right, up, and down can be referred to naturally. """ return self._graph.previous_arc(0) is not None
def _set_persistent_shift(self): r""" Creates the persistent shift operator (:math:`S`) based on the ``_graph`` attribute. The operator is set for future usage. If an evolution operator was set previously, it is unset for coherence. """ num_arcs = self._graph.number_of_arcs() S_cols = [self._graph.previous_arc(i) for i in range(num_arcs)] # Using csr_array((data, indices, indptr), shape) # Note that there is only one entry per row and column S = scipy.sparse.csr_array( ( np.ones(num_arcs, dtype=np.int8), S_cols, np.arange(num_arcs+1) ), shape=(num_arcs, num_arcs) ) self._shift = S def _set_shift(self, shift='default'): valid_vals = ['default', 'flipflop', 'persistent', 'ff', 'p'] # check if string try: shift = shift.lower() if shift not in valid_vals: raise ValueError( "Invalid `shift` value. Expected one of " + str(valid_vals) + ". But received '" + str(shift) + "' instead." ) if shift == 'default': shift = 'p' if self.has_persistent_shift() else 'ff' if shift == 'ff': shift = 'flipflop' elif shift == 'p': shift = 'persistent' if str(self._shift) != shift: if shift == 'flipflop': self._set_flipflop_shift() else: self._set_persistent_shift() return True return False except AttributeError: pass # check if explict matrix try: shift[0][0] #if this works, then shift is numpy or list of list # convert to sparse shift = scipy.sparse.csr_array(shift) except NotImplementedError: # already sparse pass if (len(shift.shape) != 2 or shift.shape[0] != shift.shape[1]): raise TypeError('Explicit coin is not a square matrix.') if (id(self._shift) != id(shift)): self._shift = shift return True return False
[docs] def set_shift(self, shift='default'): r""" Set the shift operator. Defines either the flipflop or the persistent shift operator. Following this, the evolution operator updates accordingly. Parameters ---------- shift: {'default', 'flipflop', 'persistent', 'ff', 'p'} Whether to create the flip flop or the persistent shift. By default, creates the persistent shift if it is defined; otherwise creates the flip flop shift. Argument ``'ff'`` is an alias for ``'flipflop'``. Argument ``'p'`` is an alias for ``'persistent'``. Raises ------ AttributeError If ``shift='persistent'`` and the persistent shift operator cannot be defined. See Also -------- has_persistent_shift set_evolution Notes ----- .. note:: Check :class:`Coined` Notes for details about the computational basis. The flip-flop shift operator :math:`S` is defined as .. math:: S \ket{v, u} = \ket{u, v}, in the context of arc notation, where :math:`(v, u)` and :math:`(u, v)` represent opposite arcs. This can be equivalently expressed as :math:`S\ket{i} = \ket{j}`, where :math:`i` is the label of the arc :math:`(v, u)` and :math:`j` is the label of the arc :math:`(u, v)`. The flip-flop shift satisfies the property :math:`S^2 = I`. The persistent shift, also known as the *moving shift*, is defined for graphs with a clear notion of direction or rotation. When the shift operator is applied repeatedly, it causes the walker to continue moving persistently in the same direction. Unlike the flip-flop shift, the persistent shift does not satisfy :math:`S^2 = I`. For a more comprehensive understanding of the shift operator, refer to Section 7.2: Coined Walks on Arbitrary Graphs in the book "Quantum Walks and Search Algorithms" [1]_. References ---------- .. [1] R. Portugal. "Quantum walks and search algorithms", 2nd edition, Springer, 2018. Examples -------- Consider the Graph presented in the :class:`Graph` Notes Section example. The corresponding flip-flop shift operator is .. testsetup:: import numpy as np import scipy.sparse from sys import path path.append('../..') .. doctest:: >>> import hiperwalk as hpw >>> A = scipy.sparse.csr_array([[0, 1, 0, 0], ... [1, 0, 1, 1], ... [0, 1, 0, 1], ... [0, 1, 1, 0]]) >>> qw = hpw.Coined(hpw.Graph(A)) >>> qw.set_shift(shift='flipflop') >>> S = qw.get_shift().todense() >>> S array([[0, 1, 0, 0, 0, 0, 0, 0], [1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 1, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1], [0, 0, 0, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 0, 0]], dtype=int8) Note that as required, :math:`S^2 = I`, :math:`S \ket 0 = \ket 1`, :math:`S \ket 1 = \ket 0`, :math:`S \ket 2 = \ket 4`, :math:`S \ket 4 = \ket 2`, etc. .. doctest:: >>> (S @ S == np.eye(8)).all() # True by definition True >>> S @ np.array([1, 0, 0, 0, 0, 0, 0, 0]) # S|0> = |1> array([0, 1, 0, 0, 0, 0, 0, 0]) >>> S @ np.array([0, 1, 0, 0, 0, 0, 0, 0]) # S|1> = |0> array([1, 0, 0, 0, 0, 0, 0, 0]) >>> S @ np.array([0, 0, 1, 0, 0, 0, 0, 0]) # S|2> = |4> array([0, 0, 0, 0, 1, 0, 0, 0]) >>> S @ np.array([0, 0, 0, 0, 1, 0, 0, 0]) # S|4> = |2> array([0, 0, 1, 0, 0, 0, 0, 0]) """ self.set_evolution(shift=shift, coin=self._coin, marked=self._marked)
[docs] def get_shift(self): r""" Retrieve the shift operator. Shift operator used for constructing the evolution operator. Returns ------- :class:`scipy.sparse.csr_array` See Also -------- set_shift """ return self._shift
def _set_coin(self, coin='default'): try: if len(coin.shape) != 2: raise TypeError('Explicit coin is not a matrix.') # explicit coin if not scipy.sparse.issparse(coin): coin = scipy.sparse.csr_array(coin) self._coin = coin return except AttributeError: pass coin_list, undefined_coin = self._coin_to_list(coin) if undefined_coin: raise ValueError('Coin was not specified for all vertices.') self._coin = coin_list
[docs] def set_coin(self, coin='default'): """ Set the coin operator based on the graph's structure. Builds a coin operator considering the degree of each vertex. The same coin can be applied to all vertices, or multiple coins can be assigned, each to a specific subset of vertices. After setting the coin operator, the evolution operator is updated accordingly. Parameters ---------- coin Coin to be used. Several types of arguments are acceptable. * str : coin type Type of the coin to be used. The following are valid entries. * 'default', 'd' : default coin, * 'fourier', 'F' : Fourier coin, * 'grover', 'G' : Grover coin, * 'hadamard', 'H' : Hadamard coin, * 'identity', 'I' : Identity, * 'minus_fourier', '-F' : Fourier coin with negative phase, * 'minus_grover', '-G' : Grover coin with negative phase, * 'minus_hadamard', '-H' : Hadamard coin with negative phase, * 'minus_identity', '-I' : Identity with negative phase. * list of str List of the coin types to be used. Expects list with 'number of vertices' entries. * dict A dictionary with structure ``{coin_type : list_of_vertices}``. That is, with any valid coin type as key and the list of vertices to be applied as values. If ``list_of_vertices = []``, the respective ``coin_type`` is applied to all vertices that were not explicitly listed. * :class:`scipy.sparse.csr_array` The explicit coin operator. See Also -------- set_evolution Notes ----- The output of this method is a block-diagonal operator, which results from the specific ordering of arcs in the computational basis (refer to the Notes in :class:`Coined` for more details). Each block is associated with a :math:`\deg(v)`-dimensional ``coin``. As a result, there are :math:`|V|` blocks in total. Note that a loop at a vertex :math:`u` is treated as the arc :math:`(u,u)`, contributing an additional one to the degree of :math:`u`. """ self.set_evolution(shift=self._shift, coin=coin, marked=self._marked)
[docs] def default_coin(self): r""" Returns the default coin name. The default coin name depends on the graph on which the quantum walk occurs. In general, the default coin is ``'grover'``. For the :class:`hiperwalk.Line` and :class:`hiperwalk.Cycle`, the default coin is ``'hadamard'``. Returns ------- str Notes ----- Although the default coin depends on the graph, the coin is not an attribute of the graph. That is, it is part of graph theory. In addition, it does not make sense to have a default coin in continuous-time quantum walks. Hence, the method is implemented in the Coined class instead of each graph class. Following duck typing, we do not verify if the graph is an instance of the line or cycle. Instead, we verify if the maximum degree on the graph is at most 2. """ degs = list(map(self._graph.degree, np.arange(self._graph.number_of_vertices()))) if max(degs) <= 2: return 'hadamard' return 'grover'
def _coin_to_valid_name(self, coin): r""" Convert a string to its respective valid coin name. """ s = coin if s == 'default' or s == 'd': s = self.default_coin() if len(s) <= 2: prefix = 'minus_' if len(coin) == 2 else '' abbrv = {'F': 'fourier', 'G' : 'grover', 'H': 'hadamard', 'I': 'identity'} s = prefix + abbrv[s[-1]] if s not in Coined._coin_funcs.keys(): raise ValueError( 'Invalid coin. Expected any of ' + str(list(Coined._coin_funcs.keys())) + ', ' + "but received '" + str(coin) + "'." ) return s def _coin_to_list(self, coin): r""" Convert str, list of str or dict to valid coin list. See Also -------- set_coin """ num_vert = self._graph.number_of_vertices() coin_list = [] undefined_coin = False if isinstance(coin, str): coin_list = [self._coin_to_valid_name(coin)] * num_vert elif isinstance(coin, dict): coin_list = [''] * num_vert for key in coin: coin_name = self._coin_to_valid_name(key) value = coin[key] if value != []: if not hasattr(value, '__iter__'): raise TypeError("Expected a list of vertices. " + "Received " + str(type(value)) + " " + "with value " + str(value) + " instead.") for v in value: coin_list[self._graph.vertex_number(v)] = coin_name else: coin_list = [coin_name if coin_list[i] == '' else coin_list[i] for i in range(num_vert)] undefined_coin = '' in coin_list else: #list of coins if len(coin) != num_vert: raise ValueError('There were ' + str(len(coin)) + ' coins specified. Expected ' + str(num_vert) + 'coins instead.') coin_list = list(map(self._coin_to_valid_name, coin)) return coin_list, undefined_coin @staticmethod def _fourier_coin(dim): return dft(dim, scale='sqrtn') @staticmethod def _grover_coin(dim): return np.array(2/dim * np.ones((dim, dim)) - np.identity(dim)) @staticmethod def _hadamard_coin(dim): return hadamard(dim) / np.sqrt(dim) @staticmethod def _identity_coin(dim): return scipy.sparse.eye(dim) @staticmethod def _minus_fourier_coin(dim): return -Coined._fourier_coin(dim) @staticmethod def _minus_grover_coin(dim): return -Coined._grover_coin(dim) @staticmethod def _minus_hadamard_coin(dim): return -Coined._hadamard_coin(dim) @staticmethod def _minus_identity_coin(dim): return -np.identity(dim) def _set_marked(self, marked=[]): try: marked.get(0) #throws exception if list except AttributeError: # list if len(marked) > 0: marked = {'-I': marked} else: marked = {} coin_list, _ = self._coin_to_list(marked) dict_values = marked.values() vertices = [vlist if hasattr(vlist, '__iter__') else [vlist] for vlist in dict_values] vertices = [v for vlist in vertices for v in vlist ] marked = vertices super()._set_marked(marked=marked) self._oracle_coin = coin_list
[docs] def set_marked(self, marked=[]): r""" Set the marked vertices. When a set or list of vertices is provided, they are set as marked. The evolution operator is updated accordingly. If a dictionary is passed, the coin of those vertices is replaced solely for the purpose of generating the evolution operator. This can only be done if the set coin operator is not an explicit matrix. Parameters ---------- marked : list of vertices or dict list of vertices to be marked and how they are going to be marked. * list of vertices Given vertices are set as marked. The coin for those vertices is '-I'. * dict A dictionary with structure ``{coin_type : list_of_vertices}``. Analogous to the one accepted by :meth:`set_coin`. See Also -------- set_coin set_evolution """ self.set_evolution(shift=self._shift, coin=self._coin, marked=marked)
def _coin_list_to_explicit_coin(self, coin_list): num_vert = self._graph.number_of_vertices() degree = self._graph.degree blocks = [self._coin_funcs[coin_list[v]](degree(v)) for v in range(num_vert)] C = scipy.sparse.block_diag(blocks, format='csr') return scipy.sparse.csr_array(C)
[docs] def get_coin(self): r""" Retrieve the coin used in the creation of the evolution operator. Returns ------- :class:`scipy.sparse.csr_array` See Also -------- set_coin """ if scipy.sparse.issparse(self._coin): if len(self._marked) == 0: return self._coin # if coin was explicitly set, # and there are different coins for the marked vertices, # change them. def get_block(vertex): g = self._graph neighbors = g.neighbors(vertex) # TODO: this technique wont work after the behavior of # neighbors() change. a1 = g.arc_number((vertex, neighbors[0])) a2 = g.arc_number((vertex, neighbors[-1])) # arc order may change start = min(a1, a2) end = max(a1, a2) + 1 return scipy.sparse.csr_array(self._coin[start:end, start:end]) num_vert = self._graph.number_of_vertices() degree = self._graph.degree oracle_coin = self._oracle_coin coin_funcs = Coined._coin_funcs blocks = [coin_funcs[oracle_coin[v]](degree(v)) if oracle_coin[v] != '' else get_block(v) for v in range(num_vert)] C = scipy.sparse.block_diag(blocks, format='csr') return scipy.sparse.csr_array(C) oracle_coin = self._oracle_coin if len(oracle_coin) > 0: coin = self._coin coin_list = [oracle_coin[i] if oracle_coin[i] != '' else coin[i] for i in range(len(coin))] else: coin_list = self._coin return self._coin_list_to_explicit_coin(coin_list)
def _set_evolution(self): # TODO: Check if matrix is sparse in pynelibna interface # TODO: Check if matrices are deleted from memory and GPU. U = None S = self.get_shift() C = self.get_coin() if nbl.get_hpc() is not None: from warnings import warn warn('HPC sparse matrix multiplication is not implemented. ' + 'Using standard scipy multiplication instead.') # # TODO: implement sparse matrix multiplication with hpc # S = S.todense() # C = C.todense() # nbl_S = nbl.send_matrix(S) # del S # nbl_C = nbl.send_matrix(C) # del C # nbl_C = nbl.multiply_matrices(nbl_S, nbl_C) # del nbl_S # U = nbl.retrieve_matrix(nbl_C) # del nbl_C # U = scipy.sparse.csr_array(U) U = S @ C self._evolution = U return U
[docs] def set_evolution(self, **kwargs): """ Set the evolution operator. Establishes the shift operator, coin operator, and the marked vertices. They are set using the appropriate ``**kwargs``. If ``**kwargs`` is empty, the default arguments are used. Subsequently, the evolution operator is constructed by multiplying the shift and coin operators. If the coin operator is given as an explicit matrix, its definition remains unaltered even in the presence of marked vertices. However, if the coin operator is defined using coin names, any marked vertices will prompt an update to the coin operator. Specifically, the coin names for each marked vertex will default to a replacement with -I, unless an alternative substitution is provided. Parameters ---------- **kwargs : dict, optional Arguments for setting the evolution operator. Accepts any valid keywords from :meth:`set_shift` :meth:`set_coin`, and :meth:`set_marked`. See Also -------- set_shift set_coin set_marked get_evolution Notes ----- The evolution operator is given by .. math:: U = SC where :math:`S` is the shift operator, and :math:`C` is the coin operator. If there are any marked vertices, the coin operator is modified accordingly [1]_. When the coin operator is set as an explicit matrix, it remains unaltered by marked vertices. However, if it's not provided in matrix form (e.g., as a list of coins), the coin for each marked vertex will be substituted based on the most recent :meth:`set_marked` invocation. .. warning:: Sparse matrix multipliation is not supported yet. Converting all matrices to dense. Then converting back to sparse. This uses unnecessary memory and computational time. References ---------- .. [1] R. Portugal. "Quantum walks and search algorithms", 2nd edition, Springer, 2018. """ S_kwargs = Coined._filter_valid_kwargs( kwargs, Coined._valid_kwargs['shift']) C_kwargs = Coined._filter_valid_kwargs( kwargs, Coined._valid_kwargs['coin']) R_kwargs = Coined._filter_valid_kwargs( kwargs, Coined._valid_kwargs['marked']) self._set_shift(**S_kwargs) self._set_coin(**C_kwargs) self._set_marked(**R_kwargs) self._set_evolution()
[docs] def probability_distribution(self, states): r""" Compute the probability distribution of given state(s). The probability of the walker being found on each vertex for the given state(s). Parameters ---------- states : :class:`numpy.ndarray` The state(s) used to compute the probabilities. It may be a single state or a list of states. Returns ------- probabilities : :class:`numpy.ndarray` If ``states`` is a single state, ``probabilities[v]`` is the probability of the walker being found on vertex ``v``. If ``states`` is a list of states, ``probabilities[i][v]`` is the probability of the walker beign found at vertex ``v`` in ``states[i]``. See Also -------- probability simulate Notes ----- The probability for a given vertex :math:`u` is calculated as the sum of the absolute squares of the amplitudes of the arcs originating from :math:`u`. More precisely, if the state of the walker is .. math:: \sum_{(v, w) \in \mathcal{A}(\vec G)} \alpha_{v,w} \ket{v,w}, where :math:`\mathcal{A}(\vec G)` is the set of arcs of the symmetric directed graph formed by replacing each edge in :math:`G` with two arcs, one for each direction, then the probability associated with vertex :math:`u` is given by .. math:: \sum_{v \in N(u)}|\alpha_{u, v}|^2, with :math:`N(u)` being the set of out neighbors of :math:`u`. A loop at :math:`u` is the arc :math:`(u,u)`. The probability distribution, which is returned by this method as a ``numpy.ndarray``, is the collection of these probabilities for all vertices. """ try: states.shape == 1 except: states = np.array(states, copy=False) if len(states.shape) == 1: states = np.array([states], copy=False) graph = self._graph num_vert = graph.number_of_vertices() prob = np.array([[Coined._elementwise_probability( states[i, graph.arcs_with_tail(v)]).sum() for v in range(num_vert)] for i in range(len(states))]) return prob
[docs] def state(self, entries): """ Generates a valid state. The state corresponds to the walker being in a superposition of the ``entries``. The final state is normalized in order to be a unit vector. Parameters ---------- entries : list of entry Each entry is a tuple (or array). An entry can be specified in two different ways: ``(amplitude, (tail, head))``, and ``(amplitude, arc_number)``. amplitude The amplitude of the given entry. tail The vertex corresponding to the position of the walker in the superposition. In other words, the tail of the arc. The tuple ``(tail, head)`` must be a valid arc. head The vertex to which the coin is pointing. The tuple ``(tail, head)`` must be a valid arc. arc_number The numerical arc label with respect to the arc ordering given by the computational basis. Notes ----- If there are repeated arcs, the amplitude of the last entry is used. Examples -------- The following commands generate the same state on a ``(dim, dim)``-dimensional grid. .. testsetup:: from sys import path path.append('../..') import hiperwalk as hpw import numpy as np dim = 10 g = hpw.Grid((dim, dim)) qw = hpw.Coined(graph=g) >>> psi = qw.state([(1, (0, 1)), [1, 1], (1, 2)]) >>> psi1 = qw.state([(1, ([0, 0], [0, 1])), ... [1, [(0, 0), (dim - 1, 0)]], ... (1, (0, 1))]) >>> psi2 = qw.state([(1, [[0, 0], [0, 1]]), ... [1, ((0, 0), (dim - 1, 0))], ... (1, [0, 1])]) >>> np.all(psi == psi1) True >>> np.all(psi1 == psi2) True """ if len(entries) == 0: raise TypeError("Entries were not specified.") dtype = np.array([entry[0] for entry in entries]).dtype dtype = (complex if np.issubdtype(dtype, np.complexfloating) else float) state = np.zeros(self.hilb_dim, dtype=dtype) for ampl, arc in entries: state[self._graph.arc_number(arc)] = ampl return self._normalize(state)
[docs] def ket(self, arc): r""" Create a computational basis state. Parameters ---------- arc: int or tuple of int The ket label. There are two different labels acceptable. (tail, head) The arc notation. arc_number The label of the arc. The number according to the computational basis order. """ ket = np.zeros(self.hilb_dim, dtype=float) ket[self._graph.arc_number(arc)] = 1 return ket
[docs] def uniform_state(self, vertices=None, arcs=None): r""" Create a uniform state. The uniform state is a unit vector with entries that have the same real amplitudes. If both ``vertices is None`` and ``arcs is None``, create the uniform superposition of all arcs. Otherwise, create the uniform superposition of all arcs in ``arcs`` and all the arcs with tail in ``vertices``. Parameters ---------- vertices: list of vertices, default=None If ``vertices is not None``, every arc with tail in ``vertices`` is added to the uniform superposition. arcs: list of arcs, default=None If ``arcs is not None``, every arc in ``arcs`` is added to the uniform superposition. Returns ------- :obj:`numpy.ndarray` Notes ----- An example of the uniform state is .. math:: \ket{d} = \frac{1}{\sqrt{N}} \sum_{i = 0}^{N - 1} \ket{i} where :math:`N` represents the dimension of the Hilbert space, and :math:`i` is a label within the graph. In the continuous-time quantum walk model, :math:`i` corresponds to the label of a vertex, while in the coined quantum walk model, :math:`i` is the label of an arc. """ if vertices is None and arcs is None: return (np.ones(self.hilb_dim, dtype=float) / np.sqrt(self.hilb_dim)) # uniform superposition of the given arcs state = np.zeros(self.hilb_dim) if arcs is not None: state[[self._graph.arc_number(a) for a in arcs]] = 1 # uniform superposition of the given vertices if vertices is not None: for v in vertices: state[self._graph.arcs_with_tail(v)] = 1 return state / np.sqrt(np.sum(state))
[docs] def probability(self, states, vertices): r""" Computes the sum of probabilities for the specified vertices. Computes the probability of the walker being located on a vertex within the set of provided vertices, given that the walk is on specified states. Parameters ---------- states : :class:`numpy.ndarray` The state(s) used to compute the probability. ``states`` can be a single state or a list of states. vertices: list of int The subset of vertices. Returns ------- probabilities : float or :class:`numpy.ndarray` float: If ``states`` is a single state. :class:`numpy.ndarray`: If ``states`` is a list of states, ``probabilities[i]`` is the probability corresponding to the ``i``-th state. See Also -------- simulate Notes ----- The probability of finding the walker on vertex :math:`v`, given the state of the walk :math:`|\psi \rangle`, is calculated as .. math:: \sum_{\substack{a\in{\mathcal{A}}\\ \operatorname{tail}(a)=v}} \, \left|{\langle a} | {\psi \rangle}\right|^2, where :math:`\mathcal{A}` denotes the set of arcs. """ return super().probability(states, vertices)