Browse code

Merge branch 'feature/nd-systems' into 'master'

First steps in ND vectorized systems

Closes #326

See merge request kwant/kwant!341

Joseph Weston authored on 24/12/2019 13:05:36
Showing 11 changed files
... ...
@@ -23,7 +23,6 @@ Abstract base classes
23 23
 .. autosummary::
24 24
    :toctree: generated/
25 25
 
26
-   Symmetry
27 26
    Lead
28 27
 
29 28
 Functions
... ...
@@ -28,6 +28,14 @@ Sites
28 28
    SiteArray
29 29
    SiteFamily
30 30
 
31
+Symmetries
32
+----------
33
+.. autosummary::
34
+   :toctree: generated/
35
+
36
+    Symmetry
37
+    NoSymmetry
38
+
31 39
 Systems
32 40
 -------
33 41
 .. autosummary::
... ...
@@ -548,13 +548,12 @@ def vectorized_cell_hamiltonian(self, args=(), sparse=False, *, params=None):
548 548
     # Site array where next cell starts
549 549
     next_cell = bisect.bisect(site_offsets, self.cell_size) - 1
550 550
 
551
-    def inside_cell(term):
552
-        (to_which, from_which), _= self.subgraphs[term.subgraph]
553
-        return to_which < next_cell and from_which < next_cell
551
+    def inside_fd(term):
552
+        return all(s == 0 for s in term.symmetry_element)
554 553
 
555 554
     cell_terms = [
556 555
         n for n, t in enumerate(self.terms)
557
-        if inside_cell(t)
556
+        if inside_fd(t)
558 557
     ]
559 558
 
560 559
     subgraphs = [
... ...
@@ -593,34 +592,38 @@ def vectorized_cell_hamiltonian(self, args=(), sparse=False, *, params=None):
593 592
 def vectorized_inter_cell_hopping(self, args=(), sparse=False, *, params=None):
594 593
     """Hopping Hamiltonian between two cells of the infinite system.
595 594
 
595
+    This method returns a complex matrix that represents the hopping from
596
+    the *interface sites* of unit cell ``n - 1`` to *all* the sites of
597
+    unit cell ``n``. It is therefore generally a *rectangular* matrix of
598
+    shape ``(N_uc, N_iface)`` where ``N_uc`` is the number of orbitals
599
+    in the unit cell, and ``N_iface`` is the number of orbitals on the
600
+    *interface* sites (i.e. the sites with hoppings *to* the next unit cell).
601
+
596 602
     Providing positional arguments via 'args' is deprecated,
597 603
     instead, provide named parameters as a dictionary via 'params'.
598 604
     """
599 605
 
600
-    # Take advantage of the fact that there are distinct entries in
601
-    # onsite_terms for sites inside the unit cell, and distinct entries
602
-    # in onsite_terms for hoppings between the unit cell sites and
603
-    # interface sites. This way we only need to check the first entry
604
-    # in onsite/hopping_terms
605
-
606 606
     site_offsets = np.cumsum([0] + [len(arr) for arr in self.site_arrays])
607
-    # Site array where next cell starts
608
-    next_cell = bisect.bisect(site_offsets, self.cell_size) - 1
609 607
 
608
+    # This method is only meaningful for systems with a 1D translational
609
+    # symmetry, and we use this fact in several places
610
+    assert all(len(t.symmetry_element) == 1 for t in self.terms)
611
+
612
+    # Symmetry element -1 means hoppings *from* the *previous*
613
+    # unit cell. These are directly the hoppings we wish to return.
610 614
     inter_cell_hopping_terms = [
611 615
         n for n, t in enumerate(self.terms)
612
-        # *from* site is in interface
613
-        if (self.subgraphs[t.subgraph][0][1] >= next_cell
614
-            and self.subgraphs[t.subgraph][0][0] < next_cell)
616
+        if t.symmetry_element[0] == -1
615 617
     ]
618
+    # Symmetry element +1 means hoppings *from* the *next* unit cell.
619
+    # These are related by translational symmetry to hoppings *to*
620
+    # the *previous* unit cell. We therefore need the *reverse*
621
+    # (and conjugate) of these hoppings.
616 622
     reversed_inter_cell_hopping_terms = [
617 623
         n for n, t in enumerate(self.terms)
618
-        # *to* site is in interface
619
-        if (self.subgraphs[t.subgraph][0][0] >= next_cell
620
-            and self.subgraphs[t.subgraph][0][1] < next_cell)
624
+        if t.symmetry_element[0] == +1
621 625
     ]
622 626
 
623
-    # Evaluate inter-cell hoppings only
624 627
     inter_cell_hams = [
625 628
         self.hamiltonian_term(n, args=args, params=params)
626 629
         for n in inter_cell_hopping_terms
... ...
@@ -642,18 +645,21 @@ def vectorized_inter_cell_hopping(self, args=(), sparse=False, *, params=None):
642 645
         for n in reversed_inter_cell_hopping_terms
643 646
     ]
644 647
 
645
-    # All the 'from' sites are in the previous domain, but to build a
646
-    # matrix we need to get the equivalent sites in the fundamental domain.
647
-    # We set the 'from' site array to the one from the fundamental domain.
648
-    subgraphs = [
649
-        ((to_sa, from_sa - next_cell), (to_off, from_off))
650
-        for (to_sa, from_sa), (to_off, from_off) in subgraphs
651
-    ]
652
-
653 648
     _, norbs, orb_offsets = self.site_ranges.transpose()
654 649
 
655
-    shape = (orb_offsets[next_cell],
656
-             orb_offsets[len(self.site_arrays) - next_cell])
650
+    # SiteArrays containing interface sites appear before SiteArrays
651
+    # containing non-interface sites, so the max of the site array
652
+    # indices that appear in the 'from' site arrays of the inter-cell
653
+    # hoppings allows us to get the number of interface orbitals.
654
+    last_iface_site_array = max(
655
+        from_site_array for (_, from_site_array), _ in subgraphs
656
+    )
657
+    iface_norbs = orb_offsets[last_iface_site_array + 1]
658
+    fd_norbs = orb_offsets[-1]
659
+
660
+    # TODO: return a square matrix when we no longer need to maintain
661
+    #       backwards compatibility with unvectorized systems.
662
+    shape = (fd_norbs, iface_norbs)
657 663
     func = _vectorized_make_sparse if sparse else _vectorized_make_dense
658 664
     mat = func(subgraphs, hams, norbs, orb_offsets, site_offsets, shape=shape)
659 665
     return mat
... ...
@@ -21,7 +21,7 @@ import tinyarray as ta
21 21
 import numpy as np
22 22
 from scipy import sparse
23 23
 from . import system, graph, KwantDeprecationWarning, UserCodeError
24
-from .system import Site, SiteArray, SiteFamily
24
+from .system import Site, SiteArray, SiteFamily, Symmetry, NoSymmetry
25 25
 from .linalg import lll
26 26
 from .operator import Density
27 27
 from .physics import DiscreteSymmetry, magnetic_gauge
... ...
@@ -29,7 +29,7 @@ from ._common import (ensure_isinstance, get_parameters, reraise_warnings,
29 29
                       interleave, deprecate_args, memoize)
30 30
 
31 31
 
32
-__all__ = ['Builder', 'Symmetry', 'HoppingKind', 'Lead',
32
+__all__ = ['Builder', 'HoppingKind', 'Lead',
33 33
            'BuilderLead', 'SelfEnergyLead', 'ModesLead', 'add_peierls_phase']
34 34
 
35 35
 
... ...
@@ -64,182 +64,6 @@ def validate_hopping(hopping):
64 64
         raise ValueError("A hopping connects the following site to itself:\n"
65 65
                          "{0}".format(a))
66 66
 
67
-
68
-
69
-################ Symmetries
70
-
71
-class Symmetry(metaclass=abc.ABCMeta):
72
-    """Abstract base class for spatial symmetries.
73
-
74
-    Many physical systems possess a discrete spatial symmetry, which results in
75
-    special properties of these systems.  This class is the standard way to
76
-    describe discrete spatial symmetries in Kwant.  An instance of this class
77
-    can be passed to a `Builder` instance at its creation.  The most important
78
-    kind of symmetry is translational symmetry, used to define scattering
79
-    leads.
80
-
81
-    Each symmetry has a fundamental domain -- a set of sites and hoppings,
82
-    generating all the possible sites and hoppings upon action of symmetry
83
-    group elements.  A class derived from `Symmetry` has to implement mapping
84
-    of any site or hopping into the fundamental domain, applying a symmetry
85
-    group element to a site or a hopping, and a method `which` to determine the
86
-    group element bringing some site from the fundamental domain to the
87
-    requested one.  Additionally, it has to have a property `num_directions`
88
-    returning the number of independent symmetry group generators (number of
89
-    elementary periods for translational symmetry).
90
-
91
-    A ``ValueError`` must be raised by the symmetry class whenever a symmetry
92
-    is used together with sites whose site family is not compatible with it.  A
93
-    typical example of this is when the vector defining a translational
94
-    symmetry is not a lattice vector.
95
-
96
-    The type of the domain objects as handled by the methods of this class is
97
-    not specified.  The only requirement is that it must support the unary
98
-    minus operation.  The reference implementation of `to_fd()` is hence
99
-    `self.act(-self.which(a), a, b)`.
100
-    """
101
-
102
-    @abc.abstractproperty
103
-    def num_directions(self):
104
-        """Number of elementary periods of the symmetry."""
105
-        pass
106
-
107
-    @abc.abstractmethod
108
-    def which(self, site):
109
-        """Calculate the domain of the site.
110
-
111
-        Parameters
112
-        ----------
113
-        site : `~kwant.system.Site` or `~kwant.system.SiteArray`
114
-
115
-        Returns
116
-        -------
117
-        group_element : tuple or sequence of tuples
118
-            A single tuple if ``site`` is a Site, or a sequence of tuples if
119
-            ``site`` is a SiteArray.  The group element(s) whose action
120
-            on a certain site(s) from the fundamental domain will result
121
-            in the given ``site``.
122
-        """
123
-        pass
124
-
125
-    @abc.abstractmethod
126
-    def act(self, element, a, b=None):
127
-        """Act with symmetry group element(s) on site(s) or hopping(s).
128
-
129
-        Parameters
130
-        ----------
131
-        element : tuple or sequence of tuples
132
-            Group element(s) with which to act on the provided site(s)
133
-            or hopping(s)
134
-        a, b : `~kwant.system.Site` or `~kwant.system.SiteArray`
135
-            If Site then ``element`` is a single tuple, if SiteArray then
136
-            ``element`` is a sequence of tuples. If only ``a`` is provided then
137
-            ``element`` acts on the site(s) of ``a``. If ``b`` is also provided
138
-            then ``element`` acts on the hopping(s) ``(a, b)``.
139
-        """
140
-        pass
141
-
142
-    def to_fd(self, a, b=None):
143
-        """Map a site or hopping to the fundamental domain.
144
-
145
-        Parameters
146
-        ----------
147
-        a, b : `~kwant.system.Site` or `~kwant.system.SiteArray`
148
-
149
-        If ``b`` is None, return a site equivalent to ``a`` within the
150
-        fundamental domain.  Otherwise, return a hopping equivalent to ``(a,
151
-        b)`` but where the first element belongs to the fundamental domain.
152
-
153
-        Equivalent to `self.act(-self.which(a), a, b)`.
154
-        """
155
-        return self.act(-self.which(a), a, b)
156
-
157
-    def in_fd(self, site):
158
-        """Tell whether ``site`` lies within the fundamental domain.
159
-
160
-        Parameters
161
-        ----------
162
-        site : `~kwant.system.Site` or `~kwant.system.SiteArray`
163
-
164
-        Returns
165
-        -------
166
-        in_fd : bool or sequence of bool
167
-            single bool if ``site`` is a Site, or a sequence of
168
-            bool if ``site`` is a SiteArray. In the latter case
169
-            we return whether each site in the SiteArray is in
170
-            the fundamental domain.
171
-        """
172
-        if isinstance(site, Site):
173
-            for d in self.which(site):
174
-                if d != 0:
175
-                    return False
176
-            return True
177
-        elif isinstance(site, SiteArray):
178
-            which = self.which(site)
179
-            return np.logical_and.reduce(which != 0, axis=1)
180
-        else:
181
-            raise TypeError("'site' must be a Site or SiteArray")
182
-
183
-    @abc.abstractmethod
184
-    def subgroup(self, *generators):
185
-        """Return the subgroup generated by a sequence of group elements."""
186
-        pass
187
-
188
-    @abc.abstractmethod
189
-    def has_subgroup(self, other):
190
-        """Test whether `self` has the subgroup `other`...
191
-
192
-        or, in other words, whether `other` is a subgroup of `self`.  The
193
-        reason why this is the abstract method (and not `is_subgroup`) is that
194
-        in general it's not possible for a subgroup to know its supergroups.
195
-
196
-        """
197
-        pass
198
-
199
-
200
-class NoSymmetry(Symmetry):
201
-    """A symmetry with a trivial symmetry group."""
202
-
203
-    def __eq__(self, other):
204
-        return isinstance(other, NoSymmetry)
205
-
206
-    def __ne__(self, other):
207
-        return not self.__eq__(other)
208
-
209
-    def __repr__(self):
210
-        return 'NoSymmetry()'
211
-
212
-    @property
213
-    def num_directions(self):
214
-        return 0
215
-
216
-    periods = ()
217
-
218
-    _empty_array = ta.array((), int)
219
-
220
-    def which(self, site):
221
-        return self._empty_array
222
-
223
-    def act(self, element, a, b=None):
224
-        if element:
225
-            raise ValueError('`element` must be empty for NoSymmetry.')
226
-        return a if b is None else (a, b)
227
-
228
-    def to_fd(self, a, b=None):
229
-        return a if b is None else (a, b)
230
-
231
-    def in_fd(self, site):
232
-        return True
233
-
234
-    def subgroup(self, *generators):
235
-        if any(generators):
236
-            raise ValueError('Generators must be empty for NoSymmetry.')
237
-        return NoSymmetry(generators)
238
-
239
-    def has_subgroup(self, other):
240
-        return isinstance(other, NoSymmetry)
241
-
242
-
243 67
 
244 68
 ################ Hopping kinds
245 69
 
... ...
@@ -658,7 +482,7 @@ class Builder:
658 482
 
659 483
     Parameters
660 484
     ----------
661
-    symmetry : `Symmetry` or `None`
485
+    symmetry : `~kwant.system.Symmetry` or `None`
662 486
         The spatial symmetry of the system.
663 487
     conservation_law : 2D array, dictionary, function, or `None`
664 488
         An onsite operator with integer eigenvalues that commutes with the
... ...
@@ -707,8 +531,8 @@ class Builder:
707 531
     that if ``builder[a, b]`` has been set, there is no need to set
708 532
     ``builder[b, a]``.
709 533
 
710
-    Builder instances can be made to automatically respect a `Symmetry` that is
711
-    passed to them during creation.  The behavior of builders with a symmetry
534
+    Builder instances can be made to automatically respect a `~kwant.system.Symmetry`
535
+    that is passed to them during creation.  The behavior of builders with a symmetry
712 536
     is slightly more sophisticated: all keys are mapped to the fundamental
713 537
     domain of the symmetry before storing them.  This may produce confusing
714 538
     results when neighbors of a site are queried.
... ...
@@ -1657,7 +1481,7 @@ class Builder:
1657 1481
         system to be returned.
1658 1482
 
1659 1483
         Currently, only Builder instances without or with a 1D translational
1660
-        `Symmetry` can be finalized.
1484
+        `~kwant.system.Symmetry` can be finalized.
1661 1485
         """
1662 1486
         if self.symmetry.num_directions == 0:
1663 1487
             if self.vectorize:
... ...
@@ -2025,7 +1849,7 @@ class _VectorizedFinalizedBuilderMixin(_FinalizedBuilderMixin):
2025 1849
             "elements at once using 'hamiltonian_term'.",
2026 1850
             KwantDeprecationWarning
2027 1851
         )
2028
-        site_offsets = np.cumsum([0] + [len(s) for s in self.site_arrays])
1852
+        site_offsets, _, _ = self.site_ranges.transpose()
2029 1853
         if i == j:
2030 1854
             which_term = self._onsite_term_by_site_id[i]
2031 1855
             (w, _), (off, _) = self.subgraphs[self.terms[which_term].subgraph]
... ...
@@ -2036,6 +1860,15 @@ class _VectorizedFinalizedBuilderMixin(_FinalizedBuilderMixin):
2036 1860
             return onsite[0]
2037 1861
         else:
2038 1862
             edge_id = self.graph.first_edge_id(i, j)
1863
+            # Map sites in previous cell to fundamental domain; vectorized
1864
+            # infinite systems only store sites in the FD. We already know
1865
+            # that this hopping is between unit cells because this is encoded
1866
+            # in the edge_id and term id.
1867
+            # Using 'is_infinite' is a bit of a hack, but 'hamiltonian' is
1868
+            # deprecated anyway, and refactoring everything to avoid this check
1869
+            # is not worth it.
1870
+            if system.is_infinite(self):
1871
+                i, j = i % self.cell_size, j % self.cell_size
2039 1872
             which_term = self._hopping_term_by_edge_id[edge_id]
2040 1873
             herm_conj = which_term < 0
2041 1874
             if herm_conj:
... ...
@@ -2087,10 +1920,15 @@ class _VectorizedFinalizedBuilderMixin(_FinalizedBuilderMixin):
2087 1920
         to_family = self.site_arrays[to_which].family
2088 1921
         to_tags = self.site_arrays[to_which].tags
2089 1922
         to_site_array = SiteArray(to_family, to_tags[to_off])
1923
+        site_arrays = (to_site_array,)
2090 1924
         if not is_onsite:
2091 1925
             from_family = self.site_arrays[from_which].family
2092 1926
             from_tags = self.site_arrays[from_which].tags
2093
-            from_site_array = SiteArray(from_family, from_tags[from_off])
1927
+            from_site_array = self.symmetry.act(
1928
+                term.symmetry_element,
1929
+                SiteArray(from_family, from_tags[from_off])
1930
+            )
1931
+            site_arrays = (to_site_array, from_site_array)
2094 1932
 
2095 1933
         # Construct args from params
2096 1934
         if params:
... ...
@@ -2107,20 +1945,10 @@ class _VectorizedFinalizedBuilderMixin(_FinalizedBuilderMixin):
2107 1945
                        ', '.join(map('"{}"'.format, missing)))
2108 1946
                 raise TypeError(''.join(msg))
2109 1947
 
2110
-        if is_onsite:
2111
-            try:
2112
-                ham = val(to_site_array, *args)
2113
-            except Exception as exc:
2114
-                _raise_user_error(exc, val)
2115
-        else:
2116
-            try:
2117
-                ham = val(
2118
-                        *self.symmetry.to_fd(
2119
-                            to_site_array,
2120
-                            from_site_array),
2121
-                        *args)
2122
-            except Exception as exc:
2123
-                _raise_user_error(exc, val)
1948
+        try:
1949
+            ham = val(*site_arrays, *args)
1950
+        except Exception as exc:
1951
+            _raise_user_error(exc, val)
2124 1952
 
2125 1953
         expected_shape = (
2126 1954
             len(to_site_array),
... ...
@@ -2250,8 +2078,8 @@ class FiniteSystem(_FinalizedBuilderMixin, system.FiniteSystem):
2250 2078
 
2251 2079
         graph = _make_graph(builder.H, id_by_site)
2252 2080
 
2253
-        finalized_leads, lead_interfaces, lead_paddings =\
2254
-            _finalize_leads(builder.leads, id_by_site)
2081
+        finalized_leads, lead_interfaces, lead_paddings = _finalize_leads(
2082
+            builder.leads, id_by_site)
2255 2083
 
2256 2084
         # Because many onsites/hoppings share the same (value, parameter)
2257 2085
         # pairs, we keep them in a cache so that we only store a given pair
... ...
@@ -2485,9 +2313,15 @@ def _make_onsite_terms(builder, sites, site_arrays, term_offset):
2485 2313
         err if isinstance(err, Exception) else None
2486 2314
         for err in onsite_term_parameters
2487 2315
     ]
2316
+
2317
+    # We store sites in the fundamental domain, so the symmetry element
2318
+    # is the same for all onsite terms; it's just the identity.
2319
+    identity = ta.array([0] * builder.symmetry.num_directions)
2320
+
2488 2321
     onsite_terms = [
2489 2322
         system.Term(
2490 2323
             subgraph=term_offset + i,
2324
+            symmetry_element=identity,
2491 2325
             hermitian=False,
2492 2326
             parameters=(
2493 2327
                 params if not isinstance(params, Exception) else None
... ...
@@ -2511,6 +2345,11 @@ def _make_hopping_terms(builder, graph, sites, site_arrays, cell_size, term_offs
2511 2345
     #   Maps hopping edge IDs to the number of the term that the hopping
2512 2346
     #   is a part of. For Hermitian conjugate hoppings "-term_number -1"
2513 2347
     #   is stored instead.
2348
+    #
2349
+    # NOTE: 'graph' contains site indices >= cell_size, however 'sites'
2350
+    #       and 'site_arrays' only contain information about the sites
2351
+    #       in the fundamental domain.
2352
+    sym = builder.symmetry
2514 2353
 
2515 2354
     site_offsets = np.cumsum([0] + [len(sa) for sa in site_arrays])
2516 2355
 
... ...
@@ -2523,11 +2362,27 @@ def _make_hopping_terms(builder, graph, sites, site_arrays, cell_size, term_offs
2523 2362
     hopping_to_term_nr = collections.OrderedDict()
2524 2363
     _hopping_term_by_edge_id = []
2525 2364
     for tail, head in graph:
2526
-        tail_site, head_site = sites[tail], sites[head]
2527
-        if tail >= cell_size:
2528
-            # The tail belongs to the previous domain.  Find the
2529
-            # corresponding hopping with the tail in the fund. domain.
2530
-            tail_site, head_site = builder.symmetry.to_fd(tail_site, head_site)
2365
+        # map 'tail' and 'head' so that they are both in the FD, and
2366
+        # assign the symmetry element to apply to 'sites[head]'
2367
+        # to make the actual hopping. Here we assume that the symmetry
2368
+        # may only be NoSymmetry or 1D TranslationalSymmetry.
2369
+        if isinstance(sym, NoSymmetry):
2370
+            tail_site = sites[tail]
2371
+            head_site = sites[head]
2372
+            sym_el = ta.array([])
2373
+        else:
2374
+            if tail < cell_size and head < cell_size:
2375
+                sym_el = ta.array([0])
2376
+            elif head >= cell_size:
2377
+                assert tail < cell_size
2378
+                head = head - cell_size
2379
+                sym_el = ta.array([-1])
2380
+            else:
2381
+                tail = tail - cell_size
2382
+                sym_el = ta.array([1])
2383
+            tail_site = sites[tail]
2384
+            head_site = sym.act(sym_el, sites[head])
2385
+
2531 2386
         val = builder._get_edge(tail_site, head_site)
2532 2387
         herm_conj = val is Other
2533 2388
         if herm_conj:
... ...
@@ -2538,11 +2393,11 @@ def _make_hopping_terms(builder, graph, sites, site_arrays, cell_size, term_offs
2538 2393
         head_site_array = bisect.bisect(site_offsets, head) - 1
2539 2394
         # "key" uniquely identifies a hopping term.
2540 2395
         if const_val:
2541
-            key = (None, tail_site_array, head_site_array)
2396
+            key = (None, sym_el, tail_site_array, head_site_array)
2542 2397
         else:
2543
-            key = (id(val), tail_site_array, head_site_array)
2398
+            key = (id(val), sym_el, tail_site_array, head_site_array)
2544 2399
         if herm_conj:
2545
-            key = (key[0], head_site_array, tail_site_array)
2400
+            key = (key[0], -sym_el, head_site_array, tail_site_array)
2546 2401
 
2547 2402
         if key not in hopping_to_term_nr:
2548 2403
             # start a new term
... ...
@@ -2584,7 +2439,7 @@ def _make_hopping_terms(builder, graph, sites, site_arrays, cell_size, term_offs
2584 2439
                         site_arrays[from_sa].family.norbs,
2585 2440
                     ),
2586 2441
                 )
2587
-            for (_, to_sa, from_sa), val in
2442
+            for (_, _, to_sa, from_sa), val in
2588 2443
             zip(hopping_to_term_nr.keys(), hopping_term_values)
2589 2444
         ]
2590 2445
         # Sort the hoppings in each term, and also sort the values
... ...
@@ -2595,8 +2450,8 @@ def _make_hopping_terms(builder, graph, sites, site_arrays, cell_size, term_offs
2595 2450
             zip(hopping_subgraphs, hopping_term_values)))
2596 2451
     # Store site array index and site offsets rather than sites themselves
2597 2452
     tmp = []
2598
-    for (_, tail_which, head_which), h in zip(hopping_to_term_nr,
2599
-                                              hopping_subgraphs):
2453
+    for (_, _, tail_which, head_which), h in zip(hopping_to_term_nr,
2454
+                                                 hopping_subgraphs):
2600 2455
         start = (site_offsets[tail_which], site_offsets[head_which])
2601 2456
         # Transpose to get a pair of arrays rather than array of pairs
2602 2457
         # We use the fact that the underlying array is stored in
... ...
@@ -2613,15 +2468,20 @@ def _make_hopping_terms(builder, graph, sites, site_arrays, cell_size, term_offs
2613 2468
         err if isinstance(err, Exception) else None
2614 2469
         for err in hopping_term_parameters
2615 2470
     ]
2471
+    term_symmetry_elements = [
2472
+        sym_el for (_, sym_el, *_) in hopping_to_term_nr.keys()
2473
+    ]
2616 2474
     hopping_terms = [
2617 2475
         system.Term(
2618 2476
             subgraph=term_offset + i,
2477
+            symmetry_element=sym_el,
2619 2478
             hermitian=True,  # Builders are always Hermitian
2620 2479
             parameters=(
2621 2480
                 params if not isinstance(params, Exception) else None
2622 2481
             ),
2623 2482
         )
2624
-        for i, params in enumerate(hopping_term_parameters)
2483
+        for i, (sym_el, params) in
2484
+        enumerate(zip(term_symmetry_elements, hopping_term_parameters))
2625 2485
     ]
2626 2486
     _hopping_term_by_edge_id = np.array(_hopping_term_by_edge_id)
2627 2487
 
... ...
@@ -2662,21 +2522,21 @@ class FiniteVectorizedSystem(_VectorizedFinalizedBuilderMixin, system.FiniteVect
2662 2522
 
2663 2523
         graph = _make_graph(builder.H, id_by_site)
2664 2524
 
2665
-        finalized_leads, lead_interfaces, lead_paddings =\
2666
-            _finalize_leads(builder.leads, id_by_site)
2525
+        finalized_leads, lead_interfaces, lead_paddings = _finalize_leads(
2526
+            builder.leads, id_by_site)
2667 2527
 
2668 2528
         del id_by_site  # cleanup due to large size
2669 2529
 
2670 2530
         site_arrays = _make_site_arrays(builder.H)
2671 2531
 
2672 2532
         (onsite_subgraphs, onsite_terms, onsite_term_values,
2673
-         onsite_term_errors, _onsite_term_by_site_id) =\
2674
-            _make_onsite_terms(builder, sites, site_arrays, term_offset=0)
2533
+         onsite_term_errors, _onsite_term_by_site_id) = _make_onsite_terms(
2534
+             builder, sites, site_arrays, term_offset=0)
2675 2535
 
2676 2536
         (hopping_subgraphs, hopping_terms, hopping_term_values,
2677
-         hopping_term_errors, _hopping_term_by_edge_id) =\
2678
-            _make_hopping_terms(builder, graph, sites, site_arrays,
2679
-                                len(sites), term_offset=len(onsite_terms))
2537
+         hopping_term_errors, _hopping_term_by_edge_id) = _make_hopping_terms(
2538
+             builder, graph, sites, site_arrays, len(sites),
2539
+             term_offset=len(onsite_terms))
2680 2540
 
2681 2541
         # Construct the combined onsite/hopping term datastructures
2682 2542
         subgraphs = tuple(onsite_subgraphs) + tuple(hopping_subgraphs)
... ...
@@ -2834,8 +2694,8 @@ class InfiniteSystem(_FinalizedBuilderMixin, system.InfiniteSystem):
2834 2694
         sym = builder.symmetry
2835 2695
         assert sym.num_directions == 1
2836 2696
 
2837
-        lsites_with, lsites_without, interface =\
2838
-            _make_lead_sites(builder, interface_order)
2697
+        lsites_with, lsites_without, interface = _make_lead_sites(
2698
+            builder, interface_order)
2839 2699
         cell_size = len(lsites_with) + len(lsites_without)
2840 2700
 
2841 2701
         # we previously sorted the interface, so don't sort it again
... ...
@@ -2943,12 +2803,13 @@ class InfiniteVectorizedSystem(_VectorizedFinalizedBuilderMixin, system.Infinite
2943 2803
         assert sym.num_directions == 1
2944 2804
         assert builder.vectorize
2945 2805
 
2946
-        lsites_with, lsites_without, interface =\
2947
-            _make_lead_sites(builder, interface_order)
2806
+        lsites_with, lsites_without, interface = _make_lead_sites(
2807
+            builder, interface_order)
2948 2808
         cell_size = len(lsites_with) + len(lsites_without)
2949 2809
 
2950 2810
 
2951
-        sites = lsites_with + lsites_without + interface
2811
+        fd_sites = lsites_with + lsites_without
2812
+        sites = fd_sites + interface
2952 2813
         id_by_site = {}
2953 2814
         for site_id, site in enumerate(sites):
2954 2815
             id_by_site[site] = site_id
... ...
@@ -2967,25 +2828,24 @@ class InfiniteVectorizedSystem(_VectorizedFinalizedBuilderMixin, system.Infinite
2967 2828
         del id_by_site  # cleanup due to large size
2968 2829
 
2969 2830
         # In order to conform to the kwant.system.InfiniteVectorizedSystem
2970
-        # interface we need to put the sites that connect to the previous
2971
-        # cell *first*, then the sites that do not couple to the previous
2972
-        # cell, then the sites in the previous cell. Because sites in
2973
-        # a SiteArray are sorted by tag this means that the sites in these
2974
-        # 3 different sets need to be in different SiteArrays.
2831
+        # interface we need to put the interface sites (which have hoppings
2832
+        # to the next unit cell) before non-interface sites.
2833
+        # Note that we do not *store* the interface sites of the previous
2834
+        # unit cell, but we still need to *handle* site indices >= cell_size
2835
+        # because those are used e.g. in the graph.
2975 2836
         site_arrays = (
2976 2837
             _make_site_arrays(lsites_with)
2977 2838
             + _make_site_arrays(lsites_without)
2978
-            + _make_site_arrays(interface)
2979 2839
         )
2980 2840
 
2981 2841
         (onsite_subgraphs, onsite_terms, onsite_term_values,
2982
-         onsite_term_errors, _onsite_term_by_site_id) =\
2983
-            _make_onsite_terms(builder, sites, site_arrays, term_offset=0)
2842
+         onsite_term_errors, _onsite_term_by_site_id) = _make_onsite_terms(
2843
+             builder, fd_sites, site_arrays, term_offset=0)
2984 2844
 
2985 2845
         (hopping_subgraphs, hopping_terms, hopping_term_values,
2986
-         hopping_term_errors, _hopping_term_by_edge_id) =\
2987
-            _make_hopping_terms(builder, graph, sites, site_arrays,
2988
-                                cell_size, term_offset=len(onsite_terms))
2846
+         hopping_term_errors, _hopping_term_by_edge_id) = _make_hopping_terms(
2847
+             builder, graph, fd_sites, site_arrays, cell_size,
2848
+             term_offset=len(onsite_terms))
2989 2849
 
2990 2850
         # Construct the combined onsite/hopping term datastructures
2991 2851
         subgraphs = tuple(onsite_subgraphs) + tuple(hopping_subgraphs)
... ...
@@ -3001,8 +2861,12 @@ class InfiniteVectorizedSystem(_VectorizedFinalizedBuilderMixin, system.Infinite
3001 2861
         parameters = frozenset(parameters)
3002 2862
 
3003 2863
         self.site_arrays = site_arrays
3004
-        self.sites = _Sites(self.site_arrays)
3005
-        self.id_by_site = _IdBySite(self.site_arrays)
2864
+        # 'sites' and 'id_by_site' have to be backwards compatible with the
2865
+        # unvectorized interface, so we have to be able to index the interface
2866
+        # sites in the previous unit cell also
2867
+        _extended_site_arrays = self.site_arrays + _make_site_arrays(interface)
2868
+        self.sites = _Sites(_extended_site_arrays)
2869
+        self.id_by_site = _IdBySite(_extended_site_arrays)
3006 2870
         self.graph = graph
3007 2871
         self.subgraphs = subgraphs
3008 2872
         self.terms = terms
... ...
@@ -18,6 +18,7 @@ import sympy
18 18
 
19 19
 import kwant.lattice
20 20
 import kwant.builder
21
+import kwant.system
21 22
 
22 23
 import kwant.continuum
23 24
 import kwant.continuum._common
... ...
@@ -144,7 +145,7 @@ def discretize_landau(hamiltonian, N, momenta=None, grid_spacing=1):
144 145
     if _has_coordinate(normal_coordinate, hamiltonian):
145 146
         sym = kwant.lattice.TranslationalSymmetry([grid_spacing, 0])
146 147
     else:
147
-        sym = kwant.builder.NoSymmetry()
148
+        sym = kwant.system.NoSymmetry()
148 149
     lat = LandauLattice(grid_spacing, norbs=norbs)
149 150
     syst = kwant.Builder(sym)
150 151
 
... ...
@@ -13,8 +13,8 @@ import sympy
13 13
 import pytest
14 14
 import itertools
15 15
 
16
-import kwant.builder
17 16
 import kwant.lattice
17
+import kwant.system
18 18
 
19 19
 from .._common import position_operators, momentum_operators, sympify
20 20
 from ..landau_levels import (
... ...
@@ -118,7 +118,7 @@ def test_discretize_landau():
118 118
     # test a basic Hamiltonian with no normal coordinate dependence
119 119
     syst = discretize_landau("k_x**2 + k_y**2", N=n_levels)
120 120
     lat = LandauLattice(1, norbs=1)
121
-    assert isinstance(syst.symmetry, kwant.builder.NoSymmetry)
121
+    assert isinstance(syst.symmetry, kwant.system.NoSymmetry)
122 122
     syst = syst.finalized()
123 123
     assert set(syst.sites) == {lat(0, j) for j in range(n_levels)}
124 124
     assert np.allclose(
... ...
@@ -153,7 +153,7 @@ class Polyatomic:
153 153
         algorithm finds and yields all the lattice sites inside the specified
154 154
         shape starting from the specified position.
155 155
 
156
-        A `~kwant.builder.Symmetry` or `~kwant.builder.Builder` may be passed as
156
+        A `~kwant.system.Symmetry` or `~kwant.builder.Builder` may be passed as
157 157
         sole argument when calling the function returned by this method.  This
158 158
         will restrict the flood-fill to the fundamental domain of the symmetry
159 159
         (or the builder's symmetry).  Note that unless the shape function has
... ...
@@ -174,8 +174,8 @@ class Polyatomic:
174 174
             Site = system.Site
175 175
 
176 176
             if symmetry is None:
177
-                symmetry = builder.NoSymmetry()
178
-            elif not isinstance(symmetry, builder.Symmetry):
177
+                symmetry = system.NoSymmetry()
178
+            elif not isinstance(symmetry, system.Symmetry):
179 179
                 symmetry = symmetry.symmetry
180 180
 
181 181
             def fd_site(lat, tag):
... ...
@@ -259,7 +259,7 @@ class Polyatomic:
259 259
         center = ta.array(center, float)
260 260
 
261 261
         def wire_sites(sym):
262
-            if not isinstance(sym, builder.Symmetry):
262
+            if not isinstance(sym, system.Symmetry):
263 263
                 sym = sym.symmetry
264 264
             if not isinstance(sym, TranslationalSymmetry):
265 265
                 raise ValueError('wire shape only works with '
... ...
@@ -514,7 +514,7 @@ class Monatomic(system.SiteFamily, Polyatomic):
514 514
 # The following class is designed such that it should avoid floating
515 515
 # point precision issues.
516 516
 
517
-class TranslationalSymmetry(builder.Symmetry):
517
+class TranslationalSymmetry(system.Symmetry):
518 518
     """A translational symmetry defined in real space.
519 519
 
520 520
     An alias exists for this common name: ``kwant.TranslationalSymmetry``.
... ...
@@ -568,7 +568,7 @@ class TranslationalSymmetry(builder.Symmetry):
568 568
         return TranslationalSymmetry(*ta.dot(generators, self.periods))
569 569
 
570 570
     def has_subgroup(self, other):
571
-        if isinstance(other, builder.NoSymmetry):
571
+        if isinstance(other, system.NoSymmetry):
572 572
             return True
573 573
         elif not isinstance(other, TranslationalSymmetry):
574 574
             raise ValueError("Unknown symmetry type.")
... ...
@@ -718,8 +718,9 @@ class TranslationalSymmetry(builder.Symmetry):
718 718
             raise ValueError("must provide a single group element when "
719 719
                              "acting on single sites.")
720 720
         if (len(element.shape) == 1 and not is_site):
721
-            raise ValueError("must provide a sequence of group elements "
722
-                             "when acting on site arrays.")
721
+            # We can act on a whole SiteArray with a single group element
722
+            # using numpy broadcasting.
723
+            element = element.reshape(1, -1)
723 724
         m_part = self._get_site_family_data(a.family)[0]
724 725
         try:
725 726
             delta = array_mod.dot(m_part, element)
... ...
@@ -6,7 +6,8 @@ import pytest
6 6
 
7 7
 import kwant
8 8
 from ... import lattice
9
-from ...builder import HoppingKind, Builder, NoSymmetry, Site
9
+from ...builder import HoppingKind, Builder, Site
10
+from ...system import NoSymmetry
10 11
 from .. import gauge
11 12
 
12 13
 
... ...
@@ -9,7 +9,7 @@
9 9
 """Low-level interface of systems"""
10 10
 
11 11
 __all__ = [
12
-    'Site', 'SiteArray', 'SiteFamily',
12
+    'Site', 'SiteArray', 'SiteFamily', 'Symmetry', 'NoSymmetry',
13 13
     'System', 'VectorizedSystem', 'FiniteSystem', 'FiniteVectorizedSystem',
14 14
     'InfiniteSystem', 'InfiniteVectorizedSystem',
15 15
     'is_finite', 'is_infinite', 'is_vectorized',
... ...
@@ -22,6 +22,7 @@ from copy import copy
22 22
 from collections import namedtuple
23 23
 from functools import total_ordering, lru_cache
24 24
 import numpy as np
25
+import tinyarray as ta
25 26
 from . import _system
26 27
 from ._common  import deprecate_args, KwantDeprecationWarning
27 28
 
... ...
@@ -269,6 +270,180 @@ class SiteFamily:
269 270
                              'site_family((1, 2))!')
270 271
         return Site(self, tag)
271 272
 
273
+
274
+################ Symmetries
275
+
276
+class Symmetry(metaclass=abc.ABCMeta):
277
+    """Abstract base class for spatial symmetries.
278
+
279
+    Many physical systems possess a discrete spatial symmetry, which results in
280
+    special properties of these systems.  This class is the standard way to
281
+    describe discrete spatial symmetries in Kwant.  An instance of this class
282
+    can be passed to a `Builder` instance at its creation.  The most important
283
+    kind of symmetry is translational symmetry, used to define scattering
284
+    leads.
285
+
286
+    Each symmetry has a fundamental domain -- a set of sites and hoppings,
287
+    generating all the possible sites and hoppings upon action of symmetry
288
+    group elements.  A class derived from `Symmetry` has to implement mapping
289
+    of any site or hopping into the fundamental domain, applying a symmetry
290
+    group element to a site or a hopping, and a method `which` to determine the
291
+    group element bringing some site from the fundamental domain to the
292
+    requested one.  Additionally, it has to have a property `num_directions`
293
+    returning the number of independent symmetry group generators (number of
294
+    elementary periods for translational symmetry).
295
+
296
+    A ``ValueError`` must be raised by the symmetry class whenever a symmetry
297
+    is used together with sites whose site family is not compatible with it.  A
298
+    typical example of this is when the vector defining a translational
299
+    symmetry is not a lattice vector.
300
+
301
+    The type of the domain objects as handled by the methods of this class is
302
+    not specified.  The only requirement is that it must support the unary
303
+    minus operation.  The reference implementation of `to_fd()` is hence
304
+    `self.act(-self.which(a), a, b)`.
305
+    """
306
+
307
+    @abc.abstractproperty
308
+    def num_directions(self):
309
+        """Number of elementary periods of the symmetry."""
310
+        pass
311
+
312
+    @abc.abstractmethod
313
+    def which(self, site):
314
+        """Calculate the domain of the site.
315
+
316
+        Parameters
317
+        ----------
318
+        site : `~kwant.system.Site` or `~kwant.system.SiteArray`
319
+
320
+        Returns
321
+        -------
322
+        group_element : tuple or sequence of tuples
323
+            A single tuple if ``site`` is a Site, or a sequence of tuples if
324
+            ``site`` is a SiteArray.  The group element(s) whose action
325
+            on a certain site(s) from the fundamental domain will result
326
+            in the given ``site``.
327
+        """
328
+        pass
329
+
330
+    @abc.abstractmethod
331
+    def act(self, element, a, b=None):
332
+        """Act with symmetry group element(s) on site(s) or hopping(s).
333
+
334
+        Parameters
335
+        ----------
336
+        element : tuple or sequence of tuples
337
+            Group element(s) with which to act on the provided site(s)
338
+            or hopping(s)
339
+        a, b : `~kwant.system.Site` or `~kwant.system.SiteArray`
340
+            If Site then ``element`` is a single tuple, if SiteArray then
341
+            ``element`` is a single tuple or a sequence of tuples.
342
+            If only ``a`` is provided then ``element`` acts on the site(s)
343
+            of ``a``. If ``b`` is also provided then ``element`` acts
344
+            on the hopping(s) ``(a, b)``.
345
+        """
346
+        pass
347
+
348
+    def to_fd(self, a, b=None):
349
+        """Map a site or hopping to the fundamental domain.
350
+
351
+        Parameters
352
+        ----------
353
+        a, b : `~kwant.system.Site` or `~kwant.system.SiteArray`
354
+
355
+        If ``b`` is None, return a site equivalent to ``a`` within the
356
+        fundamental domain.  Otherwise, return a hopping equivalent to ``(a,
357
+        b)`` but where the first element belongs to the fundamental domain.
358
+
359
+        Equivalent to `self.act(-self.which(a), a, b)`.
360
+        """
361
+        return self.act(-self.which(a), a, b)
362
+
363
+    def in_fd(self, site):
364
+        """Tell whether ``site`` lies within the fundamental domain.
365
+
366
+        Parameters
367
+        ----------
368
+        site : `~kwant.system.Site` or `~kwant.system.SiteArray`
369
+
370
+        Returns
371
+        -------
372
+        in_fd : bool or sequence of bool
373
+            single bool if ``site`` is a Site, or a sequence of
374
+            bool if ``site`` is a SiteArray. In the latter case
375
+            we return whether each site in the SiteArray is in
376
+            the fundamental domain.
377
+        """
378
+        if isinstance(site, Site):
379
+            for d in self.which(site):
380
+                if d != 0:
381
+                    return False
382
+            return True
383
+        elif isinstance(site, SiteArray):
384
+            which = self.which(site)
385
+            return np.logical_and.reduce(which != 0, axis=1)
386
+        else:
387
+            raise TypeError("'site' must be a Site or SiteArray")
388
+
389
+    @abc.abstractmethod
390
+    def subgroup(self, *generators):
391
+        """Return the subgroup generated by a sequence of group elements."""
392
+        pass
393
+
394
+    @abc.abstractmethod
395
+    def has_subgroup(self, other):
396
+        """Test whether `self` has the subgroup `other`...
397
+
398
+        or, in other words, whether `other` is a subgroup of `self`.  The
399
+        reason why this is the abstract method (and not `is_subgroup`) is that
400
+        in general it's not possible for a subgroup to know its supergroups.
401
+
402
+        """
403
+        pass
404
+
405
+
406
+class NoSymmetry(Symmetry):
407
+    """A symmetry with a trivial symmetry group."""
408
+
409
+    def __eq__(self, other):
410
+        return isinstance(other, NoSymmetry)
411
+
412
+    def __ne__(self, other):
413
+        return not self.__eq__(other)
414
+
415
+    def __repr__(self):
416
+        return 'NoSymmetry()'
417
+
418
+    @property
419
+    def num_directions(self):
420
+        return 0
421
+
422
+    periods = ()
423
+
424
+    _empty_array = ta.array((), int)
425
+
426
+    def which(self, site):
427
+        return self._empty_array
428
+
429
+    def act(self, element, a, b=None):
430
+        if element:
431
+            raise ValueError('`element` must be empty for NoSymmetry.')
432
+        return a if b is None else (a, b)
433
+
434
+    def to_fd(self, a, b=None):
435
+        return a if b is None else (a, b)
436
+
437
+    def in_fd(self, site):
438
+        return True
439
+
440
+    def subgroup(self, *generators):
441
+        if any(generators):
442
+            raise ValueError('Generators must be empty for NoSymmetry.')
443
+        return NoSymmetry(generators)
444
+
445
+    def has_subgroup(self, other):
446
+        return isinstance(other, NoSymmetry)
272 447
 
273 448
 
274 449
 ################ Systems
... ...
@@ -354,7 +529,7 @@ class System(metaclass=abc.ABCMeta):
354 529
 
355 530
 Term = namedtuple(
356 531
     "Term",
357
-    ["subgraph", "hermitian", "parameters"],
532
+    ["subgraph", "symmetry_element", "hermitian", "parameters"],
358 533
 )
359 534
 
360 535
 
... ...
@@ -363,6 +538,8 @@ class VectorizedSystem(System, metaclass=abc.ABCMeta):
363 538
 
364 539
     Attributes
365 540
     ----------
541
+    symmetry : kwant.system.Symmetry
542
+        The symmetry of the system.
366 543
     graph : kwant.graph.CGraph
367 544
         The system graph.
368 545
     subgraphs : sequence of tuples
... ...
@@ -371,9 +548,11 @@ class VectorizedSystem(System, metaclass=abc.ABCMeta):
371 548
         indexed by 'idx1' and 'idx2'.
372 549
     terms : sequence of tuples
373 550
         Each tuple has the following structure:
374
-        (subgraph: int, hermitian: bool, parameters: List(str))
551
+        (subgraph: int, symmetry_element: tuple, hermitian: bool, parameters: List(str))
375 552
         'subgraph' indexes 'subgraphs' and supplies the to/from sites of this
376
-        term. 'hermitian' is 'True' if the term needs its Hermitian
553
+        term. 'symmetry_element' is the symmetry group element that should be
554
+        applied to the 'to-sites' of this term.
555
+        'hermitian' is 'True' if the term needs its Hermitian
377 556
         conjugate to be added when evaluating the Hamiltonian, and 'parameters'
378 557
         contains a list of parameter names used when evaluating this term.
379 558
     site_arrays : sequence of SiteArray
... ...
@@ -576,50 +755,7 @@ def is_finite(syst):
576 755
 
577 756
 
578 757
 class InfiniteSystemMixin(metaclass=abc.ABCMeta):
579
-    """Abstract infinite low-level system.
580
-
581
-    An infinite system consists of an infinite series of identical cells.
582
-    Adjacent cells are connected by identical inter-cell hoppings.
583
-
584
-    Attributes
585
-    ----------
586
-    cell_size : integer
587
-        The number of sites in a single cell of the system.
588
-
589
-    Notes
590
-    -----
591
-    The system graph of an infinite systems contains a single cell, as well as
592
-    the part of the previous cell which is connected to it.  The first
593
-    `cell_size` sites form one complete single cell.  The remaining ``N`` sites
594
-    of the graph (``N`` equals ``graph.num_nodes - cell_size``) belong to the
595
-    previous cell.  They are included so that hoppings between cells can be
596
-    represented.  The N sites of the previous cell correspond to the first
597
-    ``N`` sites of the fully included cell.  When an ``InfiniteSystem`` is used
598
-    as a lead, ``N`` acts also as the number of interface sites to which it
599
-    must be connected.
600
-
601
-    The drawing shows three cells of an infinite system.  Each cell consists
602
-    of three sites.  Numbers denote sites which are included into the system
603
-    graph.  Stars denote sites which are not included.  Hoppings are included
604
-    in the graph if and only if they occur between two sites which are part of
605
-    the graph::
606
-
607
-            * 2 *
608
-        ... | | | ...
609
-            * 0 3
610
-            |/|/|
611
-            *-1-4
612
-
613
-        <-- order of cells
614 758
 
615
-    The numbering of sites in the drawing is one of the two valid ones for that
616
-    infinite system.  The other scheme has the numbers of site 0 and 1
617
-    exchanged, as well as of site 3 and 4.
618
-
619
-    Sites in the fundamental domain cell must belong to a different site array
620
-    than the sites in the previous cell. In the above example this means that
621
-    sites '(0, 1, 2)' and '(3, 4)' must belong to different site arrays.
622
-    """
623 759
     @deprecate_args
624 760
     def modes(self, energy=0, args=(), *, params=None):
625 761
         """Return mode decomposition of the lead
... ...
@@ -704,6 +840,46 @@ class InfiniteSystemMixin(metaclass=abc.ABCMeta):
704 840
 
705 841
 
706 842
 class InfiniteSystem(System, InfiniteSystemMixin, metaclass=abc.ABCMeta):
843
+    """Abstract infinite low-level system.
844
+
845
+    An infinite system consists of an infinite series of identical cells.
846
+    Adjacent cells are connected by identical inter-cell hoppings.
847
+
848
+    Attributes
849
+    ----------
850
+    cell_size : integer
851
+        The number of sites in a single cell of the system.
852
+
853
+    Notes
854
+    -----
855
+    The system graph of an infinite systems contains a single cell, as well as
856
+    the part of the previous cell which is connected to it.  The first
857
+    `cell_size` sites form one complete single cell.  The remaining ``N`` sites
858
+    of the graph (``N`` equals ``graph.num_nodes - cell_size``) belong to the
859
+    previous cell.  They are included so that hoppings between cells can be
860
+    represented.  The N sites of the previous cell correspond to the first
861
+    ``N`` sites of the fully included cell.  When an ``InfiniteSystem`` is used
862
+    as a lead, ``N`` acts also as the number of interface sites to which it
863
+    must be connected.
864
+
865
+    The drawing shows three cells of an infinite system.  Each cell consists
866
+    of three sites.  Numbers denote sites which are included into the system
867
+    graph.  Stars denote sites which are not included.  Hoppings are included
868
+    in the graph if and only if they occur between two sites which are part of
869
+    the graph::
870
+
871
+            * 2 *
872
+        ... | | | ...
873
+            * 0 3
874
+            |/|/|
875
+            *-1-4
876
+
877
+        <-- order of cells
878
+
879
+    The numbering of sites in the drawing is one of the two valid ones for that
880
+    infinite system.  The other scheme has the numbers of site 0 and 1
881
+    exchanged, as well as of site 3 and 4.
882
+    """
707 883
 
708 884
     @deprecate_args
709 885
     def cell_hamiltonian(self, args=(), sparse=False, *, params=None):
... ...
@@ -730,9 +906,46 @@ class InfiniteSystem(System, InfiniteSystemMixin, metaclass=abc.ABCMeta):
730 906
 
731 907
 
732 908
 class InfiniteVectorizedSystem(VectorizedSystem, InfiniteSystemMixin, metaclass=abc.ABCMeta):
909
+    """Abstract vectorized infinite low-level system.
910
+
911
+    An infinite system consists of an infinite series of identical cells.
912
+    Adjacent cells are connected by identical inter-cell hoppings.
913
+
914
+    Attributes
915
+    ----------
916
+    cell_size : integer
917
+        The number of sites in a single cell of the system.
918
+
919
+    Notes
920
+    -----
921
+    Unlike `~kwant.system.InfiniteSystem`, vectorized infinite systems do
922
+    not explicitly store the sites in the previous unit cell; only the
923
+    sites in the fundamental domain are stored. Nevertheless, the
924
+    SiteArrays of `~kwant.system.InfiniteVectorizedSystem` are ordered
925
+    in an analogous way, in order to facilitate the representation of
926
+    inter-cell hoppings. The ordering is as follows. The *interface sites*
927
+    of a unit cell are the sites that have hoppings to the *next* unit cell
928
+    (along the symmetry direction). Interface sites are always in different
929
+    SiteArrays than non-interface sites, i.e. the sites in a given SiteArray
930
+    are either all interface sites, or all non-interface sites.
931
+    The SiteArrays consisting of interface sites always appear *before* the
932
+    SiteArrays consisting of non-interface sites in ``self.site_arrays``.
933
+    This is backwards compatible with `kwant.system.InfiniteSystem`.
934
+
935
+    For backwards compatibility, `~kwant.system.InfiniteVectorizedSystem`
936
+    maintains a ``graph``, that includes nodes for the sites
937
+    in the previous unit cell.
938
+    """
733 939
     cell_hamiltonian = _system.vectorized_cell_hamiltonian
734 940
     inter_cell_hopping = _system.vectorized_inter_cell_hopping
735 941
 
942
+    def hamiltonian_submatrix(self, args=(), sparse=False,
943
+                              return_norb=False, *, params=None):
944
+        raise ValueError(
945
+            "'hamiltonian_submatrix' is not meaningful for infinite"
946
+            "systems. Use 'cell_hamiltonian' or 'inter_cell_hopping."
947
+        )
948
+
736 949
 
737 950
 def is_infinite(syst):
738 951
     return isinstance(syst, (InfiniteSystem, InfiniteVectorizedSystem))
... ...
@@ -153,7 +153,7 @@ def test_site_families():
153 153
     assert fam1 < fam2  # string '1' is lexicographically less than '2'
154 154
 
155 155
 
156
-class VerySimpleSymmetry(builder.Symmetry):
156
+class VerySimpleSymmetry(system.Symmetry):
157 157
     def __init__(self, period):
158 158
         self.period = period
159 159
 
... ...
@@ -162,7 +162,7 @@ class VerySimpleSymmetry(builder.Symmetry):
162 162
         return 1
163 163
 
164 164
     def has_subgroup(self, other):
165
-        if isinstance(other, builder.NoSymmetry):
165
+        if isinstance(other, system.NoSymmetry):
166 166
             return True
167 167
         elif isinstance(other, VerySimpleSymmetry):
168 168
             return not other.period % self.period
... ...
@@ -321,10 +321,10 @@ def random_hopping_integral(rng):
321 321
 
322 322
 
323 323
 def check_onsite(fsyst, sites, subset=False, check_values=True):
324
-    vectorized = isinstance(fsyst, (system.FiniteVectorizedSystem, system.InfiniteVectorizedSystem))
324
+    vectorized = system.is_vectorized(fsyst)
325 325
 
326 326
     if vectorized:
327
-        site_offsets = np.cumsum([0] + [len(s) for s in fsyst.site_arrays])
327
+        site_offsets, _, _ = fsyst.site_ranges.transpose()
328 328
 
329 329
     freq = {}
330 330
     for node in range(fsyst.graph.num_nodes):
... ...
@@ -353,10 +353,10 @@ def check_onsite(fsyst, sites, subset=False, check_values=True):
353 353
 
354 354
 
355 355
 def check_hoppings(fsyst, hops):
356
-    vectorized = isinstance(fsyst, (system.FiniteVectorizedSystem, system.InfiniteVectorizedSystem))
356
+    vectorized = system.is_vectorized(fsyst)
357 357
 
358 358
     if vectorized:
359
-        site_offsets = np.cumsum([0] + [len(s) for s in fsyst.site_arrays])
359
+        site_offsets, _, _ = fsyst.site_ranges.transpose()
360 360
 
361 361
     assert fsyst.graph.num_edges == 2 * len(hops)
362 362
     for edge_id, edge in enumerate(fsyst.graph):
... ...
@@ -366,6 +366,12 @@ def check_hoppings(fsyst, hops):
366 366
 
367 367
         if vectorized:
368 368
             term = fsyst._hopping_term_by_edge_id[edge_id]
369
+            # Map sites in previous cell to fundamental domain; vectorized
370
+            # infinite systems only store sites in the FD. We already know
371
+            # that this hopping is between unit cells because this is encoded
372
+            # in the edge_id and term id.
373
+            if system.is_infinite(fsyst):
374
+                i, j = i % fsyst.cell_size, j % fsyst.cell_size
369 375
             if term < 0:  # Hermitian conjugate
370 376
                 assert (head, tail) in hops
371 377
             else:
... ...
@@ -464,7 +470,8 @@ def test_finalization(vectorize):
464 470
     assert tuple(fsyst.sites) == tuple(sorted(fam(*site) for site in sr_sites))
465 471
 
466 472
     # Build lead from blueprint and test it.
467
-    lead = builder.Builder(kwant.TranslationalSymmetry((size, 0)))
473
+    lead = builder.Builder(kwant.TranslationalSymmetry((size, 0)),
474
+                           vectorize=vectorize)
468 475
     for site, value in lead_sites.items():
469 476
         shift = rng.randrange(-5, 6) * size
470 477
         site = site[0] + shift, site[1]
... ...
@@ -757,10 +764,6 @@ def test_vectorized_hamiltonian_evaluation():
757 764
     syst_simple[(lat(2, 1), lat(1, 1))] = hopping
758 765
     fsyst_simple = syst_simple.finalized()
759 766
 
760
-    assert np.allclose(
761
-        fsyst_vectorized.hamiltonian_submatrix(),
762
-        fsyst_simple.hamiltonian_submatrix(),
763
-    )
764 767
     assert np.allclose(
765 768
         fsyst_vectorized.cell_hamiltonian(),
766 769
         fsyst_simple.cell_hamiltonian(),
... ...
@@ -812,7 +815,7 @@ def test_vectorized_value_normalization():
812 815
 
813 816
 
814 817
 @pytest.mark.parametrize("sym", [
815
-    builder.NoSymmetry(),
818
+    system.NoSymmetry(),
816 819
     kwant.TranslationalSymmetry([-1]),
817 820
 ])
818 821
 def test_vectorized_requires_norbs(sym):
... ...
@@ -921,7 +924,7 @@ def test_fill():
921 924
     ## Test that copying a builder by "fill" preserves everything.
922 925
     for sym, func in [(kwant.TranslationalSymmetry(*np.diag([3, 4, 5])),
923 926
                        lambda pos: True),
924
-                      (builder.NoSymmetry(),
927
+                      (system.NoSymmetry(),
925 928
                        lambda pos: ta.dot(pos, pos) < 17)]:
926 929
         cubic = kwant.lattice.general(ta.identity(3), norbs=1)
927 930
 
... ...
@@ -1642,7 +1645,7 @@ def test_subs():
1642 1645
 
1643 1646
     lat = kwant.lattice.chain(norbs=1)
1644 1647
 
1645
-    def make_system(sym=kwant.builder.NoSymmetry(), n=3):
1648
+    def make_system(sym=system.NoSymmetry(), n=3):
1646 1649
         syst = kwant.Builder(sym)
1647 1650
         syst[(lat(i) for i in range(n))] = onsite
1648 1651
         syst[lat.neighbors()] = hopping
... ...
@@ -11,7 +11,7 @@ from math import sqrt
11 11
 import numpy as np
12 12
 import tinyarray as ta
13 13
 from pytest import raises
14
-from kwant import lattice, builder
14
+from kwant import lattice, builder, system
15 15
 from kwant._common import ensure_rng
16 16
 import pytest
17 17
 
... ...
@@ -285,7 +285,7 @@ def test_symmetry_has_subgroup():
285 285
     ## test whether actual subgroups are detected as such
286 286
     vecs = rng.randn(3, 3)
287 287
     sym1 = lattice.TranslationalSymmetry(*vecs)
288
-    ns = builder.NoSymmetry()
288
+    ns = system.NoSymmetry()
289 289
     assert ns.has_subgroup(ns)
290 290
     assert sym1.has_subgroup(sym1)
291 291
     assert sym1.has_subgroup(ns)