Specifying the parameter-dependent channel

First, let us start by explaining how to encode the channel using the ParamChannel class. We can do that in one of the four available ways:

  • from a list of Kraus operators and their derivatives with respect to \(\theta\),

  • from a CJ matrix and its derivative over \(\theta\),

  • using a predefined creator function,

  • from a Lindbladian, its derivative with respect to \(\omega\) and a specified evolution time.

The following listing shows how the above options are implemented in practice using the example of the dephasing channel (16):

import numpy as np
from qmetro import *

# Pauli z-matrix:
sz = np.array([
    [1, 0],
    [0, -1]
])

# Kraus operators and their derivatives for dephasing channel:
p = 0.75
krauses = [np.sqrt(p) * np.identity(2), np.sqrt(1-p) * sz]
dkrauses = [-1j/2 * sz @ K for K in krauses]

# ParamChannel instance created from Kraus operators:
channel1 = ParamChannel(krauses=krauses, dkrauses=dkrauses)

# CJ matrix and its derivative created from Kraus operators:
choi = choi_from_krauses(krauses)
dchoi = dchoi_from_krauses(krauses, dkrauses)

# ParamChannel instance created from CJ matrix:
channel2 = ParamChannel(choi=choi, dchoi=dchoi)

# ParamChannel instance created using creator function:
channel3 = par_dephasing(p)

By design the ParamChannel class represents a discrete time quantum evolution. Thus to create its objects from \(\mathcal{L}_\omega, \dot{\mathcal{L}}_\omega\) one needs to move from continuous to discrete time regime by integrating over specified evolution time \(t\). This can be done using choi_from_lindblad function from qtools which performs this integration for time-independent Lindbladian specified either as a function or a pair of Hamiltonian and a list of jump operators:

# continuing previous example

# Lindbladian and its derivative.
omega = 0.0  # Parameter.
t = 1        # Time.
# Dephasing strength:
gamma = -np.log(2*p-1)/t
# Hilbert space dimension
dim = 2

def lindblad(rho):
    # Rotation around z part:
    rot = 0.5j*omega * (rho@sz - sz@rho)
    # Dephasing part:
    deph = gamma * (sz@rho@sz - rho)
    return rot + deph

def dlindblad(rho):
    return 0.5j * (rho@sz - sz@rho)

choi, dchoi = choi_from_lindblad(lindblad, dlindblad, t, dim=dim)
channel5 = ParamChannel(choi=choi, dchoi=dchoi)

# or

# Hamiltonian
H = 0.5 * omega * sz
# Jump operators rescaled by sqrt(gamma)
Ls = [np.sqrt(gamma/2) * sz]
# Derivative of the Hamiltonian
dH = 0.5 * sz
# Derivative of the jump operators
dLs = [np.zeros_like(L) for L in Ls]
choi, dchoi = choi_from_lindblad((H, Ls), (dH, dLs), t)
channel6 = ParamChannel(choi=choi, dchoi=dchoi)

Objects of the ParamChannel class can be used to compute \(\rho_\theta = \Lambda_\theta(\rho)\) and \(\dot\rho_\theta = \frac{d}{d\theta}\Lambda_\theta(\rho)\), e.g.:

channel = channel1
rho = np.array([
    [1, 0],
    [0, 0]
])

rho_t, drho_t = channel(rho)

or to obtain the CJ operator from Kraus operators and vice versa, e.g.:

channel1 = ParamChannel(krauses=krauses, dkrauses=dkrauses)
# CJ matrix:
choi = channel.choi()
# Derivative of CJ matrix:
dchoi = channel.dchoi()

channel2 = ParamChannel(choi=choi, dchoi=dchoi)
# Kraus operators:
krauses = channel2.krauses()
# Kraus operators and their derivatives:
krauses, dkrauses = channel2.dkrauses()

They can be combined with each other to create new channels using:

  • scalar multiplication \(\left(\alpha \Lambda_\theta \right)(\rho) = \alpha\Lambda_\theta(\rho)\), e.g.:

    a = 0.2
    new_channel = channel.scalar_mul(a)
    # or equivalently
    new_channel = a * channel
    
  • addition \((\Lambda_\theta + \mathrm{\Phi}_\theta)(\rho) = \Lambda_\theta(\rho) + \Phi_\theta(\rho)\), e.g.:

    new_channel = channel1.add(channel2)
    # or equivalently
    new_channel = channel1 + channel2
    
  • composition \((\Lambda_\theta \circ \mathrm{\Phi}_\theta)(\rho) = \Lambda_\theta\left( \Phi_\theta(\rho) \right)\), e.g.:

    new_channel = channel1.compose(channel2)
    # or equivalently
    new_channel = channel1 @ channel2
    
  • Kronecker product \(\left( \Lambda_\theta \otimes \Phi_\theta \right) (\rho_1 \otimes \rho_2) = \Lambda_\theta(\rho_1) \otimes \Phi_\theta(\rho_2)\), e.g.:

    new_channel = channel1.kron(channel2)
    
  • Kronecker power \(\Lambda_\theta^{\otimes N} = \underbrace{\Lambda_\theta \otimes \dots \otimes \Lambda_\theta}_{N}\), e.g.:

    N = 3
    new_channel = channel1.kron_pow(N)