diff --git a/doc/source/pre/whatsnew/0.2.rst b/doc/source/pre/whatsnew/0.2.rst index 3a0e8e95a4a30a86168855fbd017be1279193cfe..2022810f63e0c00d3df442d91ff1e772f45ba0d8 100644 --- a/doc/source/pre/whatsnew/0.2.rst +++ b/doc/source/pre/whatsnew/0.2.rst @@ -42,7 +42,7 @@ description check `~kwant.plotter.plot` documentation. The behavior of `~kwant.plotter.plot` with low level systems has changed. Arguments of plot which are functions are given site numbers in place of -`~kwant.builder.Site` objects when plotting a low level system. This +`~kwant.system.Site` objects when plotting a low level system. This provides an easy way to make the appearance of lines and symbols depend on computation results. diff --git a/doc/source/pre/whatsnew/1.0.rst b/doc/source/pre/whatsnew/1.0.rst index 1eb4cf07efa8cda9b030497893b99b263afbabd7..473e414af1052f24ef271e38a2bd2b92ba34d361 100644 --- a/doc/source/pre/whatsnew/1.0.rst +++ b/doc/source/pre/whatsnew/1.0.rst @@ -39,7 +39,7 @@ now it suffices to write :: syst[lat.neighbors()] = t This is possible because `~kwant.builder.Builder` now accepts *functions* as -keys in addition to `~kwant.builder.Site` objects and tuples of them +keys in addition to `~kwant.system.Site` objects and tuples of them (hoppings). These functions are expected to yield either sites or hoppings, when given a builder instance as the sole argument. The use of such keys is to implement sets of sites or hoppings that depend on what is already present in diff --git a/doc/source/pre/whatsnew/1.5.rst b/doc/source/pre/whatsnew/1.5.rst index 6204a4e02059822c4ab123ebec43b4e59afd5a5e..4a4d39755a638cfa2a749d19bcd2040b1ceeb5b0 100644 --- a/doc/source/pre/whatsnew/1.5.rst +++ b/doc/source/pre/whatsnew/1.5.rst @@ -3,6 +3,40 @@ What's new in Kwant 1.5 This article explains the user-visible changes in Kwant 1.5.0. + +Value functions may now be vectorized +------------------------------------- +It is now possible to define value functions that act on whole +arrays of sites at a time. +:: + + def onsite(sites): + r = sites.positions() + return np.exp(-np.linalg.norm(r, axis=1)**2) + + def hopping(to_sites, from_sites): + dr = to_sites.positions() - from_sites.positions() + return 1 / np.linalg.norm(dr, axis=1)**2 + + + lat = kwant.lattice.square(norbs=1) + syst = kwant.Builder(vectorize=True) + syst[(lat(i, j) for i in range(4) for j in range(10)] = onsite + syst[lat.neighbors()] = hopping + syst = syst.finalized() + + syst.hamiltonian_submatrix() + +Notice that when creating the Builder we provide the ``vectorize=True`` flag, +and that the value functions now take `~kwant.system.SiteArray` objects +(which have a ``positions`` method, rather than a ``pos`` property as for +`~kwant.system.Site`). We can now operate on the array of site positions +using ``numpy``. Using vectorized value functions in this way can produce an +order of magnitude speedup when evaluating system Hamiltonians (though this +speedup may be masked by other parts of your computation e.g. calculating +the scattering matrix). + + Deprecation of leaving 'norbs' unset for site families ------------------------------------------------------ When constructing site families (e.g. lattices) it is now deprecated to diff --git a/doc/source/reference/kwant.builder.rst b/doc/source/reference/kwant.builder.rst index 327afca9e9f44c797378b918baa0799ef30af45f..6b470427c75c5ddb2594cdd8edacb705a36187ee 100644 --- a/doc/source/reference/kwant.builder.rst +++ b/doc/source/reference/kwant.builder.rst @@ -9,7 +9,6 @@ Types :toctree: generated/ Builder - Site HoppingKind SimpleSiteFamily BuilderLead @@ -17,13 +16,14 @@ Types ModesLead FiniteSystem InfiniteSystem + FiniteVectorizedSystem + InfiniteVectorizedSystem Abstract base classes --------------------- .. autosummary:: :toctree: generated/ - SiteFamily Symmetry Lead @@ -33,3 +33,11 @@ Functions :toctree: generated/ add_peierls_phase + +Mixin Classes +------------- +.. autosummary:: + :toctree: generated/ + + _FinalizedBuilderMixin + _VectorizedFinalizedBuilderMixin diff --git a/doc/source/reference/kwant.system.rst b/doc/source/reference/kwant.system.rst index 3b6f26274da393807c37c6c2826ce5518fe41c7d..2a9bd89c2529d65e5cc3cad63a144dc31e7a03f1 100644 --- a/doc/source/reference/kwant.system.rst +++ b/doc/source/reference/kwant.system.rst @@ -19,10 +19,32 @@ In practice, very often Kwant systems are finalized `kwant.builder.FiniteSystem` or `kwant.builder.InfiniteSystem`) and offer some additional attributes. +Sites +----- +.. autosummary:: + :toctree: generated/ + + Site + SiteArray + SiteFamily + +Systems +------- .. autosummary:: :toctree: generated/ System + VectorizedSystem InfiniteSystem + InfiniteVectorizedSystem FiniteSystem + FiniteVectorizedSystem PrecalculatedLead + +Mixin Classes +------------- +.. autosummary:: + :toctree: generated/ + + FiniteSystemMixin + InfiniteSystemMixin diff --git a/doc/source/tutorial/first_steps.rst b/doc/source/tutorial/first_steps.rst index 41ed21bb7fdfef0279b95f000db6d9199fa9ed63..b0760b6432e4d22003aa8bdfb9c18e5352999972 100644 --- a/doc/source/tutorial/first_steps.rst +++ b/doc/source/tutorial/first_steps.rst @@ -358,13 +358,13 @@ subbands that increases with energy. **sites** as indices. Sites themselves have a certain type, and belong to a **site family**. A site family is also used to convert something that represents a site (like a tuple) into a - proper `~kwant.builder.Site` object that can be used with + proper `~kwant.system.Site` object that can be used with `~kwant.builder.Builder`. In the above example, `lat` is the site family. ``lat(i, j)`` then translates the description of a lattice site in terms of two integer indices (which is the natural way to do here) into - a proper `~kwant.builder.Site` object. + a proper `~kwant.system.Site` object. The concept of site families and sites allows `~kwant.builder.Builder` to mix arbitrary lattices and site families diff --git a/doc/source/tutorial/operators.rst b/doc/source/tutorial/operators.rst index ed346f178fc3582a2f8909c8bc4cc76de03cad53..1fdd27871a4f3852df6b32ea92f1b9d633bb6897 100644 --- a/doc/source/tutorial/operators.rst +++ b/doc/source/tutorial/operators.rst @@ -438,7 +438,7 @@ the `~kwant.operator.Current`, instead of a constant matrix: # evaluate the operator m_current = J_m(psi, params=dict(r0=25, delta=10)) -The function must take a `~kwant.builder.Site` as its first parameter, +The function must take a `~kwant.system.Site` as its first parameter, and may optionally take other parameters (i.e. it must have the same signature as a Hamiltonian onsite function), and must return the square matrix that defines the operator we wish to calculate. @@ -529,8 +529,8 @@ We see that the probability current is conserved across the scattering region, but the z-projected spin current is not due to the fact that the Hamiltonian does not commute with :math:`σ_z` everywhere in the scattering region. -.. note:: ``where`` can also be provided as a sequence of `~kwant.builder.Site` - or a sequence of hoppings (i.e. pairs of `~kwant.builder.Site`), +.. note:: ``where`` can also be provided as a sequence of `~kwant.system.Site` + or a sequence of hoppings (i.e. pairs of `~kwant.system.Site`), rather than a function. diff --git a/doc/source/tutorial/plotting.rst b/doc/source/tutorial/plotting.rst index bf6b51ae7e148fc6fdad8c37c5f24ed57421a079..48b6ed4796251b64aa9de10922ce911cd3aade86 100644 --- a/doc/source/tutorial/plotting.rst +++ b/doc/source/tutorial/plotting.rst @@ -105,7 +105,7 @@ the start end end site of hopping as arguments: kwant.plot(syst, site_lw=0.1, site_color=family_color, hop_lw=hopping_lw); Note that since we are using an unfinalized Builder, a ``site`` is really an -instance of `~kwant.builder.Site`. With these adjustments we arrive at a plot +instance of `~kwant.system.Site`. With these adjustments we arrive at a plot that carries the same information, but is much easier to interpret. Apart from plotting the *system* itself, `~kwant.plotter.plot` can also be used @@ -175,7 +175,7 @@ this is interpreted as the radius of the inner circle). Finally, note that since we are dealing with a finalized system now, a site `i` is represented by an integer. In order to obtain the original -`~kwant.builder.Site`, ``syst.sites[i]`` can be used. +`~kwant.system.Site`, ``syst.sites[i]`` can be used. The way how data is presented of course influences what features of the data are best visible in a given plot. With `~kwant.plotter.plot` one can easily go diff --git a/doc/source/tutorial/spin_potential_shape.rst b/doc/source/tutorial/spin_potential_shape.rst index b87eaa7f6afced106110ca10823e48a6cb667069..23d90bf8bbf384af9d3644a34867c635c5eaf115 100644 --- a/doc/source/tutorial/spin_potential_shape.rst +++ b/doc/source/tutorial/spin_potential_shape.rst @@ -285,7 +285,7 @@ define the potential profile of a quantum well as: else: return 0 -This function takes two arguments: the first of type `~kwant.builder.Site`, +This function takes two arguments: the first of type `~kwant.system.Site`, from which you can get the real-space coordinates using ``site.pos``, and the value of the potential as the second. Note that in `potential` we can access variables `L` and `L_well` that are defined globally. @@ -375,10 +375,10 @@ oscillatory transmission behavior through resonances in the quantum well. .. specialnote:: Technical details - Functions can also be used for hoppings. In this case, they take - two `~kwant.builder.Site`'s as arguments and then an arbitrary number + two `~kwant.system.Site`'s as arguments and then an arbitrary number of additional arguments. - - Apart from the real-space position `pos`, `~kwant.builder.Site` has also an + - Apart from the real-space position `pos`, `~kwant.system.Site` has also an attribute `tag` containing the lattice indices of the site. .. _tutorial-abring: @@ -449,7 +449,7 @@ that returns ``True`` whenever a point is inside the shape, and return (r1 ** 2 < rsq < r2 ** 2) Note that this function takes a real-space position as argument (not a -`~kwant.builder.Site`). +`~kwant.system.Site`). We can now simply add all of the lattice points inside this shape at once, using the function `~kwant.lattice.Square.shape` diff --git a/kwant/_system.pyx b/kwant/_system.pyx index 71860ed674dbabfade12a69c9eb2df9f31b6f65e..2f58dd4ff3bdb725acdf00238636006af337cde4 100644 --- a/kwant/_system.pyx +++ b/kwant/_system.pyx @@ -1,4 +1,4 @@ -# Copyright 2011-2013 Kwant authors. +# Copyright 2011-2019 Kwant authors. # # This file is part of Kwant. It is subject to the license terms in the file # LICENSE.rst found in the top-level directory of this distribution and at @@ -12,12 +12,16 @@ import numpy as np from scipy import sparse as sp from itertools import chain import types +import bisect from .graph.core cimport CGraph, gintArraySlice from .graph.defs cimport gint from .graph.defs import gint_dtype from ._common import deprecate_args + +### Non-vectorized methods + msg = ('Hopping from site {0} to site {1} does not match the ' 'dimensions of onsite Hamiltonians of these sites.') @@ -352,3 +356,383 @@ def hamiltonian_submatrix(self, args=(), to_sites=None, from_sites=None, mat = func(ham, args, params, self.graph, diag, from_sites, n_by_to_site, to_norb, to_off, from_norb, from_off) return (mat, to_norb, from_norb) if return_norb else mat + + +### Vectorized methods + + +_shape_error_msg = ( + "The following hoppings have matrix elements of incompatible shape " + "with other matrix elements: {}" +) + + +@cython.boundscheck(False) +def _vectorized_make_sparse(subgraphs, hams, long [:] norbs, long [:] orb_offsets, + long [:] site_offsets, shape=None): + ndata = sum(h.shape[0] * h.shape[1] * h.shape[2] for h in hams) + + cdef long [:] rows_view, cols_view + cdef complex [:] data_view + + rows = np.empty((ndata,), dtype=long) + cols = np.empty((ndata,), dtype=long) + data = np.empty((ndata,), dtype=complex) + rows_view = rows + cols_view = cols + data_view = data + + cdef long i, j, k, m, N, M, P, to_off, from_off,\ + ta, fa, to_norbs, from_norbs + cdef long [:] ts_offs, fs_offs + cdef complex [:, :, :] h + + m = 0 + # This outer loop zip() is pure Python, but that's ok, as it + # has very few entries and the inner loops are fully vectorized + for ((ta, fa), (ts_offs, fs_offs)), h in zip(subgraphs, hams): + N = h.shape[0] + M = h.shape[1] + P = h.shape[2] + + if norbs[ta] != M or norbs[fa] != P: + to_sites = site_offsets[ta] + np.array(ts_offs) + from_sites = site_offsets[fa] + np.array(fs_offs) + hops = np.array([to_sites, from_sites]).transpose() + raise ValueError(_shape_error_msg.format(hops)) + + for i in range(N): + to_off = orb_offsets[ta] + norbs[ta] * ts_offs[i] + from_off = orb_offsets[fa] + norbs[fa] * fs_offs[i] + for j in range(M): + for k in range(P): + rows_view[m] = to_off + j + cols_view[m] = from_off + k + data_view[m] = h[i, j, k] + m += 1 + + if shape is None: + shape = (orb_offsets[-1], orb_offsets[-1]) + + return sp.coo_matrix((data, (rows, cols)), shape=shape) + + +@cython.boundscheck(False) +def _vectorized_make_dense(subgraphs, hams, long [:] norbs, long [:] orb_offsets, + long [:] site_offsets, shape=None): + if shape is None: + shape = (orb_offsets[-1], orb_offsets[-1]) + mat = np.zeros(shape, dtype=complex) + cdef complex [:, :] mat_view + mat_view = mat + + cdef long i, j, k, N, M, P, to_off, from_off,\ + ta, fa, to_norbs, from_norbs + cdef long [:] ts_offs, fs_offs + cdef complex [:, :, :] h + + # This outer loop zip() is pure Python, but that's ok, as it + # has very few entries and the inner loops are fully vectorized + for ((ta, fa), (ts_offs, fs_offs)), h in zip(subgraphs, hams): + N = h.shape[0] + M = h.shape[1] + P = h.shape[2] + + if norbs[ta] != M or norbs[fa] != P: + to_sites = site_offsets[ta] + np.array(ts_offs) + from_sites = site_offsets[fa] + np.array(fs_offs) + hops = np.array([to_sites, from_sites]).transpose() + raise ValueError(_shape_error_msg.format(hops)) + + for i in range(N): + to_off = orb_offsets[ta] + norbs[ta] * ts_offs[i] + from_off = orb_offsets[fa] + norbs[fa] * fs_offs[i] + for j in range(M): + for k in range(P): + mat_view[to_off + j, from_off + k] = h[i, j, k] + return mat + + +def _count_norbs(syst, site_offsets, hams, args=(), params=None): + """Return the norbs and orbital offset of each site family in 'syst' + + Parameters + ---------- + syst : `kwant.system.System` + site_offsets : sequence of int + Contains the index of the first site for each site array + in 'syst.site_arrays'. + hams : sequence of arrays or 'None' + The Hamiltonian for each term in 'syst.terms'. 'None' + if the corresponding term has not been evaluated. + args, params : positional and keyword arguments to the system Hamiltonian + """ + site_ranges = syst.site_ranges + if site_ranges is None: + # NOTE: Remove this branch once we can rely on + # site families storing the norb information. + + site_arrays = syst.site_arrays + family_norbs = {s.family: None for s in site_arrays} + + # Compute the norbs per site family using already evaluated + # Hamiltonian terms where possible + for ham, t in zip(hams, syst.terms): + (to_w, from_w), _ = syst.subgraphs[t.subgraph] + if ham is not None: + family_norbs[site_arrays[to_w].family] = ham.shape[1] + family_norbs[site_arrays[from_w].family] = ham.shape[2] + + # Evaluate Hamiltonian terms where we do not already have them + for n, t in enumerate(syst.terms): + (to_w, from_w), _ = syst.subgraphs[t.subgraph] + to_fam = site_arrays[to_w].family + from_fam = site_arrays[from_w].family + if family_norbs[to_fam] is None or family_norbs[from_fam] is None: + ham = syst.hamiltonian_term(n, args=args, params=params) + family_norbs[to_fam] = ham.shape[1] + family_norbs[from_fam] = ham.shape[2] + + # This case is technically allowed by the format (some sites present + # but no hamiltonian terms that touch them) but is very unlikely. + if any(norbs is None for norbs in family_norbs.values()): + raise ValueError("Cannot determine the number of orbitals for " + "some site families.") + + orb_offset = 0 + site_ranges = [] + for start, site_array in zip(site_offsets, syst.site_arrays): + norbs = family_norbs[site_array.family] + site_ranges.append((start, norbs, orb_offset)) + orb_offset += len(site_array) * norbs + site_ranges.append((site_offsets[-1], 0, orb_offset)) + site_ranges = np.array(site_ranges) + + _, norbs, orb_offsets = site_ranges.transpose() + # The final (extra) element in orb_offsets is the total number of orbitals + assert len(orb_offsets) == len(syst.site_arrays) + 1 + + return norbs, orb_offsets + + +def _expand_norbs(compressed_norbs, site_offsets): + "Return norbs per site, given norbs per site array." + norbs = np.empty(site_offsets[-1], int) + for start, stop, norb in zip(site_offsets, site_offsets[1:], + compressed_norbs): + norbs[start:stop] = norb + return norbs + + +def _reverse_subgraph(subgraph): + (to_sa, from_sa), (to_off, from_off) = subgraph + return ((from_sa, to_sa), (from_off, to_off)) + + +@deprecate_args +@cython.binding(True) +def vectorized_hamiltonian_submatrix(self, args=(), sparse=False, + return_norb=False, *, params=None): + """Return The system Hamiltonian. + + Parameters + ---------- + args : tuple, defaults to empty + Positional arguments to pass to ``hamiltonian_term``. Mutually + exclusive with 'params'. + sparse : bool + Whether to return a sparse or a dense matrix. Defaults to ``False``. + return_norb : bool + Whether to return arrays of numbers of orbitals. Defaults to ``False``. + params : dict, optional + Dictionary of parameter names and their values. Mutually exclusive + with 'args'. + + Returns + ------- + hamiltonian_part : numpy.ndarray or scipy.sparse.coo_matrix + The Hamiltonian of the system. + norb : array of integers + Numbers of orbitals on each site. Only returned when ``return_norb`` + is true. + + Notes + ----- + Providing positional arguments via 'args' is deprecated, + instead, provide named parameters as a dictionary via 'params'. + """ + + site_offsets = np.cumsum([0] + [len(arr) for arr in self.site_arrays]) + + subgraphs = [ + self.subgraphs[t.subgraph] + for t in self.terms + ] + # Add Hermitian conjugates + subgraphs += [ + _reverse_subgraph(self.subgraphs[t.subgraph]) + for t in self.terms + if t.hermitian + ] + + hams = [ + self.hamiltonian_term(n, args=args, params=params) + for n, _ in enumerate(self.terms) + ] + # Add Hermitian conjugates + hams += [ + ham.conjugate().transpose(0, 2, 1) + for ham, t in zip(hams, self.terms) + if t.hermitian + ] + + norbs, orb_offsets = _count_norbs(self, site_offsets, hams, + args=args, params=params) + + func = _vectorized_make_sparse if sparse else _vectorized_make_dense + mat = func(subgraphs, hams, norbs, orb_offsets, site_offsets) + + if return_norb: + return (mat, _expand_norbs(norbs, site_offsets)) + else: + return mat + + +@deprecate_args +@cython.binding(True) +def vectorized_cell_hamiltonian(self, args=(), sparse=False, *, params=None): + """Hamiltonian of a single cell of the infinite system. + + Providing positional arguments via 'args' is deprecated, + instead, provide named parameters as a dictionary via 'params'. + """ + + site_offsets = np.cumsum([0] + [len(arr) for arr in self.site_arrays]) + # Site array where next cell starts + next_cell = bisect.bisect(site_offsets, self.cell_size) - 1 + + def inside_cell(term): + (to_which, from_which), _= self.subgraphs[term.subgraph] + return to_which < next_cell and from_which < next_cell + + cell_terms = [ + n for n, t in enumerate(self.terms) + if inside_cell(t) + ] + + subgraphs = [ + self.subgraphs[self.terms[n].subgraph] + for n in cell_terms + ] + # Add Hermitian conjugates + subgraphs += [ + _reverse_subgraph(self.subgraphs[self.terms[n].subgraph]) + for n in cell_terms + if self.terms[n].hermitian + ] + + hams = [ + self.hamiltonian_term(n, args=args, params=params) + for n in cell_terms + ] + # Add Hermitian conjugates + hams += [ + ham.conjugate().transpose(0, 2, 1) + for ham, n in zip(hams, cell_terms) + if self.terms[n].hermitian + ] + + # _count_norbs requires passing hamiltonians for all terms, or 'None' + # if it has not been evaluated + all_hams = [None] * len(self.terms) + for n, ham in zip(cell_terms, hams): + all_hams[n] = ham + norbs, orb_offsets = _count_norbs(self, site_offsets, all_hams, + args=args, params=params) + + shape = (orb_offsets[next_cell], orb_offsets[next_cell]) + func = _vectorized_make_sparse if sparse else _vectorized_make_dense + mat = func(subgraphs, hams, norbs, orb_offsets, site_offsets, shape=shape) + + return mat + + +@deprecate_args +@cython.binding(True) +def vectorized_inter_cell_hopping(self, args=(), sparse=False, *, params=None): + """Hopping Hamiltonian between two cells of the infinite system. + + Providing positional arguments via 'args' is deprecated, + instead, provide named parameters as a dictionary via 'params'. + """ + + # Take advantage of the fact that there are distinct entries in + # onsite_terms for sites inside the unit cell, and distinct entries + # in onsite_terms for hoppings between the unit cell sites and + # interface sites. This way we only need to check the first entry + # in onsite/hopping_terms + + site_offsets = np.cumsum([0] + [len(arr) for arr in self.site_arrays]) + # Site array where next cell starts + next_cell = bisect.bisect(site_offsets, self.cell_size) - 1 + + inter_cell_hopping_terms = [ + n for n, t in enumerate(self.terms) + # *from* site is in interface + if (self.subgraphs[t.subgraph][0][1] >= next_cell + and self.subgraphs[t.subgraph][0][0] < next_cell) + ] + reversed_inter_cell_hopping_terms = [ + n for n, t in enumerate(self.terms) + # *to* site is in interface + if (self.subgraphs[t.subgraph][0][0] >= next_cell + and self.subgraphs[t.subgraph][0][1] < next_cell) + ] + + # Evaluate inter-cell hoppings only + inter_cell_hams = [ + self.hamiltonian_term(n, args=args, params=params) + for n in inter_cell_hopping_terms + ] + reversed_inter_cell_hams = [ + self.hamiltonian_term(n, args=args, params=params) + .conjugate().transpose(0, 2, 1) + for n in reversed_inter_cell_hopping_terms + ] + + hams = inter_cell_hams + reversed_inter_cell_hams + + subgraphs = [ + self.subgraphs[self.terms[n].subgraph] + for n in inter_cell_hopping_terms + ] + subgraphs += [ + _reverse_subgraph(self.subgraphs[self.terms[n].subgraph]) + for n in reversed_inter_cell_hopping_terms + ] + + # All the 'from' sites are in the previous domain, but to build a + # matrix we need to get the equivalent sites in the fundamental domain. + # We set the 'from' site array to the one from the fundamental domain. + subgraphs = [ + ((to_sa, from_sa - next_cell), (to_off, from_off)) + for (to_sa, from_sa), (to_off, from_off) in subgraphs + ] + + # _count_norbs requires passing hamiltonians for all terms, or 'None' + # if it has not been evaluated + all_hams = [None] * len(self.terms) + for n, ham in zip(inter_cell_hopping_terms, inter_cell_hams): + all_hams[n] = ham + for n, ham in zip(reversed_inter_cell_hopping_terms, + reversed_inter_cell_hams): + # Transpose to get back correct shape wrt. original term + all_hams[n] = ham.transpose(0, 2, 1) + norbs, orb_offsets = _count_norbs(self, site_offsets, all_hams, + args=args, params=params) + + shape = (orb_offsets[next_cell], + orb_offsets[len(self.site_arrays) - next_cell]) + func = _vectorized_make_sparse if sparse else _vectorized_make_dense + mat = func(subgraphs, hams, norbs, orb_offsets, site_offsets, shape=shape) + return mat diff --git a/kwant/builder.py b/kwant/builder.py index 390ca0ba3c54d30d080d67c5002b43a49b74f4bf..31019c34533e879d383192f377e857b4e74f06d1 100644 --- a/kwant/builder.py +++ b/kwant/builder.py @@ -1,4 +1,4 @@ -# Copyright 2011-2016 Kwant authors. +# Copyright 2011-2019 Kwant authors. # # This file is part of Kwant. It is subject to the license terms in the file # LICENSE.rst found in the top-level directory of this distribution and at @@ -14,11 +14,14 @@ import copy from functools import total_ordering, wraps, update_wrapper from itertools import islice, chain import textwrap +import bisect +import numbers import inspect import tinyarray as ta import numpy as np from scipy import sparse from . import system, graph, KwantDeprecationWarning, UserCodeError +from .system import Site, SiteArray, SiteFamily from .linalg import lll from .operator import Density from .physics import DiscreteSymmetry, magnetic_gauge @@ -26,182 +29,13 @@ from ._common import (ensure_isinstance, get_parameters, reraise_warnings, interleave, deprecate_args, memoize) -__all__ = ['Builder', 'Site', 'SiteFamily', 'SimpleSiteFamily', 'Symmetry', - 'HoppingKind', 'Lead', 'BuilderLead', 'SelfEnergyLead', 'ModesLead', - 'add_peierls_phase'] +__all__ = ['Builder', 'SimpleSiteFamily', 'Symmetry', 'HoppingKind', 'Lead', + 'BuilderLead', 'SelfEnergyLead', 'ModesLead', 'add_peierls_phase'] -################ Sites and site families - -class Site(tuple): - """A site, member of a `SiteFamily`. - - Sites are the vertices of the graph which describes the tight binding - system in a `Builder`. - - A site is uniquely identified by its family and its tag. - - Parameters - ---------- - family : an instance of `SiteFamily` - The 'type' of the site. - tag : a hashable python object - The unique identifier of the site within the site family, typically a - vector of integers. - - Raises - ------ - ValueError - If `tag` is not a proper tag for `family`. - - Notes - ----- - For convenience, ``family(*tag)`` can be used instead of ``Site(family, - tag)`` to create a site. - - The parameters of the constructor (see above) are stored as instance - variables under the same names. Given a site ``site``, common things to - query are thus ``site.family``, ``site.tag``, and ``site.pos``. - """ - __slots__ = () - - family = property(operator.itemgetter(0), - doc="The site family to which the site belongs.") - tag = property(operator.itemgetter(1), doc="The tag of the site.") - - - def __new__(cls, family, tag, _i_know_what_i_do=False): - if _i_know_what_i_do: - return tuple.__new__(cls, (family, tag)) - try: - tag = family.normalize_tag(tag) - except (TypeError, ValueError) as e: - msg = 'Tag {0} is not allowed for site family {1}: {2}' - raise type(e)(msg.format(repr(tag), repr(family), e.args[0])) - return tuple.__new__(cls, (family, tag)) - - def __repr__(self): - return 'Site({0}, {1})'.format(repr(self.family), repr(self.tag)) - - def __str__(self): - sf = self.family - return '<Site {0} of {1}>'.format(self.tag, sf.name if sf.name else sf) - - def __getnewargs__(self): - return (self.family, self.tag, True) - - @property - def pos(self): - """Real space position of the site. - - This relies on ``family`` having a ``pos`` method (see `SiteFamily`). - """ - return self.family.pos(self.tag) - +################ Site families @total_ordering -class SiteFamily(metaclass=abc.ABCMeta): - """Abstract base class for site families. - - Site families are the 'type' of `Site` objects. Within a family, individual - sites are uniquely identified by tags. Valid tags must be hashable Python - objects, further details are up to the family. - - Site families must be immutable and fully defined by their initial - arguments. They must inherit from this abstract base class and call its - __init__ function providing it with two arguments: a canonical - representation and a name. The canonical representation will be returned as - the objects representation and must uniquely identify the site family - instance. The name is a string used to distinguish otherwise identical site - families. It may be empty. ``norbs`` defines the number of orbitals - on sites associated with this site family; it may be `None`, in which case - the number of orbitals is not specified. - - - All site families must define the method `normalize_tag` which brings a tag - to the standard format for this site family. - - Site families that are intended for use with plotting should also provide a - method `pos(tag)`, which returns a vector with real-space coordinates of the - site belonging to this family with a given tag. - - If the ``norbs`` of a site family are provided, and sites of this family - are used to populate a `~kwant.builder.Builder`, then the associated - Hamiltonian values must have the correct shape. That is, if a site family - has ``norbs = 2``, then any on-site terms for sites belonging to this - family should be 2x2 matrices. Similarly, any hoppings to/from sites - belonging to this family must have a matrix structure where there are two - rows/columns. This condition applies equally to Hamiltonian values that - are given by functions. If this condition is not satisfied, an error will - be raised. - """ - - def __init__(self, canonical_repr, name, norbs): - self.canonical_repr = canonical_repr - self.hash = hash(canonical_repr) - self.name = name - if norbs is None: - warnings.warn("Not specfying norbs is deprecated. Always specify " - "norbs when creating site families.", - KwantDeprecationWarning, stacklevel=3) - if norbs is not None: - if int(norbs) != norbs or norbs <= 0: - raise ValueError('The norbs parameter must be an integer > 0.') - norbs = int(norbs) - self.norbs = norbs - - def __repr__(self): - return self.canonical_repr - - def __str__(self): - if self.name: - msg = '<{0} site family {1}{2}>' - else: - msg = '<unnamed {0} site family{2}>' - orbs = ' with {0} orbitals'.format(self.norbs) if self.norbs else '' - return msg.format(self.__class__.__name__, self.name, orbs) - - def __hash__(self): - return self.hash - - def __eq__(self, other): - try: - return self.canonical_repr == other.canonical_repr - except AttributeError: - return False - - def __ne__(self, other): - try: - return self.canonical_repr != other.canonical_repr - except AttributeError: - return True - - def __lt__(self, other): - # If this raises an AttributeError, we were trying - # to compare it to something non-comparable anyway. - return self.canonical_repr < other.canonical_repr - - @abc.abstractmethod - def normalize_tag(self, tag): - """Return a normalized version of the tag. - - Raises TypeError or ValueError if the tag is not acceptable. - """ - pass - - def __call__(self, *tag): - """ - A convenience function. - - This function allows to write fam(1, 2) instead of Site(fam, (1, 2)). - """ - # Catch a likely and difficult to find mistake. - if tag and isinstance(tag[0], tuple): - raise ValueError('Use site_family(1, 2) instead of ' - 'site_family((1, 2))!') - return Site(self, tag) - - class SimpleSiteFamily(SiteFamily): """A site family used as an example and for testing. @@ -307,19 +141,44 @@ class Symmetry(metaclass=abc.ABCMeta): def which(self, site): """Calculate the domain of the site. - Return the group element whose action on a certain site from the - fundamental domain will result in the given ``site``. + Parameters + ---------- + site : `~kwant.system.Site` or `~kwant.system.SiteArray` + + Returns + ------- + group_element : tuple or sequence of tuples + A single tuple if ``site`` is a Site, or a sequence of tuples if + ``site`` is a SiteArray. The group element(s) whose action + on a certain site(s) from the fundamental domain will result + in the given ``site``. """ pass @abc.abstractmethod def act(self, element, a, b=None): - """Act with a symmetry group element on a site or hopping.""" + """Act with symmetry group element(s) on site(s) or hopping(s). + + Parameters + ---------- + element : tuple or sequence of tuples + Group element(s) with which to act on the provided site(s) + or hopping(s) + a, b : `~kwant.system.Site` or `~kwant.system.SiteArray` + If Site then ``element`` is a single tuple, if SiteArray then + ``element`` is a sequence of tuples. If only ``a`` is provided then + ``element`` acts on the site(s) of ``a``. If ``b`` is also provided + then ``element`` acts on the hopping(s) ``(a, b)``. + """ pass def to_fd(self, a, b=None): """Map a site or hopping to the fundamental domain. + Parameters + ---------- + a, b : `~kwant.system.Site` or `~kwant.system.SiteArray` + If ``b`` is None, return a site equivalent to ``a`` within the fundamental domain. Otherwise, return a hopping equivalent to ``(a, b)`` but where the first element belongs to the fundamental domain. @@ -329,11 +188,30 @@ class Symmetry(metaclass=abc.ABCMeta): return self.act(-self.which(a), a, b) def in_fd(self, site): - """Tell whether ``site`` lies within the fundamental domain.""" - for d in self.which(site): - if d != 0: - return False - return True + """Tell whether ``site`` lies within the fundamental domain. + + Parameters + ---------- + site : `~kwant.system.Site` or `~kwant.system.SiteArray` + + Returns + ------- + in_fd : bool or sequence of bool + single bool if ``site`` is a Site, or a sequence of + bool if ``site`` is a SiteArray. In the latter case + we return whether each site in the SiteArray is in + the fundamental domain. + """ + if isinstance(site, Site): + for d in self.which(site): + if d != 0: + return False + return True + elif isinstance(site, SiteArray): + which = self.which(site) + return np.logical_and.reduce(which != 0, axis=1) + else: + raise TypeError("'site' must be a Site or SiteArray") @abc.abstractmethod def subgroup(self, *generators): @@ -412,8 +290,8 @@ class HoppingKind(tuple): ---------- delta : Sequence of integers The sequence is interpreted as a vector with integer elements. - family_a : `~kwant.builder.SiteFamily` - family_b : `~kwant.builder.SiteFamily` or ``None`` (default) + family_a : `~kwant.system.SiteFamily` + family_b : `~kwant.system.SiteFamily` or ``None`` (default) The default value means: use the same family as `family_a`. Notes @@ -570,7 +448,7 @@ class BuilderLead(Lead): The tight-binding system of a lead. It has to possess appropriate symmetry, and it may not contain hoppings between further than neighboring images of the fundamental domain. - interface : sequence of `Site` instances + interface : sequence of `~kwant.system.Site` instances Sequence of sites in the scattering region to which the lead is attached. @@ -578,9 +456,9 @@ class BuilderLead(Lead): ---------- builder : `Builder` The tight-binding system of a lead. - interface : list of `Site` instances + interface : list of `~kwant.system.Site` instances A sorted list of interface sites. - padding : list of `Site` instances + padding : list of `~kwant.system.Site` instances A sorted list of sites that originate from the lead, have the same onsite Hamiltonian, and are connected by the same hoppings as the lead sites. @@ -607,7 +485,10 @@ class BuilderLead(Lead): The order of interface sites is kept during finalization. """ - return InfiniteSystem(self.builder, self.interface) + if self.builder.vectorize: + return InfiniteVectorizedSystem(self.builder, self.interface) + else: + return InfiniteSystem(self.builder, self.interface) def _ensure_signature(func): @@ -638,7 +519,7 @@ class SelfEnergyLead(Lead): selfenergy_func : function 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 + interface : sequence of `~kwant.system.Site` instances parameters : sequence of strings The parameters on which the lead depends. """ @@ -669,7 +550,7 @@ class ModesLead(Lead): and returns the modes of the lead as a tuple of `~kwant.physics.PropagatingModes` and `~kwant.physics.StabilizedModes`. interface : - sequence of `Site` instances + sequence of `~kwant.system.Site` instances parameters : sequence of strings The parameters on which the lead depends. """ @@ -797,10 +678,11 @@ class Builder: This is one of the central types in Kwant. It is used to construct tight binding systems in a flexible way. - The nodes of the graph are `Site` instances. The edges, i.e. the hoppings, - are pairs (2-tuples) of sites. Each node and each edge has a value - associated with it. The values associated with nodes are interpreted as - on-site Hamiltonians, the ones associated with edges as hopping integrals. + The nodes of the graph are `~kwant.system.Site` instances. The edges, + i.e. the hoppings, are pairs (2-tuples) of sites. Each node and each + edge has a value associated with it. The values associated with nodes + are interpreted as on-site Hamiltonians, the ones associated with edges + as hopping integrals. To make the graph accessible in a way that is natural within the Python language it is exposed as a *mapping* (much like a built-in Python @@ -827,6 +709,14 @@ class Builder: chiral : 2D array, dictionary, function or `None` The unitary part of the onsite chiral symmetry operator. Same format as that of `conservation_law`. + vectorize : bool, default: False + If True then the finalized Builder will evaluate its Hamiltonian in + a vectorized manner. This requires that all value functions take + `~kwant.system.SiteArray` as first/second parameter rather than + `~kwant.system.Site`, and return an array of values. The returned + array must have the same length as the provided SiteArray(s), and + may contain either scalars (i.e. a vector of values) or matrices + (i.e. a 3d array of values). Notes ----- @@ -905,7 +795,7 @@ class Builder: """ def __init__(self, symmetry=None, *, conservation_law=None, time_reversal=None, - particle_hole=None, chiral=None): + particle_hole=None, chiral=None, vectorize=False): if symmetry is None: symmetry = NoSymmetry() else: @@ -915,6 +805,7 @@ class Builder: self.time_reversal = time_reversal self.particle_hole = particle_hole self.chiral = chiral + self.vectorize = vectorize self.leads = [] self.H = {} @@ -998,6 +889,7 @@ class Builder: result.time_reversal = self.time_reversal result.particle_hole = self.particle_hole result.chiral = self.chiral + result.vectorize = self.vectorize result.leads = self.leads result.H = self.H return result @@ -1452,7 +1344,7 @@ class Builder: A boolean function of site returning whether the site should be added to the target builder or not. The shape must be compatible with the symmetry of the target builder. - start : `Site` instance or iterable thereof or iterable of numbers + start : `~kwant.system.Site` or iterable thereof or iterable of numbers The site(s) at which the the flood-fill starts. If start is an iterable of numbers, the starting site will be ``template.closest(start)``. @@ -1462,7 +1354,7 @@ class Builder: Returns ------- - added_sites : list of `Site` objects that were added to the system. + added_sites : list of `~kwant.system.Site` that were added to the system. Raises ------ @@ -1614,7 +1506,7 @@ class Builder: ---------- lead_builder : `Builder` with 1D translational symmetry Builder of the lead which has to be attached. - origin : `Site` + origin : `~kwant.system.Site` The site which should belong to a domain where the lead should begin. It is used to attach a lead inside the system, e.g. to an inner radius of a ring. @@ -1624,7 +1516,7 @@ class Builder: Returns ------- - added_sites : list of `Site` objects that were added to the system. + added_sites : list of `~kwant.system.Site` Raises ------ @@ -1677,6 +1569,13 @@ class Builder: if add_cells < 0 or int(add_cells) != add_cells: raise ValueError('add_cells must be an integer >= 0.') + if self.vectorize != lead_builder.vectorize: + raise ValueError( + "Sites of the lead were added to the scattering " + "region, but only one of these systems is vectorized. " + "Vectorize both systems to allow attaching leads." + ) + sym = lead_builder.symmetry H = lead_builder.H @@ -1697,6 +1596,7 @@ class Builder: time_reversal=lead_builder.time_reversal, particle_hole=lead_builder.particle_hole, chiral=lead_builder.chiral, + vectorize=lead_builder.vectorize, ) with reraise_warnings(): new_lead.fill(lead_builder, lambda site: True, @@ -1793,9 +1693,15 @@ class Builder: `Symmetry` can be finalized. """ if self.symmetry.num_directions == 0: - return FiniteSystem(self) + if self.vectorize: + return FiniteVectorizedSystem(self) + else: + return FiniteSystem(self) elif self.symmetry.num_directions == 1: - return InfiniteSystem(self) + if self.vectorize: + return InfiniteVectorizedSystem(self) + else: + return InfiniteSystem(self) else: raise ValueError('Currently, only builders without or with a 1D ' 'translational symmetry can be finalized.') @@ -1969,6 +1875,10 @@ def add_peierls_phase(syst, peierls_parameter='phi', fix_gauge=True): return f + if syst.vectorize: + raise TypeError("'add_peierls_phase' does not work with " + "vectorized Builders") + ret = _add_peierls_phase(syst, peierls_parameter).finalized() if fix_gauge: @@ -2115,6 +2025,135 @@ class _FinalizedBuilderMixin: return DiscreteSymmetry(projectors, *(evaluate(symm) for symm in self._symmetries)) + def pos(self, i): + return self.sites[i].pos + + +class _VectorizedFinalizedBuilderMixin(_FinalizedBuilderMixin): + """Common functionality for all vectorized finalized builders + + Attributes + ---------- + _term_values : sequence + Each item is either an array of values (if 'param_names is None') + or a vectorized value function (in which case 'param_names' is a + sequence of strings: the extra parameters to the value function). + _onsite_term_by_site_id : sequence of int + Maps site (integer) to the term that evaluates the associated + onsite matrix element. + _hopping_term_by_edge_id : sequence of int + Maps edge id in the graph to the term that evaluates the associated + hopping matrix element. If negative, then the associated hopping is + the Hermitian conjugate of another hopping; if the number stored is + 't' (< 0) then the associated hopping is stored in term '-t - 1'. + """ + def hamiltonian(self, i, j, *args, params=None): + warnings.warn( + "Querying individual matrix elements with 'hamiltonian' is " + "deprecated, and now takes O(log N) time where N is the number " + "of matrix elements per hamiltonian term. Query many matrix " + "elements at once using 'hamiltonian_term'.", + KwantDeprecationWarning + ) + site_offsets = np.cumsum([0] + [len(s) for s in self.site_arrays]) + if i == j: + which_term = self._onsite_term_by_site_id[i] + (w, _), (off, _) = self.subgraphs[self.terms[which_term].subgraph] + i_off = i - site_offsets[w] + selector = np.searchsorted(off, i_off) + onsite = self.hamiltonian_term( + which_term, [selector], args, params=params) + return onsite[0] + else: + edge_id = self.graph.first_edge_id(i, j) + which_term = self._hopping_term_by_edge_id[edge_id] + herm_conj = which_term < 0 + if herm_conj: + which_term = -which_term - 1 + term = self.terms[which_term] + # To search for hopping (i, j) in hopping_terms, we need + # to treat hopping_terms as a record array of integer pairs + dtype = np.dtype([('f0', int), ('f1', int)]) + # For hermitian conjugate terms search through the + # *other* term's hoppings because those are sorted. + + (to_w, from_w), hoppings = self.subgraphs[term.subgraph] + hoppings = hoppings.transpose() # to get array-of-pairs + if herm_conj: + hop = (j - site_offsets[to_w], i - site_offsets[from_w]) + else: + hop = (i - site_offsets[to_w], j - site_offsets[from_w]) + hop = np.array(hop, dtype=dtype) + hoppings = hoppings.view(dtype).reshape(-1) + selector = np.recarray.searchsorted(hoppings, hop) + h = self.hamiltonian_term( + which_term, [selector], args, params=params) + h = h[0] + if herm_conj: + h = h.conjugate().transpose() + return h + + def hamiltonian_term(self, term_number, selector=slice(None), + args=(), params=None): + if args and params: + raise TypeError("'args' and 'params' are mutually exclusive.") + + term = self.terms[term_number] + val = self._term_values[term_number] + + if not callable(val): + return val[selector] + + # Construct site arrays to pass to the vectorized value function. + subgraph = self.subgraphs[self.terms[term_number].subgraph] + (to_which, from_which), (to_off, from_off) = subgraph + is_onsite = to_off is from_off + to_off = to_off[selector] + from_off = from_off[selector] + assert len(to_off) == len(from_off) + + to_family = self.site_arrays[to_which].family + to_tags = self.site_arrays[to_which].tags + to_site_array = SiteArray(to_family, to_tags[to_off]) + if not is_onsite: + from_family = self.site_arrays[from_which].family + from_tags = self.site_arrays[from_which].tags + from_site_array = SiteArray(from_family, from_tags[from_off]) + + # Construct args from params + if params: + # There was a problem extracting parameter names from the value + # function (probably an illegal signature) and we are using + # keyword parameters. + if self._term_errors[term_number] is not None: + raise self._term_errors[term_number] + try: + args = [params[p] for p in term.parameters] + except KeyError: + missing = [p for p in term.parameters if p not in params] + msg = ('System is missing required arguments: ', + ', '.join(map('"{}"'.format, missing))) + raise TypeError(''.join(msg)) + + if is_onsite: + try: + ham = val(to_site_array, *args) + except Exception as exc: + _raise_user_error(exc, val) + else: + try: + ham = val( + *self.symmetry.to_fd( + to_site_array, + from_site_array), + *args) + except Exception as exc: + _raise_user_error(exc, val) + + ham = _normalize_term_value(ham, len(to_site_array)) + + return ham + # The same (value, parameters) pair will be used for many sites/hoppings, # so we cache it to avoid wasting extra memory. @@ -2147,6 +2186,64 @@ def _value_params_pair_cache(nstrip): return get +def _make_graph(H, id_by_site): + g = graph.Graph() + g.num_nodes = len(id_by_site) # Some sites could not appear in any edge. + for tail, hvhv in H.items(): + for head in islice(hvhv, 2, None, 2): + if tail == head: + continue + g.add_edge(id_by_site[tail], id_by_site[head]) + return g.compressed() + + +def _finalize_leads(leads, id_by_site): + #### Connect leads. + finalized_leads = [] + lead_interfaces = [] + lead_paddings = [] + for lead_nr, lead in enumerate(leads): + try: + with warnings.catch_warnings(record=True) as ws: + warnings.simplefilter("always") + # The following line is the whole "payload" of the entire + # try-block. + finalized_leads.append(lead.finalized()) + except ValueError as e: + # Re-raise the exception with an additional message. + msg = 'Problem finalizing lead {0}:'.format(lead_nr) + e.args = (' '.join((msg,) + e.args),) + raise + else: + for w in ws: + # Re-raise any warnings with an additional message and the + # proper stacklevel. + w = w.message + msg = 'When finalizing lead {0}:'.format(lead_nr) + warnings.warn(w.__class__(' '.join((msg,) + w.args)), + stacklevel=3) + try: + interface = [id_by_site[isite] for isite in lead.interface] + except KeyError as e: + msg = ("Lead {0} is attached to a site that does not " + "belong to the scattering region:\n {1}") + raise ValueError(msg.format(lead_nr, e.args[0])) + + lead_interfaces.append(np.array(interface)) + + padding = getattr(lead, 'padding', []) + # Some padding sites might have been removed after the lead was + # attached. Unlike in the case of the interface, this is not a + # problem. + finalized_padding = [ + id_by_site[isite] for isite in padding if isite in id_by_site + ] + + lead_paddings.append(np.array(finalized_padding)) + + return finalized_leads, lead_interfaces, lead_paddings + + class FiniteSystem(_FinalizedBuilderMixin, system.FiniteSystem): """Finalized `Builder` with leads. @@ -2155,11 +2252,11 @@ class FiniteSystem(_FinalizedBuilderMixin, system.FiniteSystem): Attributes ---------- sites : sequence - ``sites[i]`` is the `~kwant.builder.Site` instance that corresponds + ``sites[i]`` is the `~kwant.system.Site` instance that corresponds to the integer-labeled site ``i`` of the low-level system. The sites are ordered first by their family and then by their tag. - id_by_site : dict - The inverse of ``sites``; maps high-level `~kwant.builder.Site` + id_by_site : mapping + The inverse of ``sites``; maps high-level `~kwant.system.Site` instances to their integer label. Satisfies ``id_by_site[sites[i]] == i``. """ @@ -2173,57 +2270,10 @@ class FiniteSystem(_FinalizedBuilderMixin, system.FiniteSystem): for site_id, site in enumerate(sites): id_by_site[site] = site_id - #### Make graph. - g = graph.Graph() - g.num_nodes = len(sites) # Some sites could not appear in any edge. - for tail, hvhv in builder.H.items(): - for head in islice(hvhv, 2, None, 2): - if tail == head: - continue - g.add_edge(id_by_site[tail], id_by_site[head]) - g = g.compressed() - - #### Connect leads. - finalized_leads = [] - lead_interfaces = [] - lead_paddings = [] - for lead_nr, lead in enumerate(builder.leads): - try: - with warnings.catch_warnings(record=True) as ws: - warnings.simplefilter("always") - # The following line is the whole "payload" of the entire - # try-block. - finalized_leads.append(lead.finalized()) - for w in ws: - # Re-raise any warnings with an additional message and the - # proper stacklevel. - w = w.message - msg = 'When finalizing lead {0}:'.format(lead_nr) - warnings.warn(w.__class__(' '.join((msg,) + w.args)), - stacklevel=3) - except ValueError as e: - # Re-raise the exception with an additional message. - msg = 'Problem finalizing lead {0}:'.format(lead_nr) - e.args = (' '.join((msg,) + e.args),) - raise - try: - interface = [id_by_site[isite] for isite in lead.interface] - except KeyError as e: - msg = ("Lead {0} is attached to a site that does not " - "belong to the scattering region:\n {1}") - raise ValueError(msg.format(lead_nr, e.args[0])) - - lead_interfaces.append(np.array(interface)) - - padding = getattr(lead, 'padding', []) - # Some padding sites might have been removed after the lead was - # attached. Unlike in the case of the interface, this is not a - # problem. - finalized_padding = [ - id_by_site[isite] for isite in padding if isite in id_by_site - ] + graph = _make_graph(builder.H, id_by_site) - lead_paddings.append(np.array(finalized_padding)) + finalized_leads, lead_interfaces, lead_paddings =\ + _finalize_leads(builder.leads, id_by_site) # Because many onsites/hoppings share the same (value, parameter) # pairs, we keep them in a cache so that we only store a given pair @@ -2232,7 +2282,7 @@ class FiniteSystem(_FinalizedBuilderMixin, system.FiniteSystem): onsites = [cache(builder.H[site][1]) for site in sites] cache = _value_params_pair_cache(2) hoppings = [cache(builder._get_edge(sites[tail], sites[head])) - for tail, head in g] + for tail, head in graph] # Compute the union of the parameters of onsites and hoppings. Here, # 'onsites' and 'hoppings' are pairs whose second element is one of @@ -2252,7 +2302,7 @@ class FiniteSystem(_FinalizedBuilderMixin, system.FiniteSystem): else: parameters = frozenset(parameters) - self.graph = g + self.graph = graph self.sites = sites self.site_ranges = _site_ranges(sites) self.id_by_site = id_by_site @@ -2265,8 +2315,502 @@ class FiniteSystem(_FinalizedBuilderMixin, system.FiniteSystem): self.lead_paddings = lead_paddings self._init_discrete_symmetries(builder) - def pos(self, i): - return self.sites[i].pos + +def _make_site_arrays(sites): + tags_by_family = {} + for family, tag in sites: # Sites are tuples + tags_by_family.setdefault(family, []).append(tag) + site_arrays = [] + for family, tags in sorted(tags_by_family.items()): + site_arrays.append(SiteArray(family, sorted(tags))) + return site_arrays + + +# Wrapper for accessing sites by their sequential number from a +# list of SiteArrays. +class _Sites: + + def __init__(self, site_arrays): + self._site_arrays = site_arrays + lengths = [0] + [len(arr.tags) for arr in site_arrays] + self._start_idxs = np.cumsum(lengths)[:-1] + + def __len__(self): + return sum(len(arr.tags) for arr in self._site_arrays) + + def __getitem__(self, idx): + if not isinstance(idx, numbers.Integral): + raise TypeError("Only individual sites may be indexed") + if idx < 0 or idx >= len(self): + raise IndexError(idx) + + which = bisect.bisect(self._start_idxs, idx) - 1 + arr = self._site_arrays[which] + start = self._start_idxs[which] + fam = arr.family + tag = arr.tags[idx - start] + return Site(fam, tag) + + def __iter__(self): + for arr in self._site_arrays: + for tag in arr.tags: + yield Site(arr.family, tag) + + +# Wrapper for accessing the sequential number of a site, given +# Site object, from a list of SiteArrays. +class _IdBySite: + + def __init__(self, site_arrays): + self._site_arrays = site_arrays + lengths = [0] + [len(arr.tags) for arr in site_arrays] + self._start_idxs = np.cumsum(lengths)[:-1] + + def __len__(self): + return sum(len(arr.tags) for arr in self._site_arrays) + + def __getitem__(self, site): + # treat tags as 1D sequence by defining custom dtype + tag_dtype = np.asarray(site.tag).dtype + dtype = np.dtype([('f{}'.format(i), tag_dtype) + for i in range(len(site.tag))]) + site_tag = np.asarray(site.tag).view(dtype)[0] + # This slightly convoluted logic is necessary because there + # may be >1 site_array with the same family for InfiniteSystems. + for start, arr in zip(self._start_idxs, self._site_arrays): + if arr.family != site.family: + continue + tags = arr.tags.view(dtype).reshape(-1) + idx_in_fam = np.recarray.searchsorted(tags, site_tag) + if idx_in_fam >= len(tags) or tags[idx_in_fam] != site_tag: + continue + return start + idx_in_fam + + raise KeyError(site) + + +def _normalize_term_value(value, expected_n_values): + try: + value = np.asarray(value, dtype=complex) + except TypeError: + raise ValueError( + "Matrix elements declared with incompatible shapes." + ) from None + # Upgrade to vector of matrices + if len(value.shape) == 1: + value = value[:, np.newaxis, np.newaxis] + if len(value.shape) != 3: + msg = ( + "Vectorized value functions must return an array of" + "scalars or an array of matrices." + ) + raise ValueError(msg) + if value.shape[0] != expected_n_values: + raise ValueError("Value functions must return a single value per " + "onsite/hopping.") + return value + + +def _sort_term(term, value): + term = np.asarray(term) + + if not callable(value): + value = _normalize_term_value(value, len(term)) + # Ensure that values still correspond to the correct + # sites in 'term' once the latter has been sorted. + value = value[term.argsort()] + + term.sort() + + return term, value + + +def _sort_hopping_term(term, value): + term = np.asarray(term) + # We want to sort the hopping terms in the same way + # as tuples (i, j), so we need to use a record array. + dtype = np.dtype([('f0', term.dtype), ('f1', term.dtype)]) + term_prime = term.view(dtype=dtype).reshape(-1) + # _normalize_term will sort 'term' in-place via 'term_prime' + _, value = _sort_term(term_prime, value) + + return term, value + + +def _make_onsite_terms(builder, sites, site_offsets, term_offset): + # Construct onsite terms. + # + # onsite_subgraphs + # Will contain tuples of the form described in + # kwant.system.VectorizedSystem.subgraphs, but during construction + # contains lists of the sites associated with each onsite term. + # + # onsite_term_values + # Contains a callable or array of constant values for each term. + # + # onsite_terms + # Constructed after the main loop. Contains a kwant.system.Term + # tuple for each onsite term. + # + # _onsite_term_by_site_id + # Maps the site ID to the number of the term that the site is + # a part of. + # lists the number of the + # Hamiltonian term associated with each site/hopping. For + # Hermitian conjugate hoppings "-term - 1" is stored instead. + + onsite_subgraphs = [] + onsite_term_values = [] + onsite_term_parameters = [] + # We just use the cache to handle non/callables and exceptions + cache = _value_params_pair_cache(1) + # maps onsite key -> onsite term number + onsite_to_term_nr = collections.OrderedDict() + _onsite_term_by_site_id = [] + for site_id, site in enumerate(sites): + val = builder.H[builder.symmetry.to_fd(site)][1] + const_val = not callable(val) + which_site_array = bisect.bisect(site_offsets, site_id) - 1 + # "key" uniquely identifies an onsite term. + if const_val: + key = (None, which_site_array) + else: + key = (id(val), which_site_array) + if key not in onsite_to_term_nr: + # Start a new term + onsite_to_term_nr[key] = len(onsite_subgraphs) + onsite_subgraphs.append([]) + if const_val: + onsite_term_values.append([]) + onsite_term_parameters.append(None) + else: + val, params = cache(val) + onsite_term_values.append(val) + onsite_term_parameters.append(params) + onsite_subgraphs[onsite_to_term_nr[key]].append(site_id) + _onsite_term_by_site_id.append(onsite_to_term_nr[key]) + if const_val: + vals = onsite_term_values[onsite_to_term_nr[key]] + vals.append(val) + # Sort the sites in each term, and normalize any constant + # values to arrays of the correct dtype and shape. + onsite_subgraphs, onsite_term_values = zip(*( + _sort_term(term, value) + for term, value in + zip(onsite_subgraphs, onsite_term_values))) + # Store site array index and site offsets rather than sites themselves + tmp = [] + for (_, which), s in zip(onsite_to_term_nr, onsite_subgraphs): + s = s - site_offsets[which] + tmp.append(((which, which), (s, s))) + onsite_subgraphs = tmp + # onsite_term_errors[i] contains an exception if the corresponding + # term has a value function with an illegal signature. We only raise + # the exception if we actually try to evaluate the offending term + # (to maintain backwards compatibility). + onsite_term_errors = [ + err if isinstance(err, Exception) else None + for err in onsite_term_parameters + ] + onsite_terms = [ + system.Term( + subgraph=term_offset + i, + hermitian=False, + parameters=( + params if not isinstance(params, Exception) else None + ), + ) + for i, params in enumerate(onsite_term_parameters) + ] + _onsite_term_by_site_id = np.array(_onsite_term_by_site_id) + + return (onsite_subgraphs, onsite_terms, onsite_term_values, + onsite_term_parameters, onsite_term_errors, _onsite_term_by_site_id) + + +def _make_hopping_terms(builder, graph, sites, site_offsets, cell_size, term_offset): + # Construct the hopping terms + # + # The logic is the same as for the onsite terms, with the following + # differences. + # + # _hopping_term_by_edge_id + # Maps hopping edge IDs to the number of the term that the hopping + # is a part of. For Hermitian conjugate hoppings "-term_number -1" + # is stored instead. + + hopping_subgraphs = [] + hopping_term_values = [] + hopping_term_parameters = [] + # We just use the cache to handle non/callables and exceptions + cache = _value_params_pair_cache(2) + # maps hopping key -> hopping term number + hopping_to_term_nr = collections.OrderedDict() + _hopping_term_by_edge_id = [] + for tail, head in graph: + tail_site, head_site = sites[tail], sites[head] + if tail >= cell_size: + # The tail belongs to the previous domain. Find the + # corresponding hopping with the tail in the fund. domain. + tail_site, head_site = builder.symmetry.to_fd(tail_site, head_site) + val = builder._get_edge(tail_site, head_site) + herm_conj = val is Other + if herm_conj: + val = builder._get_edge(*builder.symmetry.to_fd(head_site, tail_site)) + const_val = not callable(val) + + tail_site_array = bisect.bisect(site_offsets, tail) - 1 + head_site_array = bisect.bisect(site_offsets, head) - 1 + # "key" uniquely identifies a hopping term. + if const_val: + key = (None, tail_site_array, head_site_array) + else: + key = (id(val), tail_site_array, head_site_array) + if herm_conj: + key = (key[0], head_site_array, tail_site_array) + + if key not in hopping_to_term_nr: + # start a new term + hopping_to_term_nr[key] = len(hopping_subgraphs) + hopping_subgraphs.append([]) + if const_val: + hopping_term_values.append([]) + hopping_term_parameters.append(None) + else: + val, params = cache(val) + hopping_term_values.append(val) + hopping_term_parameters.append(params) + + if herm_conj: + # Hopping terms come after all onsite terms, so we need + # to offset the term ID by that many + term_id = -term_offset - hopping_to_term_nr[key] - 1 + _hopping_term_by_edge_id.append(term_id) + else: + # Hopping terms come after all onsite terms, so we need + # to offset the term ID by that many + term_id = term_offset + hopping_to_term_nr[key] + _hopping_term_by_edge_id.append(term_id) + hopping_subgraphs[hopping_to_term_nr[key]].append((tail, head)) + if const_val: + vals = hopping_term_values[hopping_to_term_nr[key]] + vals.append(val) + # Sort the hoppings in each term, and normalize any constant + # values to arrays of the correct dtype and shape. + if hopping_subgraphs: + hopping_subgraphs, hopping_term_values = zip(*( + _sort_hopping_term(term, value) + for term, value in + zip(hopping_subgraphs, hopping_term_values))) + # Store site array index and site offsets rather than sites themselves + tmp = [] + for (_, tail_which, head_which), h in zip(hopping_to_term_nr, + hopping_subgraphs): + start = (site_offsets[tail_which], site_offsets[head_which]) + # Transpose to get a pair of arrays rather than array of pairs + # We use the fact that the underlying array is stored in + # array-of-pairs order to search through it in 'hamiltonian'. + tmp.append(((tail_which, head_which), (h - start).transpose())) + hopping_subgraphs = tmp + # hopping_term_errors[i] contains an exception if the corresponding + # term has a value function with an illegal signature. We only raise + # the exception if this becomes a problem (to maintain backwards + # compatibility) + hopping_term_errors = [ + err if isinstance(err, Exception) else None + for err in hopping_term_parameters + ] + hopping_terms = [ + system.Term( + subgraph=term_offset + i, + hermitian=True, # Builders are always Hermitian + parameters=( + params if not isinstance(params, Exception) else None + ), + ) + for i, params in enumerate(hopping_term_parameters) + ] + _hopping_term_by_edge_id = np.array(_hopping_term_by_edge_id) + + return (hopping_subgraphs, hopping_terms, hopping_term_values, + hopping_term_parameters, hopping_term_errors, + _hopping_term_by_edge_id) + + +class FiniteVectorizedSystem(_VectorizedFinalizedBuilderMixin, system.FiniteVectorizedSystem): + """Finalized `Builder` with leads. + + Usable as input for the solvers in `kwant.solvers`. + + Attributes + ---------- + site_arrays : sequence of `~kwant.system.SiteArray` + The sites of the system. + sites : sequence + ``sites[i]`` is the `~kwant.system.Site` instance that corresponds + to the integer-labeled site ``i`` of the low-level system. The sites + are ordered first by their family and then by their tag. + id_by_site : mapping + The inverse of ``sites``; maps high-level `~kwant.system.Site` + instances to their integer label. + Satisfies ``id_by_site[sites[i]] == i``. + """ + + def __init__(self, builder): + assert builder.symmetry.num_directions == 0 + assert builder.vectorize + + sites = sorted(builder.H) + id_by_site = {site: site_id for site_id, site in enumerate(sites)} + + if not all(s.family.norbs for s in sites): + raise ValueError("All sites must belong to families with norbs " + "specified for vectorized Builders. Specify " + "norbs when creating site families.") + + graph = _make_graph(builder.H, id_by_site) + + finalized_leads, lead_interfaces, lead_paddings =\ + _finalize_leads(builder.leads, id_by_site) + + del id_by_site # cleanup due to large size + + site_arrays = _make_site_arrays(builder.H) + # We need this to efficiently find which array a given + # site belongs to + site_offsets = np.cumsum([0] + [len(arr) for arr in site_arrays]) + + (onsite_subgraphs, onsite_terms, onsite_term_values, + onsite_term_parameters, onsite_term_errors, _onsite_term_by_site_id) =\ + _make_onsite_terms(builder, sites, site_offsets, term_offset=0) + + (hopping_subgraphs, hopping_terms, hopping_term_values, + hopping_term_parameters, hopping_term_errors, + _hopping_term_by_edge_id) =\ + _make_hopping_terms(builder, graph, sites, site_offsets, + len(sites), term_offset=len(onsite_terms)) + + # Construct the combined onsite/hopping term datastructures + subgraphs = tuple(onsite_subgraphs) + tuple(hopping_subgraphs) + terms = tuple(onsite_terms) + tuple(hopping_terms) + term_values = tuple(onsite_term_values) + tuple(hopping_term_values) + term_errors = tuple(onsite_term_errors) + tuple(hopping_term_errors) + + # Construct system parameters + parameters = set() + for params in chain(onsite_term_parameters, hopping_term_parameters): + if params is not None: + parameters.update(params) + parameters = frozenset(parameters) + + self.site_arrays = site_arrays + self.sites = _Sites(self.site_arrays) + self.id_by_site = _IdBySite(self.site_arrays) + self.graph = graph + self.subgraphs = subgraphs + self.terms = terms + self._term_values = term_values + self._term_errors = term_errors + self._hopping_term_by_edge_id = _hopping_term_by_edge_id + self._onsite_term_by_site_id = _onsite_term_by_site_id + 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 _make_lead_sites(builder, interface_order): + #### For each site of the fundamental domain, determine whether it has + #### neighbors in the previous domain or not. + sym = builder.symmetry + lsites_with = [] # Fund. domain sites with neighbors in prev. dom + lsites_without = [] # Remaining sites of the fundamental domain + for tail in builder.H: # Loop over all sites of the fund. domain. + for head in builder._out_neighbors(tail): + fd = sym.which(head)[0] + if fd == 1: + # Tail belongs to fund. domain, head to the next domain. + lsites_with.append(tail) + break + else: + # Tail is a fund. domain site not connected to prev. domain. + lsites_without.append(tail) + + if not lsites_with: + warnings.warn('Infinite system with disconnected cells.', + RuntimeWarning, stacklevel=3) + + ### Create list of sites and a lookup table + minus_one = ta.array((-1,)) + plus_one = ta.array((1,)) + if interface_order is None: + # interface must be sorted + interface = [sym.act(minus_one, s) for s in lsites_with] + interface.sort() + else: + lsites_with_set = set(lsites_with) + lsites_with = [] + interface = [] + if interface_order: + shift = ta.array((-sym.which(interface_order[0])[0] - 1,)) + for shifted_iface_site in interface_order: + # Shift the interface domain before the fundamental domain. + # That's the right place for the interface of a lead to be, but + # the sites of interface_order might live in a different + # domain. + iface_site = sym.act(shift, shifted_iface_site) + lsite = sym.act(plus_one, iface_site) + + try: + lsites_with_set.remove(lsite) + except KeyError: + if (-sym.which(shifted_iface_site)[0] - 1,) != shift: + raise ValueError( + 'The sites in interface_order do not all ' + 'belong to the same lead cell.') + else: + raise ValueError('A site in interface_order is not an ' + 'interface site:\n' + str(iface_site)) + interface.append(iface_site) + lsites_with.append(lsite) + if lsites_with_set: + raise ValueError( + 'interface_order did not contain all interface sites.') + # `interface_order` *must* be sorted, hence `interface` should also + if interface != sorted(interface): + raise ValueError('Interface sites must be sorted.') + del lsites_with_set + + return sorted(lsites_with), sorted(lsites_without), interface + + +def _make_lead_graph(builder, sites, id_by_site, cell_size): + sym = builder.symmetry + g = graph.Graph() + g.num_nodes = len(sites) # Some sites could not appear in any edge. + for tail_id, tail in enumerate(sites[:cell_size]): + for head in builder._out_neighbors(tail): + head_id = id_by_site.get(head) + if head_id is None: + # Head belongs neither to the fundamental domain nor to the + # previous domain. Check that it belongs to the next + # domain and ignore it otherwise as an edge corresponding + # to this one has been added already or will be added. + fd = sym.which(head)[0] + if fd != 1: + msg = ('Further-than-nearest-neighbor cells ' + 'are connected by hopping\n{0}.') + raise ValueError(msg.format((tail, head))) + continue + if head_id >= cell_size: + # Head belongs to previous domain. The edge added here + # correspond to one left out just above. + g.add_edge(head_id, tail_id) + g.add_edge(tail_id, head_id) + + return g.compressed() class InfiniteSystem(_FinalizedBuilderMixin, system.InfiniteSystem): @@ -2275,10 +2819,10 @@ class InfiniteSystem(_FinalizedBuilderMixin, system.InfiniteSystem): Attributes ---------- sites : sequence - ``sites[i]`` is the `~kwant.builder.Site` instance that corresponds + ``sites[i]`` is the `~kwant.system.Site` instance that corresponds to the integer-labeled site ``i`` of the low-level system. - id_by_site : dict - The inverse of ``sites``; maps high-level `~kwant.builder.Site` + id_by_site : mapping + The inverse of ``sites``; maps high-level `~kwant.system.Site` instances to their integer label. Satisfies ``id_by_site[sites[i]] == i``. @@ -2302,69 +2846,12 @@ class InfiniteSystem(_FinalizedBuilderMixin, system.InfiniteSystem): sym = builder.symmetry assert sym.num_directions == 1 - #### For each site of the fundamental domain, determine whether it has - #### neighbors in the previous domain or not. - lsites_with = [] # Fund. domain sites with neighbors in prev. dom - lsites_without = [] # Remaining sites of the fundamental domain - for tail in builder.H: # Loop over all sites of the fund. domain. - for head in builder._out_neighbors(tail): - fd = sym.which(head)[0] - if fd == 1: - # Tail belongs to fund. domain, head to the next domain. - lsites_with.append(tail) - break - else: - # Tail is a fund. domain site not connected to prev. domain. - lsites_without.append(tail) + lsites_with, lsites_without, interface =\ + _make_lead_sites(builder, interface_order) cell_size = len(lsites_with) + len(lsites_without) - if not lsites_with: - warnings.warn('Infinite system with disconnected cells.', - RuntimeWarning, stacklevel=3) - - ### Create list of sites and a lookup table - minus_one = ta.array((-1,)) - plus_one = ta.array((1,)) - if interface_order is None: - # interface must be sorted - interface = [sym.act(minus_one, s) for s in lsites_with] - interface.sort() - else: - lsites_with_set = set(lsites_with) - lsites_with = [] - interface = [] - if interface_order: - shift = ta.array((-sym.which(interface_order[0])[0] - 1,)) - for shifted_iface_site in interface_order: - # Shift the interface domain before the fundamental domain. - # That's the right place for the interface of a lead to be, but - # the sites of interface_order might live in a different - # domain. - iface_site = sym.act(shift, shifted_iface_site) - lsite = sym.act(plus_one, iface_site) - - try: - lsites_with_set.remove(lsite) - except KeyError: - if (-sym.which(shifted_iface_site)[0] - 1,) != shift: - raise ValueError( - 'The sites in interface_order do not all ' - 'belong to the same lead cell.') - else: - raise ValueError('A site in interface_order is not an ' - 'interface site:\n' + str(iface_site)) - interface.append(iface_site) - lsites_with.append(lsite) - if lsites_with_set: - raise ValueError( - 'interface_order did not contain all interface sites.') - # `interface_order` *must* be sorted, hence `interface` should also - if interface != sorted(interface): - raise ValueError('Interface sites must be sorted.') - del lsites_with_set - # we previously sorted the interface, so don't sort it again - sites = sorted(lsites_with) + sorted(lsites_without) + interface + sites = lsites_with + lsites_without + interface del lsites_with del lsites_without del interface @@ -2372,41 +2859,20 @@ class InfiniteSystem(_FinalizedBuilderMixin, system.InfiniteSystem): for site_id, site in enumerate(sites): id_by_site[site] = site_id + graph = _make_lead_graph(builder, sites, id_by_site, cell_size) + # In the following, because many onsites/hoppings share the same # (value, parameter) pairs, we keep them in 'cache' so that we only # store a given pair in memory *once*. This is like interning strings. - #### Make graph and extract onsite Hamiltonians. + #### Extract onsites cache = _value_params_pair_cache(1) - g = graph.Graph() - g.num_nodes = len(sites) # Some sites could not appear in any edge. - onsites = [] - for tail_id, tail in enumerate(sites[:cell_size]): - onsites.append(cache(builder.H[tail][1])) - for head in builder._out_neighbors(tail): - head_id = id_by_site.get(head) - if head_id is None: - # Head belongs neither to the fundamental domain nor to the - # previous domain. Check that it belongs to the next - # domain and ignore it otherwise as an edge corresponding - # to this one has been added already or will be added. - fd = sym.which(head)[0] - if fd != 1: - msg = ('Further-than-nearest-neighbor cells ' - 'are connected by hopping\n{0}.') - raise ValueError(msg.format((tail, head))) - continue - if head_id >= cell_size: - # Head belongs to previous domain. The edge added here - # correspond to one left out just above. - g.add_edge(head_id, tail_id) - g.add_edge(tail_id, head_id) - g = g.compressed() + onsites = [cache(builder.H[tail][1]) for tail in sites[:cell_size]] #### Extract hoppings. cache = _value_params_pair_cache(2) hoppings = [] - for tail_id, head_id in g: + for tail_id, head_id in graph: tail = sites[tail_id] head = sites[head_id] if tail_id >= cell_size: @@ -2433,7 +2899,7 @@ class InfiniteSystem(_FinalizedBuilderMixin, system.InfiniteSystem): else: parameters = frozenset(parameters) - self.graph = g + self.graph = graph self.sites = sites self.site_ranges = _site_ranges(sites) self.id_by_site = id_by_site @@ -2444,6 +2910,125 @@ class InfiniteSystem(_FinalizedBuilderMixin, system.InfiniteSystem): self.cell_size = cell_size self._init_discrete_symmetries(builder) + def hamiltonian(self, i, j, *args, params=None): + cs = self.cell_size + if i == j >= cs: + i -= cs + j -= cs + return super().hamiltonian(i, j, *args, params=params) + + +class InfiniteVectorizedSystem(_VectorizedFinalizedBuilderMixin, system.InfiniteVectorizedSystem): + """Finalized infinite system, extracted from a `Builder`. + + Attributes + ---------- + site_arrays : sequence of `~kwant.system.SiteArray` + The sites of the system. + sites : sequence + ``sites[i]`` is the `~kwant.system.Site` instance that corresponds + to the integer-labeled site ``i`` of the low-level system. + id_by_site : mapping + The inverse of ``sites``; maps high-level `~kwant.system.Site` + instances to their integer label. + Satisfies ``id_by_site[sites[i]] == i``. + + Notes + ----- + In infinite systems ``sites`` and ``site_arrays`` consists of 3 parts: + sites in the fundamental domain (FD) with hoppings to neighboring cells, + sites in the FD with no hoppings to neighboring cells, and sites in FD+1 + attached to the FD by hoppings. Each of these three subsequences is + individually sorted. + """ + + def __init__(self, builder, interface_order=None): + """ + Finalize a builder instance which has to have exactly a single + symmetry direction. + + If interface_order is not set, the order of the interface sites in the + finalized system will be arbitrary. If interface_order is set to a + sequence of interface sites, this order will be kept. + """ + sym = builder.symmetry + assert sym.num_directions == 1 + assert builder.vectorize + + lsites_with, lsites_without, interface =\ + _make_lead_sites(builder, interface_order) + cell_size = len(lsites_with) + len(lsites_without) + + + sites = lsites_with + lsites_without + interface + id_by_site = {} + for site_id, site in enumerate(sites): + id_by_site[site] = site_id + # these are needed for constructing hoppings + lsites_with = frozenset(lsites_with) + lsites_without = frozenset(lsites_without) + interface = frozenset(interface) + + if not all(s.family.norbs for s in sites): + raise ValueError("All sites must belong to families with norbs " + "specified for vectorized Builders. Specify " + "norbs when creating site families.") + + graph = _make_lead_graph(builder, sites, id_by_site, cell_size) + + del id_by_site # cleanup due to large size + + # In order to conform to the kwant.system.InfiniteVectorizedSystem + # interface we need to put the sites that connect to the previous + # cell *first*, then the sites that do not couple to the previous + # cell, then the sites in the previous cell. Because sites in + # a SiteArray are sorted by tag this means that the sites in these + # 3 different sets need to be in different SiteArrays. + site_arrays = ( + _make_site_arrays(lsites_with) + + _make_site_arrays(lsites_without) + + _make_site_arrays(interface) + ) + + site_offsets = np.cumsum([0] + [len(arr) for arr in site_arrays]) + + (onsite_subgraphs, onsite_terms, onsite_term_values, + onsite_term_parameters, onsite_term_errors, _onsite_term_by_site_id) =\ + _make_onsite_terms(builder, sites, site_offsets, term_offset=0) + + (hopping_subgraphs, hopping_terms, hopping_term_values, + hopping_term_parameters, hopping_term_errors, + _hopping_term_by_edge_id) =\ + _make_hopping_terms(builder, graph, sites, site_offsets, + cell_size, term_offset=len(onsite_terms)) + + # Construct the combined onsite/hopping term datastructures + subgraphs = tuple(onsite_subgraphs) + tuple(hopping_subgraphs) + terms = tuple(onsite_terms) + tuple(hopping_terms) + term_values = tuple(onsite_term_values) + tuple(hopping_term_values) + term_errors = tuple(onsite_term_errors) + tuple(hopping_term_errors) + + # Construct system parameters + parameters = set() + for params in chain(onsite_term_parameters, hopping_term_parameters): + if params is not None: + parameters.update(params) + parameters = frozenset(parameters) + + self.site_arrays = site_arrays + self.sites = _Sites(self.site_arrays) + self.id_by_site = _IdBySite(self.site_arrays) + self.graph = graph + self.subgraphs = subgraphs + self.terms = terms + self._term_values = term_values + self._term_errors = term_errors + self._hopping_term_by_edge_id = _hopping_term_by_edge_id + self._onsite_term_by_site_id = _onsite_term_by_site_id + self.parameters = parameters + self.symmetry = builder.symmetry + self.cell_size = cell_size + self._init_discrete_symmetries(builder) def hamiltonian(self, i, j, *args, params=None): cs = self.cell_size @@ -2452,5 +3037,15 @@ class InfiniteSystem(_FinalizedBuilderMixin, system.InfiniteSystem): j -= cs return super().hamiltonian(i, j, *args, params=params) - def pos(self, i): - return self.sites[i].pos + +def is_finite_system(syst): + return isinstance(syst, (FiniteSystem, FiniteVectorizedSystem)) + + +def is_infinite_system(syst): + return isinstance(syst, (FiniteSystem, FiniteVectorizedSystem)) + + +def is_system(syst): + return isinstance(syst, (FiniteSystem, FiniteVectorizedSystem, + InfiniteSystem, InfiniteVectorizedSystem)) diff --git a/kwant/continuum/discretizer.py b/kwant/continuum/discretizer.py index 53f2379cd53f17588dc9e59cb0300a11e90c54c3..ce9e6fe2f65bffeed3da826b02f1618e474a1757 100644 --- a/kwant/continuum/discretizer.py +++ b/kwant/continuum/discretizer.py @@ -1,4 +1,4 @@ -# Copyright 2011-2017 Kwant authors. +# Copyright 2011-2019 Kwant authors. # # This file is part of Kwant. It is subject to the license terms in the file # LICENSE.rst found in the top-level directory of this distribution and at @@ -20,7 +20,7 @@ from sympy.printing.lambdarepr import LambdaPrinter from sympy.printing.precedence import precedence from sympy.core.function import AppliedUndef -from .. import builder, lattice +from .. import builder, lattice, system from .. import KwantDeprecationWarning from .._common import reraise_warnings from ._common import (sympify, gcd, position_operators, momentum_operators, @@ -63,7 +63,7 @@ class _DiscretizedBuilder(builder.Builder): for key, val in itertools.chain(self.site_value_pairs(), self.hopping_value_pairs()): - if isinstance(key, builder.Site): + if isinstance(key, system.Site): result.append("# Onsite element:\n") else: a, b = key diff --git a/kwant/continuum/landau_levels.py b/kwant/continuum/landau_levels.py index 13e70c4e6b7224d3dab456d5a4497587514ea8af..c653c55ef2634cd13e932f0c3f9c5fd026c10ff9 100644 --- a/kwant/continuum/landau_levels.py +++ b/kwant/continuum/landau_levels.py @@ -176,7 +176,7 @@ class LandauLattice(kwant.lattice.Monatomic): """ A `~kwant.lattice.Monatomic` lattice with a Landau level index per site. - Site tags (see `~kwant.builder.SiteFamily`) are pairs of integers, where + Site tags (see `~kwant.system.SiteFamily`) are pairs of integers, where the first integer describes the real space position and the second the Landau level index. diff --git a/kwant/kpm.py b/kwant/kpm.py index 5da09b097e394bc57c07c7de97fb958a3665fc3b..44cf9758f4ddfff3f098be074ca929689c562f11 100644 --- a/kwant/kpm.py +++ b/kwant/kpm.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2011-2016 Kwant authors. +# Copyright 2011-2019 Kwant authors. # # This file is part of Kwant. It is subject to the license terms in the file # LICENSE.rst found in the top-level directory of this distribution and at @@ -928,7 +928,7 @@ def RandomVectors(syst, where=None, rng=None): ---------- syst : `~kwant.system.FiniteSystem` or matrix Hamiltonian If a system is passed, it should contain no leads. - where : sequence of `int` or `~kwant.builder.Site`, or callable, optional + where : sequence of `int` or `~kwant.system.Site`, or callable, optional Spatial range of the vectors produced. If ``syst`` is a `~kwant.system.FiniteSystem`, where behaves as in `~kwant.operator.Density`. If ``syst`` is a matrix, ``where`` @@ -950,7 +950,7 @@ class LocalVectors: ---------- syst : `~kwant.system.FiniteSystem` or matrix Hamiltonian If a system is passed, it should contain no leads. - where : sequence of `int` or `~kwant.builder.Site`, or callable, optional + where : sequence of `int` or `~kwant.system.Site`, or callable, optional Spatial range of the vectors produced. If ``syst`` is a `~kwant.system.FiniteSystem`, where behaves as in `~kwant.operator.Density`. If ``syst`` is a matrix, ``where`` diff --git a/kwant/lattice.py b/kwant/lattice.py index 899ce5474216421b97f35b922a51a62cd85b9602..e2557796f127c14ffde5a14ac7e2681a98ba7b60 100644 --- a/kwant/lattice.py +++ b/kwant/lattice.py @@ -1,4 +1,4 @@ -# Copyright 2011-2013 Kwant authors. +# Copyright 2011-2019 Kwant authors. # # This file is part of Kwant. It is subject to the license terms in the file # LICENSE.rst found in the top-level directory of this distribution and at @@ -13,7 +13,7 @@ from math import sqrt from itertools import product import numpy as np import tinyarray as ta -from . import builder +from . import builder, system from .linalg import lll from ._common import ensure_isinstance @@ -70,7 +70,7 @@ class Polyatomic: A Bravais lattice with an arbitrary number of sites in the basis. Contains `Monatomic` sublattices. Note that an instance of ``Polyatomic`` is - not itself a `~kwant.builder.SiteFamily`, only its sublattices are. + not itself a `~kwant.system.SiteFamily`, only its sublattices are. Parameters ---------- @@ -171,7 +171,7 @@ class Polyatomic: >>> syst[lat.neighbors()] = 1 """ def shape_sites(symmetry=None): - Site = builder.Site + Site = system.Site if symmetry is None: symmetry = builder.NoSymmetry() @@ -306,7 +306,7 @@ class Polyatomic: """ # This algorithm is not designed to be fast and can be improved, # however there is no real need. - Site = builder.Site + Site = system.Site sls = self.sublattices shortest_hopping = sls[0].n_closest( sls[0].pos(([0] * sls[0].lattice_dim)), 2)[-1] @@ -396,12 +396,12 @@ def short_array_str(array): return full[1 : -1] -class Monatomic(builder.SiteFamily, Polyatomic): +class Monatomic(system.SiteFamily, Polyatomic): """ A Bravais lattice with a single site in the basis. - Instances of this class provide the `~kwant.builder.SiteFamily` interface. - Site tags (see `~kwant.builder.SiteFamily`) are sequences of integers and + Instances of this class provide the `~kwant.system.SiteFamily` interface. + Site tags (see `~kwant.system.SiteFamily`) are sequences of integers and describe the lattice coordinates of a site. ``Monatomic`` instances are used as site families on their own or as @@ -470,6 +470,13 @@ class Monatomic(builder.SiteFamily, Polyatomic): def __str__(self): return self.cached_str + def normalize_tags(self, tags): + tags = np.asarray(tags, int) + tags.flags.writeable = False + if tags.shape[1] != self.lattice_dim: + raise ValueError("Dimensionality mismatch.") + return tags + def normalize_tag(self, tag): tag = ta.array(tag, int) if len(tag) != self.lattice_dim: @@ -495,6 +502,10 @@ class Monatomic(builder.SiteFamily, Polyatomic): """ return ta.array(self.n_closest(pos)[0]) + def positions(self, tags): + """Return the real-space positions of the sites with the given tags.""" + return tags @ self._prim_vecs + self.offset + def pos(self, tag): """Return the real-space position of the site with a given tag.""" return ta.dot(tag, self._prim_vecs) + self.offset @@ -687,26 +698,48 @@ class TranslationalSymmetry(builder.Symmetry): def which(self, site): det_x_inv_m_part, det_m = self._get_site_family_data(site.family)[-2:] - result = ta.dot(det_x_inv_m_part, site.tag) // det_m + if isinstance(site, system.Site): + result = ta.dot(det_x_inv_m_part, site.tag) // det_m + elif isinstance(site, system.SiteArray): + result = np.dot(det_x_inv_m_part, site.tags.transpose()) // det_m + else: + raise TypeError("'site' must be a Site or a SiteArray") + return -result if self.is_reversed else result def act(self, element, a, b=None): - element = ta.array(element) - if element.dtype is not int: + is_site = isinstance(a, system.Site) + # Tinyarray for small arrays (single site) else numpy + array_mod = ta if is_site else np + element = array_mod.array(element) + if not np.issubdtype(element.dtype, np.integer): raise ValueError("group element must be a tuple of integers") + if (len(element.shape) == 2 and is_site): + raise ValueError("must provide a single group element when " + "acting on single sites.") + if (len(element.shape) == 1 and not is_site): + raise ValueError("must provide a sequence of group elements " + "when acting on site arrays.") m_part = self._get_site_family_data(a.family)[0] try: - delta = ta.dot(m_part, element) + delta = array_mod.dot(m_part, element) except ValueError: msg = 'Expecting a {0}-tuple group element, but got `{1}` instead.' raise ValueError(msg.format(self.num_directions, element)) if self.is_reversed: delta = -delta if b is None: - return builder.Site(a.family, a.tag + delta, True) + if is_site: + return system.Site(a.family, a.tag + delta, True) + else: + return system.SiteArray(a.family, a.tags + delta.transpose()) elif b.family == a.family: - return (builder.Site(a.family, a.tag + delta, True), - builder.Site(b.family, b.tag + delta, True)) + if is_site: + return (system.Site(a.family, a.tag + delta, True), + system.Site(b.family, b.tag + delta, True)) + else: + return (system.SiteArray(a.family, a.tags + delta.transpose()), + system.SiteArray(b.family, b.tags + delta.transpose())) else: m_part = self._get_site_family_data(b.family)[0] try: @@ -717,8 +750,12 @@ class TranslationalSymmetry(builder.Symmetry): raise ValueError(msg.format(self.num_directions, element)) if self.is_reversed: delta2 = -delta2 - return (builder.Site(a.family, a.tag + delta, True), - builder.Site(b.family, b.tag + delta2, True)) + if is_site: + return (system.Site(a.family, a.tag + delta, True), + system.Site(b.family, b.tag + delta2, True)) + else: + return (system.SiteArray(a.family, a.tags + delta.transpose()), + system.SiteArray(b.family, b.tags + delta2.transpose())) def reversed(self): """Return a reversed copy of the symmetry. diff --git a/kwant/operator.pyx b/kwant/operator.pyx index a10624d1ccf3d7d6a2a175e16d6e00e3126c3bad..2a3e71edd01d492814d97dcf702216833c96fb42 100644 --- a/kwant/operator.pyx +++ b/kwant/operator.pyx @@ -13,7 +13,7 @@ import cython from operator import itemgetter import functools as ft import collections -import numbers +import warnings import numpy as np import tinyarray as ta @@ -25,8 +25,10 @@ from .graph.core cimport EdgeIterator from .graph.core import DisabledFeatureError, NodeDoesNotExistError from .graph.defs cimport gint from .graph.defs import gint_dtype -from .system import InfiniteSystem -from ._common import UserCodeError, get_parameters, deprecate_args +from .system import is_infinite, Site +from ._common import ( + UserCodeError, KwantDeprecationWarning, get_parameters, deprecate_args +) ################ Generic Utility functions @@ -150,7 +152,7 @@ def _get_all_orbs(gint[:, :] where, gint[:, :] site_ranges): def _get_tot_norbs(syst): cdef gint _unused, tot_norbs - is_infinite_system = isinstance(syst, InfiniteSystem) + is_infinite_system = is_infinite(syst) n_sites = syst.cell_size if is_infinite_system else syst.graph.num_nodes _get_orbs(np.asarray(syst.site_ranges, dtype=gint_dtype), n_sites, &tot_norbs, &_unused) @@ -166,7 +168,7 @@ def _normalize_site_where(syst, where): otherwise it should contain integers. """ if where is None: - if isinstance(syst, InfiniteSystem): + if is_infinite(syst): where = list(range(syst.cell_size)) else: where = list(range(syst.graph.num_nodes)) @@ -174,13 +176,12 @@ def _normalize_site_where(syst, where): try: where = [syst.id_by_site[s] for s in filter(where, syst.sites)] except AttributeError: - if isinstance(syst, InfiniteSystem): + if is_infinite(syst): where = [s for s in range(syst.cell_size) if where(s)] else: where = [s for s in range(syst.graph.num_nodes) if where(s)] else: - # Cannot check for builder.Site due to circular imports - if not isinstance(where[0], numbers.Integral): + if isinstance(where[0], Site): try: where = [syst.id_by_site[s] for s in where] except AttributeError: @@ -189,7 +190,7 @@ def _normalize_site_where(syst, where): where = np.asarray(where, dtype=gint_dtype).reshape(-1, 1) - if isinstance(syst, InfiniteSystem) and np.any(where >= syst.cell_size): + if is_infinite(syst) and np.any(where >= syst.cell_size): raise ValueError('Only sites in the fundamental domain may be ' 'specified using `where`.') if np.any(np.logical_or(where < 0, where >= syst.graph.num_nodes)): @@ -210,7 +211,7 @@ def _normalize_hopping_where(syst, where): if where is None: # we cannot extract the hoppings in the same order as they are in the # graph while simultaneously excluding all inter-cell hoppings - if isinstance(syst, InfiniteSystem): + if is_infinite(syst): raise ValueError('`where` must be provided when calculating ' 'current in an InfiniteSystem.') where = list(syst.graph) @@ -223,8 +224,7 @@ def _normalize_hopping_where(syst, where): else: where = list(filter(lambda h: where(*h), syst.graph)) else: - # Cannot check for builder.Site due to circular imports - if not isinstance(where[0][0], numbers.Integral): + if isinstance(where[0][0], Site): try: where = list((syst.id_by_site[a], syst.id_by_site[b]) for a, b in where) @@ -244,7 +244,7 @@ def _normalize_hopping_where(syst, where): where = np.asarray(where, dtype=gint_dtype) - if isinstance(syst, InfiniteSystem) and np.any(where > syst.cell_size): + if is_infinite(syst) and np.any(where > syst.cell_size): raise ValueError('Only intra-cell hoppings may be specified ' 'using `where`.') @@ -702,7 +702,11 @@ cdef class _LocalOperator: return mat offsets, norbs = _get_all_orbs(self.where, self._site_ranges) - return BlockSparseMatrix(self.where, offsets, norbs, get_ham) + # TODO: update operators to use 'hamiltonian_term' rather than + # 'hamiltonian'. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=KwantDeprecationWarning) + return BlockSparseMatrix(self.where, offsets, norbs, get_ham) def __getstate__(self): return ( @@ -735,10 +739,10 @@ cdef class Density(_LocalOperator): maps from site families to square matrices. If a function is given it must take the same arguments as the onsite Hamiltonian functions of the system. - where : sequence of `int` or `~kwant.builder.Site`, or callable, optional + where : sequence of `int` or `~kwant.system.Site`, or callable, optional Where to evaluate the operator. If ``syst`` is not a finalized Builder, then this should be a sequence of integers. If a function is provided, - it should take a single `int` or `~kwant.builder.Site` (if ``syst`` is + it should take a single `int` or `~kwant.system.Site` (if ``syst`` is a finalized builder) and return True or False. If not provided, the operator will be calculated over all sites in the system. check_hermiticity: bool @@ -884,11 +888,11 @@ cdef class Current(_LocalOperator): matrices (scalars are allowed if the site family has 1 orbital per site). If a function is given it must take the same arguments as the onsite Hamiltonian functions of the system. - where : sequence of pairs of `int` or `~kwant.builder.Site`, or callable, optional + where : sequence of pairs of `int` or `~kwant.system.Site`, or callable, optional Where to evaluate the operator. If ``syst`` is not a finalized Builder, then this should be a sequence of pairs of integers. If a function is provided, it should take a pair of integers or a pair of - `~kwant.builder.Site` (if ``syst`` is a finalized builder) and return + `~kwant.system.Site` (if ``syst`` is a finalized builder) and return True or False. If not provided, the operator will be calculated over all hoppings in the system. check_hermiticity : bool @@ -1016,10 +1020,10 @@ cdef class Source(_LocalOperator): matrices (scalars are allowed if the site family has 1 orbital per site). If a function is given it must take the same arguments as the onsite Hamiltonian functions of the system. - where : sequence of `int` or `~kwant.builder.Site`, or callable, optional + where : sequence of `int` or `~kwant.system.Site`, or callable, optional Where to evaluate the operator. If ``syst`` is not a finalized Builder, then this should be a sequence of integers. If a function is provided, - it should take a single `int` or `~kwant.builder.Site` (if ``syst`` is + it should take a single `int` or `~kwant.system.Site` (if ``syst`` is a finalized builder) and return True or False. If not provided, the operator will be calculated over all sites in the system. check_hermiticity : bool diff --git a/kwant/physics/gauge.py b/kwant/physics/gauge.py index eb4e696164ac9c7c3f9e5ce1c986df601d4d0bb2..cfb927a6fb1e3397ad4d739160f0e4031b736060 100644 --- a/kwant/physics/gauge.py +++ b/kwant/physics/gauge.py @@ -1,4 +1,4 @@ -# Copyright 2011-2018 Kwant authors. +# Copyright 2011-2019 Kwant authors. # # This file is part of Kwant. It is subject to the license terms in the file # LICENSE.rst found in the top-level directory of this distribution and at @@ -303,7 +303,7 @@ def _loops_in_infinite(syst): extended_sites : callable : int -> Site Given a site index in the extended system consisting of two unit cells, returns the associated high-level - `kwant.builder.Site`. + `kwant.system.Site`. """ assert isinstance(syst, system.InfiniteSystem) _check_infinite_syst(syst) @@ -375,7 +375,7 @@ def _loops_in_composite(syst): -1 if the site is part of the reduced scattering region (see notes). extended_sites : callable : int -> Site Given a site index in the extended scattering region (see notes), - returns the associated high-level `kwant.builder.Site`. + returns the associated high-level `kwant.system.Site`. Notes ----- @@ -401,7 +401,7 @@ def _loops_in_composite(syst): # Get distance matrix for the extended scattering region, # a function that maps sites to their lead patches (-1 for sites # in the reduced scattering region), and a function that maps sites - # to high-level 'kwant.builder.Site' objects. + # to high-level 'kwant.system.Site' objects. distance_matrix, which_patch, extended_sites =\ _extended_scattering_region(syst) @@ -439,7 +439,7 @@ def _extended_scattering_region(syst): -1 if the site is part of the reduced scattering region. extended_sites : callable : int -> Site Given a site index in the extended scattering region, returns - the associated high-level `kwant.builder.Site`. + the associated high-level `kwant.system.Site`. Notes ----- @@ -1027,7 +1027,7 @@ class magnetic_gauge: The first callable computes the Peierls phase in the scattering region and the remaining callables compute the Peierls phases in each of the leads. Each callable takes a pair of - `~kwant.builder.Site` (a hopping) and returns a unit complex + `~kwant.system.Site` (a hopping) and returns a unit complex number (Peierls phase) that multiplies that hopping. """ return self._peierls(syst_field, *lead_fields, tol=tol, average=False) diff --git a/kwant/plotter.py b/kwant/plotter.py index fabe5f61c9234b2323e0c11c860962b3b5b2bd5d..31a0aba6658b18eb3c79fccc3c0ffcdb2804bedf 100644 --- a/kwant/plotter.py +++ b/kwant/plotter.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Copyright 2011-2018 Kwant authors. +# Copyright 2011-2019 Kwant authors. # # This file is part of Kwant. It is subject to the license terms in the file # LICENSE.rst found in the top-level directory of this distribution and at @@ -402,7 +402,7 @@ def sys_leads_sites(sys, num_lead_cells=2): Returns ------- sites : list of (site, lead_number, copy_number) tuples - A site is a `~kwant.builder.Site` instance if the system is not finalized, + A site is a `~kwant.system.Site` instance if the system is not finalized, and an integer otherwise. For system sites `lead_number` is `None` and `copy_number` is `0`, for leads both are integers. lead_cells : list of slices @@ -427,7 +427,7 @@ def sys_leads_sites(sys, num_lead_cells=2): lead.builder.sites() for i in range(num_lead_cells))) lead_cells.append(slice(start, len(sites))) - elif isinstance(syst, system.FiniteSystem): + elif system.is_finite(syst): sites = [(i, None, 0) for i in range(syst.graph.num_nodes)] for leadnr, lead in enumerate(syst.leads): start = len(sites) @@ -528,7 +528,7 @@ def sys_leads_hoppings(sys, num_lead_cells=2): Returns ------- hoppings : list of (hopping, lead_number, copy_number) tuples - A site is a `~kwant.builder.Site` instance if the system is not finalized, + A site is a `~kwant.system.Site` instance if the system is not finalized, and an integer otherwise. For system sites `lead_number` is `None` and `copy_number` is `0`, for leads both are integers. lead_cells : list of slices @@ -812,7 +812,7 @@ def plot(sys, num_lead_cells=2, unit='nn', - The meaning of "site" depends on whether the system to be plotted is a builder or a low level system. For builders, a site is a - kwant.builder.Site object. For low level systems, a site is an integer + kwant.system.Site object. For low level systems, a site is an integer -- the site number. - color and symbol definitions may be tuples, but not lists or arrays. @@ -961,7 +961,7 @@ def plot(sys, num_lead_cells=2, unit='nn', if site_color is None: cycle = _color_cycle() - if isinstance(syst, (builder.FiniteSystem, builder.InfiniteSystem)): + if builder.is_system(syst): # Skipping the leads for brevity. families = sorted({site.family for site in syst.sites}) color_mapping = dict(zip(families, cycle)) @@ -1291,7 +1291,7 @@ def map(sys, value, colorbar=True, cmap=None, vmin=None, vmax=None, a=None, if callable(value): value = [value(site[0]) for site in sites] else: - if not isinstance(syst, system.FiniteSystem): + if not system.is_finite(syst): raise ValueError('List of values is only allowed as input ' 'for finalized systems.') value = np.array(value) @@ -1407,7 +1407,7 @@ def bands(sys, args=(), momenta=65, file=None, show=True, dpi=None, "for bands()") syst = sys # for naming consistency inside function bodies - _common.ensure_isinstance(syst, system.InfiniteSystem) + _common.ensure_isinstance(syst, (system.InfiniteSystem, system.InfiniteVectorizedSystem)) momenta = np.array(momenta) if momenta.ndim != 1: @@ -1483,7 +1483,7 @@ def spectrum(syst, x, y=None, params=None, mask=None, file=None, if y is not None and not _p.has3d: raise RuntimeError("Installed matplotlib does not support 3d plotting") - if isinstance(syst, system.FiniteSystem): + if system.is_finite(syst): def ham(**kwargs): return syst.hamiltonian_submatrix(params=kwargs, sparse=False) elif callable(syst): @@ -1751,7 +1751,7 @@ def interpolate_current(syst, current, relwidth=None, abswidth=None, n=9): the extents of `field`: ((x0, x1), (y0, y1), ...) """ - if not isinstance(syst, builder.FiniteSystem): + if not builder.is_finite_system(syst): raise TypeError("The system needs to be finalized.") if len(current) != syst.graph.num_edges: @@ -1844,7 +1844,7 @@ def interpolate_density(syst, density, relwidth=None, abswidth=None, n=9, the extents of ``field``: ((x0, x1), (y0, y1), ...) """ - if not isinstance(syst, builder.FiniteSystem): + if not builder.is_finite_system(syst): raise TypeError("The system needs to be finalized.") if len(density) != len(syst.sites): diff --git a/kwant/system.py b/kwant/system.py index d270691508189866a0d75809917048303443c883..a1c287bb97a982353d79410b8c2cd4fc2ca4bbad 100644 --- a/kwant/system.py +++ b/kwant/system.py @@ -1,4 +1,4 @@ -# Copyright 2011-2013 Kwant authors. +# Copyright 2011-2019 Kwant authors. # # This file is part of Kwant. It is subject to the license terms in the file # LICENSE.rst found in the top-level directory of this distribution and at @@ -8,13 +8,269 @@ """Low-level interface of systems""" -__all__ = ['System', 'FiniteSystem', 'InfiniteSystem'] +__all__ = [ + 'Site', 'SiteArray', 'SiteFamily', + 'System', 'VectorizedSystem', 'FiniteSystem', 'FiniteVectorizedSystem', + 'InfiniteSystem', 'InfiniteVectorizedSystem', +] import abc import warnings +import operator from copy import copy +from collections import namedtuple +from functools import total_ordering, lru_cache +import numpy as np from . import _system -from ._common import deprecate_args +from ._common import deprecate_args, KwantDeprecationWarning + + + +################ Sites and Site families + +class Site(tuple): + """A site, member of a `SiteFamily`. + + Sites are the vertices of the graph which describes the tight binding + system in a `Builder`. + + A site is uniquely identified by its family and its tag. + + Parameters + ---------- + family : an instance of `SiteFamily` + The 'type' of the site. + tag : a hashable python object + The unique identifier of the site within the site family, typically a + vector of integers. + + Raises + ------ + ValueError + If `tag` is not a proper tag for `family`. + + Notes + ----- + For convenience, ``family(*tag)`` can be used instead of ``Site(family, + tag)`` to create a site. + + The parameters of the constructor (see above) are stored as instance + variables under the same names. Given a site ``site``, common things to + query are thus ``site.family``, ``site.tag``, and ``site.pos``. + """ + __slots__ = () + + family = property(operator.itemgetter(0), + doc="The site family to which the site belongs.") + tag = property(operator.itemgetter(1), doc="The tag of the site.") + + + def __new__(cls, family, tag, _i_know_what_i_do=False): + if _i_know_what_i_do: + return tuple.__new__(cls, (family, tag)) + try: + tag = family.normalize_tag(tag) + except (TypeError, ValueError) as e: + msg = 'Tag {0} is not allowed for site family {1}: {2}' + raise type(e)(msg.format(repr(tag), repr(family), e.args[0])) + return tuple.__new__(cls, (family, tag)) + + def __repr__(self): + return 'Site({0}, {1})'.format(repr(self.family), repr(self.tag)) + + def __str__(self): + sf = self.family + return '<Site {0} of {1}>'.format(self.tag, sf.name if sf.name else sf) + + def __getnewargs__(self): + return (self.family, self.tag, True) + + @property + def pos(self): + """Real space position of the site. + + This relies on ``family`` having a ``pos`` method (see `SiteFamily`). + """ + return self.family.pos(self.tag) + + +class SiteArray: + """An array of sites, members of a `SiteFamily`. + + Parameters + ---------- + family : an instance of `SiteFamily` + The 'type' of the sites. + tags : a sequence of python objects + Sequence of unique identifiers of the sites within the + site array family, typically vectors of integers. + + Raises + ------ + ValueError + If `tags` are not proper tags for `family`. + + See Also + -------- + kwant.system.Site + """ + + def __init__(self, family, tags): + self.family = family + try: + tags = family.normalize_tags(tags) + except (TypeError, ValueError) as e: + msg = 'Tags {0} are not allowed for site family {1}: {2}' + raise type(e)(msg.format(repr(tags), repr(family), e.args[0])) + self.tags = tags + + def __repr__(self): + return 'SiteArray({0}, {1})'.format(repr(self.family), repr(self.tags)) + + def __str__(self): + sf = self.family + return ('<SiteArray {0} of {1}>' + .format(self.tags, sf.name if sf.name else sf)) + + def __len__(self): + return len(self.tags) + + def __eq__(self, other): + if not isinstance(other, SiteArray): + raise NotImplementedError() + return self.family == other.family and np.all(self.tags == other.tags) + + def positions(self): + """Real space position of the site. + + This relies on ``family`` having a ``pos`` method (see `SiteFamily`). + """ + return self.family.positions(self.tags) + + +@total_ordering +class SiteFamily: + """Abstract base class for site families. + + Site families are the 'type' of `Site` objects. Within a family, individual + sites are uniquely identified by tags. Valid tags must be hashable Python + objects, further details are up to the family. + + Site families must be immutable and fully defined by their initial + arguments. They must inherit from this abstract base class and call its + __init__ function providing it with two arguments: a canonical + representation and a name. The canonical representation will be returned as + the objects representation and must uniquely identify the site family + instance. The name is a string used to distinguish otherwise identical site + families. It may be empty. ``norbs`` defines the number of orbitals + on sites associated with this site family; it may be `None`, in which case + the number of orbitals is not specified. + + + All site families must define either 'normalize_tag' or 'normalize_tags', + which brings a tag (or, in the latter case, a sequence of tags) to the + standard format for this site family. + + Site families may also implement methods ``pos(tag)`` and + ``positions(tags)``, which return a vector of realspace coordinates or an + array of vectors of realspace coordinates of the site(s) belonging to this + family with the given tag(s). These methods are used in plotting routines. + ``positions(tags)`` should return an array with shape ``(N, M)`` where + ``N`` is the length of ``tags``, and ``M`` is the realspace dimension. + + If the ``norbs`` of a site family are provided, and sites of this family + are used to populate a `~kwant.builder.Builder`, then the associated + Hamiltonian values must have the correct shape. That is, if a site family + has ``norbs = 2``, then any on-site terms for sites belonging to this + family should be 2x2 matrices. Similarly, any hoppings to/from sites + belonging to this family must have a matrix structure where there are two + rows/columns. This condition applies equally to Hamiltonian values that + are given by functions. If this condition is not satisfied, an error will + be raised. + """ + + def __init__(self, canonical_repr, name, norbs): + self.canonical_repr = canonical_repr + self.hash = hash(canonical_repr) + self.name = name + if norbs is None: + warnings.warn("Not specfying norbs is deprecated. Always specify " + "norbs when creating site families.", + KwantDeprecationWarning, stacklevel=3) + if norbs is not None: + if int(norbs) != norbs or norbs <= 0: + raise ValueError('The norbs parameter must be an integer > 0.') + norbs = int(norbs) + self.norbs = norbs + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + if (cls.normalize_tag is SiteFamily.normalize_tag + and cls.normalize_tags is SiteFamily.normalize_tags): + raise TypeError("Must redefine either 'normalize_tag' or " + "'normalize_tags'") + + def __repr__(self): + return self.canonical_repr + + def __str__(self): + if self.name: + msg = '<{0} site family {1}{2}>' + else: + msg = '<unnamed {0} site family{2}>' + orbs = ' with {0} orbitals'.format(self.norbs) if self.norbs else '' + return msg.format(self.__class__.__name__, self.name, orbs) + + def __hash__(self): + return self.hash + + def __eq__(self, other): + try: + return self.canonical_repr == other.canonical_repr + except AttributeError: + return False + + def __ne__(self, other): + try: + return self.canonical_repr != other.canonical_repr + except AttributeError: + return True + + def __lt__(self, other): + # If this raises an AttributeError, we were trying + # to compare it to something non-comparable anyway. + return self.canonical_repr < other.canonical_repr + + def normalize_tag(self, tag): + """Return a normalized version of the tag. + + Raises TypeError or ValueError if the tag is not acceptable. + """ + tag, = self.normalize_tags([tag]) + return tag + + def normalize_tags(self, tags): + """Return a normalized version of the tags. + + Raises TypeError or ValueError if the tags are not acceptable. + """ + return np.array([self.normalize_tag(tag) for tag in tags]) + + def __call__(self, *tag): + """ + A convenience function. + + This function allows to write fam(1, 2) instead of Site(fam, (1, 2)). + """ + # Catch a likely and difficult to find mistake. + if tag and isinstance(tag[0], tuple): + raise ValueError('Use site_family(1, 2) instead of ' + 'site_family((1, 2))!') + return Site(self, tag) + + + +################ Systems class System(metaclass=abc.ABCMeta): @@ -92,12 +348,100 @@ class System(metaclass=abc.ABCMeta): details = ', and '.join((', '.join(details[:-1]), details[-1])) return '<{} with {}>'.format(self.__class__.__name__, details) + hamiltonian_submatrix = _system.hamiltonian_submatrix + + +Term = namedtuple( + "Term", + ["subgraph", "hermitian", "parameters"], +) + + +class VectorizedSystem(System, metaclass=abc.ABCMeta): + """Abstract general low-level system with support for vectorization. + + Attributes + ---------- + graph : kwant.graph.CGraph + The system graph. + subgraphs : sequence of tuples + Each subgraph has the form '((idx1, idx2), (offsets1, offsets2))' + where 'offsets1' and 'offsets2' index sites within the site arrays + indexed by 'idx1' and 'idx2'. + terms : sequence of tuples + Each tuple has the following structure: + (subgraph: int, hermitian: bool, parameters: List(str)) + 'subgraph' indexes 'subgraphs' and supplies the to/from sites of this + term. 'hermitian' is 'True' if the term needs its Hermitian + conjugate to be added when evaluating the Hamiltonian, and 'parameters' + contains a list of parameter names used when evaluating this term. + site_arrays : sequence of SiteArray + The sites of the system. + site_ranges : None or Nx3 integer array + Has 1 row per site array, plus one extra row. Each row consists + of ``(first_site, norbs, orb_offset)``: the index of the first + site in the site array, the number of orbitals on each site in + the site array, and the offset of the first orbital of the first + site in the site array. In addition, the final row has the form + ``(len(graph.num_nodes), 0, tot_norbs)`` where ``tot_norbs`` is the + total number of orbitals in the system. ``None`` if any site array + in 'site_arrays' does not have 'norbs' specified. Note 'site_ranges' + is directly computable from 'site_arrays'. + 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 + ----- + The sites of the system are indexed by integers ranging from 0 to + ``self.graph.num_nodes - 1``. + + Optionally, a class derived from ``System`` can provide a method ``pos`` which + is assumed to return the real-space position of a site given its index. + """ + @abc.abstractmethod + def hamiltonian_term(self, term_number, selector=slice(None), + args=(), params=None): + """Return the Hamiltonians for hamiltonian term number k. -# Add a C-implemented function as an unbound method to class System. -System.hamiltonian_submatrix = _system.hamiltonian_submatrix + Parameters + ---------- + term_number : int + The number of the term to evaluate. + selector : slice or sequence of int, default: slice(None) + The elements of the term to evaluate. + args : tuple + Positional arguments to the term. (Deprecated) + params : dict + Keyword parameters to the term + Returns + ------- + hamiltonian : 3d complex array + Has shape ``(N, P, Q)`` where ``N`` is the number of matrix + elements in this term (or the number selected by 'selector' + if provided), ``P`` and ``Q`` are the number of orbitals in the + 'to' and 'from' site arrays associated with this term. -class FiniteSystem(System, metaclass=abc.ABCMeta): + Providing positional arguments via 'args' is deprecated, + instead, provide named parameters as a dictionary via 'params'. + """ + @property + @lru_cache(1) + def site_ranges(self): + site_offsets = np.cumsum([0] + [len(arr) for arr in self.site_arrays]) + norbs = [arr.family.norbs for arr in self.site_arrays] + [0] + if any(norb is None for norb in norbs): + return None + orb_offsets = np.cumsum( + [0] + [len(arr) * arr.family.norbs for arr in self.site_arrays] + ) + return np.array([site_offsets, norbs, orb_offsets]).transpose() + + hamiltonian_submatrix = _system.vectorized_hamiltonian_submatrix + + +class FiniteSystemMixin(metaclass=abc.ABCMeta): """Abstract finite low-level system, possibly with leads. Attributes @@ -220,7 +564,19 @@ class FiniteSystem(System, metaclass=abc.ABCMeta): return symmetries.validate(ham) -class InfiniteSystem(System, metaclass=abc.ABCMeta): +class FiniteSystem(System, FiniteSystemMixin, metaclass=abc.ABCMeta): + pass + + +class FiniteVectorizedSystem(VectorizedSystem, FiniteSystemMixin, metaclass=abc.ABCMeta): + pass + + +def is_finite(syst): + return isinstance(syst, (FiniteSystem, FiniteVectorizedSystem)) + + +class InfiniteSystemMixin(metaclass=abc.ABCMeta): """Abstract infinite low-level system. An infinite system consists of an infinite series of identical cells. @@ -261,30 +617,10 @@ class InfiniteSystem(System, metaclass=abc.ABCMeta): infinite system. The other scheme has the numbers of site 0 and 1 exchanged, as well as of site 3 and 4. + Sites in the fundamental domain cell must belong to a different site array + than the sites in the previous cell. In the above example this means that + sites '(0, 1, 2)' and '(3, 4)' must belong to different site arrays. """ - @deprecate_args - def cell_hamiltonian(self, args=(), sparse=False, *, params=None): - """Hamiltonian of a single cell of the infinite system. - - Providing positional arguments via 'args' is deprecated, - instead, provide named parameters as a dictionary via 'params'. - """ - cell_sites = range(self.cell_size) - return self.hamiltonian_submatrix(args, cell_sites, cell_sites, - sparse=sparse, params=params) - - @deprecate_args - def inter_cell_hopping(self, args=(), sparse=False, *, params=None): - """Hopping Hamiltonian between two cells of the infinite system. - - Providing positional arguments via 'args' is deprecated, - instead, provide named parameters as a dictionary via 'params'. - """ - cell_sites = range(self.cell_size) - interface_sites = range(self.cell_size, self.graph.num_nodes) - return self.hamiltonian_submatrix(args, cell_sites, interface_sites, - sparse=sparse, params=params) - @deprecate_args def modes(self, energy=0, args=(), *, params=None): """Return mode decomposition of the lead @@ -368,6 +704,41 @@ class InfiniteSystem(System, metaclass=abc.ABCMeta): return list(broken) +class InfiniteSystem(System, InfiniteSystemMixin, metaclass=abc.ABCMeta): + + @deprecate_args + def cell_hamiltonian(self, args=(), sparse=False, *, params=None): + """Hamiltonian of a single cell of the infinite system. + + Providing positional arguments via 'args' is deprecated, + instead, provide named parameters as a dictionary via 'params'. + """ + cell_sites = range(self.cell_size) + return self.hamiltonian_submatrix(args, cell_sites, cell_sites, + sparse=sparse, params=params) + + @deprecate_args + def inter_cell_hopping(self, args=(), sparse=False, *, params=None): + """Hopping Hamiltonian between two cells of the infinite system. + + Providing positional arguments via 'args' is deprecated, + instead, provide named parameters as a dictionary via 'params'. + """ + cell_sites = range(self.cell_size) + interface_sites = range(self.cell_size, self.graph.num_nodes) + return self.hamiltonian_submatrix(args, cell_sites, interface_sites, + sparse=sparse, params=params) + + +class InfiniteVectorizedSystem(VectorizedSystem, InfiniteSystemMixin, metaclass=abc.ABCMeta): + cell_hamiltonian = _system.vectorized_cell_hamiltonian + inter_cell_hopping = _system.vectorized_inter_cell_hopping + + +def is_infinite(syst): + return isinstance(syst, (InfiniteSystem, InfiniteVectorizedSystem)) + + class PrecalculatedLead: def __init__(self, modes=None, selfenergy=None): """A general lead defined by its self energy. diff --git a/kwant/tests/test_builder.py b/kwant/tests/test_builder.py index 7ba00b403513ffe3100acd184820adf1c788ed15..253c9d7cca43251dce2c608a50150b9fb2da7f4f 100644 --- a/kwant/tests/test_builder.py +++ b/kwant/tests/test_builder.py @@ -1,4 +1,4 @@ -# Copyright 2011-2018 Kwant authors. +# Copyright 2011-2019 Kwant authors. # # This file is part of Kwant. It is subject to the license terms in the file # LICENSE.rst found in the top-level directory of this distribution and at @@ -19,8 +19,8 @@ from pytest import raises, warns from numpy.testing import assert_almost_equal import kwant -from kwant import builder -from kwant._common import ensure_rng +from kwant import builder, system +from kwant._common import ensure_rng, KwantDeprecationWarning def test_bad_keys(): @@ -290,12 +290,28 @@ def random_hopping_integral(rng): def check_onsite(fsyst, sites, subset=False, check_values=True): + vectorized = isinstance(fsyst, (system.FiniteVectorizedSystem, system.InfiniteVectorizedSystem)) + + if vectorized: + site_offsets = np.cumsum([0] + [len(s) for s in fsyst.site_arrays]) + freq = {} for node in range(fsyst.graph.num_nodes): site = fsyst.sites[node].tag freq[site] = freq.get(site, 0) + 1 if check_values and site in sites: - assert fsyst.onsites[node][0] is sites[site] + if vectorized: + term = fsyst._onsite_term_by_site_id[node] + value = fsyst._term_values[term] + if callable(value): + assert value is sites[site] + else: + (w, _), (off, _) = fsyst.subgraphs[fsyst.terms[term].subgraph] + node_off = node - site_offsets[w] + selector = np.searchsorted(off, node_off) + assert np.allclose(value[selector], sites[site]) + else: + assert fsyst.onsites[node][0] is sites[site] if not subset: # Check that all sites of `fsyst` are in `sites`. for site in freq.keys(): @@ -306,24 +322,50 @@ def check_onsite(fsyst, sites, subset=False, check_values=True): def check_hoppings(fsyst, hops): + vectorized = isinstance(fsyst, (system.FiniteVectorizedSystem, system.InfiniteVectorizedSystem)) + + if vectorized: + site_offsets = np.cumsum([0] + [len(s) for s in fsyst.site_arrays]) + assert fsyst.graph.num_edges == 2 * len(hops) for edge_id, edge in enumerate(fsyst.graph): - tail, head = edge - tail = fsyst.sites[tail].tag - head = fsyst.sites[head].tag - value = fsyst.hoppings[edge_id][0] - if value is builder.Other: - assert (head, tail) in hops + i, j = edge + tail = fsyst.sites[i].tag + head = fsyst.sites[j].tag + + if vectorized: + term = fsyst._hopping_term_by_edge_id[edge_id] + if term < 0: # Hermitian conjugate + assert (head, tail) in hops + else: + value = fsyst._term_values[term] + assert (tail, head) in hops + if callable(value): + assert value is hops[tail, head] + else: + dtype = np.dtype([('f0', int), ('f1', int)]) + subgraph = fsyst.terms[term].subgraph + (to_w, from_w), hoppings = fsyst.subgraphs[subgraph] + hop = (i - site_offsets[to_w], j - site_offsets[from_w]) + hop = np.array(hop, dtype=dtype) + hoppings = hoppings.transpose().view(dtype).reshape(-1) + selector = np.recarray.searchsorted(hoppings, hop) + assert np.allclose(value[selector], hops[tail, head]) else: - assert (tail, head) in hops - assert value is hops[tail, head] + value = fsyst.hoppings[edge_id][0] + if value is builder.Other: + assert (head, tail) in hops + else: + assert (tail, head) in hops + assert value is hops[tail, head] def check_id_by_site(fsyst): for i, site in enumerate(fsyst.sites): assert fsyst.id_by_site[site] == i -def test_finalization(): +@pytest.mark.parametrize("vectorize", [False, True]) +def test_finalization(vectorize): """Test the finalization of finite and infinite systems. In order to exactly verify the finalization, low-level features of the @@ -377,7 +419,7 @@ def test_finalization(): neighbors = sorted(neighbors) # Build scattering region from blueprint and test it. - syst = builder.Builder() + syst = builder.Builder(vectorize=vectorize) fam = kwant.lattice.general(ta.identity(2), norbs=1) for site, value in sr_sites.items(): syst[fam(*site)] = value @@ -388,7 +430,7 @@ def test_finalization(): check_onsite(fsyst, sr_sites) check_hoppings(fsyst, sr_hops) # check that sites are sorted - assert fsyst.sites == tuple(sorted(fam(*site) for site in sr_sites)) + assert tuple(fsyst.sites) == tuple(sorted(fam(*site) for site in sr_sites)) # Build lead from blueprint and test it. lead = builder.Builder(kwant.TranslationalSymmetry((size, 0))) @@ -421,12 +463,12 @@ def test_finalization(): # Attach lead with improper interface. syst.leads[-1] = builder.BuilderLead( - lead, 2 * tuple(builder.Site(fam, n) for n in neighbors)) + lead, 2 * tuple(system.Site(fam, n) for n in neighbors)) raises(ValueError, syst.finalized) # Attach lead properly. syst.leads[-1] = builder.BuilderLead( - lead, (builder.Site(fam, n) for n in neighbors)) + lead, (system.Site(fam, n) for n in neighbors)) fsyst = syst.finalized() assert len(fsyst.lead_interfaces) == 1 assert ([fsyst.sites[i].tag for i in fsyst.lead_interfaces[0]] == @@ -434,15 +476,15 @@ def test_finalization(): # test that we cannot finalize a system with a badly sorted interface order raises(ValueError, builder.InfiniteSystem, lead, - [builder.Site(fam, n) for n in reversed(neighbors)]) + [system.Site(fam, n) for n in reversed(neighbors)]) # site ordering independent of whether interface was specified - flead_order = builder.InfiniteSystem(lead, [builder.Site(fam, n) + flead_order = builder.InfiniteSystem(lead, [system.Site(fam, n) for n in neighbors]) - assert flead.sites == flead_order.sites + assert tuple(flead.sites) == tuple(flead_order.sites) syst.leads[-1] = builder.BuilderLead( - lead, (builder.Site(fam, n) for n in neighbors)) + lead, (system.Site(fam, n) for n in neighbors)) fsyst = syst.finalized() assert len(fsyst.lead_interfaces) == 1 assert ([fsyst.sites[i].tag for i in fsyst.lead_interfaces[0]] == @@ -457,47 +499,62 @@ def test_finalization(): raises(ValueError, lead.finalized) -def test_site_ranges(): +def _make_system_from_sites(sites, vectorize): + syst = builder.Builder(vectorize=vectorize) + for s in sites: + norbs = s.family.norbs + if norbs: + syst[s] = np.eye(s.family.norbs) + else: + syst[s] = None + return syst.finalized() + + +@pytest.mark.parametrize("vectorize", [False, True]) +def test_site_ranges(vectorize): lat1a = kwant.lattice.chain(norbs=1, name='a') lat1b = kwant.lattice.chain(norbs=1, name='b') lat2 = kwant.lattice.chain(norbs=2) - site_ranges = builder._site_ranges # simple case -- single site family for lat in (lat1a, lat2): sites = list(map(lat, range(10))) - ranges = site_ranges(sites) + syst = _make_system_from_sites(sites, vectorize) + ranges = syst.site_ranges expected = [(0, lat.norbs, 0), (10, 0, 10 * lat.norbs)] - assert ranges == expected + assert np.array_equal(ranges, expected) # pair of site families - sites = it.chain(map(lat1a, range(4)), map(lat1b, range(6)), - map(lat1a, range(4))) - expected = [(0, 1, 0), (4, 1, 4), (10, 1, 10), (14, 0, 14)] - assert expected == site_ranges(tuple(sites)) + sites = it.chain(map(lat1a, range(4)), map(lat1b, range(6))) + syst = _make_system_from_sites(sites, vectorize) + expected = [(0, 1, 0), (4, 1, 4), (10, 0, 10)] + assert np.array_equal(expected, syst.site_ranges) sites = it.chain(map(lat2, range(4)), map(lat1a, range(6)), map(lat1b, range(4))) + syst = _make_system_from_sites(sites, vectorize) expected = [(0, 2, 0), (4, 1, 4*2), (10, 1, 4*2+6), (14, 0, 4*2+10)] - assert expected == site_ranges(tuple(sites)) + assert np.array_equal(expected, syst.site_ranges) # test with an actual builder for lat in (lat1a, lat2): sites = list(map(lat, range(10))) - syst = kwant.Builder() + syst = kwant.Builder(vectorize=vectorize) syst[sites] = np.eye(lat.norbs) ranges = syst.finalized().site_ranges expected = [(0, lat.norbs, 0), (10, 0, 10 * lat.norbs)] - assert ranges == expected - # poison system with a single site with no norbs defined. - # Also catch the deprecation warning. - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - syst[kwant.lattice.chain(norbs=None)(0)] = 1 - ranges = syst.finalized().site_ranges - assert ranges == None - - -def test_hamiltonian_evaluation(): + assert np.array_equal(ranges, expected) + if not vectorize: # vectorized systems *must* have all norbs + # poison system with a single site with no norbs defined. + # Also catch the deprecation warning. + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + syst[kwant.lattice.chain(norbs=None)(0)] = 1 + ranges = syst.finalized().site_ranges + assert ranges is None + + +@pytest.mark.parametrize("vectorize", [False, True]) +def test_hamiltonian_evaluation(vectorize): def f_onsite(site): return site.tag[0] @@ -505,14 +562,26 @@ def test_hamiltonian_evaluation(): a, b = a.tag, b.tag return complex(a[0] + b[0], a[1] - b[1]) + def f_onsite_vectorized(sites): + return sites.tags[:, 0] + + def f_hopping_vectorized(a, b): + a, b = a.tags, b.tags + return a[:, 0] + b[:, 0] + 1j * (a[:, 1] - b[:, 1]) + tags = [(0, 0), (1, 1), (2, 2), (3, 3)] edges = [(0, 1), (0, 2), (0, 3), (1, 2)] - syst = builder.Builder() + syst = builder.Builder(vectorize=vectorize) fam = builder.SimpleSiteFamily(norbs=1) sites = [fam(*tag) for tag in tags] - syst[(fam(*tag) for tag in tags)] = f_onsite - syst[((fam(*tags[i]), fam(*tags[j])) for (i, j) in edges)] = f_hopping + hoppings = [(sites[i], sites[j]) for i, j in edges] + if vectorize: + syst[sites] = f_onsite_vectorized + syst[hoppings] = f_hopping_vectorized + else: + syst[sites] = f_onsite + syst[hoppings] = f_hopping fsyst = syst.finalized() assert fsyst.graph.num_nodes == len(tags) @@ -521,12 +590,16 @@ def test_hamiltonian_evaluation(): for i in range(len(tags)): site = fsyst.sites[i] assert site in sites - assert fsyst.hamiltonian(i, i) == syst[site](site) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=KwantDeprecationWarning) + assert fsyst.hamiltonian(i, i) == f_onsite(site) for t, h in fsyst.graph: tsite = fsyst.sites[t] hsite = fsyst.sites[h] - assert fsyst.hamiltonian(t, h) == syst[tsite, hsite](tsite, hsite) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=KwantDeprecationWarning) + assert fsyst.hamiltonian(t, h) == f_hopping(tsite, hsite) # test when user-function raises errors def onsite_raises(site): @@ -539,13 +612,18 @@ def test_hamiltonian_evaluation(): a, b = hop # exceptions are converted to kwant.UserCodeError and we add our message with raises(kwant.UserCodeError) as exc_info: - fsyst.hamiltonian(a, a) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=KwantDeprecationWarning) + fsyst.hamiltonian(a, a) msg = 'Error occurred in user-supplied value function "onsite_raises"' assert msg in str(exc_info.value) for hop in [(a, b), (b, a)]: with raises(kwant.UserCodeError) as exc_info: - fsyst.hamiltonian(*hop) + with warnings.catch_warnings(): + # Ignore deprecation warnings for 'hamiltonian' + warnings.filterwarnings("ignore", category=KwantDeprecationWarning) + fsyst.hamiltonian(*hop) msg = ('Error occurred in user-supplied ' 'value function "hopping_raises"') assert msg in str(exc_info.value) @@ -555,7 +633,7 @@ def test_hamiltonian_evaluation(): syst[new_hop[0]] = onsite_raises syst[new_hop] = hopping_raises fsyst = syst.finalized() - hop = tuple(map(fsyst.sites.index, new_hop)) + hop = tuple(map(fsyst.id_by_site.__getitem__, new_hop)) test_raising(fsyst, hop) # test with infinite system @@ -563,10 +641,118 @@ def test_hamiltonian_evaluation(): for k, v in it.chain(syst.site_value_pairs(), syst.hopping_value_pairs()): inf_syst[k] = v inf_fsyst = inf_syst.finalized() - hop = tuple(map(inf_fsyst.sites.index, new_hop)) + hop = tuple(map(inf_fsyst.id_by_site.__getitem__, new_hop)) test_raising(inf_fsyst, hop) +def test_vectorized_hamiltonian_evaluation(): + + def onsite(site): + return site.tag[0] + + def vectorized_onsite(sites): + return sites.tags[:, 0] + + def hopping(to_site, from_site): + a, b = to_site.tag, from_site.tag + return a[0] + b[0] + 1j * (a[1] - b[1]) + + def vectorized_hopping(to_sites, from_sites): + a, b = to_sites.tags, from_sites.tags + return a[:, 0] + b[:, 0] + 1j * (a[:, 1] - b[:, 1]) + + tags = [(0, 0), (1, 1), (2, 2), (3, 3)] + edges = [(0, 1), (0, 2), (0, 3), (1, 2)] + + fam = builder.SimpleSiteFamily(norbs=1) + sites = [fam(*tag) for tag in tags] + hops = [(fam(*tags[i]), fam(*tags[j])) for (i, j) in edges] + + syst_simple = builder.Builder(vectorize=False) + syst_simple[sites] = onsite + syst_simple[hops] = hopping + fsyst_simple = syst_simple.finalized() + + syst_vectorized = builder.Builder(vectorize=True) + syst_vectorized[sites] = vectorized_onsite + syst_vectorized[hops] = vectorized_hopping + fsyst_vectorized = syst_vectorized.finalized() + + assert fsyst_vectorized.graph.num_nodes == len(tags) + assert fsyst_vectorized.graph.num_edges == 2 * len(edges) + assert len(fsyst_vectorized.site_arrays) == 1 + assert fsyst_vectorized.site_arrays[0] == system.SiteArray(fam, tags) + + assert np.allclose( + fsyst_simple.hamiltonian_submatrix(), + fsyst_vectorized.hamiltonian_submatrix(), + ) + + for i in range(len(tags)): + site = fsyst_vectorized.sites[i] + assert site in sites + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=KwantDeprecationWarning) + assert ( + fsyst_vectorized.hamiltonian(i, i) + == fsyst_simple.hamiltonian(i, i)) + + for t, h in fsyst_vectorized.graph: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=KwantDeprecationWarning) + assert ( + fsyst_vectorized.hamiltonian(t, h) + == fsyst_simple.hamiltonian(t, h)) + + # Test infinite system, including hoppings that go both ways into + # the next cell + lat = kwant.lattice.square(norbs=1) + + syst_vectorized = builder.Builder(kwant.TranslationalSymmetry((-1, 0)), + vectorize=True) + syst_vectorized[lat(0, 0)] = 4 + syst_vectorized[lat(0, 1)] = 5 + syst_vectorized[lat(0, 2)] = vectorized_onsite + syst_vectorized[(lat(1, 0), lat(0, 0))] = 1j + syst_vectorized[(lat(2, 1), lat(1, 1))] = vectorized_hopping + fsyst_vectorized = syst_vectorized.finalized() + + syst_simple = builder.Builder(kwant.TranslationalSymmetry((-1, 0)), + vectorize=False) + syst_simple[lat(0, 0)] = 4 + syst_simple[lat(0, 1)] = 5 + syst_simple[lat(0, 2)] = onsite + syst_simple[(lat(1, 0), lat(0, 0))] = 1j + syst_simple[(lat(2, 1), lat(1, 1))] = hopping + fsyst_simple = syst_simple.finalized() + + assert np.allclose( + fsyst_vectorized.hamiltonian_submatrix(), + fsyst_simple.hamiltonian_submatrix(), + ) + assert np.allclose( + fsyst_vectorized.cell_hamiltonian(), + fsyst_simple.cell_hamiltonian(), + ) + assert np.allclose( + fsyst_vectorized.inter_cell_hopping(), + fsyst_simple.inter_cell_hopping(), + ) + + +def test_vectorized_requires_norbs(): + + # Catch deprecation warning for lack of norbs + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + fam = builder.SimpleSiteFamily() + + syst = builder.Builder(vectorize=True) + syst[fam(0, 0)] = 1 + + raises(ValueError, syst.finalized) + + def test_dangling(): def make_system(): # 1 @@ -1204,15 +1390,22 @@ def test_discrete_symmetries(): # We need to keep testing 'args', but we don't want to see # all the deprecation warnings in the test logs @pytest.mark.filterwarnings("ignore:.*'args' parameter") -def test_argument_passing(): +@pytest.mark.parametrize("vectorize", [False, True]) +def test_argument_passing(vectorize): chain = kwant.lattice.chain(norbs=1) # Test for passing parameters to hamiltonian matrix elements def onsite(site, p1, p2): - return p1 + p2 + if vectorize: + return (p1 + p2) * np.ones(len(site)) + else: + return p1 + p2 def hopping(site1, site2, p1, p2): - return p1 - p2 + if vectorize: + return (p1 - p2) * np.ones(len(site1)) + else: + return p1 - p2 def gen_fill_syst(onsite, hopping, syst): syst[(chain(i) for i in range(3))] = onsite @@ -1221,8 +1414,9 @@ def test_argument_passing(): fill_syst = ft.partial(gen_fill_syst, onsite, hopping) - syst = fill_syst(kwant.Builder()) - inf_syst = fill_syst(kwant.Builder(kwant.TranslationalSymmetry((-3,)))) + syst = fill_syst(kwant.Builder(vectorize=vectorize)) + inf_syst = fill_syst(kwant.Builder(kwant.TranslationalSymmetry((-3,)), + vectorize=vectorize)) tests = ( syst.hamiltonian_submatrix, @@ -1238,28 +1432,34 @@ def test_argument_passing(): # test that mixing 'args' and 'params' raises TypeError with raises(TypeError): - syst.hamiltonian(0, 0, *(2, 1), params=dict(p1=2, p2=1)) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=KwantDeprecationWarning) + syst.hamiltonian(0, 0, *(2, 1), params=dict(p1=2, p2=1)) + with raises(TypeError): - inf_syst.hamiltonian(0, 0, *(2, 1), params=dict(p1=2, p2=1)) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=KwantDeprecationWarning) + inf_syst.hamiltonian(0, 0, *(2, 1), params=dict(p1=2, p2=1)) # Missing parameters raises TypeError with raises(TypeError): - syst.hamiltonian(0, 0, params=dict(p1=2)) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=KwantDeprecationWarning) + syst.hamiltonian(0, 0, params=dict(p1=2)) + with raises(TypeError): - syst.hamiltonian_submatrix(params=dict(p1=2)) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=KwantDeprecationWarning) + syst.hamiltonian_submatrix(params=dict(p1=2)) # test that passing parameters without default values works, and that # passing parameters with default values fails - def onsite(site, p1, p2): - return p1 + p2 - - def hopping(site, site2, p1, p2): - return p1 - p2 fill_syst = ft.partial(gen_fill_syst, onsite, hopping) - syst = fill_syst(kwant.Builder()) - inf_syst = fill_syst(kwant.Builder(kwant.TranslationalSymmetry((-3,)))) + syst = fill_syst(kwant.Builder(vectorize=vectorize)) + inf_syst = fill_syst(kwant.Builder(kwant.TranslationalSymmetry((-3,)), + vectorize=vectorize)) tests = ( syst.hamiltonian_submatrix, @@ -1275,12 +1475,18 @@ def test_argument_passing(): # Some common, some different args for value functions def onsite2(site, a, b): - return site.pos + a + b + if vectorize: + return site.positions()[:, 0] + a + b + else: + return site.pos[0] + a + b def hopping2(site1, site2, a, c, b): - return a + b + c + if vectorize: + return (a + b + c) * np.ones(len(site1)) + else: + return a + b + c - syst = kwant.Builder() + syst = kwant.Builder(vectorize=vectorize) syst[(chain(i) for i in range(3))] = onsite2 syst[((chain(i), chain(i + 1)) for i in range(2))] = hopping2 fsyst = syst.finalized() @@ -1391,6 +1597,7 @@ def test_subs(): lead = lead.finalized() assert lead.parameters == {'lead_a', 'lead_b', 'lead_c'} + def test_attach_stores_padding(): lat = kwant.lattice.chain(norbs=1) syst = kwant.Builder() diff --git a/kwant/tests/test_operator.py b/kwant/tests/test_operator.py index bb8a185e6675356d90552ee3d21b06d241fda83c..c5111b75d88033a74654eb5aa8e0661ee2dc51e9 100644 --- a/kwant/tests/test_operator.py +++ b/kwant/tests/test_operator.py @@ -445,7 +445,7 @@ def test_arg_passing(A): lat1 = kwant.lattice.chain(norbs=1) syst = kwant.Builder() - syst[lat1(0)] = syst[lat1(1)] = lambda s0, a, b: s0.pos + a + b + syst[lat1(0)] = syst[lat1(1)] = lambda s0, a, b: s0.pos[0] + a + b syst[lat1.neighbors()] = lambda s0, s1, a, b: a - b fsyst = syst.finalized() diff --git a/kwant/tests/test_system.py b/kwant/tests/test_system.py index 9bba152bc881013cd57bc949f46fca3244aa4153..3a4034776ee8492221910d0b00dd5a8e1aa97cf1 100644 --- a/kwant/tests/test_system.py +++ b/kwant/tests/test_system.py @@ -1,4 +1,4 @@ -# Copyright 2011-2016 Kwant authors. +# Copyright 2011-2019 Kwant authors. # # This file is part of Kwant. It is subject to the license terms in the file # LICENSE.rst found in the top-level directory of this distribution and at @@ -8,6 +8,7 @@ import pickle import copy +import pytest from pytest import raises import numpy as np from scipy import sparse @@ -15,9 +16,11 @@ import kwant from kwant._common import ensure_rng -def test_hamiltonian_submatrix(): - syst = kwant.Builder() +@pytest.mark.parametrize("vectorize", [False, True]) +def test_hamiltonian_submatrix(vectorize): + syst = kwant.Builder(vectorize=vectorize) chain = kwant.lattice.chain(norbs=1) + chain2 = kwant.lattice.chain(norbs=2) for i in range(3): syst[chain(i)] = 0.5 * i for i in range(2): @@ -27,7 +30,11 @@ def test_hamiltonian_submatrix(): mat = syst2.hamiltonian_submatrix() assert mat.shape == (3, 3) # Sorting is required due to unknown compression order of builder. - perm = np.argsort([os[0] for os in syst2.onsites]) + if vectorize: + _, (site_offsets, _) = syst2.subgraphs[0] + else: + site_offsets = [os[0] for os in syst2.onsites] + perm = np.argsort(site_offsets) mat_should_be = np.array([[0, 1j, 0], [-1j, 0.5, 2j], [0, -2j, 1]]) mat = mat[perm, :] @@ -41,19 +48,12 @@ def test_hamiltonian_submatrix(): mat = mat[:, perm] np.testing.assert_array_equal(mat, mat_should_be) - mat = syst2.hamiltonian_submatrix((), perm[[0, 1]], perm[[2]]) - np.testing.assert_array_equal(mat, mat_should_be[:2, 2:3]) - - mat = syst2.hamiltonian_submatrix((), perm[[0, 1]], perm[[2]], sparse=True) - mat = mat.toarray() - np.testing.assert_array_equal(mat, mat_should_be[:2, 2:3]) - # Test for correct treatment of matrix input. - syst = kwant.Builder() - syst[chain(0)] = np.array([[0, 1j], [-1j, 0]]) + syst = kwant.Builder(vectorize=vectorize) + syst[chain2(0)] = np.array([[0, 1j], [-1j, 0]]) syst[chain(1)] = np.array([[1]]) syst[chain(2)] = np.array([[2]]) - syst[chain(1), chain(0)] = np.array([[1, 2j]]) + syst[chain(1), chain2(0)] = np.array([[1, 2j]]) syst[chain(2), chain(1)] = np.array([[3j]]) syst2 = syst.finalized() mat_dense = syst2.hamiltonian_submatrix() @@ -62,9 +62,10 @@ def test_hamiltonian_submatrix(): # Test precalculation of modes. rng = ensure_rng(5) - lead = kwant.Builder(kwant.TranslationalSymmetry((-1,))) - lead[chain(0)] = np.zeros((2, 2)) - lead[chain(0), chain(1)] = rng.randn(2, 2) + lead = kwant.Builder(kwant.TranslationalSymmetry((-1,)), + vectorize=vectorize) + lead[chain2(0)] = np.zeros((2, 2)) + lead[chain2(0), chain2(1)] = rng.randn(2, 2) syst.attach_lead(lead) syst2 = syst.finalized() smatrix = kwant.smatrix(syst2, .1).data @@ -74,19 +75,26 @@ def test_hamiltonian_submatrix(): raises(ValueError, kwant.solvers.default.greens_function, syst3, 0.2) # Test for shape errors. - syst[chain(0), chain(2)] = np.array([[1, 2]]) + syst[chain2(0), chain(2)] = np.array([[1, 2]]) syst2 = syst.finalized() raises(ValueError, syst2.hamiltonian_submatrix) raises(ValueError, syst2.hamiltonian_submatrix, sparse=True) - syst[chain(0), chain(2)] = 1 + syst[chain2(0), chain(2)] = 1 syst2 = syst.finalized() raises(ValueError, syst2.hamiltonian_submatrix) raises(ValueError, syst2.hamiltonian_submatrix, sparse=True) + if vectorize: # non-vectorized systems don't check this at finalization + # Add another hopping of the same type but with a different + # (and still incompatible) shape. + syst[chain2(0), chain(1)] = np.array([[1, 2]]) + raises(ValueError, syst.finalized) -def test_pickling(): - syst = kwant.Builder() - lead = kwant.Builder(symmetry=kwant.TranslationalSymmetry([1.])) +@pytest.mark.parametrize("vectorize", [False, True]) +def test_pickling(vectorize): + syst = kwant.Builder(vectorize=vectorize) + lead = kwant.Builder(symmetry=kwant.TranslationalSymmetry([1.]), + vectorize=vectorize) lat = kwant.lattice.chain(norbs=1) syst[lat(0)] = syst[lat(1)] = 0 syst[lat(0), lat(1)] = 1 diff --git a/kwant/wraparound.py b/kwant/wraparound.py index 08efec8c668567acb374b12bfbed7777f0571bb5..e5688bab42919e664124fd73d8da0bdf7bb52ae6 100644 --- a/kwant/wraparound.py +++ b/kwant/wraparound.py @@ -183,6 +183,9 @@ def wraparound(builder, keep=None, *, coordinate_names='xyz'): f.__signature__ = inspect.Signature(params.values()) return f + if builder.vectorize: + raise TypeError("'wraparound' does not work with vectorized Builders.") + try: momenta = ['k_{}'.format(coordinate_names[i]) for i in range(len(builder.symmetry.periods))]