diff --git a/doc/source/images/magnetic_texture.py.diff b/doc/source/images/magnetic_texture.py.diff
new file mode 100644
index 0000000000000000000000000000000000000000..bc7acb4dcf1b7bda417c3dc3458eb61655435e05
--- /dev/null
+++ b/doc/source/images/magnetic_texture.py.diff
@@ -0,0 +1,143 @@
+--- original
++++ modified
+@@ -11,6 +11,9 @@
+ #  - operators
+ #  - plotting vector fields
+ 
++import _defs
++from contextlib import redirect_stdout
++
+ from math import sin, cos, tanh, pi
+ import itertools
+ import numpy as np
+@@ -79,7 +82,13 @@
+     return syst
+ 
+ 
+-def plot_vector_field(syst, params):
++def save_plot(fname):
++    for extension in ('.pdf', '.png'):
++        plt.savefig(fname + extension,
++                    dpi=_defs.dpi, bbox_inches='tight')
++
++
++def plot_vector_field(syst, params, fname=None):
+     xmin, ymin = min(s.tag for s in syst.sites)
+     xmax, ymax = max(s.tag for s in syst.sites)
+     x, y = np.meshgrid(np.arange(xmin, xmax+1), np.arange(ymin, ymax+1))
+@@ -88,28 +97,38 @@
+     m_i = np.reshape(m_i, x.shape + (3,))
+     m_i = np.rollaxis(m_i, 2, 0)
+ 
+-    fig, ax = plt.subplots(1, 1)
++    fig, ax = plt.subplots(1, 1, figsize=(9, 7))
+     im = ax.quiver(x, y, *m_i, pivot='mid', scale=75)
+     fig.colorbar(im)
+-    plt.show()
++    if fname:
++        save_plot(fname)
++    else:
++        plt.show()
+ 
+ 
+-def plot_densities(syst, densities):
+-    fig, axes = plt.subplots(1, len(densities))
++def plot_densities(syst, densities, fname=None):
++    fig, axes = plt.subplots(1, len(densities), figsize=(7*len(densities), 7))
+     for ax, (title, rho) in zip(axes, densities):
+         kwant.plotter.map(syst, rho, ax=ax, a=4)
+         ax.set_title(title)
+-    plt.show()
++    if fname:
++        save_plot(fname)
++    else:
++        plt.show()
++
+ 
+ 
+-def plot_currents(syst, currents):
+-    fig, axes = plt.subplots(1, len(currents))
++def plot_currents(syst, currents, fname=None):
++    fig, axes = plt.subplots(1, len(currents), figsize=(7*len(currents), 7))
+     if not hasattr(axes, '__len__'):
+         axes = (axes,)
+     for ax, (title, current) in zip(axes, currents):
+         kwant.plotter.current(syst, current, ax=ax, colorbar=False)
+         ax.set_title(title)
+-    plt.show()
++    if fname:
++        save_plot(fname)
++    else:
++        plt.show()
+ 
+ 
+ def main():
+@@ -119,7 +138,7 @@
+     wf = kwant.wave_function(syst, energy=-1, params=params)
+     psi = wf(0)[0]
+ 
+-    plot_vector_field(syst, dict(r0=20, delta=10))
++    plot_vector_field(syst, dict(r0=20, delta=10), fname='mag_field_direction')
+ 
+     # even (odd) indices correspond to spin up (down)
+     up, down = psi[::2], psi[1::2]
+@@ -144,7 +163,7 @@
+         ('$σ_0$', density),
+         ('$σ_z$', spin_z),
+         ('$σ_y$', spin_y),
+-    ])
++    ], fname='spin_densities')
+ 
+     J_0 = kwant.operator.Current(syst)
+     J_z = kwant.operator.Current(syst, sigma_z)
+@@ -159,7 +178,7 @@
+         ('$J_{σ_0}$', current),
+         ('$J_{σ_z}$', spin_z_current),
+         ('$J_{σ_y}$', spin_y_current),
+-    ])
++    ], fname='spin_currents')
+ 
+     def following_m_i(site, r0, delta):
+         m_i = field_direction(site.pos, r0, delta)
+@@ -173,7 +192,7 @@
+     plot_currents(syst, [
+         (r'$J_{\mathbf{m}_i}$', m_current),
+         ('$J_{σ_z}$', spin_z_current),
+-    ])
++    ], fname='spin_current_comparison')
+ 
+ 
+     def circle(site):
+@@ -183,7 +202,9 @@
+ 
+     all_states = np.vstack((wf(0), wf(1)))
+     dos_in_circle = sum(rho_circle(p) for p in all_states) / (2 * pi)
+-    print('density of states in circle:', dos_in_circle)
++    with open('circle_dos.txt', 'w') as f:
++        with redirect_stdout(f):
++            print('density of states in circle:', dos_in_circle)
+ 
+     def left_cut(site_to, site_from):
+         return site_from.pos[0] <= -39 and site_to.pos[0] > -39
+@@ -197,8 +218,10 @@
+     Jz_left = kwant.operator.Current(syst, sigma_z, where=left_cut, sum=True)
+     Jz_right = kwant.operator.Current(syst, sigma_z, where=right_cut, sum=True)
+ 
+-    print('J_left:', J_left(psi), ' J_right:', J_right(psi))
+-    print('Jz_left:', Jz_left(psi), ' Jz_right:', Jz_right(psi))
++    with open('current_cut.txt', 'w') as f:
++        with redirect_stdout(f):
++            print('J_left:', J_left(psi), ' J_right:', J_right(psi))
++            print('Jz_left:', Jz_left(psi), ' Jz_right:', Jz_right(psi))
+ 
+     J_m = kwant.operator.Current(syst, following_m_i)
+     J_z = kwant.operator.Current(syst, sigma_z)
+@@ -214,7 +237,7 @@
+     plot_currents(syst, [
+         (r'$J_{\mathbf{m}_i}$ (from left)', J_m_left),
+         (r'$J_{σ_z}$ (from left)', J_z_left),
+-    ])
++    ], fname='bound_current')
+ 
+ 
+ if __name__ == '__main__':
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/magnetic_texture.py b/doc/source/tutorial/magnetic_texture.py
new file mode 100644
index 0000000000000000000000000000000000000000..89c7acfb793aecce1126a50fa45f31b360c2e053
--- /dev/null
+++ b/doc/source/tutorial/magnetic_texture.py
@@ -0,0 +1,245 @@
+# Tutorial 2.7. Spin textures
+# ===========================
+#
+# Physics background
+# ------------------
+#  - Spin textures
+#  - Skyrmions
+#
+# Kwant features highlighted
+# --------------------------
+#  - operators
+#  - plotting vector fields
+
+from math import sin, cos, tanh, pi
+import itertools
+import numpy as np
+import tinyarray as ta
+import matplotlib.pyplot as plt
+
+import kwant
+
+sigma_0 = ta.array([[1, 0], [0, 1]])
+sigma_x = ta.array([[0, 1], [1, 0]])
+sigma_y = ta.array([[0, -1j], [1j, 0]])
+sigma_z = ta.array([[1, 0], [0, -1]])
+
+# vector of Pauli matrices σ_αiβ where greek
+# letters denote spinor indices
+sigma = np.rollaxis(np.array([sigma_x, sigma_y, sigma_z]), 1)
+
+#HIDDEN_BEGIN_model
+def field_direction(pos, r0, delta):
+    x, y = pos
+    r = np.linalg.norm(pos)
+    r_tilde = (r - r0) / delta
+    theta = (tanh(r_tilde) - 1) * (pi / 2)
+
+    if r == 0:
+        m_i = [0, 0, 1]
+    else:
+        m_i = [
+            (x / r) * sin(theta),
+            (y / r) * sin(theta),
+            cos(theta),
+        ]
+
+    return np.array(m_i)
+
+
+def scattering_onsite(site, r0, delta, J):
+    m_i = field_direction(site.pos, r0, delta)
+    return J * np.dot(m_i, sigma)
+
+
+def lead_onsite(site, J):
+    return J * sigma_z
+#HIDDEN_END_model
+
+
+#HIDDEN_BEGIN_syst
+lat = kwant.lattice.square(norbs=2)
+
+def make_system(L=80):
+
+    syst = kwant.Builder()
+
+    def square(pos):
+        return all(-L/2 < p < L/2 for p in pos)
+
+    syst[lat.shape(square, (0, 0))] = scattering_onsite
+    syst[lat.neighbors()] = -sigma_0
+
+    lead = kwant.Builder(kwant.TranslationalSymmetry((-1, 0)),
+                         conservation_law=-sigma_z)
+
+    lead[lat.shape(square, (0, 0))] = lead_onsite
+    lead[lat.neighbors()] = -sigma_0
+
+    syst.attach_lead(lead)
+    syst.attach_lead(lead.reversed())
+
+    return syst
+#HIDDEN_END_syst
+
+
+def plot_vector_field(syst, params):
+    xmin, ymin = min(s.tag for s in syst.sites)
+    xmax, ymax = max(s.tag for s in syst.sites)
+    x, y = np.meshgrid(np.arange(xmin, xmax+1), np.arange(ymin, ymax+1))
+
+    m_i = [field_direction(p, **params) for p in zip(x.flat, y.flat)]
+    m_i = np.reshape(m_i, x.shape + (3,))
+    m_i = np.rollaxis(m_i, 2, 0)
+
+    fig, ax = plt.subplots(1, 1)
+    im = ax.quiver(x, y, *m_i, pivot='mid', scale=75)
+    fig.colorbar(im)
+    plt.show()
+
+
+def plot_densities(syst, densities):
+    fig, axes = plt.subplots(1, len(densities))
+    for ax, (title, rho) in zip(axes, densities):
+        kwant.plotter.map(syst, rho, ax=ax, a=4)
+        ax.set_title(title)
+    plt.show()
+
+
+def plot_currents(syst, currents):
+    fig, axes = plt.subplots(1, len(currents))
+    if not hasattr(axes, '__len__'):
+        axes = (axes,)
+    for ax, (title, current) in zip(axes, currents):
+        kwant.plotter.current(syst, current, ax=ax, colorbar=False)
+        ax.set_title(title)
+    plt.show()
+
+
+def main():
+    syst = make_system().finalized()
+
+#HIDDEN_BEGIN_wavefunction
+    params = dict(r0=20, delta=10, J=1)
+    wf = kwant.wave_function(syst, energy=-1, params=params)
+    psi = wf(0)[0]
+#HIDDEN_END_wavefunction
+
+    plot_vector_field(syst, dict(r0=20, delta=10))
+
+#HIDDEN_BEGIN_ldos
+    # even (odd) indices correspond to spin up (down)
+    up, down = psi[::2], psi[1::2]
+    density = np.abs(up)**2 + np.abs(down)**2
+#HIDDEN_END_ldos
+
+#HIDDEN_BEGIN_lsdz
+    # spin down components have a minus sign
+    spin_z = np.abs(up)**2 - np.abs(down)**2
+#HIDDEN_END_lsdz
+
+#HIDDEN_BEGIN_lsdy
+    # spin down components have a minus sign
+    spin_y = 1j * (down.conjugate() * up - up.conjugate() * down)
+#HIDDEN_END_lsdy
+
+#HIDDEN_BEGIN_lden
+    rho = kwant.operator.Density(syst)
+    rho_sz = kwant.operator.Density(syst, sigma_z)
+    rho_sy = kwant.operator.Density(syst, sigma_y)
+
+    # calculate the expectation values of the operators with 'psi'
+    density = rho(psi)
+    spin_z = rho_sz(psi)
+    spin_y = rho_sy(psi)
+#HIDDEN_END_lden
+
+    plot_densities(syst, [
+        ('$σ_0$', density),
+        ('$σ_z$', spin_z),
+        ('$σ_y$', spin_y),
+    ])
+
+#HIDDEN_BEGIN_current
+    J_0 = kwant.operator.Current(syst)
+    J_z = kwant.operator.Current(syst, sigma_z)
+    J_y = kwant.operator.Current(syst, sigma_y)
+
+    # calculate the expectation values of the operators with 'psi'
+    current = J_0(psi)
+    spin_z_current = J_z(psi)
+    spin_y_current = J_y(psi)
+#HIDDEN_END_current
+
+    plot_currents(syst, [
+        ('$J_{σ_0}$', current),
+        ('$J_{σ_z}$', spin_z_current),
+        ('$J_{σ_y}$', spin_y_current),
+    ])
+
+#HIDDEN_BEGIN_following
+    def following_m_i(site, r0, delta):
+        m_i = field_direction(site.pos, r0, delta)
+        return np.dot(m_i, sigma)
+
+    J_m = kwant.operator.Current(syst, following_m_i)
+
+    # evaluate the operator
+    m_current = J_m(psi, params=dict(r0=25, delta=10))
+#HIDDEN_END_following
+
+    plot_currents(syst, [
+        (r'$J_{\mathbf{m}_i}$', m_current),
+        ('$J_{σ_z}$', spin_z_current),
+    ])
+
+
+#HIDDEN_BEGIN_density_cut
+    def circle(site):
+        return np.linalg.norm(site.pos) < 20
+
+    rho_circle = kwant.operator.Density(syst, where=circle, sum=True)
+
+    all_states = np.vstack((wf(0), wf(1)))
+    dos_in_circle = sum(rho_circle(p) for p in all_states) / (2 * pi)
+    print('density of states in circle:', dos_in_circle)
+#HIDDEN_END_density_cut
+
+#HIDDEN_BEGIN_current_cut
+    def left_cut(site_to, site_from):
+        return site_from.pos[0] <= -39 and site_to.pos[0] > -39
+
+    def right_cut(site_to, site_from):
+        return site_from.pos[0] < 39 and site_to.pos[0] >= 39
+
+    J_left = kwant.operator.Current(syst, where=left_cut, sum=True)
+    J_right = kwant.operator.Current(syst, where=right_cut, sum=True)
+
+    Jz_left = kwant.operator.Current(syst, sigma_z, where=left_cut, sum=True)
+    Jz_right = kwant.operator.Current(syst, sigma_z, where=right_cut, sum=True)
+
+    print('J_left:', J_left(psi), ' J_right:', J_right(psi))
+    print('Jz_left:', Jz_left(psi), ' Jz_right:', Jz_right(psi))
+#HIDDEN_END_current_cut
+
+#HIDDEN_BEGIN_bind
+    J_m = kwant.operator.Current(syst, following_m_i)
+    J_z = kwant.operator.Current(syst, sigma_z)
+
+    J_m_bound = J_m.bind(params=dict(r0=25, delta=10, J=1))
+    J_z_bound = J_z.bind(params=dict(r0=25, delta=10, J=1))
+
+    # Sum current local from all scattering states on the left at energy=-1
+    wf_left = wf(0)
+    J_m_left = sum(J_m_bound(p) for p in wf_left)
+    J_z_left = sum(J_z_bound(p) for p in wf_left)
+#HIDDEN_END_bind
+
+    plot_currents(syst, [
+        (r'$J_{\mathbf{m}_i}$ (from left)', J_m_left),
+        (r'$J_{σ_z}$ (from left)', J_z_left),
+    ])
+
+
+if __name__ == '__main__':
+    main()
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
index dbf267042eb65f06d614fe3a04d18594b5cd3ec2..c2c314acd3f9a89464ef2ea67d503fbe4d111b4e 100644
--- a/doc/source/tutorial/tutorial6.rst
+++ b/doc/source/tutorial/tutorial6.rst
@@ -1,286 +1,367 @@
-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
-................................
+Computing local quantities: densities and currents
+==================================================
+In the previous tutorials we have mainly concentrated on calculating *global*
+properties such as conductance and band structures. Often, however, insight can
+be gained from calculating *locally-defined* quantities, that is, quantities
+defined over individual sites or hoppings in your system. In the
+:ref:`closed-systems` tutorial we saw how we could visualize the density
+associated with the eigenstates of a system using `kwant.plotter.map`.
+
+In this tutorial we will see how we can calculate more general quantities than
+simple densities by studying spin transport in a system with a magnetic
+texture.
 
 .. 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:
+    :download:`tutorial/magnetic_texture.py <../../../tutorial/magnetic_texture.py>`
 
-.. 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 [#]_.
+Introduction
+------------
+Our starting point will be the following spinful tight-binding model on
+a square lattice:
 
-Of course, the system can be plotted simply with default settings:
+.. math::
+    H = - \sum_{⟨ij⟩}\sum_{α} |iα⟩⟨jα|
+        + J \sum_{i}\sum_{αβ} \mathbf{m}_i⋅ \mathbf{σ}_{αβ} |iα⟩⟨iβ|,
 
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_plotsyst1
-    :end-before: #HIDDEN_END_plotsyst1
+where latin indices run over sites, and greek indices run over spin.  We can
+identify the first term as a nearest-neighbor hopping between like-spins, and
+the second as a term that couples spins on the same site.  The second term acts
+like a magnetic field of strength :math:`J` that varies from site to site and
+that, on site :math:`i`, points in the direction of the unit vector
+:math:`\mathbf{m}_i`. :math:`\mathbf{σ}_{αβ}` is a vector of Pauli matrices.
+We shall take the following form for :math:`\mathbf{m}_i`:
 
-However, due to the richer structure of the lattice, this results in a rather
-busy plot:
+.. math::
+    \mathbf{m}_i &=&\ \left(
+        \frac{x_i}{x_i^2 + y_i^2} \sin θ_i,\
+        \frac{y_i}{x_i^2 + y_i^2} \sin θ_i,\
+        \cos θ_i \right)^T,
+    \\
+    θ_i &=&\ \frac{π}{2} \tanh \frac{r_i - r_0}{δ},
 
-.. image:: ../images/plot_graphene_syst1.*
+where :math:`x_i` and :math:`y_i` are the :math:`x` and :math:`y` coordinates
+of site :math:`i`, and :math:`r_i = \sqrt{x_i^2 + y_i^2}`.
 
-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:
+To define this model in Kwant we start as usual by defining functions that
+depend on the model parameters:
 
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_plotsyst2
-    :end-before: #HIDDEN_END_plotsyst2
+.. literalinclude:: magnetic_texture.py
+    :start-after: #HIDDEN_BEGIN_model
+    :end-before: #HIDDEN_END_model
 
-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:
+and define our system as a square shape on a square lattice with two orbitals
+per site, with leads attached on the left and right:
 
-.. image:: ../images/plot_graphene_syst2.*
+.. literalinclude:: magnetic_texture.py
+    :start-after: #HIDDEN_BEGIN_syst
+    :end-before: #HIDDEN_END_syst
 
-Apart from plotting the *system* itself, `~kwant.plotter.plot` can also be used
-to plot *data* living on the system.
+Below is a plot of a projection of :math:`\mathbf{m}_i` onto the x-y plane
+inside the scattering region. The z component is shown by the color scale:
 
-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):
+.. image:: ../images/mag_field_direction.*
 
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_plotdata1
-    :end-before: #HIDDEN_END_plotdata1
+We will now be interested in analyzing the form of the scattering states
+that originate from the left lead:
 
-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:: magnetic_texture.py
+    :start-after: #HIDDEN_BEGIN_wavefunction
+    :end-before: #HIDDEN_END_wavefunction
 
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_plotdata2
-    :end-before: #HIDDEN_END_plotdata2
+Local densities
+---------------
+If we were simulating a spinless system with only a single degree of freedom,
+then calculating the density on each site would be as simple as calculating the
+absolute square of the wavefunction like::
 
-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.
+    density = np.abs(psi)**2
 
-.. image:: ../images/plot_graphene_data1.*
+When there are multiple degrees of freedom per site, however, one has to be
+more careful. In the present case with two (spin) degrees of freedom per site
+one could calculate the per-site density like:
 
-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:: magnetic_texture.py
+    :start-after: #HIDDEN_BEGIN_ldos
+    :end-before: #HIDDEN_END_ldos
 
-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:
+With more than one degree of freedom per site we have more freedom as to what
+local quantities we can meaningfully compute. For example, we may wish to
+calculate the local z-projected spin density. We could calculate
+this in the following way:
 
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_plotdata3
-    :end-before: #HIDDEN_END_plotdata3
+.. literalinclude:: magnetic_texture.py
+    :start-after: #HIDDEN_BEGIN_lsdz
+    :end-before: #HIDDEN_END_lsdz
 
-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).
+If we wanted instead to calculate the local y-projected spin density, we would
+need to use an even more complicated expression:
 
-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:: magnetic_texture.py
+    :start-after: #HIDDEN_BEGIN_lsdy
+    :end-before: #HIDDEN_END_lsdy
 
-With this we arrive at
+The `kwant.operator` module aims to alleviate somewhat this tedious
+book-keeping by providing a simple interface for defining operators that act on
+wavefunctions. To calculate the above quantities we would use the
+`~kwant.operator.Density` operator like so:
 
-.. image:: ../images/plot_graphene_data2.*
+.. literalinclude:: magnetic_texture.py
+    :start-after: #HIDDEN_BEGIN_lden
+    :end-before: #HIDDEN_END_lden
 
-with the same information as `~kwant.plotter.map`, but with a cleaner look.
+`~kwant.operator.Density` takes a `~kwant.system.System` as its first parameter
+as well as (optionally) a square matrix that defines the quantity that you wish
+to calculate per site. When an instance of a `~kwant.operator.Density` is then
+evaluated with a wavefunction, the quantity
 
-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:
+.. math:: ρ_i = \mathbf{ψ}^†_i \mathbf{M} \mathbf{ψ}_i
 
-.. literalinclude:: plot_graphene.py
-    :start-after: #HIDDEN_BEGIN_plotdata4
-    :end-before: #HIDDEN_END_plotdata4
+is calculated for each site :math:`i`, where :math:`\mathbf{ψ}_{i}` is a vector
+consisting of the wavefunction components on that site and :math:`\mathbf{M}`
+is the square matrix referred to previously.
 
-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.
+Below we can see colorplots of the above-calculated quantities. The array that
+is returned by evaluating a `~kwant.operator.Density` can be used directly with
+`kwant.plotter.map`:
 
-With this, we arrive at
+.. image:: ../images/spin_densities.*
 
-.. image:: ../images/plot_graphene_data3.*
 
-which shows the edge state nature of the wave function most clearly.
+.. specialnote:: Technical Details
 
-.. rubric:: Footnotes
+    Although we refer loosely to "densities" and "operators" above, a
+    `~kwant.operator.Density` actually represents a *collection* of linear
+    operators. This can be made clear by rewriting the above definition
+    of :math:`ρ_i` in the following way:
 
-.. [#] 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.
+    .. math::
+        ρ_i = \sum_{αβ} ψ^*_{α} \mathcal{M}_{iαβ} ψ_{β}
 
-3D example: zincblende structure
-................................
+    where greek indices run over the degrees of freedom in the Hilbert space of
+    the scattering region and latin indices run over sites.  We can this
+    identify :math:`\mathcal{M}_{iαβ}` as the components of a rank-3 tensor and can
+    represent them as a "vector of matrices":
 
-.. seealso::
-    The complete source code of this example can be found in
-    :download:`tutorial/plot_zincblende.py <../../../tutorial/plot_zincblende.py>`
+    .. math::
+        \mathcal{M} = \left[
+        \left(\begin{matrix}
+            \mathbf{M} & 0 & … \\
+            0 & 0 & … \\
+            ⋮ & ⋮ & ⋱
+        \end{matrix}\right)
+        ,\
+        \left(\begin{matrix}
+            0 & 0 & … \\
+            0 & \mathbf{M} & … \\
+            ⋮ & ⋮ & ⋱
+        \end{matrix}\right)
+        , … \right]
+
+    where :math:`\mathbf{M}` is defined as in the main text, and the :math:`0`
+    are zero matrices of the same shape as :math:`\mathbf{M}`.
 
-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`:
+Local currents
+--------------
+`kwant.operator` also has a class `~kwant.operator.Current` for calculating
+local currents, analogously to the local "densities" described above. If
+one has defined a density via a matrix :math:`\mathbf{M}` and the above
+equation, then one can define a local current flowing from site :math:`b`
+to site :math:`a`:
+
+.. math:: J_{ab} = i \left(
+    \mathbf{ψ}^†_b (\mathbf{H}_{ab})^† \mathbf{M} \mathbf{ψ}_a
+    - \mathbf{ψ}^†_a \mathbf{M} \mathbf{H}_{ab} \mathbf{ψ}_b
+    \right),
 
-.. literalinclude:: plot_zincblende.py
-    :start-after: #HIDDEN_BEGIN_zincblende1
-    :end-before: #HIDDEN_END_zincblende1
+where :math:`\mathbf{H}_{ab}` is the hopping matrix from site :math:`b` to site
+:math:`a`.  For example, to calculate the local current and
+spin current:
+
+.. literalinclude:: magnetic_texture.py
+    :start-after: #HIDDEN_BEGIN_current
+    :end-before: #HIDDEN_END_current
 
-Note how we keep references to the two different sublattices for later use.
+Evaluating a `~kwant.operator.Current` operator on a wavefunction returns a
+1D array of values that can be directly used with `kwant.plotter.current`:
 
-A three-dimensional structure is created as easily as in two dimensions, by
-using the `~kwant.lattice.PolyatomicLattice.shape`-functionality:
+.. image:: ../images/spin_currents.*
 
-.. literalinclude:: plot_zincblende.py
-    :start-after: #HIDDEN_BEGIN_zincblende2
-    :end-before: #HIDDEN_END_zincblende2
+.. note::
 
-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).
+    Evaluating a `~kwant.operator.Current` operator on a wavefunction
+    returns a 1D array of the same length as the number of hoppings in the
+    system, ordered in the same way as the edges in the system's graph.
 
-`~kwant.plotter.plot` can plot 3D systems just as easily as its two-dimensional
-counterparts:
+.. specialnote:: Technical Details
 
-.. literalinclude:: plot_zincblende.py
-    :start-after: #HIDDEN_BEGIN_plot1
-    :end-before: #HIDDEN_END_plot1
+    Similarly to how we saw in the previous section that `~kwant.operator.Density`
+    can be thought of as a collection of operators, `~kwant.operator.Current`
+    can be defined in a similar way. Starting from the definition of a "density":
 
-resulting in
+    .. math:: ρ_a = \sum_{αβ} ψ^*_{α} \mathcal{M}_{aαβ} ψ_{β},
 
-.. image:: ../images/plot_zincblende_syst1.*
+    we can define *currents* :math:`J_{ab}` via the continuity equation:
 
-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.
+    .. math:: \frac{∂ρ_a}{∂t} - \sum_{b} J_{ab} = 0
 
-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.
+    where the sum runs over sites :math:`b` neigboring site :math:`a`.
+    Plugging in the definition for :math:`ρ_a`, along with the Schrödinger
+    equation and the assumption that :math:`\mathcal{M}` is time independent,
+    gives:
 
-.. note::
+    .. math:: J_{ab} = \sum_{αβ}
+        ψ^*_α \left(i \sum_{γ}
+            \mathcal{H}^*_{abγα} \mathcal{M}_{aγβ}
+            - \mathcal{M}_{aαγ} \mathcal{H}_{abγβ}
+        \right)  ψ_β,
+
+    where latin indices run over sites and greek indices run over the Hilbert
+    space degrees of freedom, and
+
+    .. math:: \mathcal{H}_{ab} = \left(\begin{matrix}
+            ⋱ & ⋮ & ⋮ & ⋮ & ⋰ \\
+            ⋯ & ⋱ & 0 & \mathbf{H}_{ab} & ⋯ \\
+            ⋯ & 0 & ⋱ & 0 & ⋯ \\
+            ⋯ & 0 & 0 & ⋱ & ⋯ \\
+            ⋰ & ⋮ & ⋮ & ⋮ & ⋱
+        \end{matrix}\right).
+
+    i.e. :math:`\mathcal{H}_{ab}` is a matrix that is zero everywhere
+    except on elements connecting *from* site :math:`b` *to* site :math:`a`,
+    where it is equal to the hopping matrix :math:`\mathbf{H}_{ab}` between
+    these two sites.
+
+    This allows us to identify the rank-4 quantity
+
+    .. math:: \mathcal{J}_{abαβ} = i \sum_{γ}
+            \mathcal{H}^*_{abγα} \mathcal{M}_{aγβ}
+            - \mathcal{M}_{aαγ} \mathcal{H}_{abγβ}
 
-    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.
+    as the local current between connected sites.
 
-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:
+    The diagonal part of this quantity, :math:`\mathcal{J}_{aa}`,
+    represents the extent to which the density defined by :math:`\mathcal{M}_a`
+    is not conserved on site :math:`a`. It can be calculated using
+    `~kwant.operator.Source`, rather than `~kwant.operator.Current`, which
+    only computes the off-diagonal part.
 
-.. 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:
+Spatially varying operators
+---------------------------
+The above examples are reasonably simple in the sense that the book-keeping
+required to manually calculate the various densities and currents is still
+manageable. Now we shall look at the case where we wish to calculate some
+projected spin currents, but where the spin projection axis varies from place
+to place. More specifically, we want to visualize the spin current along the
+direction of :math:`\mathbf{m}_i`, which changes continuously over the whole
+scattering region.
 
-.. image:: ../images/plot_zincblende_syst2.*
+Doing this is as simple as passing a *function* when instantiating
+the `~kwant.operator.Current`, instead of a constant matrix:
 
-Hence, a few lines of code using Kwant allow to explore all the different
-crystal lattices out there!
+.. literalinclude:: magnetic_texture.py
+    :start-after: #HIDDEN_BEGIN_following
+    :end-before: #HIDDEN_END_following
+
+The function must take a `~kwant.builder.Site` as its first parameter,
+and may optionally take other parameters (i.e. it must have the same
+signature as a Hamiltonian onsite function), and must return the square
+matrix that defines the operator we wish to calculate.
 
 .. 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.
+    In the above example we had to pass the extra parameters needed by the
+    ``following_operator`` function via the ``param`` keyword argument.  In
+    general you must pass all the parameters needed by the Hamiltonian via
+    ``params`` (as you would when calling `~kwant.solvers.default.smatrix` or
+    `~kwant.solvers.default.wave_function`).  In the previous examples,
+    however, we used the fact that the system hoppings do not depend on any
+    parameters (these are the only Hamiltonian elements required to calculate
+    currents) to avoid passing the system parameters for the sake of brevity.
 
+Using this we can see that the spin current is essentially oriented along
+the direction of :math:`m_i` in the present regime where the onsite term
+in the Hamiltonian is dominant:
 
-Interpolated density and current: QPC with disorder
-...................................................
+.. image:: ../images/spin_current_comparison.*
 
-.. seealso::
-    The complete source code of this example can be found in
-    :download:`tutorial/plot_qpc.py <../../../tutorial/plot_qpc.py>`
+.. note:: Although this example used exclusively `~kwant.operator.Current`,
+          you can do the same with `~kwant.operator.Density`.
 
-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:
+Defining operators over parts of a system
+-----------------------------------------
 
-.. literalinclude:: plot_qpc.py
-    :start-after: #HIDDEN_BEGIN_syst
-    :end-before: #HIDDEN_END_syst
+Another useful feature of `kwant.operator` is the ability to calculate
+operators over selected parts of a system. For example, we may wish to
+calculate the total density of states in a certain part
+of the system, or the current flowing through a cut in the system.
+We can do this selection when creating the operator by using the
+keyword parameter ``where``.
 
-.. image:: ../images/plot_qpc_syst.*
+Density of states in a circle
+*****************************
 
-Now we will compute the density of particles and current due to states
-originating in the left lead with energy 0.15.
+To calculate the density of states inside a circle of radius
+20 we can simply do:
 
-.. literalinclude:: plot_qpc.py
-    :start-after: #HIDDEN_BEGIN_wf
-    :end-before: #HIDDEN_END_wf
+.. literalinclude:: magnetic_texture.py
+    :start-after: #HIDDEN_BEGIN_density_cut
+    :end-before: #HIDDEN_END_density_cut
 
-We can then plot the density using `~kwant.plotter.map`:
+.. literalinclude:: ../images/circle_dos.txt
 
-.. literalinclude:: plot_qpc.py
-    :start-after: #HIDDEN_BEGIN_density
-    :end-before: #HIDDEN_END_density
+note that we also provide ``sum=True``, which means that evaluating the
+operator on a wavefunction will produce a single scalar. This is semantically
+equivalent to providing ``sum=False`` (the default) and running ``numpy.sum``
+on the output.
 
-.. image:: ../images/plot_qpc_density.*
+Current flowing through a cut
+*****************************
 
-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.
+Below we calculate the probability current and z-projected spin current near
+the interfaces with the left and right leads.
 
-Plotting the current in the system will enable us to make even more sense
-of what is going on:
+.. literalinclude:: magnetic_texture.py
+    :start-after: #HIDDEN_BEGIN_current_cut
+    :end-before: #HIDDEN_END_current_cut
 
-.. literalinclude:: plot_qpc.py
-    :start-after: #HIDDEN_BEGIN_current
-    :end-before: #HIDDEN_END_current
+.. literalinclude:: ../images/current_cut.txt
+
+We see that the probability current is conserved across the scattering region,
+but the z-projected spin current is not due to the fact that the Hamiltonian
+does not commute with :math:`σ_z` everywhere in the scattering region.
+
+.. note:: ``where`` can also be provided as a sequence of `~kwant.builder.Site`
+          or a sequence of hoppings (i.e. pairs of `~kwant.builder.Site`),
+          rather than a function.
+
+
+Advanced Topics
+---------------
+
+Using ``bind`` for speed
+************************
+In most of the above examples we only used each operator *once* after creating
+it. Often one will want to evaluate an operator with many different
+wavefunctions, for example with all scattering wavefunctions at a certain
+energy, but with the *same set of parameters*. In such cases it is best to tell
+the operator to pre-compute the onsite matrices and any necessary Hamiltonian
+elements using the given set of parameters, so that this work is not duplicated
+every time the operator is evaluated.
+
+This can be achieved with `~kwant.operator.Current.bind`:
+
+.. warning:: Take care that you do not use an operator that was bound to a
+             particular set of parameters with wavefunctions calculated with a
+             *different* set of parameters. This will almost certainly give
+             incorrect results.
 
-.. image:: ../images/plot_qpc_current.*
+.. literalinclude:: magnetic_texture.py
+    :start-after: #HIDDEN_BEGIN_bind
+    :end-before: #HIDDEN_END_bind
 
-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.
+.. image:: ../images/bound_current.*
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>`_.
diff --git a/kwant/operator.pyx b/kwant/operator.pyx
index 4ef1371eca0384ce0d9ab02a252d5862974db1ae..8fc66e81c5e856b274e2e422f7466e38fbe61952 100644
--- a/kwant/operator.pyx
+++ b/kwant/operator.pyx
@@ -815,7 +815,7 @@ cdef class Density(_LocalOperator):
 
 
 cdef class Current(_LocalOperator):
-    """An operator for calculating general currents.
+    r"""An operator for calculating general currents.
 
     An instance of this class can be called like a function to evaluate the
     expectation value with a wavefunction. See the documentation of the
@@ -850,9 +850,9 @@ cdef class Current(_LocalOperator):
     represented by a square matrix :math:`M_i` associated with each site
     :math:`i` and :math:`H_{ij}` is the hopping Hamiltonian from site :math:`j`
     to site `i`, then an instance of this class represents the tensor
-    :math:`J_{ijαβ}` which is equal to :math:`(H_{ij})^† M_i - M_i H_{ij}` when
-    α and β are orbitals on sites :math:`i` and :math:`j` respectively, and
-    zero otherwise.
+    :math:`J_{ijαβ}` which is equal to :math:`i\left[(H_{ij})^† M_i - M_i
+    H_{ij}\right]` when α and β are orbitals on sites :math:`i` and :math:`j`
+    respectively, and zero otherwise.
 
     The tensor :math:`J_{ijαβ}` will also be referred to as :math:`Q_{nαβ}`,
     where :math:`n` is the index of hopping :math:`(i, j)` in ``where``.