Dear Christoph,

Thank you so much for your email and for your advice.

I have attached the full code for my carbon nanotube system (simplified 
slightly) which, when run, should output the animation I have also attached.

The system is evolved for 5 time steps over a short period of time (0 to 
0.5e-8s). It seems that when I increase the number of lattice points, the 
probability density function becomes 'spiky' over time and the resolution of 
the graph deteriorates. However, this might just be a problem with my code.

Thank you again for any help with this.
Best wishes,
Isobel

________________________________
From: Christoph Groth
Sent: Thursday, December 01, 2022 15:34
To: Clarke, Isobel
Cc: kwant-discuss@python.org; Buitelaar, Mark
Subject: Re: [Kwant] Resolution of Tkwant

Clarke, Isobel wrote:

> I was wondering if there is a limit on the resolution of Kwant and
> Tkwant when calculating the density of an evolved wavefunction.

Hi, I’m not aware of any particular issue where a Tkwant simulation
“explodes” after some time.  Obviously, the numerical calculations have
a limited precision.

It would be most helpful if you could provide a complete example that
demonstrates the problem that you experience.  It doesn’t have to be the
same physical system that you use in research.  It’s actually better if
it’s simpler, but one should be able to observe the concrete problems
that trouble you.

> In addition, I was wondering if you had any advice for making the code
> run faster? I ran the simulation for 10 time steps over a very short
> time period to obtain the attached animation and it took around
> 4 hours on my desktop.

As a vague rule of thumb, one needs to step up to a a computing cluster
for tkwant when a laptop/desktop is sufficient with kwant alone.  (This
assumes that one wants to treat similar systems with comparable numbers
of orbitals.)

Or one has to wait longer...

Tkwant calculations need to be integrated over many energies, while with
Kwant at zero temperature that’s not necessary.  So there’s literally
more to do.

There are bottlenecks and problems that could be improved in both Kwant
and Tkwant, and there may be also possible workarounds.  It can help to
profile a run to see where time is actually spent.

Having a small but representative running example (that ideally does
not take four hours!) would help here as well.

Cheers
Christoph

Attachment: Animation - 20 lattice points.mp4
Description: Animation - 20 lattice points.mp4

import kwant.continuum
import numpy as np
import matplotlib.pyplot as plt
import kwant
from numpy import vectorize
from matplotlib import animation
from tkwant import onebody
import tkwant
import os.path
import json
import time as timelib
from matplotlib import animation

class System:
    def __init__(self, hamiltonian, pertubation_type, number_of_lattices, 
potential_type):
        self.potential_type = potential_type
        self.hamiltonian = hamiltonian
        self.number_of_lattices = number_of_lattices
        self.pertubation_type = pertubation_type
        self.evolve_state = False
       
        self.a_0_SI = 5.2917721090380e-11    
        self.total_length_SI = 0.66e-6
        self.omega_0_eV = 1e-3  # in eV
        self.B_0_au = 2.127659574468085e-08
        self.b_sl_au = 2.946499088192983e-12
        self.eV_0_au = 0.0003674930360069677
        self.pulse_frequency_au = self.ev_to_hartree(self.omega_0_eV)
        self.total_length_au = self.m_to_au(self.total_length_SI)
        self.lattice_size_au = self.total_length_au / self.number_of_lattices
        self.mu_B_au = .5
        self.z_au = np.arange(-self.total_length_au / 2, self.total_length_au / 
2, self.lattice_size_au,
                              dtype=np.double)
       
    def ev_to_hartree(self, ev):
        return ev / 2.72114e1
   
    def hartree_to_ev(self, hartree):
        return hartree * 2.72114e1
   
    def m_to_au(self, m):
        return m / self.a_0_SI
   
    def second_to_au(self, time):
        return time * 4.1341373336493e16
   
    def au_to_second(self, time):
        return time / 4.1341373336493e16
   
    def cosine_v_ac(self, time, z, eV_0_au, pulse_frequency_au, 
total_length_au):
        return ((eV_0_au * np.cos(2 * np.pi * pulse_frequency_au * time)) * z) 
/ total_length_au

    def sine_v_ac(self, time, z, eV_0_au, pulse_frequency_au, total_length_au):
        return ((eV_0_au * np.sin(2 * np.pi * pulse_frequency_au * time)) * z) 
/ total_length_au
       
    def potential(self, z, time):
        if self.potential_type == 0: #infinite square well
            total_potential = 0  
        elif self.potential_type == 1: #parabolic potential
            total_potential = .5 * (z * self.omega_0_au) ** 2        
        if self.pertubation_type == "cos":
            self.pertubation = self.cosine_v_ac
        else:
            self.pertubation = self.sine_v_ac
        total_potential += self.pertubation(time, z, self.eV_0_au, 
self.pulse_frequency_au, self.total_length_au)
        return total_potential

    def kwant_shape(self, site):
        (z,) = site.pos
        return (-self.total_length_au / 2 <= z < self.total_length_au / 2)

    def make_system(self):
        self.template = kwant.continuum.discretize(self.hamiltonian, 
grid=self.lattice_size_au)  
        self.syst = kwant.Builder()
        # add the nanotube to the system
        self.syst.fill(self.template, self.kwant_shape, (0,))
        self.syst = self.syst.finalized()

        self.B_y_au = 0    
        def B_constant(z):
            return  -self.B_0_au
        def C_constant(z):
            return -self.b_sl_au * z
        def D_constant(z):
            return -self.B_y_au

        # coefficient for the kinetic energy term
        A_constant = 1

        # parameters of hamiltonian
        self.params = dict(A=A_constant, V=self.potential, B=B_constant, 
C=C_constant, D=D_constant)
        self.tparams = self.params.copy()  # copy the params array
        self.tparams['time'] = 0  # initial time = 0

        # compute the Hamiltonian matrix
        hamiltonian = self.syst.hamiltonian_submatrix(params=self.tparams)
        # compute the eigenvalues (energies) and eigenvectors (wavefunctions).
        eigenValues, eigenVectors = np.linalg.eig(hamiltonian)

        # sort the eigenvectors and eigenvalues according the ascending 
eigenvalues.
        idx = eigenValues.argsort()
        self.initial_eigenvalues = eigenValues[idx]
        eigenVectors = eigenVectors[:, idx]

        # initial wave functions of unperturbed hamiltonian
        self.psi_1_init = eigenVectors[:, 0]
        self.psi_2_init = eigenVectors[:, 1]

        # tkwant object representing the spin-up state
        self.spin_up_state = 
tkwant.onebody.WaveFunction.from_kwant(syst=self.syst,
                                                                    
psi_init=self.psi_1_init,
                                                                    
energy=eigenValues[0],
                                                                    
params=self.params)
        return self.syst

    def evolve(self, time_steps, lattices):  
        self.evolve_state = True
       
        # extract lowest two energies (our qubit states)
        E_1 = np.real(self.initial_eigenvalues[0])
        E_2 = np.real(self.initial_eigenvalues[1])
        # compute the difference in these energies
        self.delta_E = np.abs(E_2 - E_1)

        # compute the resonant frequency for the rabi oscillations
        self.pulse_frequency_au = self.delta_E / (2 * np.pi)  
       
        total_osc_time = self.second_to_au(0.5e-8)
        times_au = np.linspace(0, total_osc_time, num=time_steps)

        density_operator = kwant.operator.Density(self.syst)  
        psi = self.spin_up_state

        if self.pertubation_type == "cos":
            y3_func = self.cosine_v_ac
        else:
            y3_func = self.sine_v_ac          

        # empty array for density and perturbation function
        density = np.zeros((time_steps, lattices))
        perturb = np.zeros((time_steps, lattices))

        array = np.arange(0, time_steps, 1)

        for i in array:
            # evolve the state with time
            psi.evolve(times_au[i])
            density[i] = psi.evaluate(density_operator)
            perturb[i] = self.hartree_to_ev(y3_func(times_au[i], self.z_au, 
self.eV_0_au, self.pulse_frequency_au, self.total_length_au))
       
        self.evolve_state = False
       
        return density, perturb
   
    def animations(self, density, y3, nsteps):

        # create a figure with two subplots
        fig_animation, axs = plt.subplots(2,1)

        # 1920 x 1080
        w_in_inches = 10
        h_in_inches = 6
        dpi = 100
        fig_animation.set_size_inches(w_in_inches, h_in_inches, True)
        fig_animation.set_dpi(dpi)
        fig_animation.tight_layout(pad=5.0)

        # set variables for each of the axes
        ax1 = axs[0]
        ax2 = axs[1]

        # initialise two line objects (one in each axes)
        line1, = ax1.plot([], [], lw=2)
        line2, = ax2.plot([], [], lw=2, color='r', label='$H_1(t)$')
        line = [line1, line2]

        z_max_au = self.m_to_au(self.total_length_SI) / 2  
        z_max_SI = self.total_length_SI / 2
        y2_max = self.hartree_to_ev((self.eV_0_au * z_max_au / 
self.total_length_au))
        y1 = density[0]
        y1_max = np.max(y1) * 2
       

        lattice_size_SI = self.total_length_SI / self.number_of_lattices
        z_SI = np.arange(-self.total_length_SI / 2, self.total_length_SI / 2, 
lattice_size_SI, dtype=np.double)

        # PDF
        ax1.set_xlim(-z_max_SI, z_max_SI)
        ax1.set_ylim(0, np.max(density[-1,:]*1.2))
        ax1.set_ylabel("$|\psi(z,t)|^2$")
        ax1.set_xlabel("z ($m$)")
        ax1.set_title("PDF over time")

        # Potential
        ax2.set_xlim(-z_max_SI, z_max_SI)
        ax2.set_ylim(-y2_max, y2_max)
        ax2.set_ylabel("$E$ (eV)")
        ax2.set_xlabel("z ($m$)")
       
        # initialization function: plot the background of each frame
        def init():
            line[0].set_data([], [])
            line[1].set_data([], [])
            return line

        def animate(i):
            # update line objects.
            line[0].set_data(z_SI, density[i,:])
            line[1].set_data(z_SI, y3[i,:])
            ax2.legend(loc="upper right")
            return line

        # call the animator.  blit=True means only re-draw the parts that have 
changed.
        anim = animation.FuncAnimation(fig_animation, animate, init_func=init,
                                       frames=nsteps-1, interval=300, blit=True)
       
        #folder_path = r"\Users\"
       
        #anim.save('{}/animation.mp4'.format(folder_path), writer='ffmpeg')
        plt.show()
        return True
   
def main():
    lattices = 20
    potential = 0  
    system = System("((A * k_z**2) + V(z, time)) * identity(2) + B(z) * sigma_z 
+ C(z) * sigma_x + D(z) * sigma_y",
                    pertubation_type="sin", number_of_lattices=lattices, 
potential_type=potential)
    system.make_system();

    nsteps = 5
    density, perturbation = system.evolve(nsteps,lattices)
   
    system.animations(density, perturbation, nsteps)
   
   
if __name__ == '__main__':
    main()

Reply via email to