Commit c02c1c03 authored by Joseph Weston's avatar Joseph Weston

Merge branch 'feature/system-parameters' into 'master'

add 'parameters' property to finalized systems, and use this
new API to check when users forget parameters.

See merge request !275
parents 784a7332 2cc472aa
Pipeline #15387 passed with stages
in 43 minutes and 52 seconds
......@@ -138,6 +138,38 @@ the scattering region and leads, one could do the following::
kwant.smatrix(syst, params=dict(V_dot=0, V_lead=1))
System parameters can now be inspected
--------------------------------------
In modern Kwant the preferred way to pass arguments to your models
is by *name*, using a dictionary and ``params``::
def onsite(site, magnetic_field, voltage):
return magnetic_field * sigma_z + voltage * sigma_0
def lead_onsite(site, lead_voltage):
return lead_voltage * sigma_0
syst = make_system(onsite)
syst.attach_lead(make_lead(lead_onsite))
syst = syst.finalized()
# naming the arguments makes things clear!
kwant.smatrix(syst, params=dict(magnetic_field=0.5, voltage=1,
lead_voltage=0.2))
This is a much clearer and less error prone than passing arguments by
*position* using ``args``, as was required in older versions of Kwant.
In this version of Kwant we introduce the ``parameters`` attribute of
*finalized systems*, which allows inspection of the names of the
parameters that the system (and its leads) expects::
>>> syst.parameters
frozenset({'magnetic_field', 'voltage'})
>>> syst.leads[0].parameters
frozenset({'V_lead'})
This is a provisional API that may be changed in a future version of Kwant.
Interpolated density plots
--------------------------
A new function `~kwant.plotter.density` has been added that can be used to
......
......@@ -242,6 +242,17 @@ def make_dense_full(ham, args, params, CGraph gr, diag,
return h_sub
def _check_parameters_match(expected_parameters, params):
if params is None:
params = {}
missing = set(expected_parameters) - set(params)
if missing:
msg = ('System is missing required parameters: ',
', '.join(map('"{}"'.format, missing)))
raise TypeError(''.join(msg))
@cython.embedsignature(True)
def hamiltonian_submatrix(self, args=(), to_sites=None, from_sites=None,
sparse=False, return_norb=False, *, params=None):
......@@ -287,6 +298,9 @@ def hamiltonian_submatrix(self, args=(), to_sites=None, from_sites=None,
n = self.graph.num_nodes
matrix = ta.matrix
if not args: # Then perhaps parameters
_check_parameters_match(self.parameters, params)
if from_sites is None:
diag = n * [None]
from_norb = np.empty(n, gint_dtype)
......
......@@ -632,13 +632,16 @@ class SelfEnergyLead(Lead):
Has the same signature as `selfenergy` (without the ``self``
parameter) and returns the self energy matrix for the interface sites.
interface : sequence of `Site` instances
parameters : sequence of strings
The parameters on which the lead depends.
"""
def __init__(self, selfenergy_func, interface):
def __init__(self, selfenergy_func, interface, parameters):
self.interface = tuple(interface)
# we changed the API of 'selfenergy_func' to have a keyword-only
# parameter 'params', but we still need to support the old API
# XXX: remove this when releasing Kwant 2.0
self.selfenergy_func = _ensure_signature(selfenergy_func)
self.parameters = frozenset(parameters)
def finalized(self):
"""Trivial finalization: the object is returned itself."""
......@@ -659,14 +662,16 @@ class ModesLead(Lead):
`~kwant.physics.PropagatingModes` and `~kwant.physics.StabilizedModes`.
interface :
sequence of `Site` instances
parameters : sequence of strings
The parameters on which the lead depends.
"""
def __init__(self, modes_func, interface):
def __init__(self, modes_func, interface, parameters):
self.interface = tuple(interface)
# we changed the API of 'selfenergy_func' to have a keyword-only
# parameter 'params', but we still need to support the old API
# XXX: remove this when releasing Kwant 2.0
self.modes_func = _ensure_signature(modes_func)
self.parameters = frozenset(parameters)
def finalized(self):
"""Trivial finalization: the object is returned itself."""
......@@ -2018,19 +2023,27 @@ class FiniteSystem(_FinalizedBuilderMixin, system.FiniteSystem):
hoppings = [cache(builder._get_edge(sites[tail], sites[head]))
for tail, head in g]
# System parameters are the union of the parameters
# of onsites and hoppings.
# Here 'onsites' and 'hoppings' are pairs whos second element
# is a tuple of parameter names when matrix element is a function,
# and None otherwise.
parameters = frozenset(chain.from_iterable(
p for _, p in chain(onsites, hoppings) if p))
self.graph = g
self.sites = sites
self.site_ranges = _site_ranges(sites)
self.id_by_site = id_by_site
self.hoppings = hoppings
self.onsites = onsites
self.parameters = parameters
self.symmetry = builder.symmetry
self.leads = finalized_leads
self.lead_interfaces = lead_interfaces
self.lead_paddings = lead_paddings
self._init_discrete_symmetries(builder)
def pos(self, i):
return self.sites[i].pos
......@@ -2179,12 +2192,21 @@ class InfiniteSystem(_FinalizedBuilderMixin, system.InfiniteSystem):
tail, head = sym.to_fd(tail, head)
hoppings.append(cache(builder._get_edge(tail, head)))
# System parameters are the union of the parameters
# of onsites and hoppings.
# Here 'onsites' and 'hoppings' are pairs whos second element
# is a tuple of parameter names when matrix element is a function,
# and None otherwise.
parameters = frozenset(chain.from_iterable(
p for _, p in chain(onsites, hoppings) if p))
self.graph = g
self.sites = sites
self.site_ranges = _site_ranges(sites)
self.id_by_site = id_by_site
self.hoppings = hoppings
self.onsites = onsites
self.parameters = parameters
self.symmetry = builder.symmetry
self.cell_size = cell_size
self._init_discrete_symmetries(builder)
......
......@@ -22,6 +22,7 @@ sq = square = kwant.lattice.square()
class LeadWithOnlySelfEnergy:
def __init__(self, lead):
self.lead = lead
self.parameters = frozenset()
def selfenergy(self, energy, args=(), *, params=None):
assert args == ()
......
......@@ -31,6 +31,9 @@ class System(metaclass=abc.ABCMeta):
range. In addition, the final triple should have the form
``(len(graph.num_nodes), 0, tot_norbs)`` where ``tot_norbs`` is the
total number of orbitals in the system.
parameters : frozenset of strings
The names of the parameters on which the system depends. This attribute
is provisional and may be changed in a future version of Kwant
Notes
-----
......@@ -79,7 +82,11 @@ class FiniteSystem(System, metaclass=abc.ABCMeta):
leads : sequence of leads
Each lead has to provide a method ``selfenergy`` that has
the same signature as `InfiniteSystem.selfenergy` (without the
``self`` parameter). It may also provide ``modes`` that has the
``self`` parameter), and must have property ``parameters``:
a collection of strings that name the system parameters (
though this requirement is provisional and may be removed in
a future version of Kwant).
It may also provide ``modes`` that has the
same signature as `InfiniteSystem.modes` (without the ``self``
parameter).
lead_interfaces : sequence of sequences of integers
......@@ -89,6 +96,10 @@ class FiniteSystem(System, metaclass=abc.ABCMeta):
Each sub-sequence contains the indices of the system sites
that belong to the lead, and therefore have the same onsite as the lead
sites, and are connected by the same hoppings as the lead sites.
parameters : frozenset of strings
The names of the parameters on which the system depends. This does
not include the parameters for any leads. This attribute
is provisional and may be changed in a future version of Kwant
Notes
-----
......@@ -326,6 +337,9 @@ class PrecalculatedLead:
raise ValueError("No precalculated values provided.")
self._modes = modes
self._selfenergy = selfenergy
# Modes/Self-energy have already been evaluated, so there
# is no parametric dependence anymore
self.parameters = frozenset()
def modes(self, energy=0, args=(), *, params=None):
if self._modes is not None:
......
......@@ -1088,7 +1088,7 @@ def test_ModesLead_and_SelfEnergyLead():
interface = [lat(L-1, lead.sites[i].tag[1]) for i in range(L)]
# Re-attach right lead as ModesLead.
syst.leads[1] = builder.ModesLead(lead.modes, interface)
syst.leads[1] = builder.ModesLead(lead.modes, interface, lead.parameters)
fsyst = syst.finalized()
ts2 = [kwant.smatrix(fsyst, e).transmission(1, 0) for e in energies]
assert_almost_equal(ts2, ts)
......@@ -1096,13 +1096,15 @@ def test_ModesLead_and_SelfEnergyLead():
# Re-attach right lead as ModesLead with old-style modes API
# that does not take a 'params' keyword parameter.
syst.leads[1] = builder.ModesLead(
lambda energy, args: lead.modes(energy, args), interface)
lambda energy, args: lead.modes(energy, args),
interface, lead.parameters)
fsyst = syst.finalized()
ts2 = [kwant.smatrix(fsyst, e).transmission(1, 0) for e in energies]
assert_almost_equal(ts2, ts)
# Re-attach right lead as SelfEnergyLead.
syst.leads[1] = builder.SelfEnergyLead(lead.selfenergy, interface)
syst.leads[1] = builder.SelfEnergyLead(lead.selfenergy, interface,
lead.parameters)
fsyst = syst.finalized()
ts2 = [kwant.greens_function(fsyst, e).transmission(1, 0) for e in energies]
assert_almost_equal(ts2, ts)
......@@ -1110,7 +1112,8 @@ def test_ModesLead_and_SelfEnergyLead():
# Re-attach right lead as SelfEnergyLead with old-style selfenergy API
# that does not take a 'params' keyword parameter.
syst.leads[1] = builder.SelfEnergyLead(
lambda energy, args: lead.selfenergy(energy, args), interface)
lambda energy, args: lead.selfenergy(energy, args),
interface, lead.parameters)
fsyst = syst.finalized()
ts2 = [kwant.greens_function(fsyst, e).transmission(1, 0) for e in energies]
assert_almost_equal(ts2, ts)
......@@ -1119,7 +1122,7 @@ def test_ModesLead_and_SelfEnergyLead():
# Also verifies that the selfenergy callback function can return exotic
# arraylikes.
syst.leads.append(builder.SelfEnergyLead(
lambda *args: list(ta.zeros((L, L))), interface))
lambda *args: list(ta.zeros((L, L))), interface, lead.parameters))
fsyst = syst.finalized()
ts2 = [kwant.greens_function(fsyst, e).transmission(1, 0) for e in energies]
assert_almost_equal(ts2, ts)
......@@ -1222,9 +1225,11 @@ def test_argument_passing():
with raises(TypeError):
inf_syst.hamiltonian(0, 0, *(2, 1), params=dict(p1=2, p2=1))
# test that missing any parameters raises TypeError
# Missing parameters raises TypeError
with raises(TypeError):
syst.hamiltonian(0, 0, params=dict(fake=10))
syst.hamiltonian(0, 0, params=dict(p1=2))
with raises(TypeError):
syst.hamiltonian_submatrix(params=dict(p1=2))
# test that passing parameters without default values works, and that
# passing parameters with default values fails
......@@ -1346,18 +1351,28 @@ def test_subs():
# test basic substitutions
syst = make_system()
assert syst.finalized().parameters == {'a', 'b', 'c'}
expected = hamiltonian(syst, a=1, b=2, c=3)
# 1 level of substitutions
sub_syst = syst.substituted(a='d', b='e')
assert sub_syst.finalized().parameters == {'d', 'e', 'c'}
assert np.allclose(hamiltonian(sub_syst, d=1, e=2, c=3), expected)
# 2 levels of substitution
sub_sub_syst = sub_syst.substituted(d='g', c='h')
assert np.allclose(hamiltonian(sub_sub_syst, g=1, e=2, h=3), expected)
assert sub_sub_syst.finalized().parameters == {'g', 'e', 'h'}
# very confusing but technically valid. 'a' does not appear in 'hopping',
# so the signature of 'onsite' is valid.
sub_syst = syst.substituted(a='sitea')
assert sub_syst.finalized().parameters == {'sitea', 'b', 'c'}
assert np.allclose(hamiltonian(sub_syst, sitea=1, b=2, c=3), expected)
# Check that this also works for infinite systems, as their finalization
# follows a different code path.
lead = make_system(kwant.TranslationalSymmetry((-1,)), n=1)
lead = lead.substituted(a='lead_a', b='lead_b', c='lead_c')
lead = lead.finalized()
assert lead.parameters == {'lead_a', 'lead_b', 'lead_c'}
def test_attach_stores_padding():
lat = kwant.lattice.chain()
......
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