"""Calculate the reservoir energy change from HOPS data. The functions in this module mostly take parameter objects which hold relevant information and compute commonly used values ahead of time. The parameter objects are passed separately but may depend on each other. """ import numpy as np import scipy.misc from . import util from typing import Optional, Tuple, Iterator, Union from stocproc import StocProc import itertools import ray from hops.util.dynamic_matrix import DynamicMatrix, DynamicMatrixList from numpy.typing import NDArray import opt_einsum as oe from hops.core.hierarchy_data import HIData from hops.core.hierarchy_parameters import HIParams import copy ############################################################################### # Interface/Parameter Object # ############################################################################### class SystemParams: """A parameter object to hold information about the physical system and global HOPS parameters. :param L: The coupling operators as system matrices. :param G: The coupling factors in the exponential expansion of the BCF. :param W: The exponents in the exponential expansion of the BCF. :param t: The time points of the evaluation. :param bcf_scale: The BCF scale factors. :param nonlinear: Whether the trajectory was obtained through the nonlinear HOPS. :param fock_hops: Whether the now fock hops hierarchy normalization is used. """ __slots__ = [ "L", "G", "W", "t", "bcf_scale", "nonlinear", "coupling_interaction_prefactors", "coupling_flow_prefactors", "dim", "apply_L", "apply_L_dot", ] def __init__( self, L: list[DynamicMatrix], G: list[np.ndarray], W: list[np.ndarray], t: NDArray[np.float64], bcf_scale: Optional[list[float]] = None, nonlinear: bool = False, fock_hops: bool = True, ): self.t = t self.G = [g * scale for g, scale in zip(G, bcf_scale)] if bcf_scale else G self.W = W self.coupling_interaction_prefactors = [ (np.sqrt(G) if fock_hops else -1j * G) for G in self.G ] self.coupling_flow_prefactors = [ fac * -W for fac, W in zip(self.coupling_interaction_prefactors, self.W) ] self.nonlinear = nonlinear #: the dimensionality of the system self.dim = L[0].shape[0] """ A fast einstein sum to apply the ``L`` operators to the zeroth hierarchy state for each time step. """ self.apply_L = oe.contract_expression( "ntij,tj->nti", (DynamicMatrixList(L))(self.t), (len(t), self.dim), constants=[0], ) """ Same as :any:`apply_L` but for the time derivatives of the ``L``. """ self.apply_L_dot = None try: self.apply_L_dot = oe.contract_expression( "ntij,tj->nti", (DynamicMatrixList(L).derivative())(self.t), (len(t), self.dim), constants=[0], ) except NotImplementedError: self.apply_L_dot = None @classmethod def from_hi_params(cls, params: HIParams, **kwargs) -> "SystemParams": """Construct a :any:`SystemParams` object from the HOPS configuration. The ``kwargs`` are forwarded to the constructor. :param params: The :any:`hops.core.hierarchy_parameters` object that holds the HOPS config. """ return cls( L=params.SysP.L, G=params.SysP.g, W=params.SysP.w, t=params.IntP.t, nonlinear=params.HiP.nonlinear, fock_hops=True, **kwargs, # no bcf scale, as this has been built in already ) class HOPSRun: """A parameter object to hold data commonly used by the energy flow calculations. :param ψ_0: The HOPS trajectory ``(time, dim-state)``. :param ψ_1: The first HOPS hierarchy states ``(time, hierarchy-width * dim-state)``. It will be reshaped to a list containing arrays of the shape``(time, hierarchy-width, dim_state)`` (one for each bath). :param power: whether to calculate the interaction power (replaces the coupling operators with their time derivatives) """ __slots__ = [ "ψ_0", "ψ_1_n", "norm_squared", "ψ_coups", "t_steps", "nonlinear", "power", ] def __init__( self, ψ_0: np.ndarray, ψ_1: np.ndarray, params: SystemParams, power=False ): """Class initializer. Computes the most useful attributes ahead of time.""" self.ψ_0 = ψ_0 self.nonlinear = params.nonlinear self.norm_squared = ( np.sum(self.ψ_0.conj() * self.ψ_0, axis=1).real if params.nonlinear else None ) """ The squared norm of the state, may be ``None`` for linear HOPS. """ if power: if params.apply_L_dot is None: raise NotImplementedError ψ_coups = params.apply_L_dot(self.ψ_0) else: ψ_coups = params.apply_L(self.ψ_0) self.ψ_coups = ψ_coups """ ψ_0 with the coupling operator (or its derivative) applied for each bath. """ self.power = power """Wether the ``L`` operators have been replaced with their derivatives.""" self.t_steps = self.ψ_0.shape[0] """The number of timesteps.""" hierarchy_width = ψ_1.shape[1] // params.dim ψ_1_rs = ψ_1.reshape(self.t_steps, hierarchy_width, params.dim) offsets = np.cumsum([0] + [len(g) for g in params.G]) self.ψ_1_n = [ ψ_1_rs[:, offsets[i] : offsets[i + 1], :] for i in range(len(offsets) - 1) ] """The first hierarchy states for each bath.""" def normalize_maybe(self, array: np.ndarray): """Normalize the ``array`` if nonlinear HOPS is being used.""" if self.nonlinear: return array / self.norm_squared return array class ThermalParams: """Aparameter object to hold information abouth the thermal stochastic process. :param ξ: the thermal stochastic processes :param τ: the time points of the trajectory :param rand_skip: how many random numbers to skip when parametrizing the stochastic process :param num_deriv: whether to calculate the derivative of the process numerically or use it directly from the :class:`StocProc`. The latter alternative is strongly preferred if available. :param dx: step size for the calculation of the derivative of ξ, only relevant if numerical differentiation is used :param order: order the calculation of the derivative of ξ, must be odd, see :any:`scipy.misc.derivative`, only relevant if numerical differentiation is used """ __slots__ = ["ξs", "τ", "dx", "order", "num_deriv", "rand_skip"] def __init__( self, ξs: list[Optional[StocProc]], τ: np.ndarray, rand_skip: int = 0, num_deriv: bool = False, dx: float = 1e-3, order: int = 3, ): self.ξs = ξs self.τ = τ self.dx = dx self.order = order self.num_deriv = num_deriv self.rand_skip = rand_skip @classmethod def from_hi_params(cls, params: HIParams, **kwargs) -> "SystemParams": """Construct a :any:`ThermalParams` object from the HOPS configuration. The ``kwargs`` are forwarded to the constructor. :param params: The :any:`hops.core.hierarchy_parameters` object that holds the HOPS config. """ ξs = [copy.deepcopy(proc) for proc in params.EtaTherm] for i, proc in enumerate(ξs): if proc is not None: proc.calc_deriv = True proc.set_scale(params.SysP.bcf_scale[i]) return cls( ξs=ξs, τ=params.IntP.t, rand_skip=params.HiP.rand_skip, **kwargs, # no bcf scale, as this has been built in already ) class ThermalRunParams: """A parameter object to hold information abouth the thermal stochastic process. :param therm_params: information abouth the thermal stochastic process :param seed: the seed used to generate the random numbers for the process realization :attr ξ_dots: the process derivatives evaluated at τ :attr :attr ξ_values: the processes evaluated at τ :attr ξ_coeff: the coefficients of the realization of the thermal stochastic processes """ __slots__ = ["ξ_coeff", "ξ_dots", "ξ_values"] def __init__(self, therm_params: ThermalParams, seed: int): """Class initializer. Computes the most useful attributes ahead of time. The processes ξs are intialized with ξ_coeff and their derivative is being calculated. """ np.random.seed(seed) np.random.rand(therm_params.rand_skip) self.ξ_coeff = [] self.ξ_values = [] self.ξ_dots = [] for ξ in therm_params.ξs: if ξ: coeff = util.uni_to_gauss(np.random.rand((ξ.get_num_y() or 0) * 2)) self.ξ_coeff.append(coeff) ξ.new_process(coeff) self.ξ_values.append(ξ(therm_params.τ)) self.ξ_dots.append( scipy.misc.derivative( ξ, therm_params.τ, dx=therm_params.dx, order=therm_params.order, ) if therm_params.num_deriv else ξ.dot(therm_params.τ) ) else: self.ξ_coeff.append(0) self.ξ_values.append(0) self.ξ_dots.append(None) ############################################################################### # Single Trajectory # ############################################################################### def flow_trajectory_coupling( run: HOPSRun, params: SystemParams, ) -> np.ndarray: r"""Calculates the :math:`\langle L^\dagger \dot{B}\rangle + c.c.` part of the energy flow for a single trajectory and for each bath. :param run: a parameter object for the current trajectory, see :any:`HOPSRun` :param params: a parameter object for the system, see :any:`SystemParams` :returns: the value of the flow for each time step """ # here we apply the prefactors to each hierarchy state ψ_hopses = [ util.mulitply_hierarchy(f, ψ_1) for f, ψ_1 in zip(params.coupling_flow_prefactors, run.ψ_1_n) ] flows = np.array( [ 2 * util.dot_with_hierarchy(ψ_coup.conj(), ψ_hops).real for ψ_coup, ψ_hops in zip(run.ψ_coups, ψ_hopses) ] ) # fromiter crashed... # optionally normalize return run.normalize_maybe(flows) def flow_trajectory_therm(run: HOPSRun, therm_run: ThermalRunParams) -> np.ndarray: r"""Calculates the :math:`\langle L^\dagger \dot{\xi} + c.c.` part of the energy flow for a single trajectory. :param run: a parameter object, see :any:`HOPSRun` :param therm_run: a parameter object, see :any:`ThermalRunParams` :returns: the value of the flow for each time step """ flows = np.array( [ 2 * ( run.normalize_maybe(np.sum(ψ_coup.conj() * run.ψ_0, axis=1)) * ξ_dot if ξ_dot is not None else np.zeros( ψ_coup.shape[0] ) # note: this is already scaled by stocproc ).real for ψ_coup, ξ_dot in zip(run.ψ_coups, therm_run.ξ_dots) ] ) return flows def flow_trajectory( run: HOPSRun, params: SystemParams, therm_run: Optional[ThermalRunParams] = None ) -> np.ndarray: r"""Calculates the total energy flow for a trajectory. :param run: a parameter object, see :any:`HOPSRun` :param therm: a parameter object, see :any:`ThermalParams` :param params: a parameter object for the system, see :any:`SystemParams` :returns: the value of the flow for each time step """ flow = flow_trajectory_coupling(run, params) if therm_run: flow += flow_trajectory_therm(run, therm_run) return flow def interaction_energy_coupling(run: HOPSRun, params: SystemParams) -> np.ndarray: r"""Calculates the coupling part of the interaction energy expectation value for a trajectory. :param run: a parameter object for the current trajectory, see :any:`HOPSRun` :param params: a parameter object for the system, see :any:`SystemParams` :returns: the value of the interaction energy (or power) for each time step """ # here we apply the prefactors to each hierarchy state ψ_hopses = [ util.mulitply_hierarchy(f, ψ_1) for f, ψ_1 in zip(params.coupling_interaction_prefactors, run.ψ_1_n) ] flows = np.array( [ 2 * util.dot_with_hierarchy(ψ_coup.conj(), ψ_hops).real for ψ_coup, ψ_hops in zip(run.ψ_coups, ψ_hopses) ] ) # fromiter crashed... # optionally normalize return run.normalize_maybe(flows) def interaction_energy_therm( run: HOPSRun, therm_run: ThermalRunParams, power: bool = False ) -> np.ndarray: r"""Calculates the thermal part of the interaction energy. :param run: a parameter object, see :any:`HOPSRun` :param params: a parameter object for the system, see :any:`SystemParams` :param therm_run: a parameter object, see :any:`ThermalParams` :returns: the value of the thermal interaction (or power) for each time step """ energies = np.array( [ 2 * ( run.normalize_maybe(np.sum(ψ_coup.conj() * run.ψ_0, axis=1)) * ξ if ξ is not None else np.zeros( ψ_coup.shape[0] ) # note: this is already scaled by stocproc ).real for ψ_coup, ξ in zip(run.ψ_coups, therm_run.ξ_values) ] ) return energies ############################################################################### # Ensemble # ############################################################################### def heat_flow_ensemble( ψ_0s: Iterator[np.ndarray], ψ_1s: Iterator[np.ndarray], params: SystemParams, therm_args: Optional[Tuple[Iterator[np.ndarray], ThermalParams]] = None, only_therm: bool = False, **kwargs, ) -> util.EnsembleValue: """Calculates the heat flow for an ensemble of trajectories. :param ψ_0s: array of trajectories ``(N, time-steps, dim-state)`` :param ψ_0s: array of the first HOPS hierarchy states ``(N, time, hierarchy-width * dim-state)`` :param params: a parameter object for the system, see :any:`SystemParams` :param therm_args: the realization parameters and the parameter object, see :any:`ThermalParams` :param only_therm: whether to only calculate the thermal part of the flow The rest of the ``kwargs`` is passed on to :any:`util.ensemble_mean`. :returns: the value of the flow for each time step """ if therm_args is None and only_therm: raise ValueError("Can't calculate only thermal part if therm_args are None.") thermal = therm_args[1] if therm_args else None def flow_worker(ψs: tuple[np.ndarray, np.ndarray, int]): ψ_0, ψ_1, seed = ψs run = HOPSRun(ψ_0, ψ_1, params) # type: ignore flow = ( flow_trajectory_coupling(run, params) # type: ignore if not only_therm else np.zeros((len(params.G), ψ_0.shape[0])) ) if thermal is not None: therm_run = ThermalRunParams(thermal, seed) # type: ignore flow += flow_trajectory_therm(run, therm_run) return flow return util.ensemble_mean( iter(zip(ψ_0s, ψ_1s, therm_args[0])) if therm_args else iter(zip(ψ_0s, ψ_1s, itertools.repeat(0))), flow_worker, **kwargs, ) def heat_flow_from_data( data: HIData, *args, thermal_params: Optional[ThermalParams] = None, **kwargs, ) -> util.EnsembleValue: """Calculates the heat flow for an ensemble of trajectories. The rest of the ``args`` and ``kwargs`` is passed on to :any:`util.heat_flow_ensemble`. :param data: The data instance that contains the trajectories. Does not have to be opened yet. :returns: the value of the flow for each time step """ with data as d: if "save" in kwargs: kwargs["save"] += "_" + data.get_hi_key_hash() if thermal_params is not None: kwargs["therm_args"] = ( d.valid_sample_iterator(d.rng_seed), thermal_params, ) return heat_flow_ensemble( d.valid_sample_iterator(d.stoc_traj), d.valid_sample_iterator(d.aux_states), *args, **(dict(N=data.samples) | kwargs), ) def interaction_energy_ensemble( ψ_0s: Iterator[np.ndarray], ψ_1s: Iterator[np.ndarray], params: SystemParams, therm_args: Optional[Tuple[Iterator[int], ThermalParams]] = None, power: bool = False, **kwargs, ) -> util.EnsembleValue: """Calculates the heat flow for an ensemble of trajectories. :param ψ_0s: array of trajectories ``(N, time-steps, dim-state)`` :param ψ_0s: array of the first HOPS hierarchy states ``(N, time, hierarchy-width * dim-state)`` :param params: a parameter object for the system, see :any:`SystemParams` :param therm_args: the realization parameters and the parameter object, see :any:`ThermalParams` :param power: whether to calculate the interaction power The ``**kwargs`` are passed to :any:`hopsflow.utility.ensemble_mean`. :returns: the value of the interaction energy (or power) for each time step """ thermal = therm_args[1] if therm_args else None def interaction_energy_task( ψs: Tuple[np.ndarray, np.ndarray, int], ) -> np.ndarray: ψ_0, ψ_1, seeds = ψs run = HOPSRun(ψ_0, ψ_1, params, power) # type: ignore energy = interaction_energy_coupling(run, params) # type: ignore if thermal is not None: therm_run = ThermalRunParams(thermal, seeds) # type: ignore energy += interaction_energy_therm(run, therm_run) return energy return util.ensemble_mean( iter(zip(ψ_0s, ψ_1s, therm_args[0])) if therm_args else iter(zip(ψ_0s, ψ_1s, itertools.repeat(0))), interaction_energy_task, **kwargs, ) def energy_change_from_interaction_power( τ: np.ndarray, *args, **kwargs, ) -> util.EnsembleValue: """Calculates the energy change due to the explicit time dependence of the interaction hamiltonian. For the arguments see :any:`interaction_energy_ensemble`. :param τ: The time points of the simulations. :returns: The value of the total energy change due to the time dependence of the interaction for each time step. """ kwargs = kwargs | dict(power=True) power = interaction_energy_ensemble(*args, **kwargs) return power.integrate(τ) def bath_energy_from_flow( τ: np.ndarray, *args, **kwargs, ) -> util.EnsembleValue: """Calculates the bath energy by integrating the flow. For the arguments see :any:`heat_flow_ensemble`. :param τ: The time points of the simulations. :returns: The value of the bath energy for each time step. """ flow = heat_flow_ensemble(*args, **kwargs) return -1 * flow.integrate(τ)