diff --git a/doc/source/tutorial/index.rst b/doc/source/tutorial/index.rst
index 1729a23126f5c0a7cfbd46aaa94e44a64881adf6..275e660e1c10c58d806422e917b77b17896dad5a 100644
--- a/doc/source/tutorial/index.rst
+++ b/doc/source/tutorial/index.rst
@@ -11,3 +11,4 @@ Tutorial: learning Kwant through examples
     tutorial6
     tutorial7
     tutorial8
+    tutorial9
diff --git a/doc/source/tutorial/plot_graphene.py b/doc/source/tutorial/plot_graphene.py
index d9181734ba869a624d03617c565f0852c3c50792..e81e331b2bedf7de7241b857a50611a562d51316 100644
--- a/doc/source/tutorial/plot_graphene.py
+++ b/doc/source/tutorial/plot_graphene.py
@@ -1,4 +1,4 @@
-# Tutorial 2.7.1. 2D example: graphene quantum dot
+# Tutorial 2.8.1. 2D example: graphene quantum dot
 # ================================================
 #
 # Physics background
diff --git a/doc/source/tutorial/plot_zincblende.py b/doc/source/tutorial/plot_zincblende.py
index 467f5767e2d5d2e0e4d08d168c6ba7b2de5f69f0..6b75a54e0df3d4b977ab1ec51a00960ef4f6f262 100644
--- a/doc/source/tutorial/plot_zincblende.py
+++ b/doc/source/tutorial/plot_zincblende.py
@@ -1,4 +1,4 @@
-# Tutorial 2.7.2. 3D example: zincblende structure
+# Tutorial 2.8.2. 3D example: zincblende structure
 # ================================================
 #
 # Physical background
diff --git a/doc/source/tutorial/tutorial6.rst b/doc/source/tutorial/tutorial6.rst
deleted file mode 100644
index dbf267042eb65f06d614fe3a04d18594b5cd3ec2..0000000000000000000000000000000000000000
--- a/doc/source/tutorial/tutorial6.rst
+++ /dev/null
@@ -1,286 +0,0 @@
-Plotting Kwant systems and data in various styles
--------------------------------------------------
-
-The plotting functionality of Kwant has been used extensively (through
-`~kwant.plotter.plot` and `~kwant.plotter.map`) in the previous tutorials. In
-addition to this basic use, `~kwant.plotter.plot` offers many options to change
-the plotting style extensively. It is the goal of this tutorial to show how
-these options can be used to achieve various very different objectives.
-
-2D example: graphene quantum dot
-................................
-
-.. seealso::
-    The complete source code of this example can be found in
-    :download:`tutorial/plot_graphene.py <../../../tutorial/plot_graphene.py>`
-
-We begin by first considering a circular graphene quantum dot (similar to what
-has been used in parts of the tutorial :ref:`tutorial-graphene`.)  In contrast
-to previous examples, we will also use hoppings beyond next-nearest neighbors:
-
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_makesyst
-    :end-before: #HIDDEN_END_makesyst
-
-Note that adding hoppings hoppings to the `n`-th nearest neighbors can be
-simply done by passing `n` as an argument to
-`~kwant.lattice.Polyatomic.neighbors`. Also note that we use the method
-`~kwant.builder.Builder.eradicate_dangling` to get rid of single atoms sticking
-out of the shape. It is necessary to do so *before* adding the
-next-nearest-neighbor hopping [#]_.
-
-Of course, the system can be plotted simply with default settings:
-
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_plotsyst1
-    :end-before: #HIDDEN_END_plotsyst1
-
-However, due to the richer structure of the lattice, this results in a rather
-busy plot:
-
-.. image:: ../images/plot_graphene_syst1.*
-
-A much clearer plot can be obtained by using different colors for both
-sublattices, and by having different line widths for different hoppings.  This
-can be achieved by passing a function to the arguments of
-`~kwant.plotter.plot`, instead of a constant. For properties of sites, this
-must be a function taking one site as argument, for hoppings a function taking
-the start end end site of hopping as arguments:
-
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_plotsyst2
-    :end-before: #HIDDEN_END_plotsyst2
-
-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
-that carries the same information, but is much easier to interpret:
-
-.. image:: ../images/plot_graphene_syst2.*
-
-Apart from plotting the *system* itself, `~kwant.plotter.plot` can also be used
-to plot *data* living on the system.
-
-As an example, we now compute the eigenstates of the graphene quantum dot and
-intend to plot the wave function probability in the quantum dot. For aesthetic
-reasons (the wave functions look a bit nicer), we restrict ourselves to
-nearest-neighbor hopping.  Computing the wave functions is done in the usual
-way (note that for a large-scale system, one would probably want to use sparse
-linear algebra):
-
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_plotdata1
-    :end-before: #HIDDEN_END_plotdata1
-
-In most cases, to plot the wave function probability, one wouldn't use
-`~kwant.plotter.plot`, but rather `~kwant.plotter.map`. Here, we plot the
-`n`-th wave function using it:
-
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_plotdata2
-    :end-before: #HIDDEN_END_plotdata2
-
-This results in a standard pseudocolor plot, showing in this case (``n=225``) a
-graphene edge state, i.e. a wave function mostly localized at the zigzag edges
-of the quantum dot.
-
-.. image:: ../images/plot_graphene_data1.*
-
-However although in general preferable, `~kwant.plotter.map` has a few
-deficiencies for this small system: For example, there are a few distortions at
-the edge of the dot. (This cannot be avoided in the type of interpolation used
-in `~kwant.plotter.map`). However, we can also use `~kwant.plotter.plot` to
-achieve a similar, but smoother result.
-
-For this note that `~kwant.plotter.plot` can also take an array of floats (or
-function returning floats) as value for the ``site_color`` argument (the same
-holds for the hoppings). Via the colormap specified in ``cmap`` these are mapped
-to color, just as `~kwant.plotter.map` does! In addition, we can also change
-the symbol shape depending on the sublattice. With a triangle pointing up and
-down on the respective sublattice, the symbols used by plot fill the space
-completely:
-
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_plotdata3
-    :end-before: #HIDDEN_END_plotdata3
-
-Note that with ``hop_lw=0`` we deactivate plotting the hoppings (that would not
-serve any purpose here). Moreover, ``site_size=0.5`` guarantees that the two
-different types of triangles touch precisely: By default, `~kwant.plotter.plot`
-takes all sizes in units of the nearest-neighbor spacing. ``site_size=0.5``
-thus means half the distance between neighboring sites (and for the triangles
-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.
-
-With this we arrive at
-
-.. image:: ../images/plot_graphene_data2.*
-
-with the same information as `~kwant.plotter.map`, but with a cleaner look.
-
-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
-beyond pseudocolor-like plots. For example, we can represent the wave function
-probability using the symbols itself:
-
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_plotdata4
-    :end-before: #HIDDEN_END_plotdata4
-
-Here, we choose the symbol size proportional to the wave function probability,
-while the site color is transparent to also allow for overlapping symbols to be
-visible. The hoppings are also plotted in order to show the underlying lattice.
-
-With this, we arrive at
-
-.. image:: ../images/plot_graphene_data3.*
-
-which shows the edge state nature of the wave function most clearly.
-
-.. rubric:: Footnotes
-
-.. [#] A dangling site is defined as having only one hopping connecting it to
-       the rest. With next-nearest-neighbor hopping also all sites that are
-       dangling with only nearest-neighbor hopping have more than one hopping.
-
-3D example: zincblende structure
-................................
-
-.. seealso::
-    The complete source code of this example can be found in
-    :download:`tutorial/plot_zincblende.py <../../../tutorial/plot_zincblende.py>`
-
-Zincblende is a very common crystal structure of semiconductors. It is a
-face-centered cubic crystal with two inequivalent atoms in the unit cell
-(i.e. two different types of atoms, unlike diamond which has the same crystal
-structure, but two equivalent atoms per unit cell).
-
-It is very easily generated in Kwant with `kwant.lattice.general`:
-
-.. literalinclude:: plot_zincblende.py
-    :start-after: #HIDDEN_BEGIN_zincblende1
-    :end-before: #HIDDEN_END_zincblende1
-
-Note how we keep references to the two different sublattices for later use.
-
-A three-dimensional structure is created as easily as in two dimensions, by
-using the `~kwant.lattice.PolyatomicLattice.shape`-functionality:
-
-.. literalinclude:: plot_zincblende.py
-    :start-after: #HIDDEN_BEGIN_zincblende2
-    :end-before: #HIDDEN_END_zincblende2
-
-We restrict ourselves here to a simple cuboid, and do not bother to add real
-values for onsite and hopping energies, but only the placeholder ``None`` (in a
-real calculation, several atomic orbitals would have to be considered).
-
-`~kwant.plotter.plot` can plot 3D systems just as easily as its two-dimensional
-counterparts:
-
-.. literalinclude:: plot_zincblende.py
-    :start-after: #HIDDEN_BEGIN_plot1
-    :end-before: #HIDDEN_END_plot1
-
-resulting in
-
-.. image:: ../images/plot_zincblende_syst1.*
-
-You might notice that the standard options for plotting are quite different in
-3D than in 2D. For example, by default hoppings are not printed, but sites are
-instead represented by little "balls" touching each other (which is achieved by
-a default ``site_size=0.5``). In fact, this style of plotting 3D shows quite
-decently the overall geometry of the system.
-
-When plotting into a window, the 3D plots can also be rotated and scaled
-arbitrarily, allowing for a good inspection of the geometry from all sides.
-
-.. note::
-
-    Interactive 3D plots usually do not have the proper aspect ratio, but are a
-    bit squashed. This is due to bugs in matplotlib's 3D plotting module that
-    does not properly honor the corresponding arguments. By resizing the plot
-    window however one can manually adjust the aspect ratio.
-
-Also for 3D it is possible to customize the plot. For example, we
-can explicitly plot the hoppings as lines, and color sites differently
-depending on the sublattice:
-
-.. literalinclude:: plot_zincblende.py
-    :start-after: #HIDDEN_BEGIN_plot2
-    :end-before: #HIDDEN_END_plot2
-
-which results in a 3D plot that allows to interactively (when plotted
-in a window) explore the crystal structure:
-
-.. image:: ../images/plot_zincblende_syst2.*
-
-Hence, a few lines of code using Kwant allow to explore all the different
-crystal lattices out there!
-
-.. note::
-
-    - The 3D plots are in fact only *fake* 3D. For example, sites will always
-      be plotted above hoppings (this is due to the limitations of matplotlib's
-      3d module)
-    - Plotting hoppings in 3D is inherently much slower than plotting sites.
-      Hence, this is not done by default.
-
-
-Interpolated density and current: QPC with disorder
-...................................................
-
-.. seealso::
-    The complete source code of this example can be found in
-    :download:`tutorial/plot_qpc.py <../../../tutorial/plot_qpc.py>`
-
-In the above examples we saw some useful methods for plotting systems where
-single-site resolution is required. Sometimes, however, having single-site
-precision is a hinderance, rather than a help, and looking at *averaged*
-quantities is more useful. This is particularly important in systems with a
-large number of sites, and systems that are discretizations of continuum
-models.
-
-Here we will show how to plot interpolated quantities using `kwant.plotter.map`
-and `kwant.plotter.current` using the example of a quantum point contact (QPC)
-with a perpendicular magnetic field and disorder:
-
-.. literalinclude:: plot_qpc.py
-    :start-after: #HIDDEN_BEGIN_syst
-    :end-before: #HIDDEN_END_syst
-
-.. image:: ../images/plot_qpc_syst.*
-
-Now we will compute the density of particles and current due to states
-originating in the left lead with energy 0.15.
-
-.. literalinclude:: plot_qpc.py
-    :start-after: #HIDDEN_BEGIN_wf
-    :end-before: #HIDDEN_END_wf
-
-We can then plot the density using `~kwant.plotter.map`:
-
-.. literalinclude:: plot_qpc.py
-    :start-after: #HIDDEN_BEGIN_density
-    :end-before: #HIDDEN_END_density
-
-.. image:: ../images/plot_qpc_density.*
-
-We pass ``method='linear'`` to ``map``, which produces a smoother plot than the
-default style. We see that density is concentrated on the edges of the sample,
-as we expect due to Hall effect induced by the perpendicular magnetic field.
-
-Plotting the current in the system will enable us to make even more sense
-of what is going on:
-
-.. literalinclude:: plot_qpc.py
-    :start-after: #HIDDEN_BEGIN_current
-    :end-before: #HIDDEN_END_current
-
-.. image:: ../images/plot_qpc_current.*
-
-We can now clearly see that current enters the system from the lower-left edge
-of the system (this matches our intuition, as we calculated the current for
-scattering states originating in the left-hand lead), and is backscattered by
-the restriction of the QPC in the centre.
diff --git a/doc/source/tutorial/tutorial7.rst b/doc/source/tutorial/tutorial7.rst
index 3ac75ba8936c8214ed2488246e045ef09d965263..dbf267042eb65f06d614fe3a04d18594b5cd3ec2 100644
--- a/doc/source/tutorial/tutorial7.rst
+++ b/doc/source/tutorial/tutorial7.rst
@@ -1,289 +1,286 @@
-##############################################################
-Calculating spectral density with the kernel polynomial method
-##############################################################
-
-We have already seen in the ":ref:`closed-systems`" tutorial that we can use
-Kwant simply to build Hamiltonians, which we can then directly diagonalize
-using routines from Scipy.
-
-This already allows us to treat systems with a few thousand sites without too
-many problems.  For larger systems one is often not so interested in the exact
-eigenenergies and eigenstates, but more in the *density of states*.
-
-The kernel polynomial method (KPM), is an algorithm to obtain a polynomial
-expansion of the density of states. It can also be used to calculate the
-spectral density of arbitrary operators.  Kwant has an implementation of the
-KPM method that is based on the algorithms presented in Ref. [1]_.
-
-Roughly speaking, KPM approximates the density of states (or any other spectral
-density) by expanding the action of the Hamiltonian (and operator of interest)
-on a (small) set of *random vectors* as a sum of Chebyshev polynomials up to
-some order, and then averaging. The accuracy of the method can be tuned by
-modifying the order of the Chebyshev expansion and the number of random
-vectors.  See notes on accuracy_ below for details.
+Plotting Kwant systems and data in various styles
+-------------------------------------------------
+
+The plotting functionality of Kwant has been used extensively (through
+`~kwant.plotter.plot` and `~kwant.plotter.map`) in the previous tutorials. In
+addition to this basic use, `~kwant.plotter.plot` offers many options to change
+the plotting style extensively. It is the goal of this tutorial to show how
+these options can be used to achieve various very different objectives.
+
+2D example: graphene quantum dot
+................................
 
 .. seealso::
     The complete source code of this example can be found in
-    :download:`tutorial/kernel_polynomial_method.py <../../../tutorial/kernel_polynomial_method.py>`
+    :download:`tutorial/plot_graphene.py <../../../tutorial/plot_graphene.py>`
+
+We begin by first considering a circular graphene quantum dot (similar to what
+has been used in parts of the tutorial :ref:`tutorial-graphene`.)  In contrast
+to previous examples, we will also use hoppings beyond next-nearest neighbors:
 
-.. _accuracy:
-.. specialnote:: Performance and accuracy
+.. literalinclude:: plot_graphene.py
+    :start-after: #HIDDEN_BEGIN_makesyst
+    :end-before: #HIDDEN_END_makesyst
 
-    The KPM method is especially well suited for large systems, and in the
-    case when one is not interested in individual eigenvalues, but rather
-    in obtaining an approximate spectral density.
+Note that adding hoppings hoppings to the `n`-th nearest neighbors can be
+simply done by passing `n` as an argument to
+`~kwant.lattice.Polyatomic.neighbors`. Also note that we use the method
+`~kwant.builder.Builder.eradicate_dangling` to get rid of single atoms sticking
+out of the shape. It is necessary to do so *before* adding the
+next-nearest-neighbor hopping [#]_.
 
-    The accuracy in the energy resolution is dominated by the number of
-    moments. The lowest accuracy is at the center of the spectrum, while
-    slightly higher accuracy is obtained at the edges of the spectrum.
-    If we use the KPM method (with the Jackson kernel, see Ref. [1]_) to
-    describe a delta peak at the center of the spectrum, we will obtain a
-    function similar to a Gaussian of width :math:`σ=πa/N`, where
-    :math:`N` is the number of moments, and :math:`a` is the width of the
-    spectrum.
+Of course, the system can be plotted simply with default settings:
 
-    On the other hand, the random vectors will *explore* the range of the
-    spectrum, and as the system gets bigger, the number of random vectors
-    that are necessary to sample the whole spectrum reduces. Thus, a small
-    number of random vectors is in general enough, and increasing this number
-    will not result in a visible improvement of the approximation.
+.. literalinclude:: plot_graphene.py
+    :start-after: #HIDDEN_BEGIN_plotsyst1
+    :end-before: #HIDDEN_END_plotsyst1
 
+However, due to the richer structure of the lattice, this results in a rather
+busy plot:
 
-Introduction
-************
+.. image:: ../images/plot_graphene_syst1.*
 
-Our aim is to use the kernel polynomial method to obtain the spectral density
-:math:`ρ_A(E)`, as a function of the energy :math:`E`, of some Hilbert space
-operator :math:`A`.  We define
+A much clearer plot can be obtained by using different colors for both
+sublattices, and by having different line widths for different hoppings.  This
+can be achieved by passing a function to the arguments of
+`~kwant.plotter.plot`, instead of a constant. For properties of sites, this
+must be a function taking one site as argument, for hoppings a function taking
+the start end end site of hopping as arguments:
 
-.. math::
+.. literalinclude:: plot_graphene.py
+    :start-after: #HIDDEN_BEGIN_plotsyst2
+    :end-before: #HIDDEN_END_plotsyst2
 
-    ρ_A(E) = ρ(E) A(E),
+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
+that carries the same information, but is much easier to interpret:
 
-where :math:`A(E)` is the expectation value of :math:`A` for all the
-eigenstates of the Hamiltonian with energy :math:`E`,  and the density of
-states is
+.. image:: ../images/plot_graphene_syst2.*
 
-.. math::
+Apart from plotting the *system* itself, `~kwant.plotter.plot` can also be used
+to plot *data* living on the system.
 
-  ρ(E) = \frac{1}{D} \sum_{k=0}^{D-1} δ(E-E_k),
+As an example, we now compute the eigenstates of the graphene quantum dot and
+intend to plot the wave function probability in the quantum dot. For aesthetic
+reasons (the wave functions look a bit nicer), we restrict ourselves to
+nearest-neighbor hopping.  Computing the wave functions is done in the usual
+way (note that for a large-scale system, one would probably want to use sparse
+linear algebra):
 
-:math:`D` being the Hilbert space dimension, and :math:`E_k` the eigenvalues.
+.. literalinclude:: plot_graphene.py
+    :start-after: #HIDDEN_BEGIN_plotdata1
+    :end-before: #HIDDEN_END_plotdata1
 
-In the special case when :math:`A` is the identity, then :math:`ρ_A(E)` is
-simply :math:`ρ(E)`, the density of states.
+In most cases, to plot the wave function probability, one wouldn't use
+`~kwant.plotter.plot`, but rather `~kwant.plotter.map`. Here, we plot the
+`n`-th wave function using it:
 
+.. literalinclude:: plot_graphene.py
+    :start-after: #HIDDEN_BEGIN_plotdata2
+    :end-before: #HIDDEN_END_plotdata2
 
-Calculating the density of states
-*********************************
+This results in a standard pseudocolor plot, showing in this case (``n=225``) a
+graphene edge state, i.e. a wave function mostly localized at the zigzag edges
+of the quantum dot.
 
-In the following example, we will use the KPM implementation in Kwant
-to obtain the density of states of a graphene disk.
+.. image:: ../images/plot_graphene_data1.*
 
-We start by importing kwant and defining our system.
+However although in general preferable, `~kwant.plotter.map` has a few
+deficiencies for this small system: For example, there are a few distortions at
+the edge of the dot. (This cannot be avoided in the type of interpolation used
+in `~kwant.plotter.map`). However, we can also use `~kwant.plotter.plot` to
+achieve a similar, but smoother result.
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_sys1
-    :end-before: #HIDDEN_END_sys1
+For this note that `~kwant.plotter.plot` can also take an array of floats (or
+function returning floats) as value for the ``site_color`` argument (the same
+holds for the hoppings). Via the colormap specified in ``cmap`` these are mapped
+to color, just as `~kwant.plotter.map` does! In addition, we can also change
+the symbol shape depending on the sublattice. With a triangle pointing up and
+down on the respective sublattice, the symbols used by plot fill the space
+completely:
 
-After making a system we can then create a `~kwant.kpm.SpectralDensity`
-object that represents the density of states for this system.
+.. literalinclude:: plot_graphene.py
+    :start-after: #HIDDEN_BEGIN_plotdata3
+    :end-before: #HIDDEN_END_plotdata3
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_kpm1
-    :end-before: #HIDDEN_END_kpm1
+Note that with ``hop_lw=0`` we deactivate plotting the hoppings (that would not
+serve any purpose here). Moreover, ``site_size=0.5`` guarantees that the two
+different types of triangles touch precisely: By default, `~kwant.plotter.plot`
+takes all sizes in units of the nearest-neighbor spacing. ``site_size=0.5``
+thus means half the distance between neighboring sites (and for the triangles
+this is interpreted as the radius of the inner circle).
 
-The `~kwant.kpm.SpectralDensity` can then be called like a function to obtain a
-sequence of energies in the spectrum of the Hamiltonian, and the corresponding
-density of states at these energies.
+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.
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_kpm2
-    :end-before: #HIDDEN_END_kpm2
+With this we arrive at
 
-When called with no arguments, an optimal set of energies is chosen (these are
-not evenly distributed over the spectrum, see Ref. [1]_ for details), however
-it is also possible to provide an explicit sequence of energies at which to
-evaluate the density of states.
+.. image:: ../images/plot_graphene_data2.*
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_kpm3
-    :end-before: #HIDDEN_END_kpm3
+with the same information as `~kwant.plotter.map`, but with a cleaner look.
 
-.. image:: ../images/kpm_dos.*
+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
+beyond pseudocolor-like plots. For example, we can represent the wave function
+probability using the symbols itself:
 
-In addition to being called like functions, `~kwant.kpm.SpectralDensity`
-objects also have a method `~kwant.kpm.SpectralDensity.average` which can be
-used to integrate the density of states against some distribution function over
-the whole spectrum. If no distribution function is specified, then the uniform
-distribution is used:
+.. literalinclude:: plot_graphene.py
+    :start-after: #HIDDEN_BEGIN_plotdata4
+    :end-before: #HIDDEN_END_plotdata4
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_av1
-    :end-before: #HIDDEN_END_av1
+Here, we choose the symbol size proportional to the wave function probability,
+while the site color is transparent to also allow for overlapping symbols to be
+visible. The hoppings are also plotted in order to show the underlying lattice.
 
-.. literalinclude:: ../images/kpm_normalization.txt
+With this, we arrive at
 
-We see that the integral of the density of states is normalized to 1. If
-we wish to calculate, say, the average number of states populated in
-equilibrium, then we should integrate with respect to a Fermi-Dirac
-distribution and multiply by the total number of available states in
-the system:
+.. image:: ../images/plot_graphene_data3.*
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_av2
-    :end-before: #HIDDEN_END_av2
+which shows the edge state nature of the wave function most clearly.
 
-.. literalinclude:: ../images/kpm_total_states.txt
+.. rubric:: Footnotes
 
-.. specialnote:: Stability and performance: spectral bounds
+.. [#] A dangling site is defined as having only one hopping connecting it to
+       the rest. With next-nearest-neighbor hopping also all sites that are
+       dangling with only nearest-neighbor hopping have more than one hopping.
 
-    The KPM method internally rescales the spectrum of the Hamiltonian to the
-    interval ``(-1, 1)`` (see Ref [1]_ for details), which requires calculating
-    the boundaries of the spectrum (using ``scipy.sparse.linalg.eigsh``). This
-    can be very costly for large systems, so it is possible to pass this
-    explicitly as via the ``bounds`` parameter when instantiating the
-    `~kwant.kpm.SpectralDensity` (see the class documentation for details).
+3D example: zincblende structure
+................................
 
-    Additionally, `~kwant.kpm.SpectralDensity` accepts a parameter ``epsilon``,
-    which ensures that the rescaled Hamiltonian (used internally), always has a
-    spectrum strictly contained in the interval ``(-1, 1)``. If bounds are not
-    provided then the tolerance on the bounds calculated with
-    ``scipy.sparse.linalg.eigsh`` is set to ``epsilon/2``.
+.. seealso::
+    The complete source code of this example can be found in
+    :download:`tutorial/plot_zincblende.py <../../../tutorial/plot_zincblende.py>`
 
+Zincblende is a very common crystal structure of semiconductors. It is a
+face-centered cubic crystal with two inequivalent atoms in the unit cell
+(i.e. two different types of atoms, unlike diamond which has the same crystal
+structure, but two equivalent atoms per unit cell).
 
-Increasing the accuracy of the approximation
-********************************************
+It is very easily generated in Kwant with `kwant.lattice.general`:
 
-`~kwant.kpm.SpectralDensity` has two methods for increasing the accuracy
-of the method, each of which offers different levels of control over what
-exactly is changed.
+.. literalinclude:: plot_zincblende.py
+    :start-after: #HIDDEN_BEGIN_zincblende1
+    :end-before: #HIDDEN_END_zincblende1
 
-The simplest way to obtain a more accurate solution is to use the
-``add_moments`` method:
+Note how we keep references to the two different sublattices for later use.
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_acc1
-    :end-before: #HIDDEN_END_acc1
+A three-dimensional structure is created as easily as in two dimensions, by
+using the `~kwant.lattice.PolyatomicLattice.shape`-functionality:
 
-This will update the number of calculated moments and also the
-number of sampling points such that the maximum distance between successive
-energy points is ``energy_resolution`` (see notes on accuracy_).
+.. literalinclude:: plot_zincblende.py
+    :start-after: #HIDDEN_BEGIN_zincblende2
+    :end-before: #HIDDEN_END_zincblende2
 
-.. image:: ../images/kpm_dos_acc.*
+We restrict ourselves here to a simple cuboid, and do not bother to add real
+values for onsite and hopping energies, but only the placeholder ``None`` (in a
+real calculation, several atomic orbitals would have to be considered).
 
-Alternatively, you can directly increase the number of moments
-with ``add_moments``, or the number of random vectors with ``add_vectors``.
+`~kwant.plotter.plot` can plot 3D systems just as easily as its two-dimensional
+counterparts:
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_acc2
-    :end-before: #HIDDEN_END_acc2
+.. literalinclude:: plot_zincblende.py
+    :start-after: #HIDDEN_BEGIN_plot1
+    :end-before: #HIDDEN_END_plot1
 
-.. image:: ../images/kpm_dos_r.*
+resulting in
 
+.. image:: ../images/plot_zincblende_syst1.*
 
-.. _operator_spectral_density:
+You might notice that the standard options for plotting are quite different in
+3D than in 2D. For example, by default hoppings are not printed, but sites are
+instead represented by little "balls" touching each other (which is achieved by
+a default ``site_size=0.5``). In fact, this style of plotting 3D shows quite
+decently the overall geometry of the system.
 
-Calculating the spectral density of an operator
-***********************************************
+When plotting into a window, the 3D plots can also be rotated and scaled
+arbitrarily, allowing for a good inspection of the geometry from all sides.
 
-Above, we saw how to calculate the density of states by creating a
-`~kwant.kpm.SpectralDensity` and passing it a finalized Kwant system.
-When instantiating a `~kwant.kpm.SpectralDensity` we may optionally
-supply an operator in addition to the system. In this case it is
-the spectral density of the given operator that is calculated.
+.. note::
 
-`~kwant.kpm.SpectralDensity` accepts the operators in a few formats:
+    Interactive 3D plots usually do not have the proper aspect ratio, but are a
+    bit squashed. This is due to bugs in matplotlib's 3D plotting module that
+    does not properly honor the corresponding arguments. By resizing the plot
+    window however one can manually adjust the aspect ratio.
 
-* *explicit matrices* (numpy array of scipy sparse matrices will work)
-* *operators* from `kwant.operator`
+Also for 3D it is possible to customize the plot. For example, we
+can explicitly plot the hoppings as lines, and color sites differently
+depending on the sublattice:
 
-If an explicit matrix is provided then it must have the same
-shape as the system Hamiltonian.
+.. literalinclude:: plot_zincblende.py
+    :start-after: #HIDDEN_BEGIN_plot2
+    :end-before: #HIDDEN_END_plot2
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_op1
-    :end-before: #HIDDEN_END_op1
+which results in a 3D plot that allows to interactively (when plotted
+in a window) explore the crystal structure:
 
+.. image:: ../images/plot_zincblende_syst2.*
 
-Or, to do the same calculation using `kwant.operator.Density`:
+Hence, a few lines of code using Kwant allow to explore all the different
+crystal lattices out there!
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_op2
-    :end-before: #HIDDEN_END_op2
+.. note::
 
-Using operators from `kwant.operator` allows us to calculate quantities
-such as the *local* density of states by telling the operator not to
-sum over all the sites of the system:
+    - The 3D plots are in fact only *fake* 3D. For example, sites will always
+      be plotted above hoppings (this is due to the limitations of matplotlib's
+      3d module)
+    - Plotting hoppings in 3D is inherently much slower than plotting sites.
+      Hence, this is not done by default.
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_op3
-    :end-before: #HIDDEN_END_op3
 
-`~kwant.kpm.SpectralDensity` will properly handle this vector output,
-which allows us to plot the local density of states at different
-point in the spectrum:
+Interpolated density and current: QPC with disorder
+...................................................
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_op4
-    :end-before: #HIDDEN_END_op4
+.. seealso::
+    The complete source code of this example can be found in
+    :download:`tutorial/plot_qpc.py <../../../tutorial/plot_qpc.py>`
 
-.. image:: ../images/kpm_ldos.*
+In the above examples we saw some useful methods for plotting systems where
+single-site resolution is required. Sometimes, however, having single-site
+precision is a hinderance, rather than a help, and looking at *averaged*
+quantities is more useful. This is particularly important in systems with a
+large number of sites, and systems that are discretizations of continuum
+models.
 
-This nicely illustrates the edge states of the graphene dot at zero
-energy, and the bulk states at higher energy.
+Here we will show how to plot interpolated quantities using `kwant.plotter.map`
+and `kwant.plotter.current` using the example of a quantum point contact (QPC)
+with a perpendicular magnetic field and disorder:
 
+.. literalinclude:: plot_qpc.py
+    :start-after: #HIDDEN_BEGIN_syst
+    :end-before: #HIDDEN_END_syst
 
-Advanced topics
-***************
+.. image:: ../images/plot_qpc_syst.*
 
-Custom distributions for random vectors
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-By default `~kwant.kpm.SpectralDensity` will use random vectors
-whose components are unit complex numbers with phases drawn
-from a uniform distribution. There are several reasons why you may
-wish to make a different choice of distribution for your random vectors,
-for example to enforce certain symmetries or to only use real-valued vectors.
+Now we will compute the density of particles and current due to states
+originating in the left lead with energy 0.15.
 
-To change how the random vectors are generated, you need only specify a
-function that takes the dimension of the Hilbert space as a single parameter,
-and which returns a vector in that Hilbert space:
+.. literalinclude:: plot_qpc.py
+    :start-after: #HIDDEN_BEGIN_wf
+    :end-before: #HIDDEN_END_wf
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_fact1
-    :end-before: #HIDDEN_END_fact1
+We can then plot the density using `~kwant.plotter.map`:
 
-Reproducible calculations
-^^^^^^^^^^^^^^^^^^^^^^^^^
-Because KPM internally uses random vectors, running the same calculation
-twice will not give bit-for-bit the same result. However, similarly to
-the funcions in `~kwant.rmt`, the random number generator can be directly
-manipulated by passing a value to the ``rng`` parameter of
-`~kwant.kpm.SpectralDensity`. ``rng`` can itself be a random number generator,
-or it may simply be a seed to pass to the numpy random number generator
-(that is used internally by default).
+.. literalinclude:: plot_qpc.py
+    :start-after: #HIDDEN_BEGIN_density
+    :end-before: #HIDDEN_END_density
 
-Defining operators as sesquilinear maps
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-`Above`__, we showed how `~kwant.kpm.SpectralDensity` can calculate the
-spectral density of operators, and how we can define operators by using
-`kwant.operator`.  If you need even more flexibility,
-`~kwant.kpm.SpectralDensity` will also accept a *function* as its ``operator``
-parameter. This function must itself take two parameters, ``(bra, ket)`` and
-must return either a scalar or a one-dimensional array. In order to be
-meaningful the function must be a sesquilinear map, i.e. antilinear in its
-first argument, and linear in its second argument. Below, we compare two
-methods for computing the local density of states, one using
-`kwant.operator.Density`, and the other using a custom function.
+.. image:: ../images/plot_qpc_density.*
 
-.. literalinclude:: kernel_polynomial_method.py
-    :start-after: #HIDDEN_BEGIN_blm
-    :end-before: #HIDDEN_END_blm
+We pass ``method='linear'`` to ``map``, which produces a smoother plot than the
+default style. We see that density is concentrated on the edges of the sample,
+as we expect due to Hall effect induced by the perpendicular magnetic field.
 
-__ operator_spectral_density_
+Plotting the current in the system will enable us to make even more sense
+of what is going on:
 
+.. literalinclude:: plot_qpc.py
+    :start-after: #HIDDEN_BEGIN_current
+    :end-before: #HIDDEN_END_current
 
-.. rubric:: References
+.. image:: ../images/plot_qpc_current.*
 
-.. [1] `Rev. Mod. Phys., Vol. 78, No. 1 (2006)
-    <https://arxiv.org/abs/cond-mat/0504627>`_.
+We can now clearly see that current enters the system from the lower-left edge
+of the system (this matches our intuition, as we calculated the current for
+scattering states originating in the left-hand lead), and is backscattered by
+the restriction of the QPC in the centre.
diff --git a/doc/source/tutorial/tutorial8.rst b/doc/source/tutorial/tutorial8.rst
index c210d9290fb158bb7f57f1a63bf853505b3debb1..f40d1920231ee7602e9d93c2f472a6e9cb023e22 100644
--- a/doc/source/tutorial/tutorial8.rst
+++ b/doc/source/tutorial/tutorial8.rst
@@ -1,323 +1,289 @@
-Discretizing continuous Hamiltonians
-------------------------------------
-
-Introduction
-............
-
-In ":ref:`tutorial_discretization_schrodinger`" we have learnt that Kwant works
-with tight-binding Hamiltonians. Often, however, one will start with a
-continuum model and will subsequently need to discretize it to arrive at a
-tight-binding model.
-Although discretizing a Hamiltonian is usually a simple
-process, it is tedious and repetitive. The situation is further exacerbated
-when one introduces additional on-site degrees of freedom, and tracking all
-the necessary terms becomes a chore.
-The `~kwant.continuum` sub-package aims to be a solution to this problem.
-It is a collection of tools for working with
-continuum models and for discretizing them into tight-binding models.
+##############################################################
+Calculating spectral density with the kernel polynomial method
+##############################################################
+
+We have already seen in the ":ref:`closed-systems`" tutorial that we can use
+Kwant simply to build Hamiltonians, which we can then directly diagonalize
+using routines from Scipy.
+
+This already allows us to treat systems with a few thousand sites without too
+many problems.  For larger systems one is often not so interested in the exact
+eigenenergies and eigenstates, but more in the *density of states*.
+
+The kernel polynomial method (KPM), is an algorithm to obtain a polynomial
+expansion of the density of states. It can also be used to calculate the
+spectral density of arbitrary operators.  Kwant has an implementation of the
+KPM method that is based on the algorithms presented in Ref. [1]_.
+
+Roughly speaking, KPM approximates the density of states (or any other spectral
+density) by expanding the action of the Hamiltonian (and operator of interest)
+on a (small) set of *random vectors* as a sum of Chebyshev polynomials up to
+some order, and then averaging. The accuracy of the method can be tuned by
+modifying the order of the Chebyshev expansion and the number of random
+vectors.  See notes on accuracy_ below for details.
 
 .. seealso::
-    The complete source code of this tutorial can be found in
-    :download:`tutorial/continuum_discretizer.py <../../../tutorial/continuum_discretizer.py>`
+    The complete source code of this example can be found in
+    :download:`tutorial/kernel_polynomial_method.py <../../../tutorial/kernel_polynomial_method.py>`
 
+.. _accuracy:
+.. specialnote:: Performance and accuracy
 
-.. _tutorial_discretizer_introduction:
+    The KPM method is especially well suited for large systems, and in the
+    case when one is not interested in individual eigenvalues, but rather
+    in obtaining an approximate spectral density.
 
-Discretizing by hand
-....................
+    The accuracy in the energy resolution is dominated by the number of
+    moments. The lowest accuracy is at the center of the spectrum, while
+    slightly higher accuracy is obtained at the edges of the spectrum.
+    If we use the KPM method (with the Jackson kernel, see Ref. [1]_) to
+    describe a delta peak at the center of the spectrum, we will obtain a
+    function similar to a Gaussian of width :math:`σ=πa/N`, where
+    :math:`N` is the number of moments, and :math:`a` is the width of the
+    spectrum.
 
-As an example, let us consider the following continuum Schrödinger equation
-for a semiconducting heterostructure (using the effective mass approximation):
+    On the other hand, the random vectors will *explore* the range of the
+    spectrum, and as the system gets bigger, the number of random vectors
+    that are necessary to sample the whole spectrum reduces. Thus, a small
+    number of random vectors is in general enough, and increasing this number
+    will not result in a visible improvement of the approximation.
 
-.. math::
 
-    \left( k_x \frac{\hbar^2}{2 m(x)} k_x \right) \psi(x) = E \, \psi(x).
+Introduction
+************
 
-Replacing the momenta by their corresponding differential operators
+Our aim is to use the kernel polynomial method to obtain the spectral density
+:math:`ρ_A(E)`, as a function of the energy :math:`E`, of some Hilbert space
+operator :math:`A`.  We define
 
 .. math::
-    k_\alpha = -i \partial_\alpha,
-
-for :math:`\alpha = x, y` or :math:`z`, and discretizing on a regular lattice of
-points with spacing :math:`a`, we obtain the tight-binding model
 
-.. math::
+    ρ_A(E) = ρ(E) A(E),
 
-    H = - \frac{1}{a^2} \sum_i A\left(x+\frac{a}{2}\right)
-            \big(\ket{i}\bra{i+1} + h.c.\big)
-        + \frac{1}{a^2} \sum_i
-            \left( A\left(x+\frac{a}{2}\right) + A\left(x-\frac{a}{2}\right)\right)
-            \ket{i} \bra{i},
-
-with :math:`A(x) = \frac{\hbar^2}{2 m(x)}`.
-
-Using `~kwant.continuum.discretize` to obtain a template
-........................................................
-
-The function `kwant.continuum.discretize` takes a symbolic Hamiltonian and
-turns it into a `~kwant.builder.Builder` instance with appropriate spatial
-symmetry that serves as a template.
-(We will see how to use the template to build systems with a particular
-shape later).
-
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_symbolic_discretization
-    :end-before: #HIDDEN_END_symbolic_discretization
-
-It is worth noting that ``discretize`` treats ``k_x`` and ``x`` as
-non-commuting operators, and so their order is preserved during the
-discretization process.
-
-Setting the ``verbose`` parameter to ``True`` prints extra information about the
-onsite and hopping functions assigned to the ``Builder`` produced
-by ``discretize``:
-
-.. literalinclude:: ../images/discretizer_intro_verbose.txt
-
-.. specialnote:: Technical details
-
-    - ``kwant.continuum`` uses ``sympy`` internally to handle symbolic
-      expressions. Strings are converted using `kwant.continuum.sympify`,
-      which essentially applies some Kwant-specific rules (such as treating
-      ``k_x`` and ``x`` as non-commutative) before calling ``sympy.sympify``
-
-    - The builder returned by ``discretize`` will have an N-D
-      translational symmetry, where ``N`` is the number of dimensions that were
-      discretized. This is the case, even if there are expressions in the input
-      (e.g. ``V(x, y)``) which in principle *may not* have this symmetry.  When
-      using the returned builder directly, or when using it as a template to
-      construct systems with different/lower symmetry, it is important to
-      ensure that any functional parameters passed to the system respect the
-      symmetry of the system. Kwant provides no consistency check for this.
-
-    - The discretization process consists of taking input
-      :math:`H(k_x, k_y, k_z)`, multiplying it from the right by
-      :math:`\psi(x, y, z)` and iteratively applying a second-order accurate
-      central derivative approximation for every
-      :math:`k_\alpha=-i\partial_\alpha`:
-
-      .. math::
-         \partial_\alpha \psi(\alpha) =
-            \frac{1}{a} \left( \psi\left(\alpha + \frac{a}{2}\right)
-                              -\psi\left(\alpha - \frac{a}{2}\right)\right).
-
-      This process is done separately for every summand in Hamiltonian.
-      Once all symbols denoting operators are applied internal algorithm is
-      calculating ``gcd`` for hoppings coming from each summand in order to
-      find best possible approximation. Please see source code for details.
-
-    - Instead of using ``discretize`` one can use
-      `~kwant.continuum.discretize_symbolic` to obtain symbolic output.
-      When working interactively in `Jupyter notebooks <https://jupyter.org/>`_
-      it can be useful to use this to see a symbolic representation of
-      the discretized Hamiltonian. This works best when combined with ``sympy``
-      `Pretty Printing <http://docs.sympy.org/latest/tutorial/printing.html#setting-up-pretty-printing>`_.
-
-    - The symbolic result of discretization obtained with
-      ``discretize_symbolic`` can be converted into a
-      builder using `~kwant.continuum.build_discretized`.
-      This can be useful if one wants to alter the tight-binding Hamiltonian
-      before building the system.
-
-
-Building a Kwant system from the template
-.........................................
-
-Let us now use the output of ``discretize`` as a template to
-build a system and plot some of its energy eigenstate. For this example the
-Hamiltonian will be
+where :math:`A(E)` is the expectation value of :math:`A` for all the
+eigenstates of the Hamiltonian with energy :math:`E`,  and the density of
+states is
 
 .. math::
 
-    H = k_x^2 + k_y^2 + V(x, y),
-
-where :math:`V(x, y)` is some arbitrary potential.
-
-First, use ``discretize`` to obtain a
-builder that we will use as a template:
-
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_template
-    :end-before: #HIDDEN_END_template
-
-We now use this system with the `~kwant.builder.Builder.fill`
-method of `~kwant.builder.Builder` to construct the system we
-want to investigate:
+  ρ(E) = \frac{1}{D} \sum_{k=0}^{D-1} δ(E-E_k),
 
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_fill
-    :end-before: #HIDDEN_END_fill
+:math:`D` being the Hilbert space dimension, and :math:`E_k` the eigenvalues.
 
-After finalizing this system, we can plot one of the system's
-energy eigenstates:
+In the special case when :math:`A` is the identity, then :math:`ρ_A(E)` is
+simply :math:`ρ(E)`, the density of states.
 
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_plot_eigenstate
-    :end-before: #HIDDEN_END_plot_eigenstate
 
-.. image:: ../images/discretizer_gs.*
+Calculating the density of states
+*********************************
 
-Note in the above that we provided the function ``V`` to
-``syst.hamiltonian_submatrix`` using ``params=dict(V=potential)``, rather than
-via ``args``.
+In the following example, we will use the KPM implementation in Kwant
+to obtain the density of states of a graphene disk.
 
-In addition, the function passed as ``V`` expects two input parameters ``x``
-and ``y``, the same as in the initial continuum Hamiltonian.
+We start by importing kwant and defining our system.
 
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_sys1
+    :end-before: #HIDDEN_END_sys1
 
-Models with more structure: Bernevig-Hughes-Zhang
-.................................................
+After making a system we can then create a `~kwant.kpm.SpectralDensity`
+object that represents the density of states for this system.
 
-When working with multi-band systems, like the Bernevig-Hughes-Zhang (BHZ)
-model [1]_ [2]_, one can provide matrix input to `~kwant.continuum.discretize`
-using ``identity`` and ``kron``. For example, the definition of the BHZ model can be
-written succinctly as:
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_kpm1
+    :end-before: #HIDDEN_END_kpm1
 
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_define_qsh
-    :end-before: #HIDDEN_END_define_qsh
+The `~kwant.kpm.SpectralDensity` can then be called like a function to obtain a
+sequence of energies in the spectrum of the Hamiltonian, and the corresponding
+density of states at these energies.
 
-We can then make a ribbon out of this template system:
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_kpm2
+    :end-before: #HIDDEN_END_kpm2
 
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_define_qsh_build
-    :end-before: #HIDDEN_END_define_qsh_build
+When called with no arguments, an optimal set of energies is chosen (these are
+not evenly distributed over the spectrum, see Ref. [1]_ for details), however
+it is also possible to provide an explicit sequence of energies at which to
+evaluate the density of states.
 
-and plot its dispersion using `kwant.plotter.bands`:
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_kpm3
+    :end-before: #HIDDEN_END_kpm3
 
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_plot_qsh_band
-    :end-before: #HIDDEN_END_plot_qsh_band
+.. image:: ../images/kpm_dos.*
 
-.. image:: ../images/discretizer_qsh_band.*
+In addition to being called like functions, `~kwant.kpm.SpectralDensity`
+objects also have a method `~kwant.kpm.SpectralDensity.average` which can be
+used to integrate the density of states against some distribution function over
+the whole spectrum. If no distribution function is specified, then the uniform
+distribution is used:
 
-In the above we see the edge states of the quantum spin Hall effect, which
-we can visualize using `kwant.plotter.map`:
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_av1
+    :end-before: #HIDDEN_END_av1
 
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_plot_qsh_wf
-    :end-before: #HIDDEN_END_plot_qsh_wf
+.. literalinclude:: ../images/kpm_normalization.txt
 
-.. image:: ../images/discretizer_qsh_wf.*
+We see that the integral of the density of states is normalized to 1. If
+we wish to calculate, say, the average number of states populated in
+equilibrium, then we should integrate with respect to a Fermi-Dirac
+distribution and multiply by the total number of available states in
+the system:
 
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_av2
+    :end-before: #HIDDEN_END_av2
 
-Limitations of discretization
-.............................
+.. literalinclude:: ../images/kpm_total_states.txt
 
-It is important to remember that the discretization of a continuum
-model is an *approximation* that is only valid in the low-energy
-limit. For example, the quadratic continuum Hamiltonian
+.. specialnote:: Stability and performance: spectral bounds
 
-.. math::
-
-    H_\textrm{continuous}(k_x) = \frac{\hbar^2}{2m}k_x^2
-
-
-and its discretized approximation
-
-.. math::
+    The KPM method internally rescales the spectrum of the Hamiltonian to the
+    interval ``(-1, 1)`` (see Ref [1]_ for details), which requires calculating
+    the boundaries of the spectrum (using ``scipy.sparse.linalg.eigsh``). This
+    can be very costly for large systems, so it is possible to pass this
+    explicitly as via the ``bounds`` parameter when instantiating the
+    `~kwant.kpm.SpectralDensity` (see the class documentation for details).
 
-    H_\textrm{tight-binding}(k_x) = 2t \big(1 - \cos(k_x a)\big),
+    Additionally, `~kwant.kpm.SpectralDensity` accepts a parameter ``epsilon``,
+    which ensures that the rescaled Hamiltonian (used internally), always has a
+    spectrum strictly contained in the interval ``(-1, 1)``. If bounds are not
+    provided then the tolerance on the bounds calculated with
+    ``scipy.sparse.linalg.eigsh`` is set to ``epsilon/2``.
 
 
-where :math:`t=\frac{\hbar^2}{2ma^2}`, are only valid in the limit
-:math:`E \lt t`. The grid spacing :math:`a` must be chosen according
-to how high in energy you need your tight-binding model to be valid.
+Increasing the accuracy of the approximation
+********************************************
 
-It is possible to set :math:`a` through the ``grid_spacing`` parameter
-to `~kwant.continuum.discretize`, as we will illustrate in the following
-example. Let us start from the continuum Hamiltonian
+`~kwant.kpm.SpectralDensity` has two methods for increasing the accuracy
+of the method, each of which offers different levels of control over what
+exactly is changed.
 
-.. math::
+The simplest way to obtain a more accurate solution is to use the
+``add_moments`` method:
 
-  H(k) = k_x^2 \mathbb{1}_{2\times2} + α k_x \sigma_y.
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_acc1
+    :end-before: #HIDDEN_END_acc1
 
-We start by defining this model as a string and setting the value of the
-:math:`α` parameter:
+This will update the number of calculated moments and also the default
+number of sampling points such that the maximum distance between successive
+energy points is ``energy_resolution`` (see notes on accuracy_).
 
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_ls_def
-    :end-before: #HIDDEN_END_ls_def
+.. image:: ../images/kpm_dos_acc.*
 
-Now we can use `kwant.continuum.lambdify` to obtain a function that computes
-:math:`H(k)`:
+Alternatively, you can directly increase the number of moments
+with ``add_moments``, or the number of random vectors with ``add_vectors``.
 
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_ls_hk_cont
-    :end-before: #HIDDEN_END_ls_hk_cont
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_acc2
+    :end-before: #HIDDEN_END_acc2
 
-We can also construct a discretized approximation using
-`kwant.continuum.discretize`, in a similar manner to previous examples:
+.. image:: ../images/kpm_dos_r.*
 
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_ls_hk_tb
-    :end-before: #HIDDEN_END_ls_hk_tb
 
-Below we can see the continuum and tight-binding dispersions for two
-different values of the discretization grid spacing :math:`a`:
+.. _operator_spectral_density:
 
-.. image:: ../images/discretizer_lattice_spacing.*
+Calculating the spectral density of an operator
+***********************************************
 
+Above, we saw how to calculate the density of states by creating a
+`~kwant.kpm.SpectralDensity` and passing it a finalized Kwant system.
+When instantiating a `~kwant.kpm.SpectralDensity` we may optionally
+supply an operator in addition to the system. In this case it is
+the spectral density of the given operator that is calculated.
 
-We clearly see that the smaller grid spacing is, the better we approximate
-the original continuous dispersion. It is also worth remembering that the
-Brillouin zone also scales with grid spacing: :math:`[-\frac{\pi}{a},
-\frac{\pi}{a}]`.
+`~kwant.kpm.SpectralDensity` accepts the operators in a few formats:
 
-.. specialnote:: Note
+* *explicit matrices* (numpy array of scipy sparse matrices will work)
+* *operators* from `kwant.operator`
 
-  The use of `~kwant.wraparound.wraparound` makes it easy to extend this
-  example to multidimensional band structures. ``wraparound`` replaces
-  the translational symmetry in the ``x`` direction by a parameter ``k_x``.
+If an explicit matrix is provided then it must have the same
+shape as the system Hamiltonian.
 
-Advanced topics
-...............
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_op1
+    :end-before: #HIDDEN_END_op1
 
-The input to `kwant.continuum.discretize` and `kwant.continuum.lambdify` can be
-not only a ``string``, as we saw above, but also a ``sympy`` expression or
-a ``sympy`` matrix.
-This functionality will probably be mostly useful to people who
-are already experienced with ``sympy``.
 
+Or, to do the same calculation using `kwant.operator.Density`:
 
-It is possible to use ``identity`` (for identity matrix), ``kron`` (for Kronecker product), as well as Pauli matrices ``sigma_0``,
-``sigma_x``, ``sigma_y``, ``sigma_z`` in the input to
-`~kwant.continuum.lambdify` and `~kwant.continuum.discretize`, in order to simplify
-expressions involving matrices. Matrices can also be provided explicitly using
-square ``[]`` brackets. For example, all following expressions are equivalent:
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_op2
+    :end-before: #HIDDEN_END_op2
 
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_subs_1
-    :end-before: #HIDDEN_END_subs_1
+Using operators from `kwant.operator` allows us to calculate quantities
+such as the *local* density of states by telling the operator not to
+sum over all the sites of the system:
 
-.. literalinclude:: ../images/discretizer_subs_1.txt
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_op3
+    :end-before: #HIDDEN_END_op3
 
-We can use the ``subs`` keyword parameter to substitute expressions
-and numerical values:
+`~kwant.kpm.SpectralDensity` will properly handle this vector output,
+which allows us to plot the local density of states at different
+point in the spectrum:
 
-.. literalinclude:: continuum_discretizer.py
-    :start-after: #HIDDEN_BEGIN_subs_2
-    :end-before: #HIDDEN_END_subs_2
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_op4
+    :end-before: #HIDDEN_END_op4
 
-.. literalinclude:: ../images/discretizer_subs_2.txt
+.. image:: ../images/kpm_ldos.*
 
-Symbolic expressions obtained in this way can be directly passed to all
-``discretizer`` functions.
+This nicely illustrates the edge states of the graphene dot at zero
+energy, and the bulk states at higher energy.
 
-.. specialnote:: Technical details
 
-  Because of the way that ``sympy`` handles commutation relations all symbols
-  representing position and momentum operators are set to be non commutative.
-  This means that the order of momentum and position operators in the input
-  expression is preserved.  Note that it is not possible to define individual
-  commutation relations within ``sympy``, even expressions such :math:`x k_y x`
-  will not be simplified, even though mathematically :math:`[x, k_y] = 0`.
+Advanced topics
+***************
+
+Custom distributions for random vectors
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+By default `~kwant.kpm.SpectralDensity` will use random vectors
+whose components are unit complex numbers with phases drawn
+from a uniform distribution. There are several reasons why you may
+wish to make a different choice of distribution for your random vectors,
+for example to enforce certain symmetries or to only use real-valued vectors.
+
+To change how the random vectors are generated, you need only specify a
+function that takes the dimension of the Hilbert space as a single parameter,
+and which returns a vector in that Hilbert space:
+
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_fact1
+    :end-before: #HIDDEN_END_fact1
+
+Reproducible calculations
+^^^^^^^^^^^^^^^^^^^^^^^^^
+Because KPM internally uses random vectors, running the same calculation
+twice will not give bit-for-bit the same result. However, similarly to
+the funcions in `~kwant.rmt`, the random number generator can be directly
+manipulated by passing a value to the ``rng`` parameter of
+`~kwant.kpm.SpectralDensity`. ``rng`` can itself be a random number generator,
+or it may simply be a seed to pass to the numpy random number generator
+(that is used internally by default).
+
+Defining operators as sesquilinear maps
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+`Above`__, we showed how `~kwant.kpm.SpectralDensity` can calculate the
+spectral density of operators, and how we can define operators by using
+`kwant.operator`.  If you need even more flexibility,
+`~kwant.kpm.SpectralDensity` will also accept a *function* as its ``operator``
+parameter. This function must itself take two parameters, ``(bra, ket)`` and
+must return either a scalar or a one-dimensional array. In order to be
+meaningful the function must be a sesquilinear map, i.e. antilinear in its
+first argument, and linear in its second argument. Below, we compare two
+methods for computing the local density of states, one using
+`kwant.operator.Density`, and the other using a custom function.
+
+.. literalinclude:: kernel_polynomial_method.py
+    :start-after: #HIDDEN_BEGIN_blm
+    :end-before: #HIDDEN_END_blm
+
+__ operator_spectral_density_
 
 
 .. rubric:: References
 
-.. [1] `Science, 314, 1757 (2006)
-    <https://arxiv.org/abs/cond-mat/0611399>`_.
-
-.. [2] `Phys. Rev. B 82, 045122 (2010)
-    <https://arxiv.org/abs/1005.1682>`_.
+.. [1] `Rev. Mod. Phys., Vol. 78, No. 1 (2006)
+    <https://arxiv.org/abs/cond-mat/0504627>`_.
diff --git a/doc/source/tutorial/tutorial9.rst b/doc/source/tutorial/tutorial9.rst
new file mode 100644
index 0000000000000000000000000000000000000000..216f24b116f0d4036ac62b0ae77d37eccc302501
--- /dev/null
+++ b/doc/source/tutorial/tutorial9.rst
@@ -0,0 +1,318 @@
+Discretizing continuous Hamiltonians
+------------------------------------
+
+Introduction
+............
+
+In ":ref:`tutorial_discretization_schrodinger`" we have learnt that Kwant works
+with tight-binding Hamiltonians. Often, however, one will start with a
+continuum model and will subsequently need to discretize it to arrive at a
+tight-binding model.
+Although discretizing a Hamiltonian is usually a simple
+process, it is tedious and repetitive. The situation is further exacerbated
+when one introduces additional on-site degrees of freedom, and tracking all
+the necessary terms becomes a chore.
+The `~kwant.continuum` sub-package aims to be a solution to this problem.
+It is a collection of tools for working with
+continuum models and for discretizing them into tight-binding models.
+
+.. seealso::
+    The complete source code of this tutorial can be found in
+    :download:`tutorial/continuum_discretizer.py <../../../tutorial/continuum_discretizer.py>`
+
+
+.. _tutorial_discretizer_introduction:
+
+Discretizing by hand
+....................
+
+As an example, let us consider the following continuum Schrödinger equation
+for a semiconducting heterostructure (using the effective mass approximation):
+
+.. math::
+
+    \left( k_x \frac{\hbar^2}{2 m(x)} k_x \right) \psi(x) = E \, \psi(x).
+
+Replacing the momenta by their corresponding differential operators
+
+.. math::
+    k_\alpha = -i \partial_\alpha,
+
+for :math:`\alpha = x, y` or :math:`z`, and discretizing on a regular lattice of
+points with spacing :math:`a`, we obtain the tight-binding model
+
+.. math::
+
+    H = - \frac{1}{a^2} \sum_i A\left(x+\frac{a}{2}\right)
+            \big(\ket{i}\bra{i+1} + h.c.\big)
+        + \frac{1}{a^2} \sum_i
+            \left( A\left(x+\frac{a}{2}\right) + A\left(x-\frac{a}{2}\right)\right)
+            \ket{i} \bra{i},
+
+with :math:`A(x) = \frac{\hbar^2}{2 m(x)}`.
+
+Using `~kwant.continuum.discretize` to obtain a template
+........................................................
+
+The function `kwant.continuum.discretize` takes a symbolic Hamiltonian and
+turns it into a `~kwant.builder.Builder` instance with appropriate spatial
+symmetry that serves as a template.
+(We will see how to use the template to build systems with a particular
+shape later).
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_symbolic_discretization
+    :end-before: #HIDDEN_END_symbolic_discretization
+
+It is worth noting that ``discretize`` treats ``k_x`` and ``x`` as
+non-commuting operators, and so their order is preserved during the
+discretization process.
+
+Setting the ``verbose`` parameter to ``True`` prints extra information about the
+onsite and hopping functions assigned to the ``Builder`` produced
+by ``discretize``:
+
+.. literalinclude:: ../images/discretizer_intro_verbose.txt
+
+.. specialnote:: Technical details
+
+    - ``kwant.continuum`` uses ``sympy`` internally to handle symbolic
+      expressions. Strings are converted using `kwant.continuum.sympify`,
+      which essentially applies some Kwant-specific rules (such as treating
+      ``k_x`` and ``x`` as non-commutative) before calling ``sympy.sympify``
+
+    - The builder returned by ``discretize`` will have an N-D
+      translational symmetry, where ``N`` is the number of dimensions that were
+      discretized. This is the case, even if there are expressions in the input
+      (e.g. ``V(x, y)``) which in principle *may not* have this symmetry.  When
+      using the returned builder directly, or when using it as a template to
+      construct systems with different/lower symmetry, it is important to
+      ensure that any functional parameters passed to the system respect the
+      symmetry of the system. Kwant provides no consistency check for this.
+
+    - The discretization process consists of taking input
+      :math:`H(k_x, k_y, k_z)`, multiplying it from the right by
+      :math:`\psi(x, y, z)` and iteratively applying a second-order accurate
+      central derivative approximation for every
+      :math:`k_\alpha=-i\partial_\alpha`:
+
+      .. math::
+         \partial_\alpha \psi(\alpha) =
+            \frac{1}{a} \left( \psi\left(\alpha + \frac{a}{2}\right)
+                              -\psi\left(\alpha - \frac{a}{2}\right)\right).
+
+      This process is done separately for every summand in Hamiltonian.
+      Once all symbols denoting operators are applied internal algorithm is
+      calculating ``gcd`` for hoppings coming from each summand in order to
+      find best possible approximation. Please see source code for details.
+
+    - Instead of using ``discretize`` one can use
+      `~kwant.continuum.discretize_symbolic` to obtain symbolic output.
+      When working interactively in `Jupyter notebooks <https://jupyter.org/>`_
+      it can be useful to use this to see a symbolic representation of
+      the discretized Hamiltonian. This works best when combined with ``sympy``
+      `Pretty Printing <http://docs.sympy.org/latest/tutorial/printing.html#setting-up-pretty-printing>`_.
+
+    - The symbolic result of discretization obtained with
+      ``discretize_symbolic`` can be converted into a
+      builder using `~kwant.continuum.build_discretized`.
+      This can be useful if one wants to alter the tight-binding Hamiltonian
+      before building the system.
+
+
+Building a Kwant system from the template
+.........................................
+
+Let us now use the output of ``discretize`` as a template to
+build a system and plot some of its energy eigenstate. For this example the
+Hamiltonian will be
+
+.. math::
+
+    H = k_x^2 + k_y^2 + V(x, y),
+
+where :math:`V(x, y)` is some arbitrary potential.
+
+First, use ``discretize`` to obtain a
+builder that we will use as a template:
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_template
+    :end-before: #HIDDEN_END_template
+
+We now use this system with the `~kwant.builder.Builder.fill`
+method of `~kwant.builder.Builder` to construct the system we
+want to investigate:
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_fill
+    :end-before: #HIDDEN_END_fill
+
+After finalizing this system, we can plot one of the system's
+energy eigenstates:
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_plot_eigenstate
+    :end-before: #HIDDEN_END_plot_eigenstate
+
+.. image:: ../images/discretizer_gs.*
+
+Note in the above that we provided the function ``V`` to
+``syst.hamiltonian_submatrix`` using ``params=dict(V=potential)``, rather than
+via ``args``.
+
+In addition, the function passed as ``V`` expects two input parameters ``x``
+and ``y``, the same as in the initial continuum Hamiltonian.
+
+
+Models with more structure: Bernevig-Hughes-Zhang
+.................................................
+
+When working with multi-band systems, like the Bernevig-Hughes-Zhang (BHZ)
+model [1]_ [2]_, one can provide matrix input to `~kwant.continuum.discretize`
+using ``identity`` and ``kron``. For example, the definition of the BHZ model can be
+written succinctly as:
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_define_qsh
+    :end-before: #HIDDEN_END_define_qsh
+
+We can then make a ribbon out of this template system:
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_define_qsh_build
+    :end-before: #HIDDEN_END_define_qsh_build
+
+and plot its dispersion using `kwant.plotter.bands`:
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_plot_qsh_band
+    :end-before: #HIDDEN_END_plot_qsh_band
+
+.. image:: ../images/discretizer_qsh_band.*
+
+In the above we see the edge states of the quantum spin Hall effect, which
+we can visualize using `kwant.plotter.map`:
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_plot_qsh_wf
+    :end-before: #HIDDEN_END_plot_qsh_wf
+
+.. image:: ../images/discretizer_qsh_wf.*
+
+
+Limitations of discretization
+.............................
+
+It is important to remember that the discretization of a continuum
+model is an *approximation* that is only valid in the low-energy
+limit. For example, the quadratic continuum Hamiltonian
+
+.. math::
+
+    H_\textrm{continuous}(k_x) = \frac{\hbar^2}{2m}k_x^2
+
+
+and its discretized approximation
+
+.. math::
+
+    H_\textrm{tight-binding}(k_x) = 2t \big(1 - \cos(k_x a)\big),
+
+
+where :math:`t=\frac{\hbar^2}{2ma^2}`, are only valid in the limit
+:math:`E \lt t`. The grid spacing :math:`a` must be chosen according
+to how high in energy you need your tight-binding model to be valid.
+
+It is possible to set :math:`a` through the ``grid_spacing`` parameter
+to `~kwant.continuum.discretize`, as we will illustrate in the following
+example. Let us start from the continuum Hamiltonian
+
+.. math::
+
+  H(k) = k_x^2 \mathbb{1}_{2\times2} + α k_x \sigma_y.
+
+We start by defining this model as a string and setting the value of the
+:math:`α` parameter:
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_ls_def
+    :end-before: #HIDDEN_END_ls_def
+
+Now we can use `kwant.continuum.lambdify` to obtain a function that computes
+:math:`H(k)`:
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_ls_hk_cont
+    :end-before: #HIDDEN_END_ls_hk_cont
+
+We can also construct a discretized approximation using
+`kwant.continuum.discretize`, in a similar manner to previous examples:
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_ls_hk_tb
+    :end-before: #HIDDEN_END_ls_hk_tb
+
+Below we can see the continuum and tight-binding dispersions for two
+different values of the discretization grid spacing :math:`a`:
+
+.. image:: ../images/discretizer_lattice_spacing.*
+
+
+We clearly see that the smaller grid spacing is, the better we approximate
+the original continuous dispersion. It is also worth remembering that the
+Brillouin zone also scales with grid spacing: :math:`[-\frac{\pi}{a},
+\frac{\pi}{a}]`.
+
+
+Advanced topics
+...............
+
+The input to `kwant.continuum.discretize` and `kwant.continuum.lambdify` can be
+not only a ``string``, as we saw above, but also a ``sympy`` expression or
+a ``sympy`` matrix.
+This functionality will probably be mostly useful to people who
+are already experienced with ``sympy``.
+
+
+It is possible to use ``identity`` (for identity matrix), ``kron`` (for Kronecker product), as well as Pauli matrices ``sigma_0``,
+``sigma_x``, ``sigma_y``, ``sigma_z`` in the input to
+`~kwant.continuum.lambdify` and `~kwant.continuum.discretize`, in order to simplify
+expressions involving matrices. Matrices can also be provided explicitly using
+square ``[]`` brackets. For example, all following expressions are equivalent:
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_subs_1
+    :end-before: #HIDDEN_END_subs_1
+
+.. literalinclude:: ../images/discretizer_subs_1.txt
+
+We can use the ``substitutions`` keyword parameter to substitute expressions
+and numerical values:
+
+.. literalinclude:: continuum_discretizer.py
+    :start-after: #HIDDEN_BEGIN_subs_2
+    :end-before: #HIDDEN_END_subs_2
+
+.. literalinclude:: ../images/discretizer_subs_2.txt
+
+Symbolic expressions obtained in this way can be directly passed to all
+``discretizer`` functions.
+
+.. specialnote:: Technical details
+
+  Because of the way that ``sympy`` handles commutation relations all symbols
+  representing position and momentum operators are set to be non commutative.
+  This means that the order of momentum and position operators in the input
+  expression is preserved.  Note that it is not possible to define individual
+  commutation relations within ``sympy``, even expressions such :math:`x k_y x`
+  will not be simplified, even though mathematically :math:`[x, k_y] = 0`.
+
+
+.. rubric:: References
+
+.. [1] `Science, 314, 1757 (2006)
+    <https://arxiv.org/abs/cond-mat/0611399>`_.
+
+.. [2] `Phys. Rev. B 82, 045122 (2010)
+    <https://arxiv.org/abs/1005.1682>`_.