From 65e0e29bd513075be792bf0543fc689be30c0923 Mon Sep 17 00:00:00 2001
From: Joseph Weston <joseph@weston.cloud>
Date: Mon, 11 Feb 2019 14:30:01 +0100
Subject: [PATCH] add 'parameters' property to all system/lead implementations

Specifically: finalized Builder, PrecalculatedLead,
              ModesLead and SelfEnergyLead

This can be used to inspect the parameters on which a system depends,
and can be later used to give more informative error messages.

Adding 'parameters' to ModesLead and SelfEnergyLead is strictly a
backwards-incompatible change, however this is an advanced feature of
Kwant and we don't want to let this block adding 'parameters' to the
system API.
---
 kwant/builder.py                    | 30 +++++++++++++++++++++++++----
 kwant/solvers/tests/_test_sparse.py |  1 +
 kwant/system.py                     |  3 +++
 kwant/tests/test_builder.py         | 23 +++++++++++++++++-----
 4 files changed, 48 insertions(+), 9 deletions(-)

diff --git a/kwant/builder.py b/kwant/builder.py
index 6441083d..c804c3a8 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 620b7c54..8b2efa92 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 6d98cd68..a670d773 100644
--- a/kwant/system.py
+++ b/kwant/system.py
@@ -326,6 +326,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 ccb40c7c..2d35df20 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)
@@ -1346,18 +1349,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()
-- 
GitLab