torch_vn_algebra.dynamics¶
Source: torch_vn_algebra/dynamics.py. Signatures and docstrings as in the source.
Module documentation¶
Time evolution on M = (+)_c M_{k_c} for Hamiltonians and Lindblad generators in M.
Because H and the jump operators are block diagonal, every sector evolves independently
(superselection) and all routines work block by block and batched over samples.
Exact solvers (time-independent generators):
propagator, schrodinger, von_neumann spectral decomposition of H
lindblad_channel exp of the Lindblad superoperator (k_c^2 x k_c^2)
Fixed-step RK4 solvers (any generator, also time dependent):
rk4, schrodinger_rk4, lindblad_evolve, solve_operator_ode
lindblad_evolve(method='kraus' | 'exact'): time steps that are channels (CPTP) for every step size
propagator¶
def propagator(H: Operator, t: float) -> Operator
U(t) = exp(-i t H) for self-adjoint H.
schrodinger¶
def schrodinger(psi0: torch.Tensor, H: Operator, times) -> torch.Tensor
psi(t) = exp(-i t H) psi0 for state vectors psi0 of shape (batch, C, k_max) (block layout,
global norm 1). Returns a tensor of shape (len(times), batch, C, k_max).
von_neumann¶
def von_neumann(rho0: Operator, H: Operator, times) -> List[Operator]
rho(t) = U(t) rho0 U(t)^* for every t in `times` (exact).
rk4¶
def rk4(f: Callable[[float, torch.Tensor], torch.Tensor], y0: torch.Tensor, times, substeps: int=10, progress: bool=False) -> torch.Tensor
Classical 4th-order Runge-Kutta for dy/dt = f(t, y) on tensors. `times` is an increasing grid;
each interval is split into `substeps` steps. Returns y at every grid point, shape (T, *y0.shape).
After the first step a CostWarning reports the expected run time if it is long;
progress=True shows a progress bar (tqdm if installed).
solve_operator_ode¶
def solve_operator_ode(f: Callable[[float, torch.Tensor], torch.Tensor], X0: Operator, times, substeps: int=10, progress: bool=False) -> List[Operator]
dX/dt = f(t, X) for X in the algebra, f acting on matrices of shape (batch, C, k_max, k_max)
and returning block-diagonal matrices of the same shape (e.g. Heisenberg, Lyapunov or
Riccati-type right-hand sides). Returns X at every time of the grid.
schrodinger_rk4¶
def schrodinger_rk4(psi0: torch.Tensor, H_of_t: Callable[[float], OperatorLike], times, substeps: int=10, progress: bool=False, algebra: Optional[TypeIAlgebra]=None) -> torch.Tensor
i dpsi/dt = H(t) psi for a (possibly time-dependent) Hamiltonian; shape (T, batch, C, k_max).
Raw tensors (psi0, or H given as a tensor) are restricted to the blocks of `algebra` (taken from H when
it is an Operator); without an algebra their padding must be zero.
lindblad_rhs¶
def lindblad_rhs(H: Optional[OperatorLike], jumps: Sequence[OperatorLike]=(), rates: Optional[Sequence[float]]=None, algebra: Optional[TypeIAlgebra]=None) -> Callable[[float, torch.Tensor], torch.Tensor]
Right-hand side of the GKSL equation
drho/dt = -i[H, rho] + sum_j g_j (L_j rho L_j^* - 1/2 {L_j^* L_j, rho})
as a function (t, rho_matrix) -> drho/dt. H and L_j are constant, or callables of t.
A jump that moves weight between sectors (particle loss N -> N-1, ...) is passed as an
InterSectorChannel Phi of the algebra into itself; it contributes Phi(rho) - 1/2 {Phi^*(1), rho}, where
Phi^*(1)_c = sum_{d,i} K_i^{dc*} K_i^{dc} is block diagonal. This is the GKSL generator of the jumps
K_i^{dc} compressed to M (the conditional expectation onto M applied to the generator on B(H)); it maps M
into M and conserves the total trace. For constant H and jumps, A = sum g L^* L and the stacked jumps are
prepared once; callables of t are evaluated at every step. Raw tensors are restricted to the blocks of
`algebra` (taken from H or the jumps when they are Operators); without an algebra their padding must be zero.
lindblad_evolve¶
def lindblad_evolve(rho0: Operator, H: Optional[OperatorLike], jumps: Sequence[OperatorLike]=(), times=(0.0, 1.0), rates: Optional[Sequence[float]]=None, substeps: int=20, progress: bool=False, method: str='rk4') -> List[Operator]
Integrate the Lindblad equation of lindblad_rhs (jumps inside and between sectors); returns rho(t) for every t
in `times`. Every interval of `times` is split into `substeps` steps of size h.
method='rk4' (default): classical Runge-Kutta, fourth order, but a step is not a positive map in general, even
for small h (Proposition 8.15 of the lecture notes): rho(t) can acquire small negative eigenvalues.
method='kraus': the splitting of Theorem 8.16, exp(-i h/2 H_eff) . exp(-i h/2 H_eff)^* around
id + h Phi + h^2/2 Phi^2 (Phi the jump part), renormalised to be exactly trace preserving. Every step is a
channel (completely positive and trace preserving) for every h; second order (error O(t h^2)).
method='exact': exp(h L) from the matrix of the generator on (+)_c M_{k_c} (size C k_max^2, so for small
sectors; jumps between sectors included). For a constant generator the result is exact and one exponential
is computed per interval length (`substeps` is not used).
For time-dependent H or jumps, 'kraus' and 'exact' freeze the generator at the midpoint of each step; every
step is still a channel. `times` must be nondecreasing for these two methods.
lindblad_superoperator¶
def lindblad_superoperator(alg: TypeIAlgebra, H: Optional[OperatorLike], jumps: Sequence[OperatorLike]=(), rates: Optional[Sequence[float]]=None) -> List[torch.Tensor]
Per-channel generator matrices acting on row-major vec(rho), shape (batch, k_c^2, k_c^2).
lindblad_channel¶
def lindblad_channel(alg: TypeIAlgebra, H: Optional[OperatorLike], jumps: Sequence[OperatorLike]=(), t: float=1.0, rates: Optional[Sequence[float]]=None) -> Channel
The channel exp(t L) of a time-independent Lindblad generator, computed exactly in double
precision from the k_c^2 x k_c^2 superoperators (practical for k_c up to a few tens); one matrix exponential
and one Choi eigendecomposition per block size.