Last active
March 3, 2024 10:46
-
-
Save marl0ny/db6aa96035046ba597d76dcf52f6921b to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Simulation of coupled Fermions by explicitly constructing the Fermion | |
operators as matrices and then diagonalizing the subsequent Hamiltonian | |
to find the energies and energy eigenvectors. | |
The primary reference for this is Chapter 4 of | |
Introduction to Many Body Physics by Piers Coleman, pg 71 - 77 on the | |
Jordan-Wigner transformation. | |
P. Coleman, Simple examples of second quantization, | |
in Introduction to Many Body Physics, pg 71-94, | |
Cambridge University Press, 2015. | |
""" | |
import numpy as np | |
import matplotlib.pyplot as plt | |
import scipy.sparse as sparse | |
import matplotlib.animation as animation | |
from matplotlib.gridspec import GridSpec | |
from typing import List, Tuple, Union | |
# Maximum number of particles, or occupancy. | |
# The two shall be used interchangeably. | |
N = 10 | |
# Fermionic creation operator for a single particle. | |
LADDER_UP = np.array([[0.0, 1.0], [0.0, 0.0]]) | |
# This corresponds to the operator exp(-n i pi), | |
# where n counts the occupation. | |
STRAND = np.array([[-1.0, 0.0], [0.0, 1.0]]) | |
I = np.array([[1.0, 0.0], [0.0, 1.0]]) # 2x2 identity matrix | |
# The state that corresponds to zero occupancy. | |
VACANT_STATE = np.array([1.0 if i == 0 else 0.0 | |
for i in range(int(2**N))])[::-1] | |
def normalize(psi: np.ndarray) -> np.ndarray: | |
return psi/np.sqrt(np.sum(np.abs(psi)**2)) | |
def kron_power(base: Union[sparse.spmatrix, np.ndarray], | |
exponent: int) -> sparse.spmatrix: | |
res = sparse.identity(base.shape[0]) if exponent == 0 else base.copy() | |
for _ in range(exponent - 1): | |
res = sparse.kron(res, base) | |
return res | |
def get_ladder_operators(n: int) -> List[sparse.spmatrix]: | |
""" | |
Returns a list of the coupled Fermionic creation operators for n particles. | |
The way this works is that one first starts with ladder spin operators | |
for a one-dimensional linear sequence of spins, where one now interprets | |
spin down as corresponding to the state of zero occupancy, and spin up as the | |
occupied state. However, the ladder spin operators do not follow | |
Fermionic anticommuting relations; rather they commute. To fix this, | |
one must use the Jordan-Wigner transformation, where each ladder spin | |
operator in the sequence is matrix multiplied by an operator exp(-i n pi), | |
where n counts the occupation of states for **only** the preceding ladder | |
spin operators in the sequence [1]. | |
[1] P. Coleman, Simple examples of second quantization. | |
in Introduction to Many Body Physics, pg 71-94, | |
Cambridge University Press, 2015. | |
""" | |
ladders_u = [] | |
for i in range(n): | |
string = sparse.identity(1) if i == 0 else kron_power(STRAND, i) | |
o1 = sparse.kron(sparse.identity(2**i) @ string, LADDER_UP) | |
o2 = sparse.kron(o1, sparse.identity(2**(N-i-1))) | |
ladders_u.append(o2) | |
return ladders_u | |
def make_to_density_vector_matrix(n): | |
n_mat = np.zeros([n, int(2**n)]) | |
val: int = 1 | |
for i in range(n): | |
for j in range(int(2**n)): | |
element = float(int((val&(~j)) > 0)) | |
# if element > 0.0: print(i, j, element) | |
n_mat[i, j] = element | |
val *= 2 | |
return np.flip(n_mat, axis=0) | |
n_mat = make_to_density_vector_matrix(N) | |
print(n_mat) | |
f_dag = get_ladder_operators(N) | |
f = [o.T for o in f_dag] | |
# Fermionic ladder operators that are decoupled when using them to construct | |
# the Hamiltonian | |
a_dag = [sum([f_dag[j]*np.exp(-2.0j*np.pi*j*k/N) for j in range(N)])/np.sqrt(N) | |
for k in range(N)] | |
a = [sum([f[j]*np.exp(-2.0j*np.pi*j*k/N) for j in range(N)])/np.sqrt(N) | |
for k in range(N)] | |
# Explicitly show the matrices that correspond to our fermionic operators | |
# for i, op in enumerate(f_dag): | |
# print(i, op) | |
# for j, op in enumerate(f): | |
# print(j, op) | |
# Sanity check to ensure that our ladder operators satisfy all the | |
# correct commutation relations. | |
# for i, fi_dag in enumerate(f_dag): | |
# for j, fj in enumerate(f): | |
# print(i, j, '\n', fi_dag @ fj + fj @ fi_dag) | |
# for i, fi_dag in enumerate(f_dag): | |
# for j, fj_dag in enumerate(f_dag): | |
# print(i, j, '\n', fi_dag @ fj_dag + fj_dag @ fi_dag) | |
# for i, fi in enumerate(f): | |
# for j, fj in enumerate(f): | |
# print(i, j, '\n', fi @ fj + fj @ fi) | |
# Construct the Hamiltonian | |
alpha, beta, gamma = 1.0, 2.0, 0.0 | |
H = sum([-alpha*(f_dag[(i+1)%N] @ f[i] + f_dag[(i-1)] @ f[i]) | |
+ beta*f_dag[i] @ f[i] | |
+ gamma*f_dag[i] @ f[i] @ f_dag[i] @ f[i] for i in range(N)] | |
) # + sparse.identity(int(2**N)) | |
# Numerically solve for the energies and their associated eigenvectors | |
energies, eigenstates = np.linalg.eigh(H.toarray()) | |
# List of singly occupied states | |
single_particle_states_list = [f_dag[i] @ VACANT_STATE for i in range(N)] | |
# List of doubly occupied states | |
two_particle_states_list = [f_dag[i] @ f_dag[j] @ VACANT_STATE | |
for i in range(N) for j in range(N)] | |
single_particle_states = np.array(single_particle_states_list) | |
two_particle_states = np.array(two_particle_states_list) | |
# Make the initial state | |
# psi0 = (f_dag[3] + f_dag[3] @ f_dag[2]) @ VACANT_STATE | |
psi0 = (a_dag[0] @ a_dag[1] + f_dag[6] | |
+ a_dag[0] @ a_dag[6] @ a_dag[7]) @ VACANT_STATE | |
# psi0 = np.exp(-np.random.rand(2**N)*2.0j*np.pi)*np.random.rand(2**N) | |
# psi0 = normalize(psi0) | |
# psi0 = (a_dag[1] + a_dag[-1] + a_dag[0] @ a_dag[-1] | |
# + a_dag[0] @ a_dag[1]) @ VACANT_STATE | |
# psi0 = (a_dag[-3] + a_dag[2] @ a_dag[1] @ a_dag[0]) @ VACANT_STATE | |
psi0_e = np.conj(eigenstates.T) @ psi0 | |
# psi0_e[0], psi0_e[1] = 0.0, 0.0 | |
# Show the energy levels for the entire system | |
# plt.scatter([i for i in range(int(2**N))], np.sort(energies)) | |
# plt.show() | |
# plt.close() | |
# Show the sum of each single particle state in the original basis | |
# plt.scatter([i for i in range(int(2**N))], sum(single_particle_states)) | |
# plt.show() | |
# plt.close() | |
# Show psi0 in terms of energy eigenstates | |
# plt.scatter([i for i in range(int(2**N))], np.real(psi0_e)) | |
# plt.scatter([i for i in range(int(2**N))], np.imag(psi0_e)) | |
# plt.show() | |
# plt.close() | |
fig = plt.figure() | |
grid_spec = GridSpec(2, 2, figure=fig) | |
axes = [fig.add_subplot(grid_spec[0, 0]), | |
fig.add_subplot(grid_spec[0, 1]), | |
fig.add_subplot(grid_spec[1, 0]), | |
fig.add_subplot(grid_spec[1, 1])] | |
re_, = axes[0].plot(np.real(np.conj(single_particle_states) @ psi0)) | |
im_, = axes[0].plot(np.imag(np.conj(single_particle_states) @ psi0)) | |
abs_, = axes[0].plot(np.abs(np.conj(single_particle_states) @ psi0), | |
color='black') | |
axes[0].set_title('Singly Occupied States ($f_i^{\dagger}|0>$)') | |
axes[0].set_ylabel('Amplitude') | |
axes[0].set_xlabel('i ($f_i^{\dagger}|0>$)') | |
axes[0].set_xlim(0, N-1) | |
axes[0].set_ylim(-0.75, 0.75) | |
e_re, = axes[3].plot([i for i in range(int(2**N))], np.real(psi0_e), | |
linewidth=1.0, label='Real') | |
e_im, = axes[3].plot([i for i in range(int(2**N))], np.imag(psi0_e), | |
linewidth=1.0, label='Imag.') | |
e_abs, = axes[3].plot([i for i in range(int(2**N))], | |
np.abs(psi0_e), color='black', linewidth=1.0, | |
label='Abs.') | |
axes[3].set_title('Energy States for Entire System') | |
axes[3].set_ylabel('Amplitude') | |
axes[3].set_xlabel('Enumeration from lowest to highest energy') | |
axes[3].set_ylim(-1.0, 1.0) | |
axes[3].set_xlim(0, 2**N-1) | |
axes[3].legend(loc='lower right') | |
axes[2].set_title('Total Occupation Density') | |
axes[2].set_ylabel('Arbitrary Units') | |
axes[2].set_xlabel('index i') | |
density = axes[2].bar([i for i in range(N)], n_mat @ np.abs(psi0)**2) | |
# print(density) | |
# print(density.__dict__) | |
bg_im = axes[1].imshow(np.zeros([N, N]), cmap='Greys_r') | |
im = axes[1].imshow(np.angle(np.reshape((np.conj(two_particle_states) | |
@ psi0), (N, N))), | |
cmap='hsv', interpolation='nearest', | |
alpha=np.abs(np.reshape((np.conj(two_particle_states) | |
@ psi0), | |
(N, N)))**2) | |
axes[1].set_title('Doubly Occupied States ' | |
+ '($\propto f_i^{\dagger}f_j^{\dagger}|0>$)') | |
axes[1].set_ylabel('$f_i^{\dagger}$') | |
axes[1].set_xlabel('$f_j^{\dagger}$') | |
# axes[2].set_xlim(0, N-1) | |
# axes[2].set_ylim(0, N-1) | |
plt.tight_layout() | |
data = {'psi': psi0_e, 't': 0.0} | |
def animation_func(*args): | |
data['t'] += 0.1 # - 1.0j*0.001 | |
data['psi'] = psi0_e*np.exp(-1.0j*energies*data['t']) | |
# data['psi'] = normalize(data['psi']) | |
psi = eigenstates @ data['psi'] | |
psi_sp = np.conj(single_particle_states) @ psi | |
re_.set_ydata(np.real(psi_sp)) | |
im_.set_ydata(np.imag(psi_sp)) | |
abs_.set_ydata(np.abs(psi_sp)) | |
e_re.set_ydata(np.real(data['psi'])) | |
e_im.set_ydata(np.imag(data['psi'])) | |
e_abs.set_ydata(np.abs(data['psi'])) | |
density.datavalues = n_mat @ np.abs(psi)**2 | |
# https://stackoverflow.com/a/42143866 | |
for i, d in enumerate(density.datavalues): | |
density[i].set_height(d) | |
im.set_data(np.angle(np.reshape((np.conj(two_particle_states) | |
@ psi), (N, N)))) | |
abs_val = np.abs(np.reshape((np.conj(two_particle_states) | |
@ psi), (N, N)))**2 | |
abs_val_show = 20.0*abs_val | |
im.set_alpha(np.where(abs_val_show > 1.0, 1.0, abs_val_show)) | |
# print(np.sum(np.abs(psi)**2)) | |
# print(np.sum(np.abs(psi_sp)**2)) | |
return re_, im_, abs_, e_re, e_im, e_abs, bg_im, *density.patches, im | |
# for i in range(N): | |
# plt.scatter([i for i in range(int(2**N))], | |
# f_dag[i] @ GROUND, label=f'{i}') | |
# plt.legend() | |
data['ani'] = animation.FuncAnimation(fig, animation_func, | |
blit=True, interval=1000//60) | |
plt.show() | |
plt.close() | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment