Commit e2a4343e authored by Joseph Weston's avatar Joseph Weston
Browse files

Merge branch 'feature/selfenergy-lead' into 'master'

Allow mode-space solvers to work with systems that have self-energy leads attached

Closes #368

See merge request kwant/kwant!366
parents c460cab8 65c9763d
......@@ -15,7 +15,7 @@ from numbers import Integral
import numpy as np
import scipy.sparse as sp
from .._common import ensure_isinstance, deprecate_args
from .. import system
from .. import system, physics
from functools import reduce
# Until v0.13.0, scipy.sparse did not support making block matrices out of
......@@ -139,8 +139,8 @@ class SparseSolver(metaclass=abc.ABCMeta):
lead_info : list of objects
Contains one entry for each lead. If `realspace=False`, this is an
instance of `~kwant.physics.PropagatingModes` with a corresponding
format, otherwise the lead self-energy matrix.
instance of `~kwant.physics.PropagatingModes` for all leads that
have modes, otherwise the lead self-energy matrix.
Notes
-----
......@@ -188,6 +188,30 @@ class SparseSolver(metaclass=abc.ABCMeta):
for leadnum, interface in enumerate(syst.lead_interfaces):
lead = syst.leads[leadnum]
if not realspace:
if system.is_selfenergy_lead(lead):
# We make equivalent checks to this in 'smatrix',
# 'greens_function' and 'wave_function' (where we
# can give more informative error messages).
# Putting this check here again is just a sanity check,
assert leadnum not in in_leads
sigma = np.asarray(lead.selfenergy(energy, args, params=params))
rhs.append(None)
lead_info.append(sigma)
indices.append(None)
# add selfenergy to the LHS
coords = np.r_[tuple(slice(offsets[i], offsets[i + 1])
for i in interface)]
if sigma.shape != 2 * coords.shape:
raise ValueError(
f"Self-energy dimension for lead {leadnum} "
"does not match the total number of orbitals "
"of the sites for which it is defined."
)
y, x = np.meshgrid(coords, coords)
sig_sparse = splhsmat((sigma.flat, [x.flat, y.flat]),
lhs.shape)
lhs = lhs + sig_sparse
else:
prop, stab = lead.modes(energy, args, params=params)
lead_info.append(prop)
u = stab.vecs
......@@ -359,6 +383,15 @@ class SparseSolver(metaclass=abc.ABCMeta):
if len(in_leads) == 0 or len(out_leads) == 0:
raise ValueError("No output is requested.")
for direction, leads in [("in", in_leads), ("out", out_leads)]:
for lead in leads:
if system.is_selfenergy_lead(syst.leads[lead]):
raise ValueError(
f"lead {lead} is only defined by a self-energy, "
"so we cannot calculate scattering matrix elements for it. "
f"Specify '{direction}_leads' without '{lead}'."
)
linsys, lead_info = self._make_linear_sys(syst, in_leads, energy, args,
check_hermiticity, False,
params=params)
......@@ -519,7 +552,7 @@ class SparseSolver(metaclass=abc.ABCMeta):
"is not implemented yet.")
for lead in syst.leads:
if not hasattr(lead, 'modes') and hasattr(lead, 'selfenergy'):
if system.is_selfenergy_lead(lead):
# TODO: fix this
raise NotImplementedError("ldos for leads with only "
"self-energy is not implemented yet.")
......@@ -604,21 +637,27 @@ class WaveFunction:
def __init__(self, solver, sys, energy, args, check_hermiticity, params):
syst = sys # ensure consistent naming across function bodies
ensure_isinstance(syst, system.System)
for lead in syst.leads:
if not hasattr(lead, 'modes'):
# TODO: figure out what to do with self-energies.
msg = ('Wave functions for leads with only self-energy'
' are not available yet.')
raise NotImplementedError(msg)
linsys = solver._make_linear_sys(syst, range(len(syst.leads)), energy,
modes_leads = [
i for i, l in enumerate(syst.leads)
if not system.is_selfenergy_lead(l)
]
linsys = solver._make_linear_sys(syst, modes_leads, energy,
args, check_hermiticity,
params=params)[0]
self.modes_leads = modes_leads
self.solve = solver._solve_linear_sys
self.rhs = linsys.rhs
self.factorized_h = solver._factorized(linsys.lhs)
self.num_orb = linsys.num_orb
def __call__(self, lead):
if lead not in self.modes_leads:
msg = ('Scattering wave functions for leads with only '
'self-energy are undefined.')
raise ValueError(msg)
result = self.solve(self.factorized_h, self.rhs[lead],
slice(self.num_orb))
return result.transpose()
......@@ -769,6 +808,8 @@ class SMatrix(BlockResult):
matrix.
lead_info : list of data
a list containing `kwant.physics.PropagatingModes` for each lead.
If a lead is a selfenergy lead, then the corresponding entry
in lead_info is a selfenergy.
out_leads, in_leads : sequence of integers
indices of the leads where current is extracted (out) or injected
(in). Only those are listed for which SMatrix contains the
......@@ -777,7 +818,22 @@ class SMatrix(BlockResult):
def __init__(self, data, lead_info, out_leads, in_leads,
current_conserving=False):
sizes = [len(i.momenta) // 2 for i in lead_info]
# An equivalent condition to this is checked in 'Solver.smatrix',
# but we add it here again as a sanity check. If 'lead_info' is not
# a 'PropagatingModes' that means that the corresponding lead is a
# selfenergy lead. Scattering matrix elements to/from a selfenergy lead
# are not defined.
assert not any(
not isinstance(info, physics.PropagatingModes)
and leadnum in (out_leads + in_leads)
for leadnum, info in enumerate(lead_info)
)
# 'getattr' in order to handle self-energy leads, for which
# 'info' is just a matrix (so no 'momenta').
sizes = [
len(getattr(info, "momenta", [])) // 2 for info in lead_info
]
super().__init__(data, lead_info, out_leads, in_leads,
sizes, current_conserving)
# in_offsets marks beginnings and ends of blocks in the scattering
......
......@@ -84,6 +84,28 @@ def ldos(solver):
return solver.ldos
# 'scope="function"' means that mutating the returned Builder
# inside tests is won't affect other tests.
@pytest.fixture(scope="function")
def twolead_builder():
rng = ensure_rng(4)
system = kwant.Builder()
left_lead = kwant.Builder(kwant.TranslationalSymmetry((-1,)))
right_lead = kwant.Builder(kwant.TranslationalSymmetry((1,)))
for b, site in [(system, chain(0)), (system, chain(1)),
(left_lead, chain(0)), (right_lead, chain(0))]:
h = rng.random_sample((n, n)) + 1j * rng.random_sample((n, n))
h += h.conjugate().transpose()
b[site] = h
for b, hopp in [(system, (chain(0), chain(1))),
(left_lead, (chain(0), chain(1))),
(right_lead, (chain(0), chain(1)))]:
b[hopp] = (10 * rng.random_sample((n, n)) +
1j * rng.random_sample((n, n)))
system.attach_lead(left_lead)
system.attach_lead(right_lead)
return system
n = 5
chain = kwant.lattice.chain(norbs=n)
sq = square = kwant.lattice.square(norbs=n)
......@@ -111,24 +133,8 @@ def assert_modes_equal(modes1, modes2):
# Test output sanity: that an error is raised if no output is requested,
# and that solving for a subblock of a scattering matrix is the same as taking
# a subblock of the full scattering matrix.
def test_output(smatrix):
rng = ensure_rng(3)
system = kwant.Builder()
left_lead = kwant.Builder(kwant.TranslationalSymmetry((-1,)))
right_lead = kwant.Builder(kwant.TranslationalSymmetry((1,)))
for b, site in [(system, chain(0)), (system, chain(1)),
(left_lead, chain(0)), (right_lead, chain(0))]:
h = rng.random_sample((n, n)) + 1j * rng.random_sample((n, n))
h += h.conjugate().transpose()
b[site] = h
for b, hopp in [(system, (chain(0), chain(1))),
(left_lead, (chain(0), chain(1))),
(right_lead, (chain(0), chain(1)))]:
b[hopp] = (10 * rng.random_sample((n, n)) +
1j * rng.random_sample((n, n)))
system.attach_lead(left_lead)
system.attach_lead(right_lead)
fsyst = system.finalized()
def test_output(twolead_builder, smatrix):
fsyst = twolead_builder.finalized()
result1 = smatrix(fsyst)
s, modes1 = result1.data, result1.lead_info
......@@ -425,25 +431,9 @@ def test_many_leads(smatrix, greens_function):
# Test equivalence between self-energy and scattering matrix representations.
# Also check that transmission works.
def test_selfenergy(greens_function, smatrix):
rng = ensure_rng(4)
system = kwant.Builder()
left_lead = kwant.Builder(kwant.TranslationalSymmetry((-1,)))
right_lead = kwant.Builder(kwant.TranslationalSymmetry((1,)))
for b, site in [(system, chain(0)), (system, chain(1)),
(left_lead, chain(0)), (right_lead, chain(0))]:
h = rng.random_sample((n, n)) + 1j * rng.random_sample((n, n))
h += h.conjugate().transpose()
b[site] = h
for b, hopp in [(system, (chain(0), chain(1))),
(left_lead, (chain(0), chain(1))),
(right_lead, (chain(0), chain(1)))]:
b[hopp] = (10 * rng.random_sample((n, n)) +
1j * rng.random_sample((n, n)))
system.attach_lead(left_lead)
system.attach_lead(right_lead)
fsyst = system.finalized()
def test_selfenergy(twolead_builder, greens_function, smatrix):
system = twolead_builder
fsyst = twolead_builder.finalized()
t = smatrix(fsyst, 0, (), [1], [0]).data
eig_should_be = np.linalg.eigvals(t * t.conjugate().transpose())
n_eig = len(eig_should_be)
......@@ -471,20 +461,44 @@ def test_selfenergy(greens_function, smatrix):
raises(ValueError, check_fsyst, fsyst.precalculate(what='modes'))
def test_selfenergy_reflection(greens_function, smatrix):
rng = ensure_rng(4)
system = kwant.Builder()
left_lead = kwant.Builder(kwant.TranslationalSymmetry((-1,)))
for b, site in [(system, chain(0)), (system, chain(1)),
(left_lead, chain(0))]:
h = rng.random_sample((n, n)) + 1j * rng.random_sample((n, n))
h += h.conjugate().transpose()
b[site] = h
for b, hopp in [(system, (chain(0), chain(1))),
(left_lead, (chain(0), chain(1)))]:
b[hopp] = (10 * rng.random_sample((n, n)) +
1j * rng.random_sample((n, n)))
system.attach_lead(left_lead)
def test_selfenergy_lead(
twolead_builder, smatrix, greens_function, wave_function, ldos
):
fsyst = twolead_builder.finalized()
fsyst_se = twolead_builder.finalized()
fsyst_se.leads[0] = LeadWithOnlySelfEnergy(fsyst.leads[0])
# We cannot ask for the smatrix between selfenergy leads
assert pytest.raises(ValueError, smatrix, fsyst_se)
assert pytest.raises(ValueError, smatrix, fsyst_se, in_leads=[0])
assert pytest.raises(ValueError, smatrix, fsyst_se, out_leads=[0])
# The subblocks of the smatrix between leads that have modes
# *should* be the same.
kwargs = dict(energy=0, in_leads=[1], out_leads=[1])
assert_almost_equal(
smatrix(fsyst_se, **kwargs).data,
smatrix(fsyst, **kwargs).data,
)
psi_se = wave_function(fsyst_se, energy=0)
psi = wave_function(fsyst, energy=0)
# Scattering wavefunctions originating in the non-selfenergy
# lead should be identical.
assert_almost_equal(psi_se(1), psi(1))
# We cannot define scattering wavefunctions originating from
# a selfenergy lead.
assert pytest.raises(ValueError, psi_se, 0)
# We could in principle calculate the ldos using a mixed
# approach where some leads are specified by selfenergies
# and others by modes, but this is not done for now.
assert pytest.raises(NotImplementedError, ldos, fsyst_se, energy=0)
def test_selfenergy_reflection(twolead_builder, greens_function, smatrix):
system = twolead_builder
fsyst = system.finalized()
t = smatrix(fsyst, 0, (), [0], [0])
......@@ -577,7 +591,7 @@ def test_wavefunc_ldos_consistency(wave_function, ldos):
check(fsyst)
raises(ValueError, check, syst.precalculate(what='selfenergy'))
syst.leads[0] = LeadWithOnlySelfEnergy(syst.leads[0])
raises(NotImplementedError, check, syst)
raises(ValueError, check, syst)
# We need to keep testing 'args', but we don't want to see
......
......@@ -955,6 +955,10 @@ def is_vectorized(syst):
return isinstance(syst, (FiniteVectorizedSystem, InfiniteVectorizedSystem))
def is_selfenergy_lead(lead):
return hasattr(lead, "selfenergy") and not hasattr(lead, "modes")
def _normalize_matrix_blocks(blocks, expected_shape, *, calling_function=None):
"""Normalize a sequence of matrices into a single 3D numpy array
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment