Source code for qibo.models.evolution

"""Models for time evolution of state vectors."""

from qibo import optimizers, solvers
from qibo.callbacks import Gap, Norm
from qibo.config import log, raise_error
from qibo.hamiltonians.abstract import AbstractHamiltonian
from qibo.hamiltonians.adiabatic import AdiabaticHamiltonian, BaseAdiabaticHamiltonian
from qibo.hamiltonians.hamiltonians import SymbolicHamiltonian


[docs]class StateEvolution: """Unitary time evolution of a state vector under a Hamiltonian. Args: hamiltonian (:class:`qibo.hamiltonians.abstract.AbstractHamiltonian`): Hamiltonian to evolve under. dt (float): Time step to use for the numerical integration of Schrondiger's equation. solver (str): Solver to use for integrating Schrodinger's equation. Available solvers are 'exp' which uses the exact unitary evolution operator and 'rk4' or 'rk45' which use Runge-Kutta methods to integrate the Schordinger's time-dependent equation in time. When the 'exp' solver is used to evolve a :class:`qibo.hamiltonians.hamiltonians.SymbolicHamiltonian` then the Trotter decomposition of the evolution operator will be calculated and used automatically. If the 'exp' is used on a dense :class:`qibo.core.hamiltonians.hamiltonians.Hamiltonian` the full Hamiltonian matrix will be exponentiated to obtain the exact evolution operator. Runge-Kutta solvers use simple matrix multiplications of the Hamiltonian to the state and no exponentiation is involved. callbacks (list): List of callbacks to calculate during evolution. accelerators (dict): Dictionary of devices to use for distributed execution. This option is available only when the Trotter decomposition is used for the time evolution. Example: .. testcode:: import numpy as np from qibo import models, hamiltonians # create critical (h=1.0) TFIM Hamiltonian for three qubits hamiltonian = hamiltonians.TFIM(3, h=1.0) # initialize evolution model with step dt=1e-2 evolve = models.StateEvolution(hamiltonian, dt=1e-2) # initialize state to |+++> initial_state = np.ones(8) / np.sqrt(8) # execute evolution for total time T=2 final_state2 = evolve(final_time=2, initial_state=initial_state) """ def __init__(self, hamiltonian, dt, solver="exp", callbacks=[], accelerators=None): hamtypes = (AbstractHamiltonian, BaseAdiabaticHamiltonian) if isinstance(hamiltonian, hamtypes): ham = hamiltonian else: ham = hamiltonian(0) if not isinstance(ham, AbstractHamiltonian): raise TypeError(f"Hamiltonian type {type(ham)} not understood.") self.nqubits = ham.nqubits self.backend = ham.backend if dt <= 0: raise_error(ValueError, f"Time step dt should be positive but is {dt}.") self.dt = dt disthamtypes = (SymbolicHamiltonian, BaseAdiabaticHamiltonian) if accelerators is not None: # pragma: no cover if not isinstance(ham, disthamtypes) or solver != "exp": raise_error( NotImplementedError, "Distributed evolution is only " + "implemented using the Trotter " + "exponential solver.", ) ham.circuit(dt, accelerators) self.solver = solvers.get_solver(solver, self.dt, hamiltonian) self.callbacks = callbacks self.accelerators = accelerators self.normalize_state = self._create_normalize_state(solver) self.calculate_callbacks = self._create_calculate_callbacks(accelerators) def _create_normalize_state(self, solver): if "rk" in solver: log.info("Normalizing state during RK solution.") return lambda s: s / self.backend.calculate_norm(s) else: return lambda s: s def _create_calculate_callbacks(self, accelerators): def calculate_callbacks(state): for callback in self.callbacks: callback.nqubits = self.nqubits # by executing callbacks.apply we also append the object to history # see callbacks module for this callback.apply(self.backend, state) if accelerators is None: return calculate_callbacks else: # pragma: no cover def calculate_callbacks_distributed(state): if not isinstance(state, self.backend.tensor_types): state = state.state() calculate_callbacks(state) return calculate_callbacks_distributed
[docs] def execute(self, final_time, start_time=0.0, initial_state=None): """Runs unitary evolution for a given total time. Args: final_time (float): Final time of evolution. start_time (float): Initial time of evolution. Defaults to t=0. initial_state (np.ndarray): Initial state of the evolution. Returns: Final state vector a ``tf.Tensor`` or a :class:`qibo.core.distutils.DistributedState` when a distributed execution is used. """ if initial_state is None: raise_error( ValueError, "StateEvolution cannot be used without " "initial state." ) state = self.backend.cast(initial_state) self.solver.t = start_time nsteps = int((final_time - start_time) / self.solver.dt) self.calculate_callbacks(state) for _ in range(nsteps): state = self.solver(state) if self.callbacks: state = self.normalize_state(state) self.calculate_callbacks(state) state = self.normalize_state(state) return state
def __call__(self, final_time, start_time=0.0, initial_state=None): """Equivalent to :meth:`qibo.models.StateEvolution.execute`.""" return self.execute(final_time, start_time, initial_state)
[docs]class AdiabaticEvolution(StateEvolution): """Adiabatic evolution of a state vector under the following Hamiltonian: .. math:: H(t) = (1 - s(t)) H_0 + s(t) H_1 Args: h0 (:class:`qibo.hamiltonians.abstract.AbstractHamiltonian`): Easy Hamiltonian. h1 (:class:`qibo.hamiltonians.abstract.AbstractHamiltonian`): Problem Hamiltonian. These Hamiltonians should be time-independent. s (callable): Function of time that defines the scheduling of the adiabatic evolution. Can be either a function of time s(t) or a function with two arguments s(t, p) where p corresponds to a vector of parameters to be optimized. dt (float): Time step to use for the numerical integration of Schrondiger's equation. solver (str): Solver to use for integrating Schrodinger's equation. Available solvers are 'exp' which uses the exact unitary evolution operator and 'rk4' or 'rk45' which use Runge-Kutta methods to integrate the Schordinger's time-dependent equation in time. When the 'exp' solver is used to evolve a :class:`qibo.hamiltonians.hamiltonians.SymbolicHamiltonian` then the Trotter decomposition of the evolution operator will be calculated and used automatically. If the 'exp' is used on a dense :class:`qibo.hamiltonians.hamiltonians.Hamiltonian` the full Hamiltonian matrix will be exponentiated to obtain the exact evolution operator. Runge-Kutta solvers use simple matrix multiplications of the Hamiltonian to the state and no exponentiation is involved. callbacks (list): List of callbacks to calculate during evolution. accelerators (dict): Dictionary of devices to use for distributed execution. This option is available only when the Trotter decomposition is used for the time evolution. """ ATOL = 1e-7 # Tolerance for checking s(0) = 0 and s(T) = 1. def __init__(self, h0, h1, s, dt, solver="exp", callbacks=[], accelerators=None): self.hamiltonian = AdiabaticHamiltonian(h0, h1) # pylint: disable=E0110 super().__init__(self.hamiltonian, dt, solver, callbacks, accelerators) # Set evolution model to "Gap" callback if one exists for callback in self.callbacks: if isinstance(callback, Gap): callback.evolution = self # Flag to control if loss messages are shown during optimization self.opt_messages = False self.opt_history = {"params": [], "loss": []} self.parametrized_schedule = None nparams = s.__code__.co_argcount if nparams == 1: # given ``s`` is a function of time only self.schedule = s elif nparams == 2: # given ``s`` has undefined parameters self.parametrized_schedule = s else: raise_error( ValueError, f"Scheduling function shoud take one or " "two arguments but it takes {nparams}.", ) @property def schedule(self): """Returns scheduling as a function of time.""" if self.hamiltonian.schedule is None: raise_error( ValueError, "Cannot access scheduling function before " "setting its free parameters.", ) return self.hamiltonian.schedule @schedule.setter def schedule(self, f): """Sets scheduling s(t) function.""" s0 = f(0) if abs(s0) > self.ATOL: raise_error(ValueError, f"s(0) should be 0 but is {s0}.") s1 = f(1) if abs(s1 - 1) > self.ATOL: raise_error(ValueError, f"s(1) should be 1 but is {s1}.") self.hamiltonian.schedule = f
[docs] def set_parameters(self, params): """Sets the variational parameters of the scheduling function.""" if self.parametrized_schedule is not None: self.schedule = lambda t: self.parametrized_schedule(t, params[:-1]) self.hamiltonian.total_time = params[-1]
def execute(self, final_time, start_time=0.0, initial_state=None): """""" if start_time != 0: raise_error( NotImplementedError, "Adiabatic evolution supports only t=0 " "as initial time.", ) self.hamiltonian.total_time = final_time - start_time if initial_state is None: initial_state = self.backend.cast( self.hamiltonian.ground_state(), copy=True ) return super().execute(final_time, start_time, initial_state) @staticmethod def _loss(params, adiabatic_evolution, h1, opt_messages, opt_history): """Expectation value of H1 for a choice of scheduling parameters. Returns a ``tf.Tensor``. """ adiabatic_evolution.set_parameters(params) ham = adiabatic_evolution.hamiltonian initial_state = ham.backend.cast(ham.h0.ground_state(), copy=True) final_state = super(AdiabaticEvolution, adiabatic_evolution).execute( params[-1], initial_state=initial_state ) loss = h1.expectation(final_state, normalize=True) if opt_messages: opt_history["params"].append(params) opt_history["loss"].append(loss) log.info(f"Params: {params} - <H1> = {loss}") return loss
[docs] def minimize(self, initial_parameters, method="BFGS", options=None, messages=False): """Optimize the free parameters of the scheduling function. Args: initial_parameters (np.ndarray): Initial guess for the variational parameters that are optimized. The last element of the given array should correspond to the guess for the total evolution time T. method (str): The desired minimization method. One of ``"cma"`` (genetic optimizer), ``"sgd"`` (gradient descent) or any of the methods supported by `scipy.optimize.minimize <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html>`_. options (dict): a dictionary with options for the different optimizers. messages (bool): If ``True`` the loss evolution is shown during optimization. """ self.opt_messages = messages if method == "sgd": loss = self._loss else: loss = lambda p, ae, h1, msg, hist: self.backend.to_numpy( self._loss(p, ae, h1, msg, hist) ) args = (self, self.hamiltonian.h1, self.opt_messages, self.opt_history) result, parameters, extra = optimizers.optimize( loss, initial_parameters, args=args, method=method, options=options ) if isinstance(parameters, self.backend.tensor_types) and not len( parameters.shape ): # pragma: no cover # some optimizers like ``Powell`` return number instead of list parameters = [parameters] self.set_parameters(parameters) return result, parameters, extra