This is a long post. On mobile some equations may go overfull.
The goal of this post is to elegantly program the Spin Hamiltonian governing
recombination in Silicon Carbide. Our system involves two electrons and two nuclei.
We define our orthonormal basis as follows:
The ↑,↓ spin basis is called the Zeeman basis.
I define the basis with the two electrons coupled and the nuclei
in the Zeeman Basis as the coupled basis. Every state in our two-electron +
two-nuclei system is given by
electrons∣s,m⟩nuclei∣mI1⟩∣mI2⟩.
We will evaluate H^Z,H^HF,H^ZFS, and H^EX
on the above state(s).
Here, ge is ≃g-factor for the electron, gn1 is the g-factor
for the first nucleus (silicon), and gn2 is the g-factor for the second
nucleus (carbon). μB and μN are the Bohr
Magneton and Nuclear
Magneton respectively.
Now we apply Equation (1-2) to every basis state; every combination of the above
electron + nuclei pairs. This will generate our 16 × 16 Zeeman
Hamiltonian. It will be a diagonal matrix with entries because the coupled-basis is already
the eigenbasis.
// zeeman constants
g_e = sp.symbol("g_e")
g_n1 = sp.symbol("g_n1")
// ...
// zeeman frequencies
omega_e = g_e * mu_B * B0
omega_n1 = g_n1 * mu_N * B0
omega_n2 = g_n2 * mu_N * B0
energies = []
for s, m in electron_pairs: // electrons run slow
for mI1, mI2 in nuclear_pairs: // nuclei run fast
energies.append(
m*omega_e + // electron
mI1*omega_n1 + // nucleus 1
mI2*omega_n2 // nucleus 2
)
// zeeman hamiltonian
H_Z = sp.diag(*energies)
However, we have to be careful. We rendered the Zeeman Hamiltonian
in a nested for-loop over the coupled-basis states. This ordering needs to be
immutable for calculating all Hamiltonians.
The Hyperfine Hamiltonian is simplest when all electrons and nuclei are in the
Zeeman {↑,↓} basis. We’ll first attack solving a
two-electron (a,b) + one-nucleus (I) system in Zeeman basis. Then we’ll expand
to the two-electron + two-nuclei system and ultimately convert to the coupled-basis
at the end via a Clebsch-Gordan transformation.
We are almost done. From ladder operators, we know
S^±∣s,m⟩=ℏs(s+1)−m(m±1)∣s,m±1⟩.
I’ll again emphasize we are working in the purely Zeeman basis for two
electrons and one nucleus. Each basis state is of the form ∣ma,mb,mI⟩,
where each mi∈{↑,↓}.
Let’s go step by step to evaluate H^HF∣ma,mb,mI⟩.
From Equation (5), we find
where ma,mb,mI all run over {↑,↓}. The equation
above defines H^HF for an 8 Zeeman-basis two-electron + one-nucleus
system. Evolving it to our two-electron + two-nuclei system in silicon carbide
is straightforward.
However, we must remember this is the Zeeman ↑,↓ basis. We
need to convert it to the same coupled basis we used for defining the Zeeman
Hamiltonian.
In Quantum mechanics, you learn about Clebsch-Gordan coefficients. These are
coefficients that describe how to map the Zeeman ∣↑,↓⟩ basis to the
coupled ∣s,m⟩ basis. We make a matrix transformation out of these
coefficients.
For mapping the coupled basis to the Zeeman basis, we have
where we ⊗I4 because we want to leave the nuclear ∣mI1,mI2⟩ in the Zeeman basis and only couple the electrons into ∣s,m⟩. Then, once we have the full 16 × 16 Hyperfine Hamiltonian in
the Zeeman Basis, we can directly apply the transformation
H^HFcoupled=W(H^HFZeeman)W†
to get the final Hyperfine Hamiltonian in the coupled basis. We have to ensure
that after this transformation, the ordering is identical to the ordering of the
Zeeman Hamiltonian.
We first build the 16 Zeeman basis states in correct order. Electron states run
slow. Nuclear states run fast.
/// builds correctly ordered zeeman basis
def _generate_zeeman_basis(self):
half = sp.Rational(1, 2)
return [
(m_a, m_b, m_I1, m_I2)
for m_a in (half, -half)
for m_b in (half, -half)
for m_I1 in (half, -half)
for m_I2 in (half, -half)
] // len 16
The trickiest part is emulating the boxed equation above. To begin, we define
the following helper functions for working with delta functions.
/// if spins are parallel
def _delta_parallel(self, m_e, m_I):
return m_e == m_I
/// if spins are antiparallel
def _delta_antiparallel(self, m_e, m_I):
return m_e == -m_I
Next, we have to write a function that takes in a ∣ma,mb,mI1,mI2⟩ and outputs the resulting coefficient in front from performing
H^HF∣ma,mb,mI1,mI2⟩.
where i∈{a,b} and j∈{1,2}. We’ll do this by generating a
dict[new_ket, coeff] that associates each key new_ket with its coeff in
front step by step. In code we set ℏ=1 to simplify. It’ll get included later.
These are unknown constants that are present in H^HF. We generate
another dict[key, sp.Symbol]. where the sp.Symbols are the actual
constants. We’ll access them via their key
self.A = {}
for e in ('a', 'b'):
for n in ('1', '2'):
for comp in ('x', 'y', 'z'):
key = f'A{e}{n}{comp}'
self.A[key.upper()] = sp.symbols(key)
The first term in the boxed H^HF expression is simplest. It leaves
∣ma,mb,mI1,mI2⟩ as an eigenstate. The coefficients
in front are just products of Ama/bmI1,2. We’ll attack these
first.
These coefficients are more difficult. They involve delta functions that
either do nothing or collapse states to 0 depending on spins
mi and mIj being parallel or antiparallel. This will be where we use
our _delta_parallel and _delta_antiparallel helpers.
Now, evaluating H^HF on every Zeeman state gives us the full
Hyperfine Hamiltonian in the Zeeman basis. We just iterate through and build.
SymPy syntax isn’t important.
/// 16x16 hyperfine hamiltonian
/// zeeman Basis
def _build_zeeman_matrix(self):
self.basis_ze = self._generate_zeeman_basis()
self.index_ze = {
ket: i for i, ket in enumerate(self.basis_ze)
}
size = len(self.basis_ze)
// initialize 16x16 matrix
H = sp.MutableDenseMatrix(size, size, lambda *_: 0)
// iterate through zeeman states
for j, ket in enumerate(self.basis_ze):
action = self._action_on_ket(ket)
// input coefficients into matrix
for new_ket, coeff in action.items():
i = self.index_ze[new_ket]
H[i, j] = coeff
return H.as_immutable()
Now all that is left is to apply the Clebsch-Gordan W transformation in
Equation (7). Let’s first build W from Equation (6). It’s a tensor product of
a 4×4 Clebsch-Gordan matrix with the 4×4 identity.
The last two terms give the forbidden m±2 transitions that create the
half-field response (to be described later). This Hamiltonian acts on the
electrons only. It’s a dipole-dipole interaction. Nuclear terms are left alone.
That makes things quite easy.
We’ll just use that again to maintain the basis. The first term, Equation (8),
give the diagonal elements (∣s,m⟩ are the eigenstates). Equation
(9-10) give the off-diagonal elements that mix m→m±2.
// zfs constants
D = sp.symbols('D')
E = sp.symbols('E')
// 4x4
// zfs on coupled electrons
H_elec = sp.zeros(4)
for i, (s, m) in enumerate(electron_pairs):
// diagonal term
// D * m^2 - (D/3) * s(s+1)
H_elec[i, i] = D*m**2 - (D/3)*s*(s + 1)
// off-diagonal term
// coupling m -> m +/- 2
for dm, expr in [
(2, E/2*(s*(s + 1) - m*(m + 1))),
(-2, E/2*(s*(s + 1) - m*(m - 1)))
]:
// m +/- 2
final_m = m + dm
if (s, final_m) in electron_pairs:
j = electron_pairs.index((s, final_m))
H_elec[i, j] = expr
H_elec[j, i] = expr
// 4x4
// nuclei identity
I_nuc = sp.eye(4)
// 16x16 zfs hamiltonian
H_ZFS = sp.kronecker_product(H_elec, I_nuc)
We take the tensor product H^ZFSelectron⊗I4 again to lift the electronic 4×4 Hamiltonian to a 16×16 one
in our Hilbert space. This gives H^ZFS in the coupled-basis.
We’ll also program this with ℏ=1. H^EX already
acts on the coupled-basis (nuclear ∣mI⟩ are left alone). Additionally,
all states are eigenstates; H^EX is diagonal. Programming
this is the easiest.
Our next challenge is to perform eigenenergy simulations. I want a plot like in
Part 1 but
for every combination of every sub-Hamiltonian in H, and for all
16 coupled-basis states.
This may seem ambitious, but it’s very straightforward. We already have matrices
for all the Hamiltonians. We just need to substitute values for all the
constants above and sweep over the B0 field parameter in H^Z. Then
we’ll use LAPACK to calculate the energy eigenvalues
at each B0 field point. Consequently we’ll have our plots for B0 vs.
energy.