Source code for hiperwalk.quantum_walk.continuous_time

import numpy as np
import scipy.sparse
import scipy.linalg
from .quantum_walk import QuantumWalk
from . import _pyneblina_interface as nbl

[docs] class ContinuousTime(QuantumWalk): r""" Manage instances of continuous-time quantum walks on graphs. For further implementation details and theoretical background, refer to the Notes Section. Parameters ---------- graph : Graph on which the quantum walk takes place. There are two acceptable inputs: * Simple graph (:class:`hiperwalk.Graph`); * Weighted graph (:class:`hiperwalk.WeightedGraph`); **kwargs : optional Additional arguments to set the Hamiltonian and evolution operator. See Also -------- set_hamiltonian set_evolution Notes ----- The continuous-time quantum walk model represents quantum particles evolving on a graph in continuous time, as directed by the Schrödinger equation. The Hamiltonian is usually chosen as the adjacency matrix or the Laplacian of the graph. A positive parameter gamma acts as a weighting factor for the Hamiltonian, adjusting the walk's spreading rate. When marked vertices are present, the Hamiltonian is suitably modified. The computational basis associated with a graph :math:`G(V, E)` comprising :math:`n` vertices :math:`v_0, \ldots, v_{n-1}` is spanned by the states :math:`\ket{i}` for :math:`0 \leq i < n`, where :math:`\ket{i}` describes the walker's position as vertex :math:`v_i`. The adjacency matrix of :math:`G(V, E)` is the :math:`n`-dimensional matrix :math:`A` such that .. math:: A_{i,j} = \begin{cases} 1, \text{ if } v_i \text{ is adjacent to } v_j,\\ 0, \text{ otherwise.} \end{cases} Similarly, the Laplacian matrix is defined as .. math:: L_{i,j} = \begin{cases} \text{degree}(v_i), \text{ if } i=j,\\ -1, \text{ if } i\neq j \text{ and } v_i \text{ is adjacent to } v_j,\\ 0, \text{ otherwise.} \end{cases} The Hamiltonian's formulation is detailed in :meth:`hiperwalk.ContinuousTime.set_hamiltonian`, depending on the choice between the adjacency or Laplacian matrix, along with the positioning of the marked vertices. The :class:`hiperwalk.ContinuousTime` class enables the simulation of real Hamiltonians. A particular Hamiltonian :math:`H`, can be simulated by creating a :class:`hiperwalk.WeightedGraph` with adjacency matrix :math:`C` such that :math:`H = -\gamma C`. Additionally, the Laplacian matrix is computed as :math:`D - A`, with :math:`D` being the degree matrix. See :meth:`hiperwalk.Graph.adjacency_matrix` and :meth:`hiperwalk.Graph.laplacian_matrix`. For a comprehensive understanding of continuous-time quantum walks, consult reference [1]_. To examine the differences between utilizing the adjacency matrix and the Laplacian matrix, refer to reference [2]_. References ---------- .. [1] E. Farhi and S. Gutmann. "Quantum computation and decision trees". Physical Review A, 58(2):915–928, 1998. ArXiv:quant-ph/9706062. .. [2] T. G. Wong, L. Tarrataca, and N. Nahimov. Laplacian versus adjacency matrix in quantum walk search. Quantum Inf Process 15, 4029-4048, 2016. """ _valid_kwargs = dict()
[docs] def __init__(self, graph=None, **kwargs): super().__init__(graph=graph) # create attributes self.hilb_dim = self._graph.number_of_vertices() self._gamma = None self._hamil_type = None self._hamiltonian = None self._time = None # import inspect if not bool(ContinuousTime._valid_kwargs): # assign static attribute ContinuousTime._valid_kwargs = { 'gamma': ContinuousTime._get_valid_kwargs( self._set_gamma), 'marked': ContinuousTime._get_valid_kwargs( self._set_marked), 'evolution': ContinuousTime._get_valid_kwargs( self._set_evolution), 'time': ContinuousTime._get_valid_kwargs( self._set_time), } self.set_evolution(**kwargs)
def _set_gamma(self, gamma=0.1): if gamma is None or gamma.imag != 0: raise TypeError("Value of 'gamma' is not float.") if self._gamma != gamma: self._gamma = gamma return True return False
[docs] def set_gamma(self, gamma=0.1): r""" Set gamma. Parameter gamma is used in the definition of the Hamiltonian to determine the hopping probability per unit of time. Upon setting gamma, both the Hamiltonian and evolution operators are updated. Parameters ---------- gamma : float, default=0.1 The value of gamma. Raises ------ TypeError If ``gamma`` is ``None`` or complex. ValueError If ``gamma < 0``. """ self.set_hamiltonian(gamma=gamma, type=self._hamil_type, marked=self._marked)
[docs] def get_gamma(self): r""" Retrieve the value of gamma used in the definition of the Hamiltonian. Returns ------- float See Also -------- set_gamma """ return self._gamma
[docs] def set_marked(self, marked=[]): self.set_hamiltonian(gamma=self._gamma, type=self._hamil_type, marked=marked)
def _set_hamiltonian(self, gamma=0.1, type='adjacency', marked=[]): update = self._set_gamma(gamma) update = self._set_hamiltonian_type(type) or update update = self._set_marked(marked) or update if update: if self._hamil_type == 'adjacency': H = -self._gamma * self._graph.adjacency_matrix() else: H = -self._gamma * self._graph.laplacian_matrix() # creating oracle if len(self._marked) > 0: data = np.ones(len(self._marked), dtype=np.int8) oracle = scipy.sparse.csr_array( (data, (self._marked, self._marked)), shape=(self.hilb_dim, self.hilb_dim)) H -= oracle self._hamiltonian = H return update
[docs] def set_hamiltonian(self, gamma=0.1, type="adjacency", marked=[]): r""" Set the Hamiltonian. The Hamiltonian takes the form of ``-gamma*C`` wheres ``C`` is either the adjacency matrix or the Laplacian matrix. If marked vertices are specified, the Hamiltonian is modified as described in the Notes section. Once the Hamiltonian has been established, the evolution operator is updated accordingly. Parameters ---------- gamma : float, default=0.1 The value of gamma. type: {'adjacency', 'laplacian'} Two types of Hamiltonian are used: ``'A'`` is shorthand for ``'adjacency'`` (default). ``'L'`` is shorthand for ``'laplacian'``. marked : list of vertices, default=[] List of vertices to be marked. If empty list, no vertex is marked. Raises ------ TypeError If ``gamma`` is ``None`` or complex. ValueError If ``gamma < 0``. See Also -------- set_gamma set_hamiltonian_type set_marked set_evolution Notes ----- The Hamiltonian is given by [1]_ [2]_ .. math:: H = -\gamma C - \sum_{m \in M} \ket m \bra m, where :math:`C` is either the adjacency matrix or the Laplacian matrix. The set :math:`M` specifies the marked vertices via the argument ``marked=M``. For instance, ``marked={0}`` specifies that :math:`v_0` is the marked vertex. The default is :math:`M=\emptyset`. References ---------- .. [1] E. Farhi and S. Gutmann. "Quantum computation and decision trees". Physical Review A, 58(2):915–928, 1998. ArXiv:quant-ph/9706062. .. [2] A. M. Childs and J. Goldstone. "Spatial search by quantum walk", Phys. Rev. A 70, 022314, 2004. """ self.set_evolution(time=self._time, gamma=gamma, type=type, marked=marked)
[docs] def get_hamiltonian(self): r""" Retrieve the Hamiltonian. Returns ------- :class:`scipy.sparse.csr_array` See Also -------- set_hamiltonian """ return self._hamiltonian
def _set_hamiltonian_type(self, type='adjacency'): if type.upper() == 'A': type = 'adjacency' elif type.upper() == 'L': type = 'laplacian' if type != self._hamil_type: self._hamil_type = type return True return False
[docs] def set_hamiltonian_type(self, type='adjacency'): r""" Set the type of the Hamiltonian. Parameters ---------- type: {'adjacency', 'laplacian'} Two types of Hamiltonian are used: ``'A'`` is shorthand for ``'adjacency'``. ``'L'`` is shorthand for ``'laplacian'``. """ self.set_hamiltonian(gamma=self._gamma, type=type, marked=self._marked)
[docs] def get_hamiltonian_type(): r""" Retrieve the type of the Hamiltonian. See Also -------- set_hamiltonian_type Returns ------- type: {'adjacency', 'laplacian'} """ return self._hamil_type
def _set_time(self, time=1): if time is None or time < 0: raise ValueError( "Expected non-negative `time` value." ) if time != self._time: self._time = time return True return False
[docs] def get_time(self): r""" Return the time used to construct the evolution operator. Returns ------- float """ return self._time
[docs] def set_time(self, time=1): r""" Set a time instant. Defines a time t and calculates the evolution operator U(t) at the specified time (see Notes). Parameters ---------- time : float, default=1 Raises ------ ValueError If ``time < 0``. See Also -------- set_evolution Notes ----- The evolution operator is given by .. math:: U(t) = \text{e}^{-\text{i}tH}, where :math:`H` is the Hamiltonian, and :math:`t` is the time. """ self.set_evolution(time=time, gamma=self._gamma, type=self._hamil_type, marked=self._marked)
def _set_evolution(self): r""" If this method is invoked, the evolution is recalculated """ time = self._time if time == 0: self._evolution = np.eye(self.hilb_dim) return H = self.get_hamiltonian().todense() evals, evecs = scipy.linalg.eigh(H) evecs = evecs.T U = np.zeros((self.hilb_dim, self.hilb_dim), dtype=complex) for i in range(len(evals)): vec = evecs[i, np.newaxis] U += np.exp(1j*time*evals[i]) * vec.T @ vec.conjugate() self._evolution = U #TODO: test if storing eigenvalues is more efficient for simulation
[docs] def set_evolution(self, **kwargs): r""" Set the evolution operator. This method defines the evolution operator for a specified ``time``. Parameters ---------- **kwargs : Additional arguments for setting Hamiltonian and time. If omitted, the default arguments are used. See :meth:`hiperwalk.ContinuousTime.set_hamiltonian`, :meth:`hiperwalk.ContinuousTime.set_time`, and See Also -------- set_hamiltonian set_time Notes ----- The evolution operator is given by .. math:: U(t) = \text{exp}(-\text{i}tH), where :math:`H` is the Hamiltonian, and :math:`t` is the time. To calculate :math:`U(t)`, the eigenvalues of :math:`H` are calculated first, .. math:: H = \sum_\lambda \lambda \ket{\lambda} \bra{\lambda}. Then, the evolution operator is computed by .. math:: U(t) = \sum_\lambda \text{exp}(-\text{i}t\lambda) \ket{\lambda} \bra{\lambda}. """ def filter_and_call(method, update): valid = self._get_valid_kwargs(method) filtered = self._filter_valid_kwargs(kwargs, valid) return method(**filtered) or update update = filter_and_call(self._set_time, False) update = filter_and_call(self._set_hamiltonian, update) if (update): filter_and_call(self._set_evolution, update)