.. _NessEx: Steady-State Examples ===================== .. contents:: :local: :depth: 2 .. _NessEx01: Steady-state Dyson equation: Anderson Impurity model ----------------------------------------------------- .. _NessEx01S01: Synopsis ~~~~~~~~ We first demonstrate the use of the steady-state implementation for a perturbative solution of transport through a single impurity Anderson model. The on-site impurity Hamiltonian is .. math:: H_{loc} = U n_\uparrow n_\downarrow + \left(\mu-\frac{U}{2} + \epsilon_d\right)(n_\uparrow + n_\downarrow), where :math:`U` is the local interaction, and :math:`\epsilon_d` the bare level energy; :math:`\mu=0` will be set to zero in the following, such that :math:`\epsilon_d=0` corresponds to a particle-hole symmetric case. The impurity is coupled to two infinite metallic leads, the left (:math:`L`) and right (:math:`R`) bath, which are kept at a voltage bias :math:`V`. Integrating out the leads gives rise to an embedding self-energy :math:`\Sigma_{\rm bath}=\Sigma_{L}+\Sigma_{R}`, which is defined via a spectral representation. We use a smooth box density of states .. math:: A_\Sigma(\omega) = \frac{\Gamma/\pi}{(1+e^{\nu(\omega-\omega_c)})(1+e^{-\nu(\omega_c+\omega)})}, where :math:`\Gamma` is the hybridisation strength, :math:`\omega_c` is the half bandwidth, and :math:`\nu` a smoothening parameter. While the density of states :math:`A_\Sigma(\omega)` is identical for the left and right baths, :math:`\Sigma^<_{L,R}` is given by the equilibrium distribution with different chemical potentials :math:`\mu_{L/R}=\pm V/2`. In the following, we use :math:`\omega_c=10\Gamma` and :math:`\nu=3/\Gamma`; :math:`\Gamma=1` defines the energy unit, and :math:`1/\Gamma` is the unit of time. The noninteracting impurity Green's function :math:`G_{0}` is therefore determined through the steady-state variant of the Dyson equation :math:`G_0 =( i\partial_t + \mu - \Sigma_{\rm bath})^{-1}`, while the interacting Green's function :math:`G` is obtained from a Dyson equation :math:`G =( i\partial_t + \mu - \Sigma_{\rm bath}+\Sigma_U)^{-1}`, with an additional self-energy :math:`\Sigma_U` due to interactions. In the example below, we approximate :math:`\Sigma_U` by a (non self-consistent) 2nd order perturbation theory, i.e., :math:`\Sigma_U` is given by .. math:: \Sigma(t,t') = U(t) G_0(t,t') G_0(t,t') G_0(t',t) U(t'), evaluated in the steady state. Finally, the current is defined by the rate of particle transfer from the left to the right reservoir, :math:`J= \frac{dN_L}{dt}= -\frac{dN_R}{dt}`. An exact expression can be derived using equations of motion for the Green's functions, and is given by equal-time convolution .. math:: J = 2\left(\Sigma_L\ast G - G\ast\Sigma_L\right)^<(t,t), where the factor :math:`2` is due to spin. This relation is in general not satisfied away from half-filling in bare (non-conserving) second order perturbation theory. In the steady state, the current can be evaluated using the ``convolution_density_matrix`` method, see :ref:`PNess02S02E`, .. math:: J = -2i\left( \rho_{\Sigma_L,G} - \rho_{G,\Sigma_L} \right) =4\text{Im} \left( \rho_{\Sigma_L,G} \right). Here the second equation uses hermitian symmetry :math:`\rho_{\Sigma_L,G} = \rho_{G,\Sigma_L}^\dagger`. .. _NessEx01S02: Details and Implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The relevant files for the implementation, found in ``nessi/examples/``, are listed in the table below. .. list-table:: :header-rows: 0 * - ``programs/ness2_siam.cpp`` - Source code. * - ``utils/demo_ness2_siam.ipynb`` - Jupyter notebook to run the program. * - ``utils/demo_ness2_siam.py`` - A Python script; same as the notebook. The implementation is built on the functions in the ``ness2`` namespace. The input parameters for the main program are the physical parameters ``U`` (interaction :math:`U`), ``beta`` (inverse temperature :math:`\beta`), ``V`` (voltage bias :math:`V`), and ``epsd`` (on-site energy :math:`\epsilon_d`), as well as the numerical parameters ``Nft`` (number of time/frequency points) and ``h`` (timestep :math:`h`). Moreover, we allow for a nonzero ``eta`` for the regularization of the Dyson equation (which can however be set to zero in the example below, and would only be relevant if the level :math:`\epsilon_d` has no spectral overlap with the baths). After reading the input parameters from the input file we initialize ``herm_matrix_ness`` objects .. code-block:: cpp herm_matrix_ness G0(Nft,size); for the noninteracting Green's functions :math:`G_0`, and similar for ``G`` (:math:`G`), ``SL`` (:math:`\Sigma_L`), ``SR`` (:math:`\Sigma_R`), ``Sbath`` (:math:`\Sigma_L+\Sigma_R`), ``SU`` (:math:`\Sigma_U`), and ``S`` (:math:`\Sigma_L+\Sigma_R+\Sigma_U`). Next, the left and right baths are initialized with the given smooth box density of states. For this, we define a class to provide the density of states. .. code-block:: cpp class box_dos{ public: double hi_,lo_,wc_,nu_; box_dos() wc_(10.0), nu_(3.0), hi_(15.0), lo_(-15.0) {} double operator()(double w){ return 1/((1+exp(nu_*(w-wc_))*(1+exp(-nu_*(wc_+w))); } }; Here ``hi_`` and ``lo_`` define the bounds of the integrals in the spectral representation. The bath self-energies are then initialized using .. code-block:: cpp box_dos dos(); green_equilibrium_ness(FERMION,SL,dos,beta,+0.5*V,h,FFT_TRAPEZ); green_equilibrium_ness(FERMION,SR,dos,beta,-0.5*V,h,FFT_TRAPEZ); Sbath=SL; Sbath.incr(SR,1.0,fft_domain::time); // Sbath+= SR on time-data Next we solve the noninteracting Dyson equation using .. code-block:: cpp cdmatrix eps_matrix(1,1); eps_matrix(0,0) = epsd; dyson(G0,mu,eps_matrix,Sbath,h,FFT_TRAPEZ,BATH_GAUSS,eta,beta,mu); Here we allow for the regularization using the gaussian bath if ``eta`` is nonzero. With the resulting ``G0`` one can determine the 2nd order self-energy. The structure of the diagram is analogous as for the previous real-time example, and hence also the implementation is similar: .. code-block:: cpp herm_matrix_ness W(Nft, size1); // temporary W Bubble1_ness(W, G0, G0); //W(t,t') <- ii*G(t,t')G(t',t) W.smul(U*U,fft_domain::time); // W(t,t') <- U^2 W(t,t') Bubble2_ness(SU,G,W); //Sigma_U(t,t')<-ii*G(t,t')W(t',t) SU.smul(-1.0,fft_domain::time); Finally, we solve the interacting Dyson equation: .. code-block:: cpp S=Sbath; S.incr(SU,1.0,fft_domain::time); // S += SU dyson(G,mu,eps_matrix,S,h,FFT_TRAPEZ,BATH_GAUSS,eta,beta,0); For postprocessing we compute the convolutions .. code-block:: cpp cdmatrix SUG,SLG,rho; convolution_density_matrix(SLG,FERMION,SL,G,h); // ii*[SL*G]^<(t=0) convolution_density_matrix(SUG,FERMION,SU,G,h); // ii*[SU*G]^<(t=0) density_matrix(rho,FERMION,G); // = ii*G^<(t=0) double Current = 4.0*SLG.trace().imag(); double Eint = (SUG).trace().real(); // interaction energy double dens = 2*rho.trace().real(); // Finally we update the frequency-domain Green's functions, such that we can later analyze the spectra: .. code-block:: cpp G.transform_to_freq(h,FFT_TRAPEZ); At the end, all output is stored into a single HDF5 file: .. code-block:: cpp // create the hdf5 file (char *flout points to the filename) hid_t file_id = open_hdf5_file(flout); // write Nft to group "/Nft" in the file : store_int_attribute_to_hid(file_id, "Nft", Nft); [...] // similar for dens, Eint, Current G0.write_to_hdf5(file_id, "G0"); // new group "/G0" in file [...] // same for G,S,... close_hdf5_file(file_id); The HDF5 output is conveniently interpreted using the Python utilities provided with the ``ReadNESS`` modules. .. _NessEx01S03: Discussion ~~~~~~~~~~~ :numref:`AIM_ness_1` c) shows converged results for the interacting impurity spectral functions :math:`A(\omega)` and the noninteracting impurity spectral functions :math:`A_0(\omega)`, for a given bath as shown in :numref:`AIM_ness_1` a). The spectrum is essentially a Lorentzian peak, which becomes slightly more broadened for nonzero interaction :math:`U`. The non-equilibrium nature of the state is evident from the distribution function .. math:: F(\omega) = \frac{G^<(\omega)}{2\pi i A(\omega)}. The latter becomes clearly non-thermal, simultaneously reflecting the Fermi edges in the left and right bath (see :numref:`AIM_ness_1` d)). Interactions support thermalization and therefore slightly reduce the sharp edges, compare the blue and red curves in :numref:`AIM_ness_1` d) for :math:`F(\omega)=G^<(\omega)/2\pi i A(\omega)` and :math:`F_0(\omega)=G_0^<(\omega)/2\pi i A_0(\omega)`, respectively. :numref:`AIM_ness_1` b) shows the current as a function of the voltage, which evolves from the linear response regime at small :math:`V` to a saturated value of :math:`J=2` (corresponding one transport channel for each spin) when :math:`V` becomes comparable to bandwidth :math:`2\omega_c`, such that the left (right) bath is full (empty). In the interacting case, the current is reduced, consistent with the broadening of the steps in the distribution function. Of course, the bare second order perturbation theory cannot correctly describe the Kondo effect at low temperatures and large :math:`U`, which is not in the scope of the present code. The steady state code can however be easily combined with a more accurate diagrammatic computation of real-time Green's functions and self-energies in the steady state. .. _AIM_ness_1: .. figure:: ../_static/ness2_siam_01.png :width: 60% :align: center Results for the Anderson model. Anderson model for :math:`U=6`, and :math:`\beta=20`. a) Bath spectral functions and occupation functions (dashed) for voltage :math:`V=2`. b) Current as function of voltage :math:`V`, for :math:`\beta=20`. c) Interacting spectral function :math:`A(\omega)` (full blue lines) and noninteracting spectral function :math:`A_0(\omega)` (full red lines) for :math:`V=2`. The dashed lines show the imaginary part of the corresponding lesser Green's functions. d) Occupation functions for the interacting case (:math:`F(\omega)`) and noninteracting case (:math:`F_0(\omega)`) for :math:`V=2`. In :numref:`AIM_ness_2`, we demonstrate the numerical convergence of the steady state approach (for :math:`U=6`, :math:`V=2`, :math:`\beta=20`). We first fix a small value :math:`h=0.001` of the timestep, and perform simulations with different length :math:`N_{\rm ft}` of the Fourier domain. This tests the convergence with the maximal real time :math:`t_{\rm c}= h N_{\rm ft}/2` which is represented by the time grid. For accurate results, it is necessary that all functions :math:`G^{R,<}(t)` and :math:`\Sigma^{R,<}(t)` essentially decay to zero within the domain :math:`|t| W*U**2 Bubble2_ness(SU_ness, Gweiss_ness, W_ness); // SU = ii * G * W SU_ness.smul(-1.0, fft_domain::time); // SU *= -1 K_ness.set_zero(fft_domain::time); // kernel K=0 K_ness.incr(SU_ness,1.0,fft_domain::time); // K += SU K_ness.incr(G_ness,1.0,fft_domain::time); // K += G (using Delta=G) Finally, we solve the :ref:`P4S1` Dyson equation, and compute the :math:`L_2` norm difference to the previous iteration (stored in ``G_old``) on the time-grid: .. code-block:: cpp dyson(G_ness, mu, ham, K_ness, h, FFT_TRAPEZ); // update G_ness err = distance_norm2(G_ness,G_old,fft_domain::time); The convergence of the DMFT iteration is typically improved if the Green's function at the new iteration is not taken as the updated result :math:`G'`, but as a linear combination :math:`G_{\rm new} = \alpha G' + (1-\alpha) G_{\rm old}` (:math:`\alpha` is the mixing parameter ``mix``). .. code-block:: cpp G_ness.smul(mix,fft_domain::time); // linear mixing for convergence G_ness.incr(G_old,1.0-mix,fft_domain::time); The DMFT iteration loop is now ended if the maximum number of iteration is reached, or if the :math:`L_2` error ``err`` falls below the cutoff provided by ``errmax``. Finally, we write all relevant Green's functions to an HDF5 file, just as in the example :ref:`NessEx01`. To read the file one can again use the Python utilities provided with the ``ReadNESS`` modules. .. _NessEx02S03: Discussion ~~~~~~~~~~~ For illustration, we first show in :numref:`ness_bethe_1` the convergence of the spectrum :math:`A(\omega)` and the occupied density of states :math:`G^<(\omega)` during the DMFT loop. In general, we observe that the linear mixing can be more relevant for the DMFT loop in the steady-state implementation than for the imaginary-time evolution within the real-time code. (In the example, we use a mixing factor :math:`\alpha=0.5`). Moreover, if the time cutoff :math:`\sim hN_{\rm ft}` in the simulation is not sufficient for the Dyson equation to be accurately solved, the decrease of the DMFT error with iteration would slow down around a value that is determined by the accuracy of the Dyson equation. .. _ness_bethe_1: .. figure:: ../_static/ness_bethe_convergence.png :width: 60% :align: center DMFT convergence analysis. Steady state DMFT loop (:math:`U=4`, :math:`\beta=10`, IPT), with :math:`N_{\rm ft}=2^{13}` and a timestep :math:`h_{\rm ness}=0.02`. (a) Convergence of spectral function :math:`A_{\rm iter}(\omega)` with iteration iter=:math:`0\,...\,30` (see color bar in a)). b) Same as (a), but for :math:`G^<(\omega)`. Inset in b) Convergence error :math:`\epsilon_{\rm iter}= |G_{\rm iter+1}-G_{\rm iter}|_2` as function of iteration; the difference is evaluated in the time-domain. In :numref:`ness_bethe_2`, we demonstrate that the NESS simulation and the real-time simulation converge to the same result. In equilibrium, the real-time solution results in Green's functions which are translationally invariant in time. Their value on a given timestep :math:`t_1` should therefore coincide with the NESS results, .. math:: X_{\rm cntr}(t_1,t_2) = X_{\rm ness}(t_1-t_2), for :math:`X=G^{R,<},\Sigma^{R,<}`. The colored lines in :numref:`ness_bethe_2` a) and b) show the results for the NESS simulation, while the dashed black lines correspond to the last timeslice (:math:`t_1=h_{\rm cntr}\cdot n_t`) of the real-time simulation. The results indeed coincide within the line-width of the plots. Moreover, :numref:`ness_bethe_2` c) and d) shows :math:`G^{R,<}(t)` and :math:`\Sigma^{R,<}(t)` on a logarithmic scale over the full time domain :math:`|t| Gcntr(tc,1,FERMION); We then read the NESS Green's functions from the HDF5 output of the NESS simulation (with name ``ness_filename``), where it is stored under a group ``G``: .. code-block:: cpp herm_matrix_ness Gness; Gness.read_from_hdf5(ness_filename,"G"); The object ``Gness`` is thereby also resized to the correct dimension ``Nft``. Since the real-time evolution will be run on a grid that is coarser by a factor :math:`d`, we must downsample the steady-state Green's function: .. code-block:: cpp herm_matrix_ness Gness1; Gness1=downsample(Gness,downsampling); This will automatically resize ``Gness1`` to the correct dimension ``Nft1``, where ``Nft1=Nft/downsampling``. In the time-domain, downsampling simply copies every :math:`d`-th value of ``Gness`` (on the fine grid) to ``Gness1`` (coarse grid). In turn, the frequency grid is restricted to the lowest frequencies ``{-Nft1/2, ..., Nft1/2-1}``. The routine therefore requires ``Nft`` to be an integer multiple of :math:`d`, and ``Nft1`` to be a multiple of :math:`2`. Here we choose both ``Nft`` and :math:`d` to be a power of :math:`2`. Finally, the memory truncated Green's function is initialized using .. code-block:: cpp ness2cntr(Gcntr, Gness1); Here ``ness2cntr`` will initialize :math:`G_{\rm cntr}` such that .. math:: G_{\rm cntr}^{<,R}(t_1,t_2)=G^{<,R}_{\rm ness}(t_1-t_2) is translationally invariant in time in the full domain :math:`\mathcal{M}[G]`. The cutoff ``tc`` of :math:`G_{\rm cntr}` should be smaller or equal to the maximum number of timesteps in ``Gness1``, which is ``Nft1/2-1``; otherwise part of :math:`G_{\rm cntr}` would be left zero. The initialization is repeated for the self-energy and for :math:`\mathcal{G}`, which are both stored in the NESS output file. After this point, the truncated time evolution in ``ness2_bethe_trunc.x`` is identical to the example of :ref:`P4S2`, where the truncated Green's functions are initialized from a full real-time simulation. Selected timeslices ``t`` of the KBE simulation will be stored in the HDF5 output file under a group with key ``t[t]/G``. Alternatively to writing the slices directly, as in the example ``trunc_bethe`` above, we can use the reverse data exchange ``cntr2ness`` to initialize a ``herm_matrix_ness`` from the real-time Green's function, and store the latter: .. code-block:: cpp void write_slice(hid_t fl_id,int t,herm_mat_moving &Gcntr){ // fl_id is a handle to an open and writable HDF5 file [...] // create herm_matrix_ness Gness with Nft/2-1>=G.tc_ cntr2ness(Gness,Gcntr); // read last timestep of G into Gness // write Gness to new group t[t]/G in file fl_id: hid_t sub_group_id = create_group(file_id,"t"+std::to_string(t)); Gness.write_to_hdf5(sub_group_id, "G"); close_group(sub_group_id); } Exemplary results are shown in :numref:`ness_bethe_4`. We perform the test for similar parameters as in the example in :ref:`P4S3`, using a self-consistent perturbation theory for :math:`U=1` and :math:`\beta=2`. Note that this high temperature rather corresponds to the order of magnitude of the final thermalised temperature in the previous example, rather than to the initial temperature before the quench. Lower temperatures typically require a longer memory cutoff, because a sharp Fermi edge in the distribution function :math:`G^<(\omega)` implies a slower decay of :math:`G^<(t)`. We also observe that the IPT simulation is less stable under a long-time evolution, which may be related to its non-conserving nature. .. _ness_bethe_4: .. figure:: ../_static/ness_bethe_trunc.png :width: 60% :align: center Memory truncated time-evolution of an equilibrium state initialized with a NESS simulation. Self-consistent perturbation theory, :math:`U=1`, :math:`\beta=2`. The truncated KBE evolution is performed with :math:`h_{\rm cntr}=0.04` and a cutoff ``tc=1000``, and the initial NESS simulation uses :math:`h_{\rm ness}=0.005`. a) Difference :math:`\epsilon^{R,<}(t,s)=|G_{\rm cntr}^{R,<}(t,t-s)-G_{\rm cntr}^{R,<}(t_0,t_0-s)|` between the Green's functions on timeslice :math:`t` and the first timeslice :math:`t_0=0`. Full (dashed) lines show :math:`\epsilon^R` (:math:`\epsilon^<`). b) Maximum difference :math:`\epsilon^{R,<}(t)=\text{max}_s (\epsilon^{R,<}(t,s))` as function of :math:`t`, for different :math:`h_{\rm ness}`. :numref:`ness_bethe_4` a) shows the comparison of the real-time result :math:`G_{\rm cntr}^{R,<}(t,t-s)` to the initial NESS Green's function :math:`G_{\rm ness}^{R,<}(s)`, which match by construction on the initial timeslice :math:`t=0`. The two timeslices :math:`t=2` and :math:`t=400` correspond to :math:`50` and :math:`10000` timesteps :math:`h_{\rm cntr}=0.04`, respectively. One can see that very early a difference is built up in the lesser component at large relative times :math:`s`, which is related to the finite cutoff :math:`t_c` in the memory-truncated evolution. Thereafter, the difference :math:`|G_{\rm cntr}^{R,<}(t,t-s)-G_{\rm ness}^{R,<}(s)|` grows slowly with time due to a regular linear error accumulation (but remains small on the absolute scale). In :numref:`ness_bethe_4` b) we analyze the maximum difference .. math:: \epsilon^{R,<}(t)=\text{max}_s\left(|G_{\rm cntr}^{R,<}(t,t-s)-G_{\rm ness}^{R,<}(s)|\right) over a timeslice :math:`t` as function of :math:`t`. If the NESS simulation is performed with the same timestep as the real-time simulation, the NESS simulation is less accurate and therefore slightly inconsistent with a translationally invariant real-time solution (see the results for :math:`h_{\rm ness}=h_{\rm cntr}=0.04` in :numref:`ness_bethe_4` b). The time-evolution then leads to the buildup of a finite difference :math:`\epsilon^{R,<}(t)` over few timesteps, so that the linear error accumulation starts from a higher level. This initial error buildup can be simply reduced by performing the NESS simulation with a smaller timestep (see the results for :math:`h_{\rm ness}=0.005` in :numref:`ness_bethe_4` b). Since the numerical cost of the NESS simulation scales only with :math:`\mathcal{O}(N_{\rm ft}\log N_{\rm ft})` due to the use of FFT, the preparation of the equilibrium state via the NESS simulation is still cheaper than the preparation via a full real-time evolution.