diff --git a/doc/source/pre/whatsnew/1.4.rst b/doc/source/pre/whatsnew/1.4.rst index 44adfb8d612a1046fdc7e337634e3aa363846e28..e29c11afcf895e160b60bf18d0d8b9f66da69257 100644 --- a/doc/source/pre/whatsnew/1.4.rst +++ b/doc/source/pre/whatsnew/1.4.rst @@ -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 diff --git a/kwant/_system.pyx b/kwant/_system.pyx index 257377ac555388ce6b490946e347d7da8200217b..fcc3987c33834db8cf8ba52348234ec62e452d0e 100644 --- a/kwant/_system.pyx +++ b/kwant/_system.pyx @@ -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) diff --git a/kwant/builder.py b/kwant/builder.py index 6441083d244d7649784d121213b8f287ce3f4356..c804c3a8f07e9617f0e953f7036c81f467fbb23c 100644 --- a/kwant/builder.py +++ b/kwant/builder.py @@ -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) diff --git a/kwant/solvers/tests/_test_sparse.py b/kwant/solvers/tests/_test_sparse.py index 620b7c54d0fb05150c78b9038d460f4d1f0b8a65..8b2efa92b5da98727afea6fd3b7e27d3e36ddd4f 100644 --- a/kwant/solvers/tests/_test_sparse.py +++ b/kwant/solvers/tests/_test_sparse.py @@ -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 == () diff --git a/kwant/system.py b/kwant/system.py index 6d98cd68cf3874ef50f54e946ce7f2c64149079c..d725359764ee4e2299743a3a3dd08de2461e45e1 100644 --- a/kwant/system.py +++ b/kwant/system.py @@ -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: diff --git a/kwant/tests/test_builder.py b/kwant/tests/test_builder.py index ccb40c7c239e1cfed3dbeffc45a5b367a6ae41da..cb2cb9c7f0d5dbe968b8f4e65fc22ffbbc0e5751 100644 --- a/kwant/tests/test_builder.py +++ b/kwant/tests/test_builder.py @@ -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()