import numpy as np
import sympy
import matplotlib.pyplot as plt
from scipy.optimize import minimize
from qrate import solvers
from pmm_tools import models, utils
from second_quantization import hilbert_space
from IPython.display import display
import logging
logging.getLogger("matplotlib.font_manager").disabled = True
plt.rcParams["text.usetex"] = False
Transport in a minimal Kitaev chain
In this example, we will simulate a minimal Kitaev chain made up of two normal quantum dots connected by a superconducting dot. We will use three tiny python packages to facilitate the simulation:
-
second_quantization: for finding the numerical representation of a second quantization Hamiltonian -
pmm_tools: for writing down the Hamiltonian and finding the sweet spot of the chain -
qrate: for calculating the transport properties
Define the Hamiltonian
We will use the pmm_tools package to define the Hamiltonian of the system
and the parameters of the system.
This package allows us to write a position dependent Hamiltonian in a very simple way.
We provide the length of the chain and the parameters of the system.
The parameters are defined as a dictionary, where the keys are the names of the parameters and the values are lists of sympy symbols corresponding to each site.
length = 3
mu_L, mu_M, mu_R = sympy.symbols("mu_L mu_M mu_R", real=True, positive=True)
U, E_Z, Delta, t, theta = sympy.symbols("U E_Z Delta t theta", real=True, positive=True)
_parameters = {
"mu": [mu_L, mu_M, mu_R],
"U": [U, 0, U],
"E_z": [E_Z, 0, E_Z],
"Delta": [sympy.S.Zero, Delta, sympy.S.Zero],
"t_n": t * sympy.cos(theta / 2),
"t_so": t * sympy.sin(theta / 2),
}
_parameters, hamiltonian, fermions = models.dot_abs_chain(length, _parameters)
terms = hilbert_space.to_matrix(hamiltonian, fermions, sparse=False)
f_hamiltonian = hilbert_space.make_dict_callable(terms)
parity_operator = hilbert_space.parity_operator(fermions, sparse=False)
display(hamiltonian)
\(\displaystyle \Delta {c_{1\downarrow}} {c_{1\uparrow}} + \Delta {{c_{1\uparrow}}^\dagger} {{c_{1\downarrow}}^\dagger} + U {{c_{0\uparrow}}^\dagger} {c_{0\uparrow}} {{c_{0\downarrow}}^\dagger} {c_{0\downarrow}} + U {{c_{2\uparrow}}^\dagger} {c_{2\uparrow}} {{c_{2\downarrow}}^\dagger} {c_{2\downarrow}} + \mu_{M} {{c_{1\downarrow}}^\dagger} {c_{1\downarrow}} + \mu_{M} {{c_{1\uparrow}}^\dagger} {c_{1\uparrow}} + t \sin{\left(\frac{\theta}{2} \right)} \left({{c_{0\downarrow}}^\dagger} {c_{1\uparrow}} + {{c_{1\uparrow}}^\dagger} {c_{0\downarrow}}\right) - t \sin{\left(\frac{\theta}{2} \right)} \left({{c_{0\uparrow}}^\dagger} {c_{1\downarrow}} + {{c_{1\downarrow}}^\dagger} {c_{0\uparrow}}\right) + t \sin{\left(\frac{\theta}{2} \right)} \left({{c_{1\downarrow}}^\dagger} {c_{2\uparrow}} + {{c_{2\uparrow}}^\dagger} {c_{1\downarrow}}\right) - t \sin{\left(\frac{\theta}{2} \right)} \left({{c_{1\uparrow}}^\dagger} {c_{2\downarrow}} + {{c_{2\downarrow}}^\dagger} {c_{1\uparrow}}\right) + t \cos{\left(\frac{\theta}{2} \right)} \left({{c_{0\downarrow}}^\dagger} {c_{1\downarrow}} + {{c_{1\downarrow}}^\dagger} {c_{0\downarrow}}\right) + t \cos{\left(\frac{\theta}{2} \right)} \left({{c_{0\uparrow}}^\dagger} {c_{1\uparrow}} + {{c_{1\uparrow}}^\dagger} {c_{0\uparrow}}\right) + t \cos{\left(\frac{\theta}{2} \right)} \left({{c_{1\downarrow}}^\dagger} {c_{2\downarrow}} + {{c_{2\downarrow}}^\dagger} {c_{1\downarrow}}\right) + t \cos{\left(\frac{\theta}{2} \right)} \left({{c_{1\uparrow}}^\dagger} {c_{2\uparrow}} + {{c_{2\uparrow}}^\dagger} {c_{1\uparrow}}\right) + \left(- E_{Z} + \mu_{L}\right) {{c_{0\downarrow}}^\dagger} {c_{0\downarrow}} + \left(- E_{Z} + \mu_{R}\right) {{c_{2\downarrow}}^\dagger} {c_{2\downarrow}} + \left(E_{Z} + \mu_{L}\right) {{c_{0\uparrow}}^\dagger} {c_{0\uparrow}} + \left(E_{Z} + \mu_{R}\right) {{c_{2\uparrow}}^\dagger} {c_{2\uparrow}}\)
Find the sweet spot
The sweet spot is the point in the parameter space where the energy difference between the even and odd ground states is minimized.
We will use scipy.optimize.minimize to find the sweet spot.
We will define a function that calculates the energy difference between the even and odd ground states.
This function will take the parameters of the system as input and return the energy difference.
We will then minimize the energy difference function over the three chemical potentials.
parameters = {
"U": 100,
"E_Z": 10,
"t": 0.6,
"theta": 0.2 * np.pi,
"Delta": 1,
}
# Define optimization functions
def f_energy(parity_operator=parity_operator, **parameters):
"""
Calculate the energy difference between the even and odd ground states.
"""
mat = f_hamiltonian(**parameters)
evals, _ = utils.fermionic_eigsh(mat, parity=parity_operator)
return evals[0][0] - evals[1][0]
initial_guess = (parameters["E_Z"], parameters["E_Z"], parameters["Delta"] / 2)
def energy_diff(values):
local_parameters = parameters.copy()
local_parameters["mu_L"] = values[0]
local_parameters["mu_R"] = values[1]
local_parameters["mu_M"] = values[2]
return f_energy(**local_parameters)
sweet_spot = minimize(energy_diff, initial_guess)
_mu_L = sweet_spot.x[0]
_mu_R = sweet_spot.x[1]
_mu_M = sweet_spot.x[2]
# Compute the phase diagram to validate that the sweet spot is correct
mus = np.linspace(-1, 1, 100) + parameters["E_Z"]
mus_ABS = np.linspace(-2, 2, 101)
energy_results = np.array(
[
f_energy(mu_L=mu, mu_M=mu_ABS, mu_R=mu, **parameters)
for mu in mus
for mu_ABS in mus_ABS
]
)
energy_results = energy_results.reshape(len(mus), len(mus_ABS))
fig, ax = plt.subplots(figsize=(4, 2))
c = ax.pcolormesh(
mus, mus_ABS, energy_results.T, shading="auto", vmin=-1e-1, vmax=1e-1, cmap="RdBu_r"
)
ax.scatter(_mu_L, _mu_M, c="black", marker="x", s=10, label="minimization result")
ax.set_xlabel(r"$\mu_L = \mu_R$")
ax.set_ylabel(r"$\mu_M$")
cbar = fig.colorbar(c, ax=ax, location="right", shrink=0.5)
cbar.set_label(r"$E_{even} - E_{odd}$")

Define the leads
We will define the leads of the system as a list of FermionOp times the coupling strength.
The leads are defined as a list of FermionOp objects, where each FermionOp represents the site where the lead is connected to the system.
We consider two leads connected to the first and last sites of the chain.
gamma_0, gamma_2 = sympy.symbols("Gamma_0 Gamma_2", real=True, positive=True)
leads = [gamma_0 * (fermions[0] + fermions[1]), gamma_2 * (fermions[4] + fermions[5])]
for lead in leads:
display(lead)
\(\displaystyle \Gamma_{0} \left({c_{0\downarrow}} + {c_{0\uparrow}}\right)\)
\(\displaystyle \Gamma_{2} \left({c_{2\downarrow}} + {c_{2\uparrow}}\right)\)
Calculate the conductance
We will use the qrate.solvers module to calculate the conductance through the system.
The solve_system function takes the following arguments:
-
hamiltonian: a callable function that returns the Hamiltonian of the system -
leads: a list of callable functions that return the leads of the system -
temperature: the thermal energy of the system -
parity_op: the parity operator used to diagonalize the Hamiltonian in parity sectors -
system_parameters: a dictionary of parameters of the system -
lead_parameters: a dictionary of parameters of the leads
To create the callable functions, we will use the hilbert_space.to_matrix function to convert the Hamiltonian and leads to a matrix representation.
# Define the callables
hamiltonian_dict = hilbert_space.to_matrix(hamiltonian, operators=fermions, sparse=False)
f_hamiltonian = hilbert_space.make_dict_callable(hamiltonian_dict)
lead_dicts = [
hilbert_space.to_matrix(lead, operators=fermions, sparse=False) for lead in leads
]
f_leads = [hilbert_space.make_dict_callable(ld) for ld in lead_dicts]
# Define the lead parameters
lead_parameters = {
"Gamma_0": 1e-3,
"Gamma_2": 1e-3,
}
# Tune the chain to the sweet spot
parameters["mu_L"] = _mu_L
parameters["mu_R"] = _mu_R
parameters["mu_M"] = _mu_M
# Compute conductance
_mus = np.linspace(-0.5, 0.5, 25)
bias = np.linspace([0, -1], [0, 1], 101) / 2
conductance = []
for _mu in _mus:
params = parameters.copy()
params["mu_L"] = _mu + _mu_L
f = solvers.solve_system(
f_hamiltonian,
f_leads,
temperature=1e-2,
parity_op=parity_operator,
system_parameters=params,
lead_parameters=lead_parameters,
)
conductance.append(f(bias))
conductance = np.array(conductance)
# Calculate the excitation spectra for validating the results
excitation_spectra = []
for _mu in _mus:
params = parameters.copy()
params["mu_L"] = _mu + _mu_L
evals, _ = utils.fermionic_eigsh(f_hamiltonian(**params), parity=parity_operator)
evals = np.array(evals)
excitation_spectra.append(
[
# transition within a parity sector
evals[0, :] - evals[0, 0, np.newaxis],
# transition between parity sectors
evals[1, :] - evals[0, 0, np.newaxis],
]
)
excitation_spectra = np.array(excitation_spectra)
bound = 1e-7
fig, ax = plt.subplots(figsize=(7, 4), nrows=2, ncols=2, sharex=True, sharey=True)
im2 = ax[1, 0].pcolormesh(
_mus, bias[:, 1], conductance[:, :, 1, 0].T, cmap="RdBu", vmin=-bound, vmax=bound
)
im3 = ax[0, 1].pcolormesh(
_mus, bias[:, 1], conductance[:, :, 0, 1].T, cmap="RdBu", vmin=-bound, vmax=bound
)
im4 = ax[1, 1].pcolormesh(
_mus, bias[:, 1], np.abs(conductance[:, :, 1, 1].T), vmin=0, vmax=bound
)
im1 = ax[0, 0].pcolormesh(
_mus, bias[:, 1], np.abs(conductance[:, :, 0, 0].T), vmin=0, vmax=bound
)
ax[1, 0].plot(_mus, excitation_spectra[:, 0, :2], "--", c="gray")
ax[1, 0].plot(_mus, excitation_spectra[:, 1, :2], "--", c="gray")
ax[1, 0].plot(_mus, -excitation_spectra[:, 0, :2], "--", c="gray")
ax[1, 0].plot(_mus, -excitation_spectra[:, 1, :2], "--", c="gray")
ax[0, 0].set_ylabel(r"$V_{bias}$")
ax[1, 0].set_ylabel(r"$V_{bias}$")
ax[1, 0].set_xlabel(r"$\mu$")
ax[1, 1].set_xlabel(r"$\mu$")
cbar1 = fig.colorbar(im1, ax=ax[0, 0], location="right", shrink=0.5, label=r"$G_{LL}$")
cbar2 = fig.colorbar(im2, ax=ax[1, 0], location="right", shrink=0.5, label=r"$G_{LR}$")
cbar3 = fig.colorbar(im3, ax=ax[0, 1], location="right", shrink=0.5, label=r"$G_{RL}$")
cbar4 = fig.colorbar(im4, ax=ax[1, 1], location="right", shrink=0.5, label=r"$G_{RR}$")
