diff --git a/TODO b/TODO index 72de50c890af3cbe6a8abc1ba67910b7b3aad14a..c3b3e7ad622d68585941e3cd739499b80a915b8c 100644 --- a/TODO +++ b/TODO @@ -4,8 +4,6 @@ Roughly in order of importance. -*-org-*- * Make kwant run on windows -* Implement "lead freezing" - * Add calculation of current density * Consider making the b parameter of _solve_linear_sys a matrix instead of a diff --git a/doc/other/linear_system.pdf b/doc/other/linear_system.pdf deleted file mode 100644 index 384562b861868e7d3b961721c4a74698f6837c7f..0000000000000000000000000000000000000000 Binary files a/doc/other/linear_system.pdf and /dev/null differ diff --git a/doc/other/reduce_modes.pdf b/doc/other/reduce_modes.pdf deleted file mode 100644 index a01aad6df8305badad3a88190589bfb5608c26a2..0000000000000000000000000000000000000000 Binary files a/doc/other/reduce_modes.pdf and /dev/null differ diff --git a/doc/source/reference/kwant.builder.rst b/doc/source/reference/kwant.builder.rst index 5d0d063c1ff23b958f0fd57fdd2698e9e0cef601..973b8f5a3f06a4fc3dfa5f2e5df794eb75a3e92e 100644 --- a/doc/source/reference/kwant.builder.rst +++ b/doc/source/reference/kwant.builder.rst @@ -14,6 +14,7 @@ Types SimpleSiteFamily BuilderLead SelfEnergy + ModesLead Abstract base classes --------------------- diff --git a/doc/source/reference/kwant.physics.rst b/doc/source/reference/kwant.physics.rst index 90128888cb7aa734a978a45e3f6ed0d099d48ff5..7b202da06aba3f9d78badfc4ba20796c34a52320 100644 --- a/doc/source/reference/kwant.physics.rst +++ b/doc/source/reference/kwant.physics.rst @@ -10,4 +10,6 @@ Leads Bands modes - self_energy + selfenergy + selfenergy_from_modes + ModesTuple diff --git a/doc/source/whatsnew/0.3.rst b/doc/source/whatsnew/0.3.rst index f6dad072149c807207fda73d4ca9450eb6047e73..84b0da4b49d9fdd7daa39c7da11980e7aa0f52b9 100644 --- a/doc/source/whatsnew/0.3.rst +++ b/doc/source/whatsnew/0.3.rst @@ -47,6 +47,8 @@ Some renames ------------ * site groups are now called site families. This affects all the names that used to contain "group" or "groups". +* ``self_energy`` has been renamed to ``selfenergy`` in all cases, most notably + in `kwant.physics.selfenergy`. * ``wave_func`` has been renamed to `~kwant.solvers.default.wave_function`, * ``MonatomicLattice`` has been renamed to `~kwant.lattice.Monatomic`, * ``PolyatomicLattice`` has been renamed to `~kwant.lattice.Polyatomic`. @@ -138,3 +140,19 @@ collection could be a dictionary, or a class instance, for example:: Arguments can be passed in an equivalent way to `~kwant.solvers.default.wave_function`, `~kwant.system.System.hamiltonian_submatrix`, etc. + +Calculation of modes separated from solving +------------------------------------------- +The interface that solvers expect from leads attached to a +`~kwant.system.FiniteSystem` has been simplified and codified (see there). +Similar to self-energy, calculation of modes is now the lead's own +responsibility. + +The new class `~kwant.builder.ModesLead` allows to attach leads that have a +custom way of calculating their modes (e.g. ideal leads) directly to a +`~kwant.builder.Builder`. + +Modes or self-energies can now be precomputed before passing the system to a +solver, using the method `~kwant.system.FiniteSystem.precalculate`. This may +save time, when the linear system has to be solved many times with the same +lead parameters. diff --git a/examples/square.py b/examples/square.py index d390d9c5f28e630906280ab3329dfd9c3054148c..19224461d04ff3b997613c0d6e8bff3485910577 100644 --- a/examples/square.py +++ b/examples/square.py @@ -6,7 +6,7 @@ from __future__ import division import numpy as np from matplotlib import pyplot import kwant -from kwant.physics.selfenergy import square_self_energy +from kwant.physics.selfenergy import square_selfenergy __all__ = ['System'] @@ -16,8 +16,13 @@ class Lead(object): self.t = t self.potential = potential +<<<<<<< HEAD def self_energy(self, fermi_energy, args=()): return square_self_energy(self.width, self.t, self.potential, +======= + def selfenergy(self, fermi_energy): + return square_selfenergy(self.width, self.t, self.potential, +>>>>>>> modes fermi_energy) class System(kwant.system.FiniteSystem): diff --git a/examples/tests/test_square.py b/examples/tests/test_square.py index e49d67fffcd208e99c58b5c468c349f4d956a9e6..9abfbbb1aad05377e33f9d0bbeb491369faf7fa3 100644 --- a/examples/tests/test_square.py +++ b/examples/tests/test_square.py @@ -26,12 +26,12 @@ def test_hamiltonian(): assert_almost_equal(m, m_herm) assert_almost_equal(m_herm, sys.hamiltonian(j, i)) -def test_self_energy(): +def test_selfenergy(): sys = square.System((2, 4), 1) for lead in xrange(len(sys.lead_interfaces)): n_orb = sum( sys.num_orbitals(site) for site in sys.lead_interfaces[lead]) - se = sys.self_energy(lead, 0) + se = sys.selfenergy(lead, 0) assert_equal(len(se.shape), 2) assert_equal(se.shape[0], se.shape[1]) assert_equal(se.shape[0], n_orb) diff --git a/kwant/builder.py b/kwant/builder.py index 82462dc9dd53251914cbc8d09dfb15167ddbc7e7..b6346e644e7635d9ae55ccc1b32f4af2eb86bc57 100644 --- a/kwant/builder.py +++ b/kwant/builder.py @@ -9,7 +9,7 @@ from __future__ import division __all__ = ['Builder', 'Site', 'SiteFamily', 'SimpleSiteFamily', 'Symmetry', - 'HoppingKind', 'Lead', 'BuilderLead', 'SelfEnergy'] + 'HoppingKind', 'Lead', 'BuilderLead', 'SelfEnergy', 'ModesLead'] import abc import sys @@ -18,7 +18,7 @@ import operator from itertools import izip, islice, chain import tinyarray as ta import numpy as np -from . import system, graph +from . import system, graph, physics ################ Sites and site families @@ -387,15 +387,12 @@ class Lead(object): Notes ----- - The finalized lead must at least have a single method - ``self_energy(energy)`` but it can be a full - `kwant.system.InfiniteSystem` as well. + The finalized lead must be an object that can be used as a lead in a + `kwant.system.FiniteSystem`. It could be an instance of + `kwant.system.InfiniteSystem` for example. - The method ``self_energy`` of the finalized lead must return a square - matrix of appropriate size. - - The order of interface sites is assumed to be preserved during - finalization. + The order of sites for the finalized lead must be the one specified in + `interface`. """ pass @@ -444,21 +441,49 @@ class SelfEnergy(Lead): Parameters ---------- - self_energy_func : function + selfenergy_func : function Function which returns the self energy matrix for the interface sites given the energy and optionally a list of extra arguments. interface : sequence of `Site` instances """ - def __init__(self, self_energy_func, interface): - self.self_energy_func = self_energy_func + def __init__(self, selfenergy_func, interface): + self._selfenergy_func = selfenergy_func + self.interface = tuple(interface) + + def finalized(self): + """Trivial finalization: the object is returned itself.""" + return self + + def selfenergy(self, energy, args=()): + return self._selfenergy_func(energy, args) + + +class ModesLead(Lead): + """A general lead defined by its modes wave functions. + + Parameters + ---------- + modes_func : function + Function which returns the modes of the lead in the format specified + in `~kwant.physics.ModesTuple` given the energy and optionally a list of + extra arguments. + interface : sequence of `Site` instances + """ + def __init__(self, modes_func, interface): + self._modes_func = modes_func self.interface = tuple(interface) def finalized(self): """Trivial finalization: the object is returned itself.""" return self - def self_energy(self, energy, args=()): - return self.self_energy_func(energy, args) + def modes(self, energy, args=()): + return self._modes_func(energy, args) + + def selfenergy(self, energy, args=()): + modes = self.modes(energy, args) + return physics.selfenergy_from_modes(modes=modes) + ################ Builder class diff --git a/kwant/physics/__init__.py b/kwant/physics/__init__.py index 2b081aa8ddde4a367c3f7a1344c0865a360e77fe..cf6a2c5b010caf97bcd58e45450c6c389dd047b0 100644 --- a/kwant/physics/__init__.py +++ b/kwant/physics/__init__.py @@ -10,7 +10,7 @@ # Merge the public interface of all submodules. __all__ = [] -for module in ['selfenergy', 'dispersion']: +for module in ['leads', 'dispersion']: exec 'from . import {0}'.format(module) exec 'from .{0} import *'.format(module) exec '__all__.extend({0}.__all__)'.format(module) diff --git a/kwant/physics/leads.py b/kwant/physics/leads.py new file mode 100644 index 0000000000000000000000000000000000000000..d417fc4cd5922b3ef25b7cc7322069fa9972128f --- /dev/null +++ b/kwant/physics/leads.py @@ -0,0 +1,670 @@ +# Copyright 2011-2013 kwant authors. +# +# This file is part of kwant. It is subject to the license terms in the +# LICENSE file found in the top-level directory of this distribution and at +# http://kwant-project.org/license. A list of kwant authors can be found in +# the AUTHORS file at the top-level directory of this distribution and at +# http://kwant-project.org/authors. + +from __future__ import division +from math import sin, cos, sqrt, pi, copysign +from collections import namedtuple +from itertools import izip +import numpy as np +import numpy.linalg as npl +import scipy.linalg as la +from .. import linalg as kla + +dot = np.dot + +__all__ = ['selfenergy', 'modes', 'ModesTuple', 'selfenergy_from_modes'] + +Linsys = namedtuple('Linsys', ['eigenproblem', 'v', 'extract', 'project']) + +def setup_linsys(h_onslice, h_hop, tol=1e6, algorithm=None): + """ + Make an eigenvalue problem for eigenvectors of translation operator. + + Parameters + ---------- + h_onslice : numpy array with shape (n, n) + Hamiltonian of a single lead slice. + h_hop : numpy array with shape (n, m), m <= n + Hopping Hamiltonian from the slice to the next one. + tol : float + Numbers are considered zero when they are smaller than `tol` times + the machine precision. + algorithm : tuple of 3 booleans or None + Which steps of the eigenvalue prolem stabilization to perform. + The first value selects whether to work in the basis of the + hopping svd, or lattice basis. If the real space basis is chosen, the + following two options do not apply. + The second value selects whether to add an anti-Hermitian term to the + slice Hamiltonian before inverting. Finally the third value selects + whether to reduce a generalized eigenvalue problem to the regular one. + The default value, `None`, results in kwant selecting the algorithm + that is the most efficient without sacrificing stability. Manual + selection may result in either slower performance, or large numerical + errors, and is mostly required for testing purposes. + + Returns + ------- + linsys : namedtuple + A named tuple containing `matrices` a matrix pencil defining + the eigenproblem, `v` a hermitian conjugate of the last matrix in + the hopping singular value decomposition, and functions for + extracting the wave function in the unit cell from the wave function + in the basis of the nonzero singular exponents of the hopping. + + Notes + ----- + The lead problem with degenerate hopping is rather complicated, and the + details of the algorithm will be published elsewhere. + """ + n = h_onslice.shape[0] + m = h_hop.shape[1] + + if not (np.any(h_hop.real) or np.any(h_hop.imag)): + # Inter-slice hopping is zero. The current algorithm is not suited to + # treat this extremely singular case. + # Note: np.any(h_hop) returns (at least from numpy 1.6.*) + # False if h_hop is purely imaginary + raise ValueError("Inter-slice hopping is exactly zero.") + + eps = np.finfo(np.common_type(h_onslice, h_hop)).eps * tol + + # First check if the hopping matrix has singular values close to 0. + # (Close to zero is defined here as |x| < eps * tol * s[0] , where + # s[0] is the largest singular value.) + + u, s, vh = la.svd(h_hop) + assert m == vh.shape[1], "Corrupt output of svd." + n_nonsing = np.sum(s > eps * s[0]) + + if (n_nonsing == n and algorithm is None) or (algorithm is not None and + algorithm[0]): + # The hopping matrix is well-conditioned and can be safely inverted. + # Hence the regular transfer matrix may be used. + hop_inv = la.inv(h_hop) + + A = np.zeros((2*n, 2*n), dtype=np.common_type(h_onslice, h_hop)) + A[:n, :n] = dot(hop_inv, -h_onslice) + A[:n, n:] = -hop_inv + A[n:, :n] = h_hop.T.conj() + + # The function that can extract the full wave function psi from the + # projected one. Here it is almost trivial, but used for simplifying + # the logic. + + def extract_wf(psi, lmbdainv): + return psi[:n] + + # Project the full wave function back (also trivial). + + def project_wf(psi, lmbdainv): + return np.r_[psi * lmbdainv, dot(h_hop.T.conj(), psi)] + + matrices = (A, None) + v_out = None + else: + if algorithm is not None: + need_to_stabilize, divide = algorithm[1:] + else: + need_to_stabilize = None + + # The hopping matrix has eigenvalues close to 0 - those + # need to be eliminated. + + # Recast the svd of h_hop = u s v^dagger such that + # u, v are matrices with shape n x n_nonsing. + u = u[:, :n_nonsing] + s = s[:n_nonsing] + u = u * np.sqrt(s) + # pad v with zeros if necessary + v = np.zeros((n, n_nonsing), dtype=vh.dtype) + v[:vh.shape[1]] = vh[:n_nonsing].T.conj() + v = v * np.sqrt(s) + + # Eliminating the zero eigenvalues requires inverting the + # on-site Hamiltonian, possibly including a self-energy-like term. + # The self-energy-like term stabilizes the inversion, but the most + # stable choice is inherently complex. This can be disadvantageous + # if the Hamiltonian is real - as staying in real arithmetics can be + # significantly faster. + # The strategy here is to add a complex self-energy-like term + # always if the original Hamiltonian is complex, and check for + # invertibility first if it is real + + h = h_onslice + sol = kla.lu_factor(h) + if issubclass(np.common_type(h_onslice, h_hop), np.floating) \ + and need_to_stabilize is None: + # Check if stabilization is needed. + rcond = kla.rcond_from_lu(sol, npl.norm(h, 1)) + + if rcond > eps: + need_to_stabilize = False + + if need_to_stabilize is None: + need_to_stabilize = True + + if need_to_stabilize: + # Matrices are complex or need self-energy-like term to be + # stabilized. + temp = dot(u, u.T.conj()) + dot(v, v.T.conj()) + h = h_onslice + 1j * temp + + sol = kla.lu_factor(h) + rcond = kla.rcond_from_lu(sol, npl.norm(h, 1)) + + # If the condition number of the stabilized h is + # still bad, there is nothing we can do. + if rcond < eps: + raise RuntimeError("Flat band encountered at the requested " + "energy, result is badly defined.") + + # The function that can extract the full wave function psi from + # the projected one (v^dagger psi lambda^-1, s u^dagger psi). + + def extract_wf(psi, lmbdainv): + wf = - dot(u, psi[: n_nonsing] * lmbdainv) - \ + dot(v, psi[n_nonsing:]) + if need_to_stabilize: + wf += 1j * (dot(v, psi[: n_nonsing]) + + dot(u, psi[n_nonsing:] * lmbdainv)) + return kla.lu_solve(sol, wf) + + # Project the full wave function back. + + def project_wf(psi, lmbdainv): + return np.r_[dot(v.T.conj(), psi * lmbdainv), + dot(u.T.conj(), psi)] + + # Setup the generalized eigenvalue problem. + + A = np.zeros((2 * n_nonsing, 2 * n_nonsing), np.common_type(h, h_hop)) + B = np.zeros((2 * n_nonsing, 2 * n_nonsing), np.common_type(h, h_hop)) + + begin, end = slice(n_nonsing), slice(n_nonsing, None) + + A[end, begin] = np.identity(n_nonsing) + temp = kla.lu_solve(sol, v) + temp2 = dot(u.T.conj(), temp) + if need_to_stabilize: + A[begin, begin] = -1j * temp2 + A[begin, end] = temp2 + temp2 = dot(v.T.conj(), temp) + if need_to_stabilize: + A[end, begin] -= 1j *temp2 + A[end, end] = temp2 + + B[begin, end] = -np.identity(n_nonsing) + temp = kla.lu_solve(sol, u) + temp2 = dot(u.T.conj(), temp) + B[begin, begin] = -temp2 + if need_to_stabilize: + B[begin, end] += 1j * temp2 + temp2 = dot(v.T.conj(), temp) + B[end, begin] = -temp2 + if need_to_stabilize: + B[end, end] = 1j * temp2 + + v_out = v[:m] + + # Solving a generalized eigenproblem is about twice as expensive + # as solving a regular eigenvalue problem. + # Computing the LU factorization is negligible compared to both + # (approximately 1/30th of a regular eigenvalue problem). + # Because of this, it makes sense to try to reduce + # the generalized eigenvalue problem to a regular one, provided + # the matrix B can be safely inverted. + + lu_b = kla.lu_factor(B) + if algorithm is None: + rcond = kla.rcond_from_lu(lu_b, npl.norm(B, 1)) + # A more stringent condition is used here since errors can + # accumulate from here to the eigenvalue calculation later. + divide = rcond > eps * tol + + if divide: + matrices = (kla.lu_solve(lu_b, A), None) + else: + matrices = (A, B) + return Linsys(matrices, v_out, extract_wf, project_wf) + + +def make_proper_modes(lmbdainv, psi, extract, project, tol=1e6): + """ + Determine the velocities and direction of the propagating eigenmodes. + + Special care is taken of the case of degenerate k-values, where the + numerically computed modes are typically a superposition of the real + modes. In this case, also the proper (orthogonal) modes are computed. + """ + vel_eps = np.finfo(psi.dtype).eps * tol + + # h_hop is either the full hopping matrix, or the singular + # values vector of the svd. + nmodes = psi.shape[1] + n = len(psi) // 2 + + # Array for the velocities. + velocities = np.empty(nmodes, dtype=float) + + # Mark the right-going modes. + rightselect = np.zeros(nmodes, dtype=bool) + + # Find clusters of nearby points. + eps = np.finfo(lmbdainv.dtype).eps * tol + angles = np.angle(lmbdainv) + sort_order = np.resize(np.argsort(angles), (2 * len(angles,))) + boundaries = np.argwhere(np.abs(np.diff(lmbdainv[sort_order])) + > eps).flatten() + 1 + + for interval in izip(boundaries[:-1], boundaries[1:]): + if interval[1] > boundaries[0] + len(angles): + break + + indx = sort_order[interval[0] : interval[1]] + + # If there is a degenerate eigenvalue with several different + # eigenvectors, the numerical routines return some arbitrary + # overlap of the real, physical solutions. In order + # to figure out the correct wave function, we need to + # have the full, not the projected wave functions + # (at least to our current knowledge). + + # Finding the true modes is done in two steps: + + # 1. The true transversal modes should be orthogonal to + # each other, as they share the same Bloch momentum (note + # that transversal modes with different Bloch momenta k1 + # and k2 need not be orthogonal, the full modes are + # orthogonal because of the longitudinal dependence + # e^{i k1 x} and e^{i k2 x}). + # The modes with the same k are therefore orthogonalized: + + if len(indx) > 1: + full_psi = extract(psi[:, indx], lmbdainv[indx]) + + # Note: Here's a workaround for the fact that the interface + # to qr changed from SciPy 0.8.0 to 0.9.0 + try: + full_psi = la.qr(full_psi, econ=True, mode='qr')[0] + except TypeError: + full_psi = la.qr(full_psi, mode='economic')[0] + + psi[:, indx] = project(full_psi, lmbdainv[indx]) + + # 2. Moving infinitesimally away from the degeneracy + # point, the modes should diagonalize the velocity + # operator (i.e. when they are non-degenerate any more) + # The modes are therefore rotated properly such that they + # diagonalize the velocity operator. + # Note that step 2. does not give a unique result if there are + # two modes with the same velocity, or if the modes stay + # degenerate even for a range of Bloch momenta (and hence + # must have the same velocity). However, this does not matter, + # since we are happy with any superposition in this case. + + vel_op = -1j * dot(psi[n:, indx].T.conj(), psi[:n, indx]) + vel_op = vel_op + vel_op.T.conj() + vel_vals, rot = la.eigh(vel_op) + + # If the eigenvectors were purely real up to this stage, + # they will typically become complex after the rotation. + + if psi.dtype != np.common_type(psi, rot): + psi = psi.astype(np.common_type(psi, rot)) + psi[:, indx] = dot(psi[:, indx], rot) + velocities[indx] = vel_vals + + rightselect = velocities > vel_eps + if np.any(abs(velocities) < vel_eps): + raise RuntimeError("Found a mode with zero or close to zero velocity.") + if 2 * np.sum(rightselect) != len(velocities): + raise RuntimeError("Numbers of left- and right-propagating " + "modes differ, possibly due to a numerical " + "instability.") + + return psi, velocities, rightselect + + +def unified_eigenproblem(a, b=None, tol=1e6): + """A helper routine for modes(), that wraps eigenproblems. + + This routine wraps the regular and general eigenproblems that can arise + in a unfied way. + + Parameters + ---------- + a : numpy array + The matrix on the left hand side of a regular or generalized eigenvalue + problem. + b : numpy array or None + The matrix on the right hand side of the generalized eigenvalue problem. + tol : float + The tolerance for separating eigenvalues with absolute value 1 from the + rest. + + Returns + ------- + ev : numpy array + An array of eigenvalues (can contain NaNs and Infs, but those + are not accessed in `modes()`) The number of eigenvalues equals + twice the number of nonzero singular values of + `h_hop` (so `2*h_onslice.shape[0]` if `h_hop` is invertible). + evanselect : numpy integer array + Index array of right-decaying modes. + propselect : numpy integer array + Index array of propagating modes (both left and right). + vec_gen(select) : function + A function that computes the eigenvectors chosen by the array select. + ord_schur(select) : function + A function that computes the unitary matrix (corresponding to the right + eigenvector space) of the (general) Schur decomposition reordered such + that the eigenvalues chosen by the array select are in the top left + block. + """ + if b is None: + eps = np.finfo(a.dtype).eps * tol + t, z, ev = kla.schur(a) + + # Right-decaying modes. + select = abs(ev) > 1 + eps + # Propagating modes. + propselect = abs(abs(ev) - 1) < eps + + vec_gen = lambda x: kla.evecs_from_schur(t, z, select=x) + ord_schur = lambda x: kla.order_schur(x, t, z, calc_ev=False)[1] + + else: + eps = np.finfo(np.common_type(a, b)).eps * tol + s, t, z, alpha, beta = kla.gen_schur(a, b, calc_q=False) + + # Right-decaying modes. + select = abs(alpha) > (1 + eps) * abs(beta) + # Propagating modes. + propselect = (abs(abs(alpha) - abs(beta)) < eps * abs(beta)) + + warning_settings = np.seterr(divide='ignore', invalid='ignore') + ev = alpha / beta + np.seterr(**warning_settings) + # Note: the division is OK here, since we later only access + # eigenvalues close to the unit circle + + vec_gen = lambda x: kla.evecs_from_gen_schur(s, t, z=z, select=x) + ord_schur = lambda x: kla.order_gen_schur(x, s, t, z=z, + calc_ev=False)[2] + + return ev, select, propselect, vec_gen, ord_schur + + +_Modes = namedtuple('ModesTuple', ['vecs', 'vecslmbdainv', 'nmodes', + 'sqrt_hop']) +modes_docstring = """Eigendecomposition of a translation operator. + +A named tuple with attributes `(vecs, vecslmbdainv, nmodes, sqrt_hop)`. The +translation eigenproblem is written in the basis `psi_n, h_hop^+ * psi_(n+1)`, +with ``h_hop`` the hopping between unit cells. If `h_hop` is not invertible +with singular value decomposition `u s v^+`, then the eigenproblem is written +in the basis `sqrt(s) v^+ psi_n, sqrt(s) u^+ psi_(n+1)`. `vecs` and +`vecslmbdainv` are the first and the second halves of the wave functions. The +first `nmodes` are eigenmodes moving in the negative direction (hence they are +incoming into the system in kwant convention), the second `nmodes` are +eigenmodes moving in the positive direction. The remaining modes are Schur +vectors of the modes evanescent in the positive direction. Propagating modes +with the same eigenvalue are orthogonalized, and all the propagating modes are +normalized to carry unit current. Finally he `sqrt_hop` attribute is `v +sqrt(s)`. +""" +d = dict(_Modes.__dict__) +d.update(__doc__=modes_docstring) +ModesTuple = type('ModesTuple', _Modes.__bases__, d) +del _Modes, d, modes_docstring + +def modes(h_onslice, h_hop, tol=1e6, algorithm=None): + """ + Compute the eigendecomposition of a translation operator of a lead. + + Parameters + ---------- + h_onslice : numpy array, real or complex, shape (N,N) The unit cell + Hamiltonian of the lead slice. + h_hop : numpy array, real or complex, shape (N,M) + The hopping matrix from a lead slice to the one on which self-energy + has to be calculated (and any other hopping in the same direction). + tol : float + Numbers and differences are considered zero when they are smaller + than `tol` times the machine precision. + algorithm : tuple or 3 booleans or None + Which steps of the eigenvalue prolem stabilization to perform. The + default value, `None`, results in kwant selecting the algorithm that is + the most efficient without sacrificing stability. Manual selection may + result in either slower performance, or large numerical errors, and is + mostly required for testing purposes. The first value selects whether + to work in the basis of the hopping svd, or lattice basis. If the real + space basis is chosen, the following two options do not apply. The + second value selects whether to add an anti-Hermitian term to the slice + Hamiltonian before inverting. Finally the third value selects whether + to reduce a generalized eigenvalue problem to the regular one. + + + Returns + ------- + ModesTuple(vecs, vecslmbdainv, nmodes, v) : a named tuple + See notes below and `~kwant.physics.ModesTuple` + documentation for details. + + Notes + ----- + Only the propagating modes and the modes decaying away from the system are + returned: The first `nmodes` columns in `vecs` correspond to incoming modes + (coming from the lead into the system), the following `nmodes` columns + correspond to outgoing modes (going into the lead, away from the system), + the remaining columns are evanescent modes, decaying away from the system. + + The propagating modes are sorted according to the longitudinal component of + their k-vector, with incoming modes having k sorted in descending order, + and outgoing modes having k sorted in ascending order. In simple cases + where bands do not cross, this ordering corresponds to "lowest modes + first". In general, however, it is necessary to examine the band structure + -- something this function is not doing by design. + + If `h_hop` is invertible, the full transverse wave functions are returned. + If it is singular, the projections (s u^dagger psi, v^dagger psi lambda^-1) + are returned. + + In order for the linear system to be well-defined, instead of the + evanescent modes, an orthogonal basis in the space of evanescent modes is + returned. + + Propagating modes with the same lambda are orthogonalized. All the + propagating modes are normalized by current. + + This function uses the most stable and efficient algorithm for calculating + the mode decomposition. Its details will be published elsewhere. + + """ + m = h_hop.shape[1] + + if (h_onslice.shape[0] != h_onslice.shape[1] or + h_onslice.shape[0] != h_hop.shape[0]): + raise ValueError("Incompatible matrix sizes for h_onslice and h_hop.") + + # Note: np.any(h_hop) returns (at least from numpy 1.6.1 - 1.8-devel) + # False if h_hop is purely imaginary + if not (np.any(h_hop.real) or np.any(h_hop.imag)): + n = h_hop.shape[0] + v = np.empty((0, m)) + return ModesTuple(np.empty((0, 0)), np.empty((0, 0)), 0, v) + + # Defer most of the calculation to helper routines. + matrices, v, extract, project = setup_linsys(h_onslice, h_hop, tol, + algorithm) + ev, evanselect, propselect, vec_gen, ord_schur =\ + unified_eigenproblem(*(matrices + (tol,))) + + if v is not None: + n = v.shape[1] + else: + n = h_onslice.shape[0] + + nprop = np.sum(propselect) + nevan = n - nprop // 2 + evanselect_bool = np.zeros((2*n), dtype='bool') + evanselect_bool[evanselect] = True + evan_vecs = ord_schur(evanselect)[:, :nevan] + + # Compute the propagating eigenvectors. + prop_vecs = vec_gen(propselect) + # Compute their velocity, and, if necessary, rotate them + prop_vecs, vel, rprop = \ + make_proper_modes(ev[propselect], prop_vecs, extract, project, tol) + + # Normalize propagating eigenvectors by velocities. + prop_vecs /= np.sqrt(abs(vel)) + + # Fix the phase factor - make maximum of the transverse wave function real + # TODO (Anton): Take care of multiple maxima when normalizing. + maxnode = prop_vecs[n + np.argmax(abs(prop_vecs[n:, :]), axis=0), + np.arange(prop_vecs.shape[1])] + maxnode /= abs(maxnode) + prop_vecs /= maxnode + + lprop = np.logical_not(rprop) + nmodes = np.sum(rprop) + + # Sort modes according to their k-vector (1/lambda = e^{-ik}): + # - right-going modes: sort that k is in ascending order + # - left-going modes: sort that k is in descending order + # (note that k can be positive or negative). With this convention, + # the modes of a simple square lattice strip are ordered as + # expected (lowest subband first, etc.) + + prop_ev = ev[propselect] + rsort = np.argsort((-1j * np.log(prop_ev[rprop])).real) + lsort = np.argsort((1j * np.log(prop_ev[lprop])).real) + + # The following is necessary due to how numpy deals with indexing of empty. + # arrays. + if nmodes == 0: + lprop = rprop = rsort = lsort = slice(None) + + vecs = np.c_[prop_vecs[n:, lprop][:, lsort], + prop_vecs[n:, rprop][:, rsort], + evan_vecs[n:]] + vecslmbdainv = np.c_[prop_vecs[:n, lprop][:, lsort], + prop_vecs[:n, rprop][:, rsort], + evan_vecs[:n]] + + return ModesTuple(vecs, vecslmbdainv, nmodes, v) + + +def selfenergy_from_modes(lead_modes): + """ + Compute the self-energy generated by lead modes. + + Parameters + ---------- + lead_modes : ModesTuple(vecs, vecslmbdainv, nmodes, v) a named tuple + The modes in the lead, with format defined in + `~kwant.physics.ModesTuple`. + + Returns + ------- + Sigma : numpy array, real or complex, shape (M,M) + The computed self-energy. Note that even if `h_onslice` and `h_hop` + are both real, `Sigma` will typically be complex. (More precisely, if + there is a propagating mode, `Sigma` will definitely be complex.) + + Notes + ----- + For simplicity this function relies on the calculation of modes as input. + This may cause a small slowdown, and can be improved if necessary. + """ + vecs, vecslmbdainv, nmodes, v = lead_modes + vecs = vecs[:, nmodes:] + vecslmbdainv = vecslmbdainv[:, nmodes:] + if v is not None: + return dot(v, dot(vecs, la.solve(vecslmbdainv, v.T.conj()))) + else: + return la.solve(vecslmbdainv.T, vecs.T).T + + +def selfenergy(h_onslice, h_hop, tol=1e6): + """ + Compute the self-energy generated by the lead. + + Parameters + ---------- + h_onslice : numpy array, real or complex, shape (N,N) The unit cell + Hamiltonian of the lead slice. + h_hop : numpy array, real or complex, shape (N,M) + The hopping matrix from a lead slice to the one on which self-energy + has to be calculated (and any other hopping in the same direction). + tol : float + Numbers are considered zero when they are smaller than `tol` times + the machine precision. + + Returns + ------- + Sigma : numpy array, real or complex, shape (M,M) + The computed self-energy. Note that even if `h_onslice` and `h_hop` + are both real, `Sigma` will typically be complex. (More precisely, if + there is a propagating mode, `Sigma` will definitely be complex.) + + Notes + ----- + For simplicity this function internally calculates the modes first. + This may cause a small slowdown, and can be improved if necessary. + """ + return selfenergy_from_modes(modes(h_onslice, h_hop, tol)) + + +def square_selfenergy(width, hopping, fermi_energy): + """ + Calculate analytically the self energy for a square lattice. + + The lattice is assumed to have a single orbital per site and + nearest-neighbor hopping. + + Parameters + ---------- + width : integer + width of the lattice + """ + + # Following appendix C of M. Wimmer's diploma thesis: + # http://www.physik.uni-regensburg.de/forschung/\ + # richter/richter/media/research/publications2004/wimmer-Diplomarbeit.pdf + + # p labels transversal modes. i and j label the sites of a slice. + + # Precalculate the transverse wave function. + psi_p_i = np.empty((width, width)) + factor = pi / (width + 1) + prefactor = sqrt(2 / (width + 1)) + for p in xrange(width): + for i in xrange(width): + psi_p_i[p, i] = prefactor * sin(factor * (p + 1) * (i + 1)) + + # Precalculate the integrals of the longitudinal wave functions. + def f(q): + if abs(q) <= 2: + return q/2 - 1j * sqrt(1 - (q / 2) ** 2) + else: + return q/2 - copysign(sqrt((q / 2) ** 2 - 1), q) + f_p = np.empty((width,), dtype=complex) + for p in xrange(width): + e = 2 * hopping * (1 - cos(factor * (p + 1))) + q = (fermi_energy - e) / hopping - 2 + f_p[p] = f(q) + + # Put everything together into the self energy and return it. + result = np.empty((width, width), dtype=complex) + for i in xrange(width): + for j in xrange(width): + result[i, j] = hopping * \ + (psi_p_i[:, i] * psi_p_i[:, j].conj() * f_p).sum() + return result diff --git a/kwant/physics/selfenergy.py b/kwant/physics/selfenergy.py deleted file mode 100644 index e741a66193ba99cf654c02001d99a4cee336178a..0000000000000000000000000000000000000000 --- a/kwant/physics/selfenergy.py +++ /dev/null @@ -1,795 +0,0 @@ -# Copyright 2011-2013 kwant authors. -# -# This file is part of kwant. It is subject to the license terms in the -# LICENSE file found in the top-level directory of this distribution and at -# http://kwant-project.org/license. A list of kwant authors can be found in -# the AUTHORS file at the top-level directory of this distribution and at -# http://kwant-project.org/authors. - -from __future__ import division -from math import sin, cos, sqrt, pi, copysign -from collections import namedtuple -import numpy as np -import numpy.linalg as npl -import scipy.linalg as la -from .. import linalg as kla - -dot = np.dot - -__all__ = ['self_energy', 'modes', 'Modes'] - - -def setup_linsys(h_onslice, h_hop, tol=1e6): - """ - Make an eigenvalue problem for eigenvectors of translation operator. - - Parameters - ---------- - h_onslice : NumPy array with shape (n, n) - Hamiltonian of a single lead slice. - h_hop : NumPy array with shape (n, m), m <= n - Hopping Hamiltonian from the slice to the next one. - - Returns - ------- - linsys : matrix or tuple - if the hopping is nonsingular, a single matrix defining an eigenvalue - problem is returned, othewise a tuple of two matrices defining a - generalized eigenvalue problem together with additional information is - returned. - - Notes - ----- - The lead problem with degenerate hopping is rather complicated, and it is - described in kwant/doc/other/lead_modes.pdf. - """ - n = h_onslice.shape[0] - m = h_hop.shape[1] - - # Inter-slice hopping is zero. The current algorithm is not suited to - # treat this extremely singular case. - # Note: np.any(h_hop) returns (at least from NumPy 1.6.1 - 1.8-devel) - # False if h_hop is purely imaginary - assert np.any(h_hop.real) or np.any(h_hop.imag) - - eps = np.finfo(np.common_type(h_onslice, h_hop)).eps - - # First check if the hopping matrix has eigenvalues close to 0. - u, s, vh = la.svd(h_hop) - - assert m == vh.shape[1], "Corrupt output of svd." - - # Count the number of singular values close to zero. - # (Close to zero is defined here as |x| < eps * tol * s[0] , where - # s[0] is the largest singular value.) - n_nonsing = np.sum(s > eps * tol * s[0]) - - if n_nonsing == n: - # The hopping matrix is well-conditioned and can be safely inverted. - sol = kla.lu_factor(h_hop) - - A = np.empty((2*n, 2*n), dtype=np.common_type(h_onslice, h_hop)) - - A[0:n, 0:n] = kla.lu_solve(sol, -h_onslice) - A[0:n, n:2*n] = kla.lu_solve(sol, -h_hop.T.conj()) - A[n:2*n, 0:n] = np.identity(n) - A[n:2*n, n:2*n] = 0 - - return A - else: - # The hopping matrix has eigenvalues close to 0 - those - # need to be eliminated. - - # Recast the svd of h_hop = u s v^dagger such that - # u, v are matrices with shape n x n_nonsing. - u = u[:, :n_nonsing] - s = s[:n_nonsing] - # pad v with zeros if necessary - v = np.zeros((n, n_nonsing), dtype=vh.dtype) - v[:vh.shape[1], :] = vh[:n_nonsing, :].T.conj() - - # Eliminating the zero eigenvalues requires inverting the - # on-site Hamiltonian, possibly including a self-energy-like term. - # The self-energy-like term stabilizes the inversion, but the most - # stable choice is inherently complex. This can be disadvantageous - # if the Hamiltonian is real - as staying in real arithmetics can be - # significantly faster. - # The strategy here is to add a complex self-energy-like term - # always if the original Hamiltonian is complex, and check for - # invertibility first if it is real - - gamma = None - - if issubclass(np.common_type(h_onslice, h_hop), np.floating): - - # Check if stabilization is needed. - h = h_onslice - - sol = kla.lu_factor(h) - rcond = kla.rcond_from_lu(sol, npl.norm(h, 1)) - - if rcond > eps * tol: - gamma = 0 - - if gamma is None: - # Matrices are complex or need self-energy-like term to be - # stabilized. - - # Normalize such that the maximum entry in the - # self-energy-like term has a value comparable to the - # maximum entry in h_onslice. - - temp = dot(u, u.T.conj()) + dot(v, v.T.conj()) - - max_h = np.amax(abs(h_onslice)) - max_temp = np.amax(abs(temp)) - - gamma = max_h / max_temp * 1j - - h = h_onslice + gamma * temp - - sol = kla.lu_factor(h) - rcond = kla.rcond_from_lu(sol, npl.norm(h, 1)) - - # If the condition number of the stabilized h is - # still bad, there is nothing we can do. - if rcond < eps * tol: - raise RuntimeError("Flat band encountered at the requested " - "energy, result is badly defined.") - - # Function that can extract the full wave function psi from - # the projected one (v^dagger psi lambda^-1, u^dagger psi). - - def extract_wf(psi, lmbdainv): - return kla.lu_solve(sol, - gamma * dot(v, psi[: n_nonsing]) + - gamma * dot(u, psi[n_nonsing:] * lmbdainv) - - dot(u * s, psi[: n_nonsing] * lmbdainv) - - dot(v * s, psi[n_nonsing:])) - - # Project a full wave function back. - - def project_wf(psi, lmbdainv): - return np.asarray(np.bmat([[dot(v.T.conj(), psi * lmbdainv)], - [dot(u.T.conj(), psi)]])) - - # Setup the generalized eigenvalue problem. - - A = np.empty((2 * n_nonsing, 2 * n_nonsing), np.common_type(h, h_hop)) - B = np.empty((2 * n_nonsing, 2 * n_nonsing), np.common_type(h, h_hop)) - - A[:n_nonsing, :n_nonsing] = -np.eye(n_nonsing) - - B[n_nonsing: 2 * n_nonsing, - n_nonsing: 2 * n_nonsing] = np.eye(n_nonsing) - - temp = kla.lu_solve(sol, v) - temp2 = dot(u.T.conj(), temp) - A[n_nonsing : 2 * n_nonsing, :n_nonsing] = gamma * temp2 - A[n_nonsing : 2 * n_nonsing, n_nonsing: 2 * n_nonsing] = - temp2 * s - temp2 = dot(v.T.conj(), temp) - A[:n_nonsing, :n_nonsing] += gamma * temp2 - A[:n_nonsing, n_nonsing : 2 * n_nonsing] = - temp2 * s - - temp = kla.lu_solve(sol, u) - temp2 = dot(u.T.conj(), temp) - B[n_nonsing : 2 * n_nonsing, :n_nonsing] = temp2 * s - B[n_nonsing : 2 * n_nonsing, n_nonsing : 2 * n_nonsing] -= \ - gamma * temp2 - temp2 = dot(v.T.conj(), temp) - B[:n_nonsing, :n_nonsing] = temp2 * s - B[:n_nonsing, n_nonsing : 2 * n_nonsing] = - gamma * temp2 - - # Solving a generalized eigenproblem is about twice as expensive - # as solving a regular eigenvalue problem. - # Computing the LU factorization is negligible compared to both - # (approximately 1/30th of a regular eigenvalue problem). - # Because of this, it makes sense to try to reduce - # the generalized eigenvalue problem to a regular one, provided - # the matrix B can be safely inverted. - - lu_b = kla.lu_factor(B) - rcond = kla.rcond_from_lu(lu_b, npl.norm(B, 1)) - - # I put a more stringent condition here - errors can accumulate - # from here to the eigenvalue calculation later. - if rcond > eps * tol**2: - return (kla.lu_solve(lu_b, A), (u, s, v[:m, :]), - (extract_wf, project_wf)) - else: - return (A, B, (u, s, v[:m]), (extract_wf, project_wf)) - - -def split_degenerate(evs, tol=1e6): - """ - Find sets of approximately degenerate list elements on a unit circle. - - Given a list of eigenvalues on the unit circle, return a list containing - tuples of indices of eigenvalues that are numerically degenerate. Two - eigenvalues ev[i] and ev[j] are considered to be numerically degenerate if - abs(ev[i] - ev[j]) < eps * tol, where eps is the machine precision. - - Example - ------- - >>> split_degenerate(np.array([1,-1,1,1], dtype=complex)) - [(1,), (0, 2, 3)]. - """ - eps = np.finfo(evs.dtype).eps - - n = evs.size - evlist = [] - - # Figure out if there are degenerate eigenvalues. - # For this, sort according to k, which is i*log(ev) (ev is exp(-ik)). - k = np.log(evs).imag - sortindx = np.argsort(k) - evs_sorted = evs[sortindx] - - # Note that we sorted eigenvalues on the unit circle, cutting - # the unit circle at -1. We thus must search for degeneracies also - # across this cut - - start = 0 - while (start - 1 > -n and - abs(evs_sorted[start - 1] - evs_sorted[start]) < eps * tol): - start = start - 1 - - stop = n + start - - while start < stop: - deglist = [sortindx[start]] - while (start + 1 < stop and - abs(evs_sorted[start] - evs_sorted[start + 1]) < eps * tol): - start += 1 - deglist.append(sortindx[start]) - - evlist.append(tuple(deglist)) - start += 1 - - return evlist - - -def make_proper_modes(lmbdainv, psi, h_hop, extract=None, - project=None, tol=1e6): - """ - Determine the velocities and direction of the propagating eigenmodes. - - Special care is taken of the case of degenerate k-values, where the - numerically computed modes are typically a superposition of the real - modes. In this case, also the proper (orthogonal) modes are computed. - """ - - vel_eps = np.finfo(np.common_type(psi, h_hop)).eps * tol - - # h_hop is either the full hopping matrix, or the singular - # values vector of the svd. - if h_hop.ndim == 2: - n = h_hop.shape[0] - m = h_hop.shape[1] - else: - n = h_hop.size - - nmodes = psi.shape[1] - - if nmodes == 0: - raise ValueError('Empty mode array.') - - # Array for the velocities. - v = np.empty(nmodes, dtype=float) - - # Mark the right-going modes. - rightselect = np.zeros(nmodes, dtype=bool) - - n_left = n_right = 0 - crossing = False - - indxclust = split_degenerate(lmbdainv) - - for indx in indxclust: - if len(indx) > 1: - # Several degenerate propagating modes. In this case, the computed - # eigenvectors do not orthogonalize the velocity - # operator, i.e. they do not have a proper velocity. - - indx = np.array(indx) - - # If there is a degenerate eigenvalue with several different - # eigenvectors, the numerical routines return some arbitrary - # overlap of the real, physical solutions. In order - # to figure out the correct wave function, we need to - # have the full, not the projected wave functions - # (at least to our current knowledge). - - if extract is not None: - full_psi = extract(psi[:, indx], lmbdainv[indx]) - else: - full_psi = psi[:n, indx] - - # Finding the true modes is done in two steps: - - # 1. The true transversal modes should be orthogonal to - # each other, as they share the same Bloch momentum (note - # that transversal modes with different Bloch momenta k1 - # and k2 need not be orthogonal, the full modes are - # orthogonal because of the longitudinal dependence - # e^{i k1 x} and e^{i k2 x}). - # The modes are therefore orthogonalized: - - # Note: Here's a workaround for the fact that the interface - # to qr changed from SciPy 0.8.0 to 0.9.0 - try: - full_psi = la.qr(full_psi, econ=True, mode='qr')[0] - except TypeError: - full_psi = la.qr(full_psi, mode='economic')[0] - - if project: - psi[:, indx] = project(full_psi, lmbdainv[indx]) - else: - psi[:n, indx] = full_psi * lmbdainv[indx] - psi[n:2*n, indx] = full_psi - - # 2. Moving infinitesimally away from the degeneracy - # point, the modes should diagonalize the velocity - # operator (i.e. when they are non-degenerate any more) - # The modes are therefore rotated properly such that they - # diagonalize the velocity operator. - # Note that step 2. does not give a unique result if there are - # two modes with the same velocity, or if the modes stay - # degenerate even for a range of Bloch momenta (and hence - # must have the same velocity). However, this does not matter, - # as we are happy with any superposition in this case. - - if h_hop.ndim == 2: - vel_op = -1j * dot(psi[n:, indx].T.conj(), - dot(h_hop, psi[:m, indx])) - else: - vel_op = -1j * dot(psi[n:, indx].T.conj() * h_hop, - psi[:n, indx]) - - vel_op = vel_op + vel_op.T.conj() - - vel_vals, rot = la.eigh(vel_op) - - # If the eigenvectors were purely real up to this stage, - # will typically become complex after the rotation. - if psi.dtype != np.common_type(psi, rot): - psi = psi.astype(np.common_type(psi, rot)) - - psi[:, indx] = dot(psi[:, indx], rot) - - v[indx] = vel_vals - - # For some of the self-energy methods it matters - # whether the degeneracy is a crossing with velocities - # of different sign - if not ((vel_vals > 0).all() or (vel_vals < 0).all()): - crossing = True - - for (vel, k) in zip(vel_vals, indx): - if vel > vel_eps: - n_right += 1 - rightselect[k] = True - elif vel < -vel_eps: - n_left += 1 - else: - raise RuntimeError("Found a mode with zero or close to " - "zero velocity.") - else: - # A single, unique propagating mode - k = indx[0] - - if h_hop.ndim == 2: - v[k] = 2 * dot(dot(psi[n:2*n, k:k+1].T.conj(), h_hop), - psi[:m, k:k+1]).imag - else: - v[k] = 2 * dot(psi[n:2*n, k:k+1].T.conj() * h_hop, - psi[0:n, k:k+1]).imag - - if v[k] > vel_eps: - rightselect[k] = True - n_right += 1 - elif v[k] < -vel_eps: - n_left += 1 - else: - raise RuntimeError("Found a mode with zero or close to " - "zero velocity.") - - if n_left != n_right: - raise RuntimeError("Numbers of left- and right-propagating " - "modes differ.") - - return psi, v, rightselect, crossing - - -def unified_eigenproblem(h_onslice, h_hop, tol): - """A helper routine for general() and modes(), that wraps eigenproblems. - - This routine wraps the different types of eigenproblems that can arise - in a unfied way. - - Returns - ------- - ev : NumPy array - an array of eigenvalues (can contain NaNs and Infs, but those - are not accessed in `general()` and `modes()`) The number of - eigenvalues is given by twice the number of nonzero singular values of - `h_hop` (i.e. `2*h_onslice.shape[0]` if `h_hop` is invertible). - evanselect : NumPy array - index array of right-decaying modes. - propselect : NumPy array - index array of propagating modes (both left and right). - vec_gen(select) : function - a function that computes the eigenvectors chosen by the array select. - ord_schur(select) : function - a function that computes the unitary matrix (corresponding to the right - eigenvector space) of the (general) Schur decomposition reordered such - that the eigenvalues chosen by the array select are in the top left - block. - u, w, v : - if the hopping is singular, the svd of the hopping matrix, otherwise - they all the three values are None. - extract, project : functions - functions to extract the full wave function from the projected wave - functions, and project it back. Both are equal to None if the hopping - is invertible. - """ - - eps = np.finfo(np.common_type(h_onslice, h_hop)).eps - - linsys = setup_linsys(h_onslice, h_hop) - - if isinstance(linsys, tuple): - # In the singular case, it depends on the details of the system - # whether one needs to solve a regular or a generalized - # eigenproblem. - - assert len(linsys) == 3 or len(linsys) == 4, \ - "Corrupt lead eigenproblem data." - - if len(linsys) == 3: - t, z, ev = kla.schur(linsys[0]) - - # Right-decaying modes. - select = abs(ev) > 1 + eps * tol - # Propagating modes. - propselect = abs(abs(ev) - 1) < eps * tol - - u, w, v = linsys[1] - extract, project = linsys[2] - - vec_gen = lambda x: kla.evecs_from_schur(t, z, select=x) - ord_schur = lambda x: kla.order_schur(x, t, z, calc_ev=False)[1] - - else: - s, t, z, alpha, beta = kla.gen_schur(linsys[0], linsys[1], - calc_q=False) - - # Right-decaying modes. - select = abs(alpha) > (1 + eps * tol) * abs(beta) - # Propagating modes. - propselect = (abs(abs(alpha) - abs(beta)) < - eps * tol * abs(beta)) - - warning_settings = np.seterr(divide='ignore', invalid='ignore') - ev = alpha / beta - np.seterr(**warning_settings) - # Note: the division is OK here, as we later only access - # eigenvalues close to the unit circle - - u, w, v = linsys[2] - extract, project = linsys[3] - - vec_gen = lambda x: kla.evecs_from_gen_schur(s, t, z=z, select=x) - ord_schur = lambda x: kla.order_gen_schur(x, s, t, - z=z, calc_ev=False)[2] - else: - # Hopping matrix can be safely inverted -> regular eigenproblem can be - # used. This also means, that the hopping matrix is n x n square. - - t, z, ev = kla.schur(linsys) - - # Right-decaying modes. - select = abs(ev) > 1 + eps * tol - # Propagating modes. - propselect = abs(abs(ev) - 1) < eps * tol - - # Signal that we are in the regular case. - u = v = w = None - extract = project = None - - vec_gen = lambda x: kla.evecs_from_schur(t, z, select=x) - ord_schur = lambda x: kla.order_schur(x, t, z, calc_ev=False)[1] - - return ev, select, propselect, vec_gen, ord_schur,\ - u, w, v, extract, project - - -def self_energy(h_onslice, h_hop, tol=1e6): - """ - Compute the self-energy generated by a lead. - - The lead is described by the unit-cell - Hamiltonian h_onslice and the hopping matrix h_hop. - - Parameters - ---------- - h_onslice : NumPy array, real or complex, shape (N,N) The unit cell - Hamiltonian of the lead slice. - h_hop : NumPy array, real or complex, shape (N,M) - the hopping matrix from a lead slice to the one on which self-energy - has to be calculated (and any other hopping in the same direction). - - Returns - ------- - Sigma : NumPy array, real or complex, shape (M,M) - The computed self-energy. Note that even if `h_onslice` and `h_hop` - are both real, `Sigma` will typically be complex. (More precisely, if - there is a propagating mode, `Sigma` will definitely be complex.) - - Notes - ----- - This function uses the most stable and efficient algorithm for calculating - self-energy, described in kwant/doc/other/lead_modes.pdf - """ - - m = h_hop.shape[1] - - # Note: np.any(h_hop) returns (at least from NumPy 1.6.1 - 1.8-devel) - # False if h_hop is purely imaginary - if not (np.any(h_hop.real) or np.any(h_hop.imag)): - return np.zeros((m, m)) - - if (h_onslice.shape[0] != h_onslice.shape[1] or - h_onslice.shape[0] != h_hop.shape[0]): - raise ValueError("Incompatible matrix sizes for h_onslice and h_hop.") - - #defer most of the calculation to a helper routine (also used by modes) - ev, select, propselect, vec_gen, ord_schur,\ - u, w, v, extract, project = unified_eigenproblem(h_onslice, h_hop, tol) - - if w is not None: - n = w.size - h_hop = w - else: - n = h_onslice.shape[0] - - # Compute the propagating eigenvectors, if they are present. - nprop = np.sum(propselect) - - if nprop > 0: - prop_vecs = vec_gen(propselect) - - prop_vecs, vel, rselect, crossing = \ - make_proper_modes(ev[propselect], prop_vecs, h_hop, - extract, project) - else: - # Without propagating modes, the Schur methods certainly work. - crossing = False - - if crossing: - # Schur decomposition method does not work in this case, we need to - # compute all the eigenvectors. - - # We already have the propagating ones, now we just need the - # evanescent ones in addition. - - if nprop > 0: - vecs = np.empty((2*n, n), - dtype=np.common_type(ev, prop_vecs)) - else: - vecs = np.empty((2*n, n), dtype=ev.dtype) - # Note: rationale for the dtype: only if all the eigenvalues are real, - # (which can only happen if the original eigenproblem was - # real) and all propagating modes are real, the matrix of - # eigenvectors will be real, too. - - if nprop > 0: - nmodes = np.sum(rselect) - vecs[:, :nmodes] = prop_vecs[:, rselect] - else: - nmodes = 0 - - vecs[:, nmodes:] = vec_gen(select) - - if v is not None: - return dot(v * w, dot(vecs[n:], dot(npl.inv(vecs[:n]), - v.T.conj()))) - else: - return dot(h_hop.T.conj(), dot(vecs[n:], npl.inv(vecs[:n]))) - else: - # Reorder all the right-going eigenmodes to the top left part of - # the Schur decomposition. - - if nprop > 0: - select[propselect] = rselect - - z = ord_schur(select) - - if v is not None: - return dot(v * w, dot(z[n:, :n], dot(npl.inv(z[:n, :n]), - v.T.conj()))) - else: - return dot(h_hop.T.conj(), dot(z[n:, :n], npl.inv(z[:n, :n]))) - -Modes = namedtuple('Modes', ['vecs', 'vecslmbdainv', 'nmodes', 'svd']) - - -def modes(h_onslice, h_hop, tol=1e6): - """ - Compute the eigendecomposition of a translation operator of a lead. - - Parameters - ---------- - h_onslice : NumPy array, real or complex, shape (N,N) The unit cell - Hamiltonian of the lead slice. - h_hop : NumPy array, real or complex, shape (N,M) - the hopping matrix from a lead slice to the one on which self-energy - has to be calculated (and any other hopping in the same direction). - - Returns - ------- - Modes(vecs, vecslmbdainv, nmodes, svd) : a named tuple - `vecs` is the matrix of eigenvectors of the translation operator. - `vecslmbdainv` is the matrix of eigenvectors multiplied with their - corresponding inverse eigenvalue. `nmodes` is the number of - propagating modes in either direction. `svd` is a tuple (u, s, v) - holding the singular value decomposition of the hopping matrix, or a - single None if `h_hop` is invertible. - - Notes - ----- - Only propagating modes and modes decaying away from the system are - returned: The first `nmodes` columns in `vecs` correspond to incoming modes - (coming from the lead into the system), the following `nmodes` columns - correspond to outgoing modes (going into the lead, away from the system), - the remaining columns are evanescent modes, decaying away from the system. - - The propagating modes are sorted according to the longitudinal component of - their k-vector, with incoming modes having k sorted in descending order, - and outgoing modes having k sorted in ascending order. In simple cases - where bands do not cross, this ordering corresponds to "lowest modes - first". In general, however, it is necessary to examine the band structure - -- something this function is not doing by design. - - If `h_hop` is invertible, the full transverse wave functions are returned. - If it is singular, the projections (u^dagger psi, v^dagger psi lambda^-1) - are returned. - - In order for the linear system to be well-defined, instead of the - evanescent modes, an orthogonal basis in the space of evanescent modes is - returned. - - Propagating modes with the same lambda are orthogonalized. All the - propagating modes are normalized by current. - - This function uses the most stable and efficient algorithm for calculating - self-energy, described in kwant/doc/other/lead_modes.pdf - """ - - m = h_hop.shape[1] - - if (h_onslice.shape[0] != h_onslice.shape[1] or - h_onslice.shape[0] != h_hop.shape[0]): - raise ValueError("Incompatible matrix sizes for h_onslice and h_hop.") - - # Note: np.any(h_hop) returns (at least from NumPy 1.6.1 - 1.8-devel) - # False if h_hop is purely imaginary - if not (np.any(h_hop.real) or np.any(h_hop.imag)): - n = h_hop.shape[0] - svd = (np.empty((n, 0)), np.empty((0, 0)), np.empty((0, m))) - return Modes(np.empty((0, 0)), np.empty((0, 0)), 0, svd) - - # Defer most of the calculation to a helper routine. - ev, evanselect, propselect, vec_gen, ord_schur, \ - u, s, v, extract, project = unified_eigenproblem(h_onslice, h_hop, tol) - - if s is not None: - n = s.size - else: - n = h_onslice.shape[0] - - nprop = np.sum(propselect) - nevan = n - nprop // 2 - evanselect_bool = np.zeros((2*n), dtype='bool') - evanselect_bool[evanselect] = True - evan_vecs = ord_schur(evanselect)[:, :nevan] - - if nprop > 0: - # Compute the propagating eigenvectors. - prop_vecs = vec_gen(propselect) - - # Compute their velocity, and, if necessary, rotate them - if s is not None: - prop_vecs, vel, rprop, crossing = \ - make_proper_modes(ev[propselect], prop_vecs, s, - extract, project) - else: - prop_vecs, vel, rprop, crossing = \ - make_proper_modes(ev[propselect], prop_vecs, h_hop) - - # Normalize propagating eigenvectors by velocities. - prop_vecs /= np.sqrt(abs(vel)) - - # Fix phase factor - make maximum of transverse wave function real - # TODO (Anton): Take care of multiple maxima when normalizing. - maxnode = prop_vecs[n + np.argmax(abs(prop_vecs[n:, :]), axis=0), - np.arange(prop_vecs.shape[1])] - maxnode /= abs(maxnode) - prop_vecs /= maxnode - - lprop = np.logical_not(rprop) - nmodes = np.sum(rprop) - - # Sort modes according to their k-vector (1/lambda = e^{-ik}): - # - right-going modes: sort that k is in ascending order - # - left-going modes: sort that k is in descending order - # (note that k can be positive or negative). With this convention, - # the modes of a simple square lattice strip are ordered as - # expected (lowest subband first, etc.) - - prop_ev = ev[propselect] - rsort = np.argsort((-1j * np.log(prop_ev[rprop])).real) - lsort = np.argsort((1j * np.log(prop_ev[lprop])).real) - - vecs = np.c_[prop_vecs[n:, lprop][:, lsort], - prop_vecs[n:, rprop][:, rsort], - evan_vecs[n:]] - vecslmbdainv = np.c_[prop_vecs[:n, lprop][:, lsort], - prop_vecs[:n, rprop][:, rsort], - evan_vecs[:n]] - - else: - vecs = evan_vecs[n:] - vecslmbdainv = evan_vecs[:n] - nmodes = 0 - - svd = None if s is None else (u, s, v) - return Modes(vecs, vecslmbdainv, nmodes, svd) - - -def square_self_energy(width, hopping, potential, fermi_energy): - """ - Calculate analytically the self energy for a square lattice. - - The lattice is assumed to have a single orbital per site and - nearest-neighbor hopping. - - Parameters - ---------- - width : integer - width of the lattice - """ - - # Following appendix C of M. Wimmer's diploma thesis: - # http://www.physik.uni-regensburg.de/forschung/\ - # richter/richter/media/research/publications2004/wimmer-Diplomarbeit.pdf - - # p labels transversal modes. i and j label the sites of a slice. - - # Precalculate the transverse wave function. - psi_p_i = np.empty((width, width)) - factor = pi / (width + 1) - prefactor = sqrt(2 / (width + 1)) - for p in xrange(width): - for i in xrange(width): - psi_p_i[p, i] = prefactor * sin(factor * (p + 1) * (i + 1)) - - # Precalculate the integrals of the longitudinal wave functions. - def f(q): - if abs(q) <= 2: - return q/2 - 1j * sqrt(1 - (q / 2) ** 2) - else: - return q/2 - copysign(sqrt((q / 2) ** 2 - 1), q) - f_p = np.empty((width,), dtype=complex) - for p in xrange(width): - e = 2 * hopping * (1 - cos(factor * (p + 1))) - q = (fermi_energy - potential - e) / hopping - 2 - f_p[p] = f(q) - - # Put everything together into the self energy and return it. - result = np.empty((width, width), dtype=complex) - for i in xrange(width): - for j in xrange(width): - result[i, j] = hopping * \ - (psi_p_i[:, i] * psi_p_i[:, j].conj() * f_p).sum() - return result diff --git a/kwant/physics/tests/test_leads.py b/kwant/physics/tests/test_leads.py new file mode 100644 index 0000000000000000000000000000000000000000..88e0ea809f90ad9ecb2889ac4d449297d8a4d1b7 --- /dev/null +++ b/kwant/physics/tests/test_leads.py @@ -0,0 +1,280 @@ +# Copyright 2011-2013 kwant authors. +# +# This file is part of kwant. It is subject to the license terms in the +# LICENSE file found in the top-level directory of this distribution and at +# http://kwant-project.org/license. A list of kwant authors can be found in +# the AUTHORS file at the top-level directory of this distribution and at +# http://kwant-project.org/authors. + +from __future__ import division +import numpy as np +from itertools import product, izip +from numpy.testing import assert_almost_equal +from kwant.physics import leads +import kwant + +modes_se = leads.selfenergy + +def h_slice(t, w, e): + h = (4 * t - e) * np.identity(w) + h.flat[1 :: w + 1] = -t + h.flat[w :: w + 1] = -t + return h + + +def test_analytic_numeric(): + w = 5 # width + t = 0.78 # hopping element + e = 1.3 # Fermi energy + + assert_almost_equal(leads.square_selfenergy(w, t, e), + modes_se(h_slice(t, w, e), -t * np.identity(w))) + + +def test_regular_fully_degenerate(): + """Selfenergy with an invertible hopping matrix, and degenerate bands.""" + + w = 6 # width + t = 0.5 # hopping element + e = 1.3 # Fermi energy + + h_hop_s = -t * np.identity(w) + h_onslice_s = h_slice(t, w, e) + + h_hop = np.zeros((2*w, 2*w)) + h_hop[:w, :w] = h_hop_s + h_hop[w:, w:] = h_hop_s + + h_onslice = np.zeros((2*w, 2*w)) + h_onslice[:w, :w] = h_onslice_s + h_onslice[w:, w:] = h_onslice_s + + g = np.zeros((2*w, 2*w), dtype=complex) + g[:w, :w] = leads.square_selfenergy(w, t, e) + g[w:, w:] = leads.square_selfenergy(w, t, e) + + assert_almost_equal(g, modes_se(h_onslice, h_hop)) + + +def test_regular_degenerate_with_crossing(): + """This is a testcase with invertible hopping matrices, + and degenerate k-values with a crossing such that one + mode has a positive velocity, and one a negative velocity + + For this case the fall-back technique must be used. + """ + + w = 4 # width + t = 0.5 # hopping element + e = 1.8 # Fermi energy + + global h_hop + h_hop_s = -t * np.identity(w) + h_onslice_s = h_slice(t, w, e) + + hop = np.zeros((2*w, 2*w)) + hop[:w, :w] = h_hop_s + hop[w:, w:] = -h_hop_s + + h_onslice = np.zeros((2*w, 2*w)) + h_onslice[:w, :w] = h_onslice_s + h_onslice[w:, w:] = -h_onslice_s + + g = np.zeros((2*w, 2*w), dtype=complex) + g[:w, :w] = leads.square_selfenergy(w, t, e) + g[w:, w:] = -np.conj(leads.square_selfenergy(w, t, e)) + + assert_almost_equal(g, modes_se(h_onslice, hop)) + + +def test_singular(): + """This testcase features a rectangular (and hence singular) + hopping matrix without degeneracies. + + This case can be treated with the Schur technique.""" + + w = 5 # width + t = .5 # hopping element + e = 0.4 # Fermi energy + + h_hop_s = -t * np.identity(w) + h_onslice_s = h_slice(t, w, e) + + h_hop = np.zeros((2*w, w)) + h_hop[w:, :w] = h_hop_s + + h_onslice = np.zeros((2*w, 2*w)) + h_onslice[:w, :w] = h_onslice_s + h_onslice[:w, w:] = h_hop_s + h_onslice[w:, :w] = h_hop_s + h_onslice[w:, w:] = h_onslice_s + g = leads.square_selfenergy(w, t, e) + + assert_almost_equal(g, modes_se(h_onslice, h_hop)) + + +def test_singular_but_square(): + """This testcase features a singular, square hopping matrices + without degeneracies. + + This case can be treated with the Schur technique.""" + + w = 5 # width + t = 0.9 # hopping element + e = 2.38 # Fermi energy + + h_hop_s = -t * np.identity(w) + h_onslice_s = h_slice(t, w, e) + + h_hop = np.zeros((2*w, 2*w)) + h_hop[w:, :w] = h_hop_s + + h_onslice = np.zeros((2*w, 2*w)) + h_onslice[:w, :w] = h_onslice_s + h_onslice[:w, w:] = h_hop_s + h_onslice[w:, :w] = h_hop_s + h_onslice[w:, w:] = h_onslice_s + + g = np.zeros((2*w, 2*w), dtype=complex) + g[:w, :w] = leads.square_selfenergy(w, t, e) + assert_almost_equal(g, modes_se(h_onslice, h_hop)) + + +def test_singular_fully_degenerate(): + """This testcase features a rectangular (and hence singular) + hopping matrix with complete degeneracy. + + This case can still be treated with the Schur technique.""" + + w = 5 # width + t = 1.5 # hopping element + e = 3.3 # Fermi energy + + h_hop_s = -t * np.identity(w) + h_onslice_s = h_slice(t, w, e) + + h_hop = np.zeros((4*w, 2*w)) + h_hop[2*w:3*w, :w] = h_hop_s + h_hop[3*w:4*w, w:2*w] = h_hop_s + + h_onslice = np.zeros((4*w, 4*w)) + h_onslice[:w, :w] = h_onslice_s + h_onslice[:w, 2*w:3*w] = h_hop_s + h_onslice[w:2*w, w:2*w] = h_onslice_s + h_onslice[w:2*w, 3*w:4*w] = h_hop_s + h_onslice[2*w:3*w, :w] = h_hop_s + h_onslice[2*w:3*w, 2*w:3*w] = h_onslice_s + h_onslice[3*w:4*w, w:2*w] = h_hop_s + h_onslice[3*w:4*w, 3*w:4*w] = h_onslice_s + + g = np.zeros((2*w, 2*w), dtype=complex) + g[:w, :w] = leads.square_selfenergy(w, t, e) + g[w:, w:] = leads.square_selfenergy(w, t, e) + + assert_almost_equal(g, modes_se(h_onslice, h_hop)) + + +def test_singular_degenerate_with_crossing(): + """This testcase features a rectangular (and hence singular) + hopping matrix with degeneracy k-values including a crossing + with velocities of opposite sign. + + This case must be treated with the fall-back technique.""" + + w = 5 # width + t = 20.5 # hopping element + e = 3.3 # Fermi energy + + h_hop_s = -t * np.identity(w) + h_onslice_s = h_slice(t, w, e) + + h_hop = np.zeros((4*w, 2*w)) + h_hop[2*w:3*w, :w] = h_hop_s + h_hop[3*w:4*w, w:2*w] = -h_hop_s + + h_onslice = np.zeros((4*w, 4*w)) + h_onslice[:w, :w] = h_onslice_s + h_onslice[:w, 2*w:3*w] = h_hop_s + h_onslice[w:2*w, w:2*w] = -h_onslice_s + h_onslice[w:2*w, 3*w:4*w] = -h_hop_s + h_onslice[2*w:3*w, :w] = h_hop_s + h_onslice[2*w:3*w, 2*w:3*w] = h_onslice_s + h_onslice[3*w:4*w, w:2*w] = -h_hop_s + h_onslice[3*w:4*w, 3*w:4*w] = -h_onslice_s + + g = np.zeros((2*w, 2*w), dtype=complex) + g[:w, :w] = leads.square_selfenergy(w, t, e) + g[w:, w:] = -np.conj(leads.square_selfenergy(w, t, e)) + + assert_almost_equal(g, modes_se(h_onslice, h_hop)) + + +def test_singular_h_and_t(): + h = 0.1 * np.identity(6) + t = np.eye(6, 6, 4) + sigma = modes_se(h, t) + sigma_should_be = np.zeros((6,6)) + sigma_should_be[4, 4] = sigma_should_be[5, 5] = -10 + assert_almost_equal(sigma, sigma_should_be) + + +def test_modes(): + h, t = .3, .7 + vecs, vecslinv, nrprop, svd = leads.modes(np.array([[h]]), np.array([[t]])) + assert nrprop == 1 + assert svd is None + np.testing.assert_almost_equal((vecs[0] * vecslinv[0].conj()).imag, + [0.5, -0.5]) + + +def test_modes_bearded_ribbon(): + # Check if bearded graphene ribbons work. + lat = kwant.lattice.honeycomb() + sys = kwant.Builder(kwant.TranslationalSymmetry((1, 0))) + sys[lat.shape((lambda pos: -20 < pos[1] < 20), + (0, 0))] = 0.3 + sys[lat.neighbors()] = -1 + sys = sys.finalized() + h, t = sys.slice_hamiltonian(), sys.inter_slice_hopping() + assert leads.modes(h, t).nmodes == 8 # Calculated by plotting dispersion. + + +def test_algorithm_equivalence(): + np.random.seed(400) + n = 12 + h = np.random.randn(n, n) + 1j * np.random.randn(n, n) + h += h.T.conj() + t = np.random.randn(n, n) + 1j * np.random.randn(n, n) + u, s, vh = np.linalg.svd(t) + u, v = u * np.sqrt(s), vh.T.conj() * np.sqrt(s) + prop_vecs = [] + evan_vecs = [] + algos = [(True,)] + list(product(*([(False,)] + 2 * [(True, False)]))) + for algo in algos: + result = leads.modes(h, t, algorithm=algo) + + vecs, vecslmbdainv = result.vecs, result.vecslmbdainv + + # Bring the calculated vectors to real space + if not algo[0]: + vecs = np.dot(v, vecs) + np.testing.assert_almost_equal(result.sqrt_hop, v) + else: + vecslmbdainv = np.dot(v.T.conj(), vecslmbdainv) + full_vecs = np.r_[vecslmbdainv, vecs] + + prop_vecs.append(full_vecs[:, : 2 * result.nmodes]) + evan_vecs.append(full_vecs[:, 2 * result.nmodes :]) + + msg = 'Algorithm {0} failed.' + for vecs, algo in izip(prop_vecs, algos): + # Propagating modes should have identical ordering, and only vary + # By a phase + np.testing.assert_allclose(np.abs(np.sum(vecs/prop_vecs[0], + axis=0)), vecs.shape[0], + err_msg=msg.format(algo)) + + for vecs, algo in izip(evan_vecs, algos): + # Evanescent modes must span the same linear space. + assert np.linalg.matrix_rank(np.c_[vecs, evan_vecs[0]], tol=1e-12) == \ + vecs.shape[1], msg.format(algo) diff --git a/kwant/physics/tests/test_noise.py b/kwant/physics/tests/test_noise.py index bf5f1d58f5a67ee9d58e1d25fce1b8a68c954d4f..4a9c25bf24c439801f74a8740ab315cfa4f71956 100644 --- a/kwant/physics/tests/test_noise.py +++ b/kwant/physics/tests/test_noise.py @@ -43,9 +43,9 @@ class LeadWithOnlySelfEnergy(object): def __init__(self, lead): self.lead = lead - def self_energy(self, energy, args=()): + def selfenergy(self, energy, args=()): assert args == () - return self.lead.self_energy(energy) + return self.lead.selfenergy(energy) def test_twoterminal(): diff --git a/kwant/physics/tests/test_selfenergy.py b/kwant/physics/tests/test_selfenergy.py deleted file mode 100644 index 2975c1415f3abd1c571a48e43a98e74c0d0b7677..0000000000000000000000000000000000000000 --- a/kwant/physics/tests/test_selfenergy.py +++ /dev/null @@ -1,250 +0,0 @@ -# Copyright 2011-2013 kwant authors. -# -# This file is part of kwant. It is subject to the license terms in the -# LICENSE file found in the top-level directory of this distribution and at -# http://kwant-project.org/license. A list of kwant authors can be found in -# the AUTHORS file at the top-level directory of this distribution and at -# http://kwant-project.org/authors. - -from __future__ import division -import numpy as np -from numpy.testing import assert_almost_equal -import kwant.physics.selfenergy as se - -def test_analytic_numeric(): - w = 5 # width - t = 0.5 # hopping element - v = 2 # potential - e = 3.3 # Fermi energy - - h_hop = -t * np.identity(w) - h_onslice = ((v + 4 * t - e) - * np.identity(w)) - h_onslice.flat[1 :: w + 1] = -t - h_onslice.flat[w :: w + 1] = -t - - assert_almost_equal(se.square_self_energy(w, t, v, e), - se.self_energy(h_onslice, h_hop)) - -def test_regular_fully_degenerate(): - """This testcase features an invertible hopping matrix, - and bands that are fully degenerate. - - This case can still be treated with the Schur technique.""" - - w = 5 # width - t = 0.5 # hopping element - v = 2 # potential - e = 3.3 # Fermi energy - - h_hop_s = -t * np.identity(w) - h_onslice_s = ((v + 4 * t - e) - * np.identity(w)) - h_onslice_s.flat[1 :: w + 1] = -t - h_onslice_s.flat[w :: w + 1] = -t - - h_hop = np.zeros((2*w, 2*w)) - h_hop[0:w, 0:w] = h_hop_s - h_hop[w:2*w, w:2*w] = h_hop_s - - h_onslice = np.zeros((2*w, 2*w)) - h_onslice[0:w, 0:w] = h_onslice_s - h_onslice[w:2*w, w:2*w] = h_onslice_s - - g = np.zeros((2*w, 2*w), dtype=complex) - g[0:w, 0:w] = se.square_self_energy(w, t, v, e) - g[w:2*w, w:2*w] = se.square_self_energy(w, t, v, e) - - assert_almost_equal(g, - se.self_energy(h_onslice, h_hop)) - -def test_regular_degenerate_with_crossing(): - """This is a testcase with invertible hopping matrices, - and degenerate k-values with a crossing such that one - mode has a positive velocity, and one a negative velocity - - For this case the fall-back technique must be used. - """ - - w = 5 # width - t = 0.5 # hopping element - v = 2 # potential - e = 3.3 # Fermi energy - - h_hop_s = -t * np.identity(w) - h_onslice_s = ((v + 4 * t - e) - * np.identity(w)) - h_onslice_s.flat[1 :: w + 1] = -t - h_onslice_s.flat[w :: w + 1] = -t - - h_hop = np.zeros((2*w, 2*w)) - h_hop[0:w, 0:w] = h_hop_s - h_hop[w:2*w, w:2*w] = -h_hop_s - - h_onslice = np.zeros((2*w, 2*w)) - h_onslice[0:w, 0:w] = h_onslice_s - h_onslice[w:2*w, w:2*w] = -h_onslice_s - - g = np.zeros((2*w, 2*w), dtype=complex) - g[0:w, 0:w] = se.square_self_energy(w, t, v, e) - g[w:2*w, w:2*w] = -np.conj(se.square_self_energy(w, t, v, e)) - - assert_almost_equal(g, - se.self_energy(h_onslice, h_hop)) - -def test_singular(): - """This testcase features a rectangular (and hence singular) - hopping matrix without degeneracies. - - This case can be treated with the Schur technique.""" - - w = 5 # width - t = 0.5 # hopping element - v = 2 # potential - e = 3.3 # Fermi energy - - h_hop_s = -t * np.identity(w) - h_onslice_s = ((v + 4 * t - e) - * np.identity(w)) - h_onslice_s.flat[1 :: w + 1] = -t - h_onslice_s.flat[w :: w + 1] = -t - - h_hop = np.zeros((2*w, w)) - h_hop[w:2*w, 0:w] = h_hop_s - - h_onslice = np.zeros((2*w, 2*w)) - h_onslice[0:w, 0:w] = h_onslice_s - h_onslice[0:w, w:2*w] = h_hop_s - h_onslice[w:2*w, 0:w] = h_hop_s - h_onslice[w:2*w, w:2*w] = h_onslice_s - - assert_almost_equal(se.square_self_energy(w, t, v, e), - se.self_energy(h_onslice, h_hop)) - -def test_singular_but_square(): - """This testcase features a singular, square hopping matrices - without degeneracies. - - This case can be treated with the Schur technique.""" - - w = 5 # width - t = 0.5 # hopping element - v = 2 # potential - e = 3.3 # Fermi energy - - h_hop_s = -t * np.identity(w) - h_onslice_s = ((v + 4 * t - e) - * np.identity(w)) - h_onslice_s.flat[1 :: w + 1] = -t - h_onslice_s.flat[w :: w + 1] = -t - - h_hop = np.zeros((2*w, 2*w)) - h_hop[w:2*w, 0:w] = h_hop_s - - h_onslice = np.zeros((2*w, 2*w)) - h_onslice[0:w, 0:w] = h_onslice_s - h_onslice[0:w, w:2*w] = h_hop_s - h_onslice[w:2*w, 0:w] = h_hop_s - h_onslice[w:2*w, w:2*w] = h_onslice_s - - g = np.zeros((2*w, 2*w), dtype=complex) - g[0:w, 0:w] = se.square_self_energy(w, t, v, e) - - assert_almost_equal(g, - se.self_energy(h_onslice, h_hop)) - -def test_singular_fully_degenerate(): - """This testcase features a rectangular (and hence singular) - hopping matrix with complete degeneracy. - - This case can still be treated with the Schur technique.""" - - w = 5 # width - t = 0.5 # hopping element - v = 2 # potential - e = 3.3 # Fermi energy - - h_hop_s = -t * np.identity(w) - h_onslice_s = ((v + 4 * t - e) - * np.identity(w)) - h_onslice_s.flat[1 :: w + 1] = -t - h_onslice_s.flat[w :: w + 1] = -t - - h_hop = np.zeros((4*w, 2*w)) - h_hop[2*w:3*w, 0:w] = h_hop_s - h_hop[3*w:4*w, w:2*w] = h_hop_s - - h_onslice = np.zeros((4*w, 4*w)) - h_onslice[0:w, 0:w] = h_onslice_s - h_onslice[0:w, 2*w:3*w] = h_hop_s - h_onslice[w:2*w, w:2*w] = h_onslice_s - h_onslice[w:2*w, 3*w:4*w] = h_hop_s - h_onslice[2*w:3*w, 0:w] = h_hop_s - h_onslice[2*w:3*w, 2*w:3*w] = h_onslice_s - h_onslice[3*w:4*w, w:2*w] = h_hop_s - h_onslice[3*w:4*w, 3*w:4*w] = h_onslice_s - - g = np.zeros((2*w, 2*w), dtype=complex) - g[0:w, 0:w] = se.square_self_energy(w, t, v, e) - g[w:2*w, w:2*w] = se.square_self_energy(w, t, v, e) - - assert_almost_equal(g, - se.self_energy(h_onslice, h_hop)) - -def test_singular_degenerate_with_crossing(): - """This testcase features a rectangular (and hence singular) - hopping matrix with degeneracy k-values including a crossing - with velocities of opposite sign. - - This case must be treated with the fall-back technique.""" - - w = 5 # width - t = 0.5 # hopping element - v = 2 # potential - e = 3.3 # Fermi energy - - h_hop_s = -t * np.identity(w) - h_onslice_s = ((v + 4 * t - e) - * np.identity(w)) - h_onslice_s.flat[1 :: w + 1] = -t - h_onslice_s.flat[w :: w + 1] = -t - - h_hop = np.zeros((4*w, 2*w)) - h_hop[2*w:3*w, 0:w] = h_hop_s - h_hop[3*w:4*w, w:2*w] = -h_hop_s - - h_onslice = np.zeros((4*w, 4*w)) - h_onslice[0:w, 0:w] = h_onslice_s - h_onslice[0:w, 2*w:3*w] = h_hop_s - h_onslice[w:2*w, w:2*w] = -h_onslice_s - h_onslice[w:2*w, 3*w:4*w] = -h_hop_s - h_onslice[2*w:3*w, 0:w] = h_hop_s - h_onslice[2*w:3*w, 2*w:3*w] = h_onslice_s - h_onslice[3*w:4*w, w:2*w] = -h_hop_s - h_onslice[3*w:4*w, 3*w:4*w] = -h_onslice_s - - g = np.zeros((2*w, 2*w), dtype=complex) - g[0:w, 0:w] = se.square_self_energy(w, t, v, e) - g[w:2*w, w:2*w] = -np.conj(se.square_self_energy(w, t, v, e)) - - assert_almost_equal(g, - se.self_energy(h_onslice, h_hop)) - -def test_singular_h_and_t(): - h = 0.1 * np.identity(6) - t = np.eye(6, 6, 4) - sigma = se.self_energy(h, t) - sigma_should_be = np.zeros((6,6)) - sigma_should_be[4, 4] = sigma_should_be[5, 5] = -10 - assert_almost_equal(se.self_energy(h, t), sigma_should_be) - -def test_modes(): - h, t = .3, .7 - vecs, vecslinv, nrpop, svd = se.modes(np.array([[h]]), np.array([[t]])) - l = (np.sqrt(h**2 - 4 * t**2 + 0j) - h) / (2 * t) - current = np.sqrt(4 * t**2 - h**2) - assert nrpop == 1 - assert svd is None - np.testing.assert_almost_equal(vecs, [2 * [1/np.sqrt(current)]]) - np.testing.assert_almost_equal(vecslinv, - vecs * np.array([1/l, 1/l.conj()])) diff --git a/kwant/solvers/common.py b/kwant/solvers/common.py index b8d25e9dbb86ddac3b75ec19ef2d70892029cfc7..2f3b654ddd028416027f34de2c0867f499a7da1a 100644 --- a/kwant/solvers/common.py +++ b/kwant/solvers/common.py @@ -94,8 +94,7 @@ class SparseSolver(object): def _make_linear_sys(self, sys, out_leads, in_leads, energy=0, force_realspace=False, check_hermiticity=True, args=()): - """ - Make a sparse linear system of equations defining a scattering + """Make a sparse linear system of equations defining a scattering problem. Parameters @@ -129,9 +128,9 @@ class SparseSolver(object): a small part of the complete solution). lead_info : list of objects Contains one entry for each lead. For a lead defined as a - tight-binding system, this is an instance of `kwant.physics.Modes` - (as returned by `kwant.physics.modes`), otherwise the lead - self-energy matrix. + tight-binding system, this is an instance of + `~kwant.physics.ModesTuple` with a corresponding format, + otherwise the lead self-energy matrix. Notes ----- @@ -139,6 +138,7 @@ class SparseSolver(object): or a low-level translationally invariant system. The system of equations that is created is described in kwant/doc/other/linear_system.pdf + """ splhsmat = getattr(sp, self.lhsformat + '_matrix') @@ -168,27 +168,18 @@ class SparseSolver(object): lead_info = [] for leadnum, interface in enumerate(sys.lead_interfaces): lead = sys.leads[leadnum] - if isinstance(lead, system.InfiniteSystem) and not force_realspace: - h = lead.slice_hamiltonian(args=args) - - if check_hermiticity: - if not np.allclose(h, h.T.conj(), rtol=1e-13): - msg = "Lead number {0} has a non-Hermitian " \ - "slice Hamiltonian." - raise ValueError(msg.format(leadnum)) - - h -= energy * np.identity(h.shape[0]) - v = lead.inter_slice_hopping(args=args) - modes = physics.modes(h, v) + if hasattr(lead, 'modes') and not force_realspace: + modes = lead.modes(energy, args=args) lead_info.append(modes) + u, ulinv, nprop, svd_v = modes - # Note: np.any(v) returns (at least from NumPy 1.6.1 - - # 1.8-devel) False if v is purely imaginary - if not (np.any(v.real) or np.any(v.imag)): + if len(u) == 0: # See comment about zero-shaped sparse matrices at the top. rhs.append(np.zeros((lhs.shape[1], 0))) continue - u, ulinv, nprop, svd = modes + + if svd_v is not None: + svd_v = svd_v.T.conj() if leadnum in out_leads: kept_vars.append( @@ -206,27 +197,25 @@ class SparseSolver(object): transf = sp.csc_matrix((np.ones(iface_orbs.size), coords), shape=(iface_orbs.size, lhs.shape[0])) - if svd is not None: - v_sp = sp.csc_matrix(svd[2].T.conj()) * transf + if svd_v is not None: + v_sp = sp.csc_matrix(svd_v) * transf vdaguout_sp = transf.T * \ - sp.csc_matrix(np.dot(svd[2] * svd[1], u_out)) + sp.csc_matrix(np.dot(svd_v.T.conj(), u_out)) lead_mat = - ulinv_out else: v_sp = transf - vdaguout_sp = transf.T * sp.csc_matrix(np.dot(v.T.conj(), - u_out)) + vdaguout_sp = transf.T * sp.csc_matrix(u_out) lead_mat = - ulinv_out lhs = sp.bmat([[lhs, vdaguout_sp], [v_sp, lead_mat]], format=self.lhsformat) if leadnum in in_leads and nprop > 0: - if svd: + if svd_v is not None: vdaguin_sp = transf.T * sp.csc_matrix( - -np.dot(svd[2] * svd[1], u_in)) + -np.dot(svd_v.T.conj(), u_in)) else: - vdaguin_sp = transf.T * sp.csc_matrix( - -np.dot(v.T.conj(), u_in)) + vdaguin_sp = transf.T * sp.csc_matrix(-u_in) # defer formation of the real matrix until the proper # system size is known @@ -235,7 +224,7 @@ class SparseSolver(object): # See comment about zero-shaped sparse matrices at the top. rhs.append(np.zeros((lhs.shape[1], 0))) else: - sigma = lead.self_energy(energy, args) + sigma = lead.selfenergy(energy, args) lead_info.append(sigma) indices = np.r_[tuple(slice(offsets[i], offsets[i + 1]) for i in interface)] @@ -404,7 +393,7 @@ class SparseSolver(object): self._make_linear_sys(fsys, [], xrange(len(fsys.leads)), energy, args=args) - Modes = physics.Modes + Modes = physics.ModesTuple num_extra_vars = sum(li.vecs.shape[1] - li.nmodes for li in lead_info if isinstance(li, Modes)) num_orb = h.shape[0] - num_extra_vars @@ -467,7 +456,7 @@ class WaveFunction(object): (h, self.rhs, kept_vars), lead_info = \ solver._make_linear_sys(sys, [], xrange(len(sys.leads)), energy, args=args) - Modes = physics.Modes + Modes = physics.ModesTuple num_extra_vars = sum(li.vecs.shape[1] - li.nmodes for li in lead_info if isinstance(li, Modes)) self.solver = solver diff --git a/kwant/solvers/tests/_test_sparse.py b/kwant/solvers/tests/_test_sparse.py index 8945a9323ad839ca539f5cbf128f8991478387b0..d6f61f7f2ac27e38e9bc477057bf6cf589d35d90 100644 --- a/kwant/solvers/tests/_test_sparse.py +++ b/kwant/solvers/tests/_test_sparse.py @@ -21,9 +21,9 @@ class LeadWithOnlySelfEnergy(object): def __init__(self, lead): self.lead = lead - def self_energy(self, energy, args=()): + def selfenergy(self, energy, args=()): assert args == () - return self.lead.self_energy(energy) + return self.lead.selfenergy(energy) # Test output sanity: that an error is raised if no output is requested, @@ -55,7 +55,7 @@ def test_output(solve): s2, modes2 = result2.data, result2.lead_info assert s2.shape == (modes2[1][2], modes2[0][2]) assert_almost_equal(s1, s2) - assert_almost_equal(np.dot(s.conjugate().transpose(), s), + assert_almost_equal(np.dot(s.T.conj(), s), np.identity(s.shape[0])) assert_raises(ValueError, solve, fsys, 0, []) modes = solve(fsys).lead_info @@ -272,7 +272,7 @@ def test_tricky_singular_hopping(solve): # Test equivalence between self-energy and scattering matrix representations. # Also check that transmission works. -def test_self_energy(solve): +def test_selfenergy(solve): np.random.seed(4) system = kwant.Builder() left_lead = kwant.Builder(kwant.TranslationalSymmetry((-1,))) @@ -315,7 +315,7 @@ def test_self_energy(solve): assert_almost_equal(t_should_be, sol.transmission(1, 0)) -def test_self_energy_reflection(solve): +def test_selfenergy_reflection(solve): np.random.seed(4) system = kwant.Builder() left_lead = kwant.Builder(kwant.TranslationalSymmetry((-1,))) diff --git a/kwant/solvers/tests/test_mumps.py b/kwant/solvers/tests/test_mumps.py index 9692420c4c5a722d4e50b0c4cd16c1c62a8a910a..2b1b8b15d1467285a00d8aa443bc3fcf4fd2b827 100644 --- a/kwant/solvers/tests/test_mumps.py +++ b/kwant/solvers/tests/test_mumps.py @@ -6,7 +6,7 @@ # the AUTHORS file at the top-level directory of this distribution and at # http://kwant-project.org/authors. -from nose.plugins.skip import Skip, SkipTest +#from nose.plugins.skip import Skip, SkipTest from numpy.testing.decorators import skipif try: from kwant.solvers.mumps import \ @@ -81,19 +81,19 @@ def test_tricky_singular_hopping(): @skipif(_no_mumps) -def test_self_energy(): +def test_selfenergy(): for opts in opt_list: reset_options() options(**opts) - _test_sparse.test_self_energy(solve) + _test_sparse.test_selfenergy(solve) @skipif(_no_mumps) -def test_self_energy_reflection(): +def test_selfenergy_reflection(): for opts in opt_list: reset_options() options(**opts) - _test_sparse.test_self_energy_reflection(solve) + _test_sparse.test_selfenergy_reflection(solve) @skipif(_no_mumps) diff --git a/kwant/solvers/tests/test_sparse.py b/kwant/solvers/tests/test_sparse.py index 9f3edd694d9d8101f0810250906d4fb6ad86daa9..46b58a16587af765bd5bf84ecb836e81c36b2847 100644 --- a/kwant/solvers/tests/test_sparse.py +++ b/kwant/solvers/tests/test_sparse.py @@ -39,12 +39,12 @@ def test_tricky_singular_hopping(): _test_sparse.test_tricky_singular_hopping(solve) -def test_self_energy(): - _test_sparse.test_self_energy(solve) +def test_selfenergy(): + _test_sparse.test_selfenergy(solve) -def test_self_energy_reflection(): - _test_sparse.test_self_energy_reflection(solve) +def test_selfenergy_reflection(): + _test_sparse.test_selfenergy_reflection(solve) def test_very_singular_leads(): diff --git a/kwant/system.py b/kwant/system.py index 925117881531eb2cba14654965c57300b526e801..fe931001ba308cc676f225abb16713dc27b199ca 100644 --- a/kwant/system.py +++ b/kwant/system.py @@ -13,6 +13,7 @@ __all__ = ['System', 'FiniteSystem', 'InfiniteSystem'] import abc import types +from copy import copy import numpy as np from . import physics, _system @@ -67,19 +68,22 @@ class FiniteSystem(System): Instance Variables ------------------ - leads : sequence of lead objects - Each lead object has to provide a method ``self_energy(energy, args)``. + leads : sequence of leads + Each lead has to provide a method + ``selfenergy(energy, args)``. + It may provide ``modes(energy, args)`` as well. lead_interfaces : sequence of sequences of integers - Each sub-sequence contains the indices of the system sites to which the - lead is connected. + Each sub-sequence contains the indices of the system sites + to which the lead is connected. Notes ----- The length of `leads` must be equal to the length of `lead_interfaces`. - For lead ``n``, the method leads[n].self_energy must return a square matrix - whose size is ``sum(self.num_orbitals(neighbor) for neighbor in - self.lead_interfaces[n])``. + For lead ``n``, the method leads[n].selfenergy must return a square matrix + whose size is ``sum(self.num_orbitals(neighbor)`` for neighbor in + self.lead_interfaces[n])``. The output format for ``leads[n].modes`` has to + be as described in `~kwant.physics.ModesTuple`. Often, the elements of `leads` will be instances of `InfiniteSystem`. If this is the case for lead ``n``, the sites ``lead_interfaces[n]`` match @@ -87,6 +91,57 @@ class FiniteSystem(System): """ __metaclass__ = abc.ABCMeta + def precalculate(self, energy, args=(), leads=None, + calculate_selfenergy=True): + """ + Precalculate modes or self-energies in the leads. + + Construct a copy of the system, with the lead modes precalculated, + which may significantly speed up calculations where only the system + is changing. + + Parameters + ---------- + energy : float + Energy at which the modes or self-energies have to be + evaluated. + args : sequence + Additional parameters required for calculating the Hamiltionians + leads : list of integers or None + Numbers of the leads to be precalculated. If `None`, all are + precalculated. + calculate_selfenergy : bool + Whether to calculate self-energy if modes are available. + + Returns + ------- + sys : FiniteSystem + A copy of the original system with some leads precalculated. + + Notes + ----- + If the leads are precalculated at certain `energy` or `args` values, + they might give wrong results if used to solve the system with + different parameter values. Use this function with caution. + """ + result = copy(self) + if leads is None: + leads = range(len(self.leads)) + new_leads = [] + for nr, lead in enumerate(self.leads): + if nr not in leads: + new_leads.append(lead) + continue + modes, selfenergy = None, None + try: + modes = lead.modes(energy, args) + if calculate_selfenergy: + selfenergy = physics.selfenergy_from_modes(modes) + except AttributeError: + selfenergy = lead.selfenergy(energy, args) + new_leads.append(PrecalculatedLead(modes, selfenergy)) + result.leads = new_leads + return result class InfiniteSystem(System): """ @@ -145,7 +200,21 @@ class InfiniteSystem(System): return self.hamiltonian_submatrix(slice_sites, neighbor_sites, sparse=sparse, args=args) - def self_energy(self, energy, args=()): + def modes(self, energy, args=()): + """Return mode decomposition of the lead + + See documentation of `~kwant.physics.ModesTuple` for the return + format details. + """ + ham = self.slice_hamiltonian(args=args) + shape = ham.shape + assert len(shape) == 2 + assert shape[0] == shape[1] + # Subtract energy from the diagonal. + ham.flat[::ham.shape[0] + 1] -= energy + return physics.modes(ham, self.inter_slice_hopping(args=args)) + + def selfenergy(self, energy, args=()): """Return self-energy of a lead. The returned matrix has the shape (s, s), where s is @@ -158,4 +227,37 @@ class InfiniteSystem(System): assert shape[0] == shape[1] # Subtract energy from the diagonal. ham.flat[::ham.shape[0] + 1] -= energy - return physics.self_energy(ham, self.inter_slice_hopping(args=args)) + return physics.selfenergy(ham, self.inter_slice_hopping(args=args)) + + +class PrecalculatedLead(object): + def __init__(self, modes=None, selfenergy=None): + """A general lead defined by its self energy. + + Parameters + ---------- + modes : kwant.physics.ModesTuple + Modes of the lead. + selfenergy : numpy array + Lead self-energy. + + Notes + ----- + At least one of `modes` and `selfenergy` must be provided. + """ + if modes is None and selfenergy is None: + raise ValueError("No precalculated values provided.") + self._modes = modes + self._selfenergy = selfenergy + + def modes(self, energy, args=()): + if self._modes is not None: + return self._modes + else: + raise ValueError("No precalculated modes were provided.") + + def selfenergy(self, energy, args=()): + if self._selfenergy is not None: + return self._selfenergy + else: + raise ValueError("No precalculated self-energy was provided.") diff --git a/kwant/tests/test_system.py b/kwant/tests/test_system.py index 72d1caf7921307c3b9c657bb655166a4ad5d19c5..93f6b9585014cf6a73e35603cee85b816e6edd7d 100644 --- a/kwant/tests/test_system.py +++ b/kwant/tests/test_system.py @@ -56,6 +56,19 @@ def test_hamiltonian_submatrix(): mat_sp = sys2.hamiltonian_submatrix(sparse=True).todense() np.testing.assert_array_equal(mat_sp, mat_dense) + # Test precalculation of modes. + np.random.seed(5) + lead = kwant.Builder(kwant.TranslationalSymmetry((-1,))) + lead[gr(0)] = np.zeros((2, 2)) + lead[gr(0), gr(1)] = np.random.randn(2, 2) + sys.attach_lead(lead) + sys2 = sys.finalized() + smatrix = kwant.solve(sys2, .1).data + sys3 = sys2.precalculate(.1, calculate_selfenergy=False) + smatrix2 = kwant.solve(sys3, .1).data + np.testing.assert_almost_equal(smatrix, smatrix2) + assert_raises(ValueError, kwant.solve, sys3, 0.2, None, None, True) + # Test for shape errors. sys[gr(0), gr(2)] = np.array([[1, 2]]) sys2 = sys.finalized() @@ -86,3 +99,4 @@ def test_hamiltonian_submatrix(): mat = mat[perm, :] mat = mat[:, perm] np.testing.assert_array_equal(mat, mat_should_be) +