# Copyright 2011-2015 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
# http://kwant-project.org/license.  A list of Kwant authors can be found in
# the file AUTHORS.rst at the top-level directory of this distribution and at
# http://kwant-project.org/authors.

__all__ = ['Builder', 'Site', 'SiteFamily', 'SimpleSiteFamily', 'Symmetry',
           'HoppingKind', 'Lead', 'BuilderLead', 'SelfEnergyLead', 'ModesLead']

import abc
import warnings
import operator
from itertools import islice, chain
import tinyarray as ta
import numpy as np
from . import system, graph, KwantDeprecationWarning
from ._common import ensure_isinstance



################ 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)

    @property
    def pos(self):
        """Real space position of the site."""
        return self.family.pos(self.tag)


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.

    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.

    """

    def __init__(self, canonical_repr, name):
        self.canonical_repr = canonical_repr
        self.hash = hash(canonical_repr)
        self.name = name

    def __repr__(self):
        return self.canonical_repr

    def __str__(self):
        if self.name:
            msg = '<{0} site family {1}>'
        else:
            msg = '<unnamed {0} site family>'
        return msg.format(self.__class__.__name__, self.name)

    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 False

    @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.

    A family of sites tagged by any python objects where object satisfied
    condition ``object == eval(repr(object))``.

    It exists to provide a basic site family that can be used for testing the
    builder module without other dependencies.  It can be also used to tag
    sites with non-numeric objects like strings should this every be useful.

    Due to its low storage efficiency for numbers it is not recommended to use
    `SimpleSiteFamily` when `kwant.lattice.Monatomic` would also work.
    """

    def __init__(self, name=None):
        canonical_repr = '{0}({1})'.format(self.__class__, repr(name))
        super().__init__(canonical_repr, name)

    def normalize_tag(self, tag):
        tag = tuple(tag)
        try:
            if eval(repr(tag)) != tag:
                raise RuntimeError()
        except:
            raise TypeError('It must be possible to recreate the tag from '
                            'its representation.')
        return tag


def validate_hopping(hopping):
    """Verify that the argument is a valid hopping."""

    # This check is essential to maintaining the requirement that hoppings must
    # be tuples.  Without it, list "hoppings" would work in some cases
    # (e.g. with Builder.__contains__).
    if not isinstance(hopping, tuple):
        raise TypeError("{0} object is not a valid key.".format(
            type(hopping).__name__))

    # The following check is not strictly necessary (there would be an error
    # anyway), but the error message would be confusing.
    try:
        a, b = hopping
    except:
        raise IndexError("Only length 2 tuples (=hoppings) are valid keys.")

    # This check is essential for Builder.__contains__ - without it a builder
    # would simply "not contain" such invalid hoppings.  In other cases it
    # provides a nicer error message.
    for site in hopping:
        if not isinstance(site, Site):
            raise TypeError("Hopping elements must be Site objects, not {0}."
                            .format(type(site).__name__))

    # This again is an essential check.  Without it, builders would accept loop
    # hoppings.
    if a == b:
        raise ValueError("A hopping connects the following site to itself:\n"
                         "{0}".format(a))



################ Symmetries

class Symmetry(metaclass=abc.ABCMeta):
    """Abstract base class for spatial symmetries.

    Many physical systems possess a discrete spatial symmetry, which results in
    special properties of these systems.  This class is the standard way to
    describe discrete spatial symmetries in Kwant.  An instance of this class
    can be passed to a `Builder` instance at its creation.  The most important
    kind of symmetry is translational symmetry, used to define scattering
    leads.

    Each symmetry has a fundamental domain -- a set of sites and hoppings,
    generating all the possible sites and hoppings upon action of symmetry
    group elements.  A class derived from `Symmetry` has to implement mapping
    of any site or hopping into the fundamental domain, applying a symmetry
    group element to a site or a hopping, and a method `which` to determine the
    group element bringing some site from the fundamental domain to the
    requested one.  Additionally, it has to have a property `num_directions`
    returning the number of independent symmetry group generators (number of
    elementary periods for translational symmetry).

    A ``ValueError`` must be raised by the symmetry class whenever a symmetry
    is used together with sites whose site family is not compatible with it.  A
    typical example of this is when the vector defining a translational
    symmetry is not a lattice vector.
    """

    @abc.abstractproperty
    def num_directions(self):
        """Number of elementary periods of the symmetry."""
        pass

    @abc.abstractmethod
    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`.
        """
        pass

    @abc.abstractmethod
    def act(self, element, a, b=None):
        """Act with a symmetry group element on a site or hopping."""
        pass

    def to_fd(self, a, b=None):
        """Map a site or hopping to the fundamental domain.

        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.

        This default implementation works but may be not efficient.

        """
        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


class NoSymmetry(Symmetry):
    """A symmetry with a trivial symmetry group."""

    def __eq__(self, other):
        return isinstance(other, NoSymmetry)

    def __ne__(self, other):
        return not self.__eq__(other)

    def __repr__(self):
        return 'NoSymmetry()'

    @property
    def num_directions(self):
        return 0

    _empty_array = ta.array((), int)

    def which(self, site):
        return self._empty_array

    def act(self, element, a, b=None):
        if element:
            raise ValueError('`element` must be empty for NoSymmetry.')
        return a if b is None else (a, b)

    def to_fd(self, a, b=None):
        return a if b is None else (a, b)

    def in_fd(self, site):
        return True


################ Hopping kinds

class HoppingKind:
    """A pattern for matching hoppings.

    A hopping ``(a, b)`` matches precisely when the site family of ``a`` equals
    `family_a` and that of ``b`` equals `family_b` and ``(a.tag - b.tag)`` is
    equal to `delta`.  In other words, the matching hoppings have the form:
    ``(family_a(x + delta), family_b(x))``

    Parameters
    ----------
    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)
        The default value means: use the same family as `family_a`.

    Notes
    -----
    A ``HoppingKind`` is a callable object: When called with a
    `~kwant.builder.Builder` as sole argument, an instance of this class will
    return an iterator over all possible matching hoppings whose sites are
    already present in the system.  The hoppings do *not* have to be already
    present in the system.  For example::

        kind = kwant.builder.HoppingKind((1, 0), lat)
        sys[kind(sys)] = 1

    Because a `~kwant.builder.Builder` can be indexed with functions or
    iterables of functions, ``HoppingKind`` instances (or any non-tuple
    iterables of them, e.g. a list) can be used directly as "wildcards" when
    setting or deleting hoppings::

        kinds = [kwant.builder.HoppingKind(v, lat) for v in [(1, 0), (0, 1)]]
        sys[kinds] = 1
    """
    __slots__ = ('delta', 'family_a', 'family_b')

    def __init__(self, delta, family_a, family_b=None):
        self.delta = ta.array(delta, int)
        ensure_isinstance(family_a, SiteFamily)
        self.family_a = family_a
        if family_b is None:
            self.family_b = family_a
        else:
            ensure_isinstance(family_b, SiteFamily)
            self.family_b = family_b

    def __call__(self, builder):
        delta = self.delta
        family_a = self.family_a
        family_b = self.family_b
        H = builder.H
        symtofd = builder.symmetry.to_fd

        for a in H:
            if a.family != family_a:
                continue
            b = Site(family_b, a.tag - delta, True)
            if symtofd(b) in H:
                yield a, b

    def __repr__(self):
        return '{0}({1}, {2}{3})'.format(
            self.__class__.__name__, repr(tuple(self.delta)),
            repr(self.family_a),
            ', ' + repr(self.family_b) if self.family_a != self.family_b else '')

    def __str__(self):
        return '{0}({1}, {2}{3})'.format(
            self.__class__.__name__, tuple(self.delta),
            self.family_a,
            ', ' + str(self.family_b) if self.family_a != self.family_b else '')



################ Support for Hermitian conjugation

def herm_conj(value):
    """
    Calculate the hermitian conjugate of a python object.

    If the object is neither a complex number nor a matrix, the original value
    is returned.  In the context of this module, this is the correct behavior
    for functions.
    """
    if hasattr(value, 'conjugate'):
        value = value.conjugate()
        if hasattr(value, 'transpose'):
            value = value.transpose()
    return value


class HermConjOfFunc:
    """Proxy returning the hermitian conjugate of the original result."""
    __slots__ = ('function')

    def __init__(self, function):
        self.function = function

    def __call__(self, i, j, *args):
        return herm_conj(self.function(j, i, *args))


################ Leads

class Lead(metaclass=abc.ABCMeta):
    """Abstract base class for leads that can be attached to a `Builder`.

    To attach a lead to a builder, append it to the builder's `~Builder.leads`
    instance variable.  See the documentation of `kwant.builder` for the
    concrete classes of leads derived from this one.

    Attributes
    ----------
    interface : sequence of sites

    """

    @abc.abstractmethod
    def finalized(self):
        """Return a finalized version of the lead.

        Returns
        -------
        finalized_lead

        Notes
        -----
        The finalized lead must be an object that can be used as a lead in a
        `kwant.system.FiniteSystem`.  It could be an instance of
        `kwant.system.InfiniteSystem` for example.

        The order of sites for the finalized lead must be the one specified in
        `interface`.
        """
        pass


class BuilderLead(Lead):
    """A lead made from a `Builder` with a spatial symmetry.

    Parameters
    ----------
    builder : `Builder`
        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
        Sequence of sites in the scattering region to which the lead is
        attached.

    Notes
    -----
    The hopping from the scattering region to the lead is assumed to be equal to
    the hopping from a lead unit cell to the next one in the direction of the
    symmetry vector (i.e. the lead is 'leaving' the system and starts with a
    hopping).

    The given order of interface sites is preserved throughout finalization.

    Every system has an attribute `leads`, which stores a list of
    `BuilderLead` objects with all the information about the leads that are
    attached.
    """
    def __init__(self, builder, interface):
        self.builder = builder
        self.interface = tuple(interface)

    def finalized(self):
        """Return a `kwant.system.InfiniteSystem` corresponding to the
        compressed lead.

        The order of interface sites is kept during finalization.
        """
        return self.builder._finalized_infinite(self.interface)


class SelfEnergyLead(Lead):
    """A general lead defined by its self energy.

    Parameters
    ----------
    selfenergy_func : function
        Function which returns the self energy matrix for the interface sites
        given the energy and optionally a sequence of extra arguments.
    interface : sequence of `Site` instances
    """
    def __init__(self, selfenergy_func, interface):
        self.selfenergy_func = selfenergy_func
        self.interface = tuple(interface)

    def finalized(self):
        """Trivial finalization: the object is returned itself."""
        return self

    def selfenergy(self, energy, args=()):
        return self.selfenergy_func(energy, args)


class ModesLead(Lead):
    """A general lead defined by its modes wave functions.

    Parameters
    ----------
    modes_func : function
        Function which returns the modes of the lead as a tuple of
        `~kwant.physics.PropagatingModes` and `~kwant.physics.StabilizedModes`
        given the energy and optionally a sequence of extra arguments.
    interface :
        sequence of `Site` instances

    """
    def __init__(self, modes_func, interface):
        self.modes_func = modes_func
        self.interface = tuple(interface)

    def finalized(self):
        """Trivial finalization: the object is returned itself."""
        return self

    def modes(self, energy, args=()):
        return self.modes_func(energy, args)

    def selfenergy(self, energy, args=()):
        stabilized = self.modes(energy, args)[1]
        return stabilized.selfenergy()



################ Builder class

# A marker, meaning for hopping (i, j): this value is given by the Hermitian
# conjugate the value of the hopping (j, i).  Used by Builder and System.
class Other:
    pass


def edges(seq):
    # izip, when given the same iterator twice, turns a sequence into a
    # sequence of pairs.
    seq_iter = iter(seq)
    result = zip(seq_iter, seq_iter)
    next(result)                # Skip the special loop edge.
    return result


class Builder:
    """A tight binding system defined on a graph.

    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.

    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
    dictionary).  Keys are sites or hoppings.  Values are 2d arrays
    (e.g. NumPy or Tinyarray) or numbers (interpreted as 1 by 1 matrices).

    Parameters
    ----------
    symmetry : `Symmetry` or `None`
        The symmetry of the system.

    Notes
    -----
    Values can be also functions that receive the site or the hopping (passed
    to the function as two sites) and possibly additional arguments and are
    expected to return a valid value.  This allows to define systems quickly,
    to modify them without reconstructing, and to save memory for many-orbital
    models.

    In addition to simple keys (single sites and hoppings) more powerful keys
    are possible as well that allow to manipulate multiple sites/hoppings in a
    single operation.  Such keys are internally expanded into a sequence of
    simple keys by using the method `Builder.expand`.  For example,
    ``sys[general_key] = value`` is equivalent to ::

        for simple_key in sys.expand(general_key):
            sys[simple_key] = value

    Builder instances automatically ensure that every hopping is Hermitian, so
    that if ``builder[a, b]`` has been set, there is no need to set
    ``builder[b, a]``.

    Builder instances can be made to automatically respect a `Symmetry` that is
    passed to them during creation.  The behavior of builders with a symmetry
    is slightly more sophisticated.  First of all, it is implicitly assumed
    throughout Kwant that **every** function assigned as a value to a builder
    with a symmetry possesses the same symmetry.  Secondly, all keys are mapped
    to the fundamental domain of the symmetry before storing them.  This may
    produce confusing results when neighbors of a site are queried.

    The method `attach_lead` *works* only if the sites affected by them have
    tags which are sequences of integers.  It *makes sense* only when these
    sites live on a regular lattice, like the ones provided by `kwant.lattice`.

    Attaching a lead manually (without the use of `~Builder.attach_lead`)
    amounts to creating a `Lead` object and appending it to this list.

    ``builder0 += builder1`` adds all the sites, hoppings, and leads of
    ``builder1`` to ``builder0``.  Sites and hoppings present in both systems
    are overwritten by those in ``builder1``.  The leads of ``builder1`` are
    appended to the leads of the system being extended.

    .. warning::

        If functions are used to set values in a builder with a symmetry, then
        they must satisfy the same symmetry.  There is (currently) no check and
        wrong results will be the consequence of a misbehaving function.

    Attributes
    ----------
    leads : list of `Lead` instances
        The leads that are attached to the system.

    Examples
    --------
    Define a site.

    >>> builder[site] = value

    Print the value of a site.

    >>> print builder[site]

    Define a hopping.

    >>> builder[site1, site2] = value

    Delete a site.

    >>> del builder[site3]

    Detach the last lead.  (This does not remove the sites that were added to
    the scattering region by `~Builder.attach_lead`.)

    >>> del builder.leads[-1]

    """

    def __init__(self, symmetry=None):
        if symmetry is None:
            symmetry = NoSymmetry()
        else:
            ensure_isinstance(symmetry, Symmetry)
        self.symmetry = symmetry
        self.leads = []
        self.H = {}

    #### Note on H ####
    #
    # This dictionary stores a directed graph optimized for efficient querying
    # and modification.  The nodes are instances of `Site`.
    #
    # Each edge, specified by a ``(tail, head)`` pair of nodes, holds an object
    # as a value.  Likewise, each tail which occurs in the graph also holds a
    # value.  (Nodes which only occur as heads are not required to have
    # values.)
    #
    # For a given `tail` site, H[tail] is a list alternately storing
    # heads and values.  (The heads occupy even locations followed by the
    # values at odd locations.)  Each pair of entries thus describes a single
    # directed edge of the graph.
    #
    # The first pair of entries in each list is special: it always
    # corresponds to a loop edge.  (The head is equal to the tail.)  This
    # special edge has two purposes: It is used to store the value
    # associated with the tail node itself, and it is necessary for the
    # method getkey_tail which helps to conserve memory by storing equal
    # node label only once.

    def _get_edge(self, tail, head):
        for h, value in edges(self.H[tail]):
            if h == head:
                return value

        # (tail, head) is not present in the system, but tail is.
        if head in self.H:
            raise KeyError((tail, head))
        else:
            # If already head is missing, we only report this.  This way the
            # behavior is symmetric with regard to tail and head.
            raise KeyError(head)

    def _set_edge(self, tail, head, value):
        hvhv = self.H[tail]
        heads = hvhv[2::2]
        if head in heads:
            i = 2 + 2 * heads.index(head)
            hvhv[i] = head
            hvhv[i + 1] = value
        else:
            hvhv.append(head)
            hvhv.append(value)

    def _del_edge(self, tail, head):
        hvhv = self.H[tail]
        heads = hvhv[2::2]

        try:
            i = 2 + 2 * heads.index(head)
        except ValueError:
            # (tail, head) is not present in the system, but tail is.
            if head in self.H:
                raise KeyError((tail, head))
            else:
                # If already head is missing, we only report this.  This way the
                # behavior is symmetric with regard to tail and head.
                raise KeyError(head)

        del hvhv[i : i + 2]

    def _out_neighbors(self, tail):
        hvhv = self.H[tail]
        return islice(hvhv, 2, None, 2)

    def _out_degree(self, tail):
        hvhv = self.H[tail]
        return len(hvhv) // 2 - 1

    # TODO: write a test for this method.
    def reversed(self):
        """Return a shallow copy of the builder with the symmetry reversed.

        This method can be used to attach the same infinite system as lead from
        two opposite sides.  It requires a builder to which an infinite
        symmetry is associated.
        """
        result = object.__new__(Builder)
        result.symmetry = self.symmetry.reversed()
        if self.leads:
            raise ValueError('System to be reversed may not have leads.')
        result.leads = []
        result.H = self.H
        return result

    def __bool__(self):
        return bool(self.H)

    def expand(self, key):
        """
        Expand a general key into an iterator over simple keys.

        Parameters
        ----------
        key : builder key (see notes)
            The key to be expanded

        Notes
        -----
        Keys are (recursively):
            * Simple keys: sites or 2-tuples of sites (=hoppings).
            * Any (non-tuple) iterable of keys, e.g. a list or a generator
              expression.
            * Any function that returns a key when passed a builder as sole
              argument, e.g. a `HoppingKind` instance or the function returned
              by `~kwant.lattice.Polyatomic.shape`.

        This method is internally used to expand the keys when getting or
        deleting items of a builder (i.e. ``sys[key] = value`` or ``del
        sys[key]``).

        """
        itr = iter((key,))
        iter_stack = [None]
        while iter_stack:
            for key in itr:
                while callable(key):
                    key = key(self)
                if isinstance(key, tuple):
                    # Site instances are also tuples.
                    yield key
                else:
                    iter_stack.append(itr)
                    try:
                        itr = iter(key)
                    except TypeError:
                        raise TypeError("{0} object is not a valid key."
                                        .format(type(key).__name__))
                    break
            else:
                itr = iter_stack.pop()

    def __getitem__(self, key):
        """Get the value of a single site or hopping."""
        if isinstance(key, Site):
            site = self.symmetry.to_fd(key)
            return self.H[site][1]

        sym = self.symmetry
        validate_hopping(key)
        a, b = sym.to_fd(*key)
        value = self._get_edge(a, b)
        if value is Other:
            if not sym.in_fd(b):
                b, a = sym.to_fd(b, a)
                assert not sym.in_fd(a)
            value = self._get_edge(b, a)
            if callable(value):
                assert not isinstance(value, HermConjOfFunc)
                value = HermConjOfFunc(value)
            else:
                value = herm_conj(value)
        return value

    def __contains__(self, key):
        """Tell whether the system contains a site or hopping."""
        if isinstance(key, Site):
            key = self.symmetry.to_fd(key)
            return key in self.H

        validate_hopping(key)
        a, b = self.symmetry.to_fd(*key)
        hvhv = self.H.get(a, ())
        return b in islice(hvhv, 2, None, 2)

    def _set_site(self, site, value):
        """Set a single site."""
        if not isinstance(site, Site):
            raise TypeError('Expecting a site, got {0} instead.'.format(type(site).__name__))
        site = self.symmetry.to_fd(site)
        hvhv = self.H.setdefault(site, [])
        if hvhv:
            hvhv[1] = value
        else:
            hvhv[:] = [site, value]

    def _set_hopping(self, hopping, value):
        """Set a single hopping."""
        sym = self.symmetry
        validate_hopping(hopping)
        a, b = sym.to_fd(*hopping)

        if sym.in_fd(b):
            # Make sure that we do not waste space by storing multiple instances
            # of identical sites.
            a2 = a = self.H[a][0]
            b2 = b = self.H[b][0]
        else:
            b2, a2 = sym.to_fd(b, a)
            assert not sym.in_fd(a2)
            if b2 not in self.H:
                raise KeyError(b)

        # It's important that we have verified that b2 belongs to the system.
        # Otherwise, we risk ending up with a half-added hopping.
        if isinstance(value, HermConjOfFunc):
            # Avoid nested HermConjOfFunc instances.
            self._set_edge(a, b, Other)            # May raise KeyError(a).
            self._set_edge(b2, a2, value.function) # Must succeed.
        else:
            self._set_edge(a, b, value)            # May raise KeyError(a).
            self._set_edge(b2, a2, Other)          # Must succeed.

    def __setitem__(self, key, value):
        """Set a single site/hopping or a bunch of them."""
        func = None
        for sh in self.expand(key):
            if func is None:
                func = (self._set_site if isinstance(sh, Site)
                        else self._set_hopping)
            func(sh, value)

    def _del_site(self, site):
        """Delete a single site and all associated hoppings."""
        if not isinstance(site, Site):
            raise TypeError('Expecting a site, got {0} instead.'.format(
                type(site).__name__))

        tfd = self.symmetry.to_fd
        site = tfd(site)

        out_neighbors = self._out_neighbors(site)

        for neighbor in out_neighbors:
            if neighbor in self.H:
                self._del_edge(neighbor, site)
            else:
                assert not self.symmetry.in_fd(neighbor)
                self._del_edge(*tfd(neighbor, site))

        del self.H[site]

    def _del_hopping(self, hopping):
        """Delete a single hopping."""
        sym = self.symmetry
        validate_hopping(hopping)
        a, b = sym.to_fd(*hopping)
        self._del_edge(a, b)

        if sym.in_fd(b):
            self._del_edge(b, a)
        else:
            b, a = sym.to_fd(b, a)
            assert not sym.in_fd(a)
            self._del_edge(b, a)

    def __delitem__(self, key):
        """Delete a single site/hopping or bunch of them."""
        func = None
        for sh in self.expand(key):
            if func is None:
                func = (self._del_site if isinstance(sh, Site)
                        else self._del_hopping)
            func(sh)

    def eradicate_dangling(self):
        """Keep deleting dangling sites until none are left."""
        sites = list(site for site in self.H
                     if self._out_degree(site) < 2)
        for site in sites:
            if site not in self.H:
                continue
            while site:
                neighbors = tuple(self._out_neighbors(site))
                if neighbors:
                    assert len(neighbors) == 1
                    neighbor = neighbors[0]
                    self._del_edge(neighbor, site)
                    if self._out_degree(neighbor) > 1:
                        neighbor = False
                else:
                    neighbor = False
                del self.H[site]
                site = neighbor

    def __iter__(self):
        """Return an iterator over all sites and hoppings."""
        return chain(self.H, self.hoppings())

    def sites(self):
        """Return a read-only set over all sites.

        The sites that are returned belong to the fundamental domain of the
        `Builder` symmetry, and are not necessarily the ones that were set
        initially (but always the equivalent ones).
        """
        try:
            return self.H.keys()
        except AttributeError:
            return frozenset(self.H)

    def site_value_pairs(self):
        """Return an iterator over all (site, value) pairs."""
        for site, hvhv in self.H.items():
            yield site, hvhv[1]

    def hoppings(self):
        """Return an iterator over all Builder hoppings.

        The hoppings that are returned belong to the fundamental domain of the
        `Builder` symmetry, and are not necessarily the ones that were set
        initially (but always the equivalent ones).
        """
        for tail, hvhv in self.H.items():
            for head, value in edges(hvhv):
                if value is Other:
                    continue
                yield tail, head

    def hopping_value_pairs(self):
        """Return an iterator over all (hopping, value) pairs."""
        for tail, hvhv in self.H.items():
            for head, value in edges(hvhv):
                if value is Other:
                    continue
                yield (tail, head), value

    def dangling(self):
        """Return an iterator over all dangling sites."""
        for site in self.H:
            if self._out_degree(site) < 2:
                yield site

    def degree(self, site):
        """Return the number of neighbors of a site."""
        if not isinstance(site, Site):
            raise TypeError('Expecting a site, got {0} instead.'.format(
                type(site).__name__))
        site = self.symmetry.to_fd(site)
        return self._out_degree(site)

    def neighbors(self, site):
        """Return an iterator over all neighbors of a site."""
        if not isinstance(site, Site):
            raise TypeError('Expecting a site, got {0} instead.'.format(
                type(site).__name__))
        a = self.symmetry.to_fd(site)
        return self._out_neighbors(a)

    def __iadd__(self, other_sys):
        for site, value in other_sys.site_value_pairs():
            self[site] = value
        for hop, value in other_sys.hopping_value_pairs():
            self[hop] = value
        self.leads.extend(other_sys.leads)
        return self

    def attach_lead(self, lead_builder, origin=None, add_cells=0):
        """Attach a lead to the builder, possibly adding missing sites.

        Returns the lead number (integer) of the attached lead.

        Parameters
        ----------
        lead_builder : `Builder` with 1D translational symmetry
            Builder of the lead which has to be attached.
        origin : `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.
        add_cells : int
            Number of complete unit cells of the lead to be added to the system
            *after* the missing sites have been added.

        Returns
        -------
            added_sites : list of `Site` objects that were added to the system.

        Raises
        ------
        ValueError
            If `lead_builder` does not have proper symmetry, has hoppings with
            range of more than one lead unit cell, or if it is not completely
            interrupted by the system.

        Notes
        -----
        This method is not fool-proof, i.e. if it returns an error, there is
        no guarantee that the system stayed unaltered.

        The lead numbering starts from zero and increments from there, i.e.
        the leads are numbered in the order in which they are attached.
        """
        if self.symmetry.num_directions:
            raise ValueError("Leads can only be attached to finite systems.")

        if add_cells < 0 or int(add_cells) != add_cells:
            raise ValueError('add_cells must be an integer >= 0.')

        sym = lead_builder.symmetry
        H = lead_builder.H

        if sym.num_directions != 1:
            raise ValueError('Only builders with a 1D symmetry are allowed.')
        for hopping in lead_builder.hoppings():
            if not -1 <= sym.which(hopping[1])[0] <= 1:
                msg = ('The following hopping connects non-neighboring lead '
                       'unit cells. Only nearest-cell hoppings are allowed '
                       '(consider increasing the lead period).\n{0}')
                raise ValueError(msg.format(hopping))
        if not H:
            raise ValueError('Lead to be attached contains no sites.')

        # Check if site families of the lead are present in the system (catches
        # a common and a hard to find bug).
        families = set(site.family for site in H)
        lead_only_families = families.copy()
        for site in self.H:
            lead_only_families.discard(site.family)
            if not lead_only_families:
                break
        else:
            msg = ('Sites with site families {0} do not appear in the system, '
                   'hence the system does not interrupt the lead.')
            raise ValueError(msg.format(tuple(lead_only_families)))

        all_doms = set()
        for site in self.H:
            if site.family not in families:
                continue
            ge = sym.which(site)
            if sym.act(-ge, site) in H:
                all_doms.add(ge[0])

        if origin is not None:
            orig_dom = sym.which(origin)[0]
            all_doms = [dom for dom in all_doms if dom <= orig_dom]
        if len(all_doms) == 0:
            raise ValueError('Builder does not intersect with the lead,'
                             ' this lead cannot be attached.')
        max_dom = max(all_doms) + add_cells
        min_dom = min(all_doms)
        del all_doms

        interface = set()
        added = set()
        all_added = []
        # Initialize flood-fill: create the outermost sites.
        for site in H:
            for neighbor in lead_builder.neighbors(site):
                neighbor = sym.act((max_dom + 1,), neighbor)
                if sym.which(neighbor)[0] == max_dom:
                    if neighbor not in self:
                        self[neighbor] = lead_builder[neighbor]
                        added.add(neighbor)
                    interface.add(neighbor)
        all_added.extend(added)

        # Do flood-fill.
        covered = True
        while covered:
            covered = False
            added2 = set()
            for site in added:
                site_dom = sym.which(site)
                move = lambda x: sym.act(site_dom, x)
                for site_new in lead_builder.neighbors(site):
                    site_new = move(site_new)
                    new_dom = sym.which(site_new)[0]
                    if new_dom == max_dom + 1:
                        continue
                    elif new_dom < min_dom:
                        raise ValueError('Builder does not interrupt the lead,'
                                         ' this lead cannot be attached.')
                    if (site_new not in self
                        and sym.which(site_new)[0] != max_dom + 1):
                        self[site_new] = lead_builder[site_new]
                        added2.add(site_new)
                        covered = True
                    self[site_new, site] = lead_builder[site_new, site]
            added = added2
            all_added.extend(added)

        self.leads.append(BuilderLead(lead_builder, tuple(interface)))
        return all_added

    def finalized(self):
        """Return a finalized (=usable with solvers) copy of the system.

        Returns
        -------
        finalized_system : `kwant.system.FiniteSystem`
            If there is no symmetry.
        finalized_system : `kwant.system.InfiniteSystem`
            If a symmetry is present.

        Notes
        -----
        This method does not modify the Builder instance for which it is
        called.

        Attached leads are also finalized and will be present in the finalized
        system to be returned.

        Currently, only Builder instances without or with a 1D translational
        `Symmetry` can be finalized.
        """
        if self.symmetry.num_directions == 0:
            return self._finalized_finite()
        elif self.symmetry.num_directions == 1:
            return self._finalized_infinite()
        else:
            raise ValueError('Currently, only builders without or with a 1D '
                             'translational symmetry can be finalized.')

    def _finalized_finite(self):
        assert self.symmetry.num_directions == 0

        #### Make translation tables.
        sites = tuple(self.H)
        id_by_site = {}
        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 self.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 = []
        for lead_nr, lead in enumerate(self.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))

        #### Assemble and return result.
        result = FiniteSystem()
        result.graph = g
        result.sites = sites
        result.leads = finalized_leads
        result.hoppings = [self._get_edge(sites[tail], sites[head])
                           for tail, head in g]
        result.onsite_hamiltonians = [self.H[site][1] for site in sites]
        result.lead_interfaces = lead_interfaces
        result.symmetry = self.symmetry
        return result

    def _finalized_infinite(self, interface_order=None):
        """
        Finalize this 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 = self.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 self.H:    # Loop over all sites of the fund. domain.
            for head in self._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)
        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 = [sym.act(minus_one, s) for s in lsites_with]
        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.')
            del lsites_with_set

        sites = lsites_with + lsites_without + interface
        del lsites_with
        del lsites_without
        del interface
        id_by_site = {}
        for site_id, site in enumerate(sites):
            id_by_site[site] = site_id

        #### Make graph and extract onsite Hamiltonians.
        g = graph.Graph()
        g.num_nodes = len(sites)  # Some sites could not appear in any edge.
        onsite_hamiltonians = []
        for tail_id, tail in enumerate(sites[:cell_size]):
            onsite_hamiltonians.append(self.H[tail][1])
            for head in self._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)
        del id_by_site
        g = g.compressed()

        #### Extract hoppings.
        hoppings = []
        for tail_id, head_id in g:
            tail = sites[tail_id]
            head = sites[head_id]
            if tail_id >= cell_size:
                # The tail belongs to the previous domain.  Find the
                # corresponding hopping with the tail in the fund. domain.
                tail, head = sym.to_fd(tail, head)
            hoppings.append(self._get_edge(tail, head))

        #### Assemble and return result.
        result = InfiniteSystem()
        result.cell_size = cell_size
        result.sites = sites
        result.graph = g
        result.hoppings = hoppings
        result.onsite_hamiltonians = onsite_hamiltonians
        result.symmetry = self.symmetry
        return result


################ Finalized systems

class FiniteSystem(system.FiniteSystem):
    """
    Finalized `Builder` with leads.

    Usable as input for the solvers in `kwant.solvers`.
    """
    def hamiltonian(self, i, j, *args):
        if i == j:
            value = self.onsite_hamiltonians[i]
            if callable(value):
                value = value(self.sites[i], *args)
        else:
            edge_id = self.graph.first_edge_id(i, j)
            value = self.hoppings[edge_id]
            conj = value is Other
            if conj:
                i, j = j, i
                edge_id = self.graph.first_edge_id(i, j)
                value = self.hoppings[edge_id]
            if callable(value):
                sites = self.sites
                value = value(sites[i], sites[j], *args)
            if conj:
                value = herm_conj(value)
        return value

    def site(self, i):
        warnings.warn("The function `site` will disappear after Kwant 1.1.  "
                      "Use `sites` instead.", KwantDeprecationWarning,
                      stacklevel=2)
        return self.sites[i]

    def pos(self, i):
        return self.sites[i].pos


class InfiniteSystem(system.InfiniteSystem):
    """Finalized infinite system, extracted from a `Builder`."""
    def hamiltonian(self, i, j, *args):
        if i == j:
            if i >= self.cell_size:
                i -= self.cell_size
            value = self.onsite_hamiltonians[i]
            if callable(value):
                value = value(self.symmetry.to_fd(self.sites[i]), *args)
        else:
            edge_id = self.graph.first_edge_id(i, j)
            value = self.hoppings[edge_id]
            conj = value is Other
            if conj:
                i, j = j, i
                edge_id = self.graph.first_edge_id(i, j)
                value = self.hoppings[edge_id]
            if callable(value):
                sites = self.sites
                site_i = sites[i]
                site_j = sites[j]
                site_i, site_j = self.symmetry.to_fd(site_i, site_j)
                value = value(site_i, site_j, *args)
            if conj:
                value = herm_conj(value)
        return value

    def site(self, i):
        warnings.warn("The function `site` will disappear after Kwant 1.1.  "
                      "Use `sites` instead.", KwantDeprecationWarning,
                      stacklevel=2)
        return self.sites[i]

    def pos(self, i):
        return self.sites[i].pos