# Source code for solcore.quantum_mechanics.kp_QW

```import numpy as np
import sys
from scipy.sparse import dia_matrix
from scipy.sparse.linalg import eigs
from solcore.constants import q, hbar, electron_mass as m0

from operator import itemgetter

sort_simultaneous = lambda *lists: [list(x) for x in zip(*sorted(zip(*lists), key=itemgetter(0)))]

[docs]def alfa(Eband, m):
return (1 - m) ** 2 * q / Eband

[docs]def kp4x4(mat, host_material):
""" Calculates the bandedges and bulk effective masses of a material under biaxial strain at the gamma point.
SO band is ignored.

This function provides the position at the zone center of the C, HH and LH bands of a material under biaxial strain.
Both, the paralell and transverse effective masses (with respect the Z axis) are provided. SO band is ignored,
therefore the masses are independent of strain. """

Delta = mat.spin_orbit_splitting
Eg = mat.band_gap
Ev0 = mat.valence_band_offset
Ec0 = mat.valence_band_offset + Eg
a0 = mat.lattice_constant
g1 = mat.gamma1
g2 = mat.gamma2
g3 = mat.gamma3
ac = mat.a_c
av = abs(mat.a_v)
b = mat.b
me = mat.eff_mass_electron_Gamma

# Strain parameters
exx = (host_material.lattice_constant - a0) / a0
ezz = - 2 * mat.c12 / mat.c11 * exx

Oe = ac * (exx + exx + ezz)
Pe = -av * (exx + exx + ezz)
Qe = -b / 2 * (exx + exx - 2 * ezz)

Ec = Ec0 + Oe
Evhh = Ev0 - Pe - Qe
Evlh = Ev0 - Pe + Qe

# Effective masses paralell to the Z axis
mhh_p = 1. / (g1 - 2 * g2)
mlh_p = 1. / (g1 + 2 * g2)

# Effective masses transverse to the Z axis, that is, in the plane of the strain
mhh_t = 1. / (g1 + g2)
mlh_t = 1. / (g1 - g2)

return Ec, Evhh, Evlh, me * m0, mhh_p * m0, mlh_p * m0, mhh_t * m0, mlh_t * m0

[docs]def kp6x6(mat, host_material):
""" Calculates the bandedges and bulk effective masses of a material under biaxial strain at the gamma point
including the SO band.

This function provides the position at the zone center of the C, HH, LH and SO bands of a material under biaxial
strain. Both, the paralell and transverse effective masses (with respect the Z axis) are provided."""

Delta = mat.spin_orbit_splitting
Eg = mat.band_gap
Ev0 = mat.valence_band_offset
Ec0 = mat.valence_band_offset + Eg
a0 = mat.lattice_constant
g1 = mat.gamma1
g2 = mat.gamma2
g3 = mat.gamma3
ac = mat.a_c
av = abs(mat.a_v)
b = mat.b
me = mat.eff_mass_electron_Gamma

# Strain parameters
exx = (host_material.lattice_constant - a0) / a0
ezz = - 2 * mat.c12 / mat.c11 * exx

Oe = ac * (exx + exx + ezz)
Pe = -av * (exx + exx + ezz)
Qe = -b / 2 * (exx + exx - 2 * ezz)

x = Qe / Delta
if x == 0: x = 1e-10
com = lambda s: x - 1 + s * np.sqrt(1 + 2 * x + 9 * x ** 2)
f = lambda s: (2 * x * (1 + 1.5 * com(s)) + 6 * x ** 2) / (
0.75 * com(s) ** 2 + com(s) - 3 * x ** 2)  # s=+1 for LH and s=-1 for SO

Ec = Ec0 + Oe
Evhh = Ev0 - Pe - Qe
Evlh = Ev0 - Pe + 0.5 * (Qe - Delta + np.sqrt(Delta ** 2 + 2 * Delta * Qe + 9 * Qe ** 2))
Evso = Ev0 - Pe + 0.5 * (Qe - Delta - np.sqrt(Delta ** 2 + 2 * Delta * Qe + 9 * Qe ** 2))

# Effective masses paralell to the Z axis
mhh_p = 1. / (g1 - 2 * g2)
mlh_p = 1. / (g1 + 2 * f(+1) * g2)
mso_p = 1. / (g1 + 2 * f(-1) * g2)

# Effective masses transverse to the Z axis, that is, in the plane of the strain
mhh_t = 1. / (g1 + g2)
mlh_t = 1. / (g1 - f(+1) * g2)
mso_t = 1. / (g1 - f(-1) * g2)

return Ec, Evhh, Evlh, Evso, me * m0, mhh_p * m0, mlh_p * m0, mso_p * m0, mhh_t * m0, mlh_t * m0, mso_t * m0

[docs]def fill_hamiltonian_holes_4x4(A1, A2, B1, B2, C, D, delta, block):
# Number of meshpoints in z direction. The number of equations to solve is 2N
N = len(A1)

# Values of the diagonals, depending on the equation: 1, 2, 3 or 4
ll14 = lambda i: (A2[i + 1] - 4 * A2[i] - A2[i - 1]) / (4 * delta ** 2)
l13 = lambda i: D[i] / (2 * delta)
d14 = lambda i: 2 * A2[i] / delta ** 2 + A1[i]
u13 = lambda i: C[i] - (D[i + 1] - D[i - 1]) / (4 * delta)
uu14 = lambda i: - (A2[i + 1] + 4 * A2[i] - A2[i - 1]) / (4 * delta ** 2)
uuu13 = lambda i: - D[i] / (2 * delta)

lll24 = lambda i: - D[i] / (2 * delta)
ll23 = lambda i: (B2[i + 1] - 4 * B2[i] - B2[i - 1]) / (4 * delta ** 2)
l24 = lambda i: C[i] + (D[i + 1] - D[i - 1]) / (4 * delta)
d23 = lambda i: 2 * B2[i] / delta ** 2 + B1[i]
u24 = lambda i: D[i] / (2 * delta)
uu23 = lambda i: - (B2[i + 1] + 4 * B2[i] - B2[i - 1]) / (4 * delta ** 2)

lll = np.zeros(2 * N)
ll = np.zeros(2 * N)
l = np.zeros(2 * N)
d = np.zeros(2 * N)
u = np.zeros(2 * N)
uu = np.zeros(2 * N)
uuu = np.zeros(2 * N)

if block == "U":
# We write the equations for the UPPER 2x2 block of the Hamiltonian, for g1 and g2

# We fill the first two equations separatelly
d = d14(0)
u = C - (D - D) / (4 * delta)
uu = - (A2 + 3 * A2) / (4 * delta ** 2)
uuu = uuu13(0)

l = C + (D - D) / (4 * delta)
d = d23(0)
u = u24(0)
uu = - (B2 + 3 * B2) / (4 * delta ** 2)

for i in range(1, N - 1):
# The first equation, for g1
k = 2 * i

ll[k] = ll14(i)
l[k] = l13(i)
d[k] = d14(i)
u[k] = u13(i)
uu[k] = uu14(i)
uuu[k] = uuu13(i)

# The second equation, for g2
k = k + 1

lll[k] = lll24(i)
ll[k] = ll23(i)
l[k] = l24(i)
d[k] = d23(i)
u[k] = u24(i)
uu[k] = uu23(i)

# We fill the last two equations separatelly
ll[2 * (N - 1)] = (- 3 * A2[N - 1] - A2[N - 2]) / (4 * delta ** 2)
l[2 * (N - 1)] = l13(N - 1)
d[2 * (N - 1)] = d14(N - 1)
u[2 * (N - 1)] = C[N - 1] - (D[N - 1] - D[N - 2]) / (4 * delta)

lll[2 * (N - 1) + 1] = lll24(N - 1)
ll[2 * (N - 1) + 1] = (- 3 * B2[N - 1] - B2[N - 2]) / (4 * delta ** 2)
l[2 * (N - 1) + 1] = C[N - 1] + (D[N - 1] - D[N - 2]) / (4 * delta)
d[2 * (N - 1) + 1] = d23(N - 1)

else:
# We write the equations for the LOWER 2x2 block of the Hamiltonian, for g3 and g4

# We fill the first two equations separatelly
d = d23(0)
u = C - (D - D) / (4 * delta)
uu = - (B2 + 3 * B2) / (4 * delta ** 2)
uuu = uuu13(0)

l = C + (D - D) / (4 * delta)
d = d14(0)
u = u24(0)
uu = - (A2 + 3 * A2) / (4 * delta ** 2)

for i in range(1, N - 1):
# The first equation, for g3
k = 2 * i

ll[k] = ll23(i)
l[k] = l13(i)
d[k] = d23(i)
u[k] = u13(i)
uu[k] = uu23(i)
uuu[k] = uuu13(i)

# The second equation, for g4
k = k + 1

lll[k] = lll24(i)
ll[k] = ll14(i)
l[k] = l24(i)
d[k] = d14(i)
u[k] = u24(i)
uu[k] = uu14(i)

# We fill the last two equations separatelly

ll[2 * (N - 1)] = (- 3 * B2[N - 1] - B2[N - 2]) / (4 * delta ** 2)
l[2 * (N - 1)] = l13(N - 1)
d[2 * (N - 1)] = d23(N - 1)
u[2 * (N - 1)] = C[N - 1] - (D[N - 1] - D[N - 2]) / (4 * delta)

lll[2 * (N - 1) + 1] = lll24(N - 1)
ll[2 * (N - 1) + 1] = (- 3 * A2[N - 1] - A2[N - 2]) / (4 * delta ** 2)
l[2 * (N - 1) + 1] = C[N - 1] + (D[N - 1] - D[N - 2]) / (4 * delta)
d[2 * (N - 1) + 1] = d14(N - 1)

lll = np.roll(lll, -3)
ll = np.roll(ll, -2)
l = np.roll(l, -1)
u = np.roll(u, 1)
uu = np.roll(uu, 2)
uuu = np.roll(uuu, 3)

offsets = [-3, -2, -1, 0, 1, 2, 3]
diagonals = np.array([lll, ll, l, d, u, uu, uuu])

output = dia_matrix((diagonals, offsets), shape=(2 * N, 2 * N))

# View hamiltonian, good for debugging
# import pylab
# pylab.imshow(np.real(output.todense()))
# pylab.show()

return output

[docs]def solve_holes_QW_at_kt_4x4(kt, z, fhh, flh, g1, g2, g3, num=(10, 10), quasiconfined=0.0, symmetric=False):
# Normalization factors
N = len(z)
L = max(z)
delta = (z - z) / L
kt = kt * L
E0 = hbar ** 2 / (m0 * L ** 2)

# We invert and shift the potential so that it is possitive with the minimum value around zero
offset = min(np.amin(fhh), np.amin(flh))
fhh = (offset - fhh) / E0
flh = (offset - flh) / E0

# Fill the components of the equations based on the effective masses, potential and k value
A1 = 0.5 * (g1 + g2) * kt ** 2 + fhh
B1 = 0.5 * (g1 - g2) * kt ** 2 + flh
A2 = 0.5 * (g1 - 2 * g2)
B2 = 0.5 * (g1 + 2 * g2)
C = np.sqrt(3) / 2 * 0.5 * (g2 + g3) * kt ** 2
D = np.sqrt(3) * g3 * kt

# Using the shift-invert mode will help to speed up the algorithm by calculating the eigenvalues only inside the QW or very close
# See explanation: https://docs.scipy.org/doc/scipy-0.15.1/reference/tutorial/arpack.html
sigma = min(np.amin(fhh), np.amin(
flh))  # The bottom of the potential (valence band edge) is the target energy for the eigenvalues

# Allow for quasiconfined levels to go through. They can be discarded later with the filter, if necessary
potmax = max(np.amax(fhh), np.amax(flh)) + quasiconfined * q / E0

# Solutions for upper hamiltonian
H_U = fill_hamiltonian_holes_4x4(A1, A2, B1, B2, C, D, delta, "U")
E_U, Psi_U = eigs(H_U, k=num, which='LR', sigma=sigma)

Psi_g1 = Psi_U[range(0, len(Psi_U), 2)]
Psi_g2 = Psi_U[range(1, len(Psi_U), 2)]

if quasiconfined < 0.0:
potmax = max(E_U)

confined_levels = [i for i, e in enumerate(E_U) if (e < potmax and i < num)]
E_U = E_U[confined_levels].real
idx_sorted = np.argsort(E_U)
E_U = E_U[idx_sorted]
Psi_g1 = np.array(Psi_g1[:, confined_levels]).transpose().real[idx_sorted]
Psi_g2 = np.array(Psi_g2[:, confined_levels]).transpose().real[idx_sorted]

norm1 = [(np.sqrt(np.trapz(p * p, x=z))) for p in Psi_g1]
norm2 = [(np.sqrt(np.trapz(p * p, x=z))) for p in Psi_g2]
# Normalise the wavefunction except if the norm is very small. That's the case for some E at k near 0 only
for i in range(len(Psi_g1)):
if norm1[i] > 0.01 * max(norm1):
Psi_g1[i] = Psi_g1[i] / norm1[i]
else:
Psi_g1[i] = Psi_g1[i] * 0.0

if norm2[i] > 0.01 * max(norm1):
Psi_g2[i] = Psi_g2[i] / norm2[i]
else:
Psi_g2[i] = Psi_g2[i] * 0.0

E_U = offset - E_U * E0

# If the structure is not symmetric, we have to solve the lower hamiltonian, too
if not symmetric:
H_L = fill_hamiltonian_holes_4x4(A1, A2, B1, B2, C, D, delta, "L")
E_L, Psi_L = eigs(H_L, k=num, which='LR', sigma=sigma)

Psi_g3 = Psi_L[range(0, len(Psi_L), 2)]
Psi_g4 = Psi_L[range(1, len(Psi_L), 2)]

if quasiconfined < 0.0:
potmax = max(E_L)

confined_levels = [i for i, e in enumerate(E_L) if (e < potmax and i < num)]
E_L = E_L[confined_levels].real
Psi_g3 = np.array(Psi_g3[:, confined_levels]).transpose().real
Psi_g4 = np.array(Psi_g4[:, confined_levels]).transpose().real

norm3 = [(np.sqrt(np.trapz(p * p, x=z))) for p in Psi_g3]
norm4 = [(np.sqrt(np.trapz(p * p, x=z))) for p in Psi_g4]
for i in range(len(Psi_g3)):
if norm3[i] > 0.01 * max(norm3):
Psi_g3[i] = Psi_g3[i] / norm3[i]
else:
Psi_g3[i] = Psi_g3[i] * 0.0

if norm4[i] > 0.01 * max(norm4):
Psi_g4[i] = Psi_g4[i] / norm4[i]
else:
Psi_g4[i] = Psi_g4[i] * 0.0

E_L = offset - E_L * E0

return {"E_U": E_U,
"E_L": E_L,
"Psi_g1": Psi_g1,
"Psi_g2": Psi_g2,
"Psi_g3": Psi_g3,
"Psi_g4": Psi_g4}

else:
# If the structure is symmetric, we simply duplicate the energies and reverse the wavefunctions
return {"E_U": E_U,
"Psi_g1": Psi_g1,
"Psi_g2": Psi_g2}

[docs]def fill_hamiltonian_holes_6x6(A1, A2, B1, B2, C, D, delta, block):
print('Fully coupled 6x6 kp solver not implemented yet. Only the band edges can be calculated. ')
sys.exit(-1)

[docs]def solve_holes_QW_at_kt_6x6(kt, z, fhh, flh, g1, g2, g3, num=(10, 10), quasiconfined=0.0, symmetric=False):
print('Fully coupled 6x6 kp solver not implemented yet. Only the band edges can be calculated. ')
sys.exit(-1)

[docs]def solve_electrons_QW_at_kt_parabolic(kt, z, fe, me, num=(10, 10), quasiconfined=0.0):
""" Returns eignvalue and eigenvectors of an arbitrary potential.

A tridiagonal matrix is constructed by writing the variable effective
mass Schrodinger equation over a series of mesh points. The eigenvalues
of the matrix correspond to the allowed energy levels of the system.

The previous solver, eig, has been replaced by the spare matrix version, eigs, that is faster to compute

"Varible effective mass Schordinger equation and tridiagonal solution method.",
"Frensley, W. R. (1991). \
Numerical evaluation of resonant states. \
Superlattices and Microstructures, 11(3), 347350. \
doi:10.1016/0749-6036(92)90396-M")

"""

# Normalization factors
L = max(z)
kt = kt * L
E0 = hbar ** 2 / (m0 * L ** 2)

# We shift the potential so that it is possitive with the minimum value around zero
offset = np.amin(fe)
fe = (fe - offset) / E0

N = len(fe)
m = me / m0

# Vectorise effective mass differences to avoid a loop
m = np.insert(m, (0, len(m)), (m, m[-1]))
m_a = m[0:-2]  # m_(j-1) from Frensley, W. R. (1991)
m_b = m[1:-1]  # m_j
m_c = m[2:]  # m_(j+1)

# These are the interior diagonals of equation 18 in the above ref.
axis = 0.25 / dz ** 2 * (
1 / m_a + 2 / m_b + 1 / m_c) + fe + 0.5 * kt ** 2 / m_b  # d_j from Frensley, W. R. (1991), modified to include kt and normalised variables
upper = 0.25 / dz ** 2 * (1 / m_a + 1 / m_b)  # s_(j+1)
lower = 0.25 / dz ** 2 * (1 / m_b + 1 / m_c)  # s_j

index = (-1, 0, 1)
diagonals = (-lower, axis, -upper)

H = dia_matrix((diagonals, index), shape=(N, N))

sigma = 0.0  # The bottom of the potential (conduction band) is the target energy for the eigenvalues

# The heavy numerical calculation
E, Psi = eigs(H, k=num, which='LR', sigma=sigma)

# Allow for quasi confined levels to go through.
potmax = max(fe) + quasiconfined * q / E0
if quasiconfined < 0.0:
potmax = max(E)

confined_levels = [i for i, e in enumerate(E) if (e < potmax and i < num)]
E = E[confined_levels].real
Psi = np.array(Psi[:, confined_levels]).transpose()
Psi = [(p / np.sqrt(np.trapz(p * p.conjugate(), x=z))) for p in Psi]

E = E * E0 + offset

# The solutions are degenerate: same energy
return {"E_e": E,
"Psi_e": Psi,
"z": z}

[docs]def band_sorting(bands, symmetric=False):
""" Sort the bands in C, HH and LH according to their character at k=0
:param bands: A dictionary containing the bandstructure as calculated by solve_electrons_QW_at_kt_parabolic and
solve_holes_QW_at_kt_4x4
:return: A dictionary with the same input information but arranged in a different way, including labels
"""

print("Organising the bands into E, HH and LH")

Ee = []
Ehh = []
Elh = []
psi_e = []
psi_hh = []
psi_lh = []

# We vectorise the energies and the wavefunctions
num_e = len(bands["E_e"])
num_hu = len(bands["E_U"])
kt = np.zeros(len(bands))
electrons = np.zeros((len(bands), num_e))
holes_U = np.zeros((len(bands), num_hu))
Psi_e = np.zeros((len(bands), num_e, len(bands["z"])))
Psi_g1 = np.zeros((len(bands), num_hu, len(bands["z"])))
Psi_g2 = np.zeros((len(bands), num_hu, len(bands["z"])))

for i in range(len(bands)):
kt[i] = bands[i]

# Electrons
for j in range(num_e):
try:
electrons[i, j] = bands[i]["E_e"][j]
Psi_e[i, j, :] = bands[i]["Psi_e"][j].real
except IndexError:
electrons[i, j] = np.inf
# Holes
for j in range(num_hu):
try:
holes_U[i, j] = bands[i]["E_U"][j]
Psi_g1[i, j, :] = bands[i]["Psi_g1"][j].real
Psi_g2[i, j, :] = bands[i]["Psi_g2"][j].real
except IndexError:
holes_U[i, j] = np.inf

# Conduction bands are easy to organise... the are all the same!!
# For the hole bands, we say it is HH if the norm of Psi_g1 at k=0 is 1, and LH otherwise
# This sorting is not very reliable as just outside k=0 the band can be strongly (mostly) LH-like
for i in range(num_e):
Ee.append(electrons[:, i])
psi_e.append(Psi_e[:, i, :])

norm = [(np.sqrt(np.trapz(p * p, x=bands["z"]))) for p in Psi_g1[0, :]]
for i in range(num_hu):
if norm[i] > 0.5:
Ehh.append(holes_U[:, i])
psi_hh.append(Psi_g1[:, i, :])
else:
Elh.append(holes_U[:, i])
psi_lh.append(Psi_g2[:, i, :])

# We repeat the process if we are using the non-symmetric mode
if not symmetric:
num_hl = len(bands["E_L"])
holes_L = np.zeros((len(bands), num_hl))
Psi_g3 = np.zeros((len(bands), num_hl, len(bands["z"])))
Psi_g4 = np.zeros((len(bands), num_hl, len(bands["z"])))

for i in range(len(bands)):

for j in range(num_hl):
try:
holes_L[i, j] = bands[i]["E_L"][j]
Psi_g3[i, j, :] = bands[i]["Psi_g3"][j].real
Psi_g4[i, j, :] = bands[i]["Psi_g4"][j].real
except IndexError:
holes_L[i, j] = np.inf

# Plot the wavefunctions vs kt and z. Good for debugging
# import matplotlib.pyplot as plt
# import sys
# K, Z = np.meshgrid(kt, bands["z"])
# plt.contourf(Z, K, Psi_e[:, 0, :].T**2, 50)
# plt.show()
# sys.exit()

norm = [(np.sqrt(np.trapz(p * p, x=bands["z"]))) for p in Psi_g4[0, :]]
for i in range(len(norm)):
if norm[i] > 0.5:
Ehh.append(holes_L[:, i])
psi_hh.append(Psi_g4[:, i, :])
else:
Elh.append(holes_L[:, i])
psi_lh.append(Psi_g3[:, i, :])

Ehh = np.array(Ehh)
Elh = np.array(Elh)
psi_hh = np.array(psi_hh)
psi_lh = np.array(psi_lh)

# Finally, we convert the lists containing the useful stuff into arrays
Ee = np.array(Ee)
Ehh = np.array(Ehh)
Elh = np.array(Elh)
psi_e = np.array(psi_e)
psi_hh = np.array(psi_hh)
psi_lh = np.array(psi_lh)

return {"kt": kt,           # k points
"Ee": Ee,           # Electron energy levels vs K
"psi_e": psi_e,     # Electron wavefunctions vs X and K
"Ehh": Ehh,         # Heavy hole energy levels vs K
"psi_hh": psi_hh,   # Heavy hole wavefunctions vs X and K
"Elh": Elh,         # Light hole energy levels vs K
"psi_lh": psi_lh,   # Light hole wavefunctions vs X and K
"symmetric": symmetric}  # If the structure is symmetric

[docs]def solve_bandstructure_QW(structure, num=10, kpoints=40, krange=5e9, quasiconfined=0.0, symmetric=False,
plot_bands=False):
allk_t = np.linspace(0, krange, kpoints)
bands = []

z = structure["x"]
fe = structure["Ve"]
me = structure["me"]
fhh = structure["Vhh"]
flh = structure["Vlh"]
g1 = structure["g1"]
g2 = structure["g2"]
g3 = structure["g3"]

# First we solve at kt = 0
new_kpoint = solve_electrons_QW_at_kt_parabolic(0.0, z, fe, me, num=(num, num), quasiconfined=quasiconfined)
new_kpoint.update(solve_holes_QW_at_kt_4x4(0.0, z, fhh, flh, g1, g2, g3, num=(num, num), symmetric=symmetric,
quasiconfined=quasiconfined))
bands.append([0.0, new_kpoint])

# We search only for the solutions corresponding to energy levels confined at k=0
num_e = len(new_kpoint['E_e'])
num_h = len(new_kpoint['E_U'])

for kt in allk_t[1:]:
new_kpoint = solve_electrons_QW_at_kt_parabolic(kt, z, fe, me, num=(num, num_e), quasiconfined=-0.1)
new_kpoint.update(solve_holes_QW_at_kt_4x4(kt, z, fhh, flh, g1, g2, g3, num=(num, num_h), symmetric=symmetric,
quasiconfined=0.1))

bands.append([kt, new_kpoint])

print("Calculation finished!!")

bands = band_sorting(bands, symmetric=symmetric)

if plot_bands:
import matplotlib.pyplot as plt

fig, (ax1, ax2) = plt.subplots(2, 1)

# Electrons
ax1.plot(bands["kt"]*1e-9, bands["Ee"] / q, 'b', label='Ee')
for i in range(1, len(bands["Ee"])):
ax1.plot(bands["kt"]*1e-9, bands["Ee"][i] / q, 'b')

# HH
ax2.plot(bands["kt"]*1e-9, bands["Ehh"] / q, 'r', label='Ehh')
for i in range(1, len(bands["Ehh"])):
ax2.plot(bands["kt"]*1e-9, bands["Ehh"][i] / q, 'r')

# LH
ax2.plot(bands["kt"]*1e-9, bands["Elh"] / q, 'g', label='Elh')
for i in range(1, len(bands["Elh"])):
ax2.plot(bands["kt"]*1e-9, bands["Elh"][i] / q, 'g')

ax1.set_ylabel('Energy (eV)')
ax2.set_ylabel('Energy (eV)')
plt.xlabel('k (nm\$^{-1}\$)')
plt.xlim(0, max(bands["kt"]*1e-9))
plt.setp(ax1.get_xticklabels(), visible=False)
plt.legend()
plt.tight_layout()
plt.show()

bands2 = structure.copy()
bands2.update(bands)

return bands2
```