diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3335af8adfbf3348ec230800996633f9e973be4c..5f8a4262d3e021803cc13f9bef5e607fb24f4a4c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -89,24 +89,18 @@ build-env:default:
   before_script:
     - source deactivate
     - source activate kwant-latest
-    - pip install qsymm
 
 .bleeding-edge-env: &bleeding_edge_env
   before_script:
     - source deactivate
     - conda env update -f /kwant-latest.yml
     - source activate kwant-latest
-    - pip install qsymm
 
 .ubuntu-env: &ubuntu_env
   image: gitlab.kwant-project.org:5005/kwant/kwant/ubuntu
-  before_script:
-    - pip3 install qsymm
 
 .debian-env: &debian_env
   image: gitlab.kwant-project.org:5005/kwant/kwant/debian
-  before_script:
-    - pip3 install qsymm
 
 ## Build Jobs
 
diff --git a/INSTALL.rst b/INSTALL.rst
index 88209df56c007829dfb910216b58811cbd8485ec..a32658ebbcdcea4c46d78d6f0b8287974c0cd71a 100644
--- a/INSTALL.rst
+++ b/INSTALL.rst
@@ -45,7 +45,7 @@ a NumPy-like Python package optimized for very small arrays,
 The following software is highly recommended though not strictly required:
  * `matplotlib <http://matplotlib.org/>`_ 1.5.1 or newer, for the module `kwant.plotter` and the tutorial,
  * `SymPy <http://sympy.org/>`_ 0.7.6 or newer, for the subpackage `kwant.continuum`.
- * `Qsymm <https://pypi.org/project/qsymm/>`_ 1.1.0 or newer, for the subpackage `kwant.qsymm`.
+ * `Qsymm <https://pypi.org/project/qsymm/>`_ 1.2.6 or newer, for the subpackage `kwant.qsymm`.
  * `MUMPS <http://graal.ens-lyon.fr/MUMPS/>`_, a sparse linear algebra library
    that will in many cases speed up Kwant several times and reduce the memory
    footprint.  (Kwant uses only the sequential, single core version
diff --git a/kwant/builder.py b/kwant/builder.py
index b31edc6beeefb0a308ed530f5d79efbf3babcd49..390ca0ba3c54d30d080d67c5002b43a49b74f4bf 100644
--- a/kwant/builder.py
+++ b/kwant/builder.py
@@ -864,6 +864,10 @@ class Builder:
     amounts to creating a `Lead` object and appending it to the list of leads
     accessbile as the `~Builder.leads` attribute.
 
+    `conservation_law`, `time_reversal`, `particle_hole`, and `chiral`
+    affect the basis in which scattering modes derived from the builder
+    are expressed - see `~kwant.physics.DiscreteSymmetry` for details.
+
     .. warning::
 
         If functions are used to set values in a builder with a symmetry, then
diff --git a/kwant/kpm.py b/kwant/kpm.py
index a897c8f9f00b949fcaf694b7f5866b191c625b0c..5da09b097e394bc57c07c7de97fb958a3665fc3b 100644
--- a/kwant/kpm.py
+++ b/kwant/kpm.py
@@ -780,6 +780,8 @@ def conductivity(hamiltonian, alpha='x', beta='x', positions=None, **kwargs):
 
     Parameters
     ----------
+    hamiltonian : `~kwant.system.FiniteSystem` or matrix Hamiltonian
+        If a system is passed, it should contain no leads.
     alpha, beta : str, or operators
         If ``hamiltonian`` is a kwant system, or if the ``positions``
         are provided, ``alpha`` and ``beta`` can be the directions of the
@@ -1051,10 +1053,11 @@ def _velocity(hamiltonian, params, op_type, positions):
     elif isinstance(op_type, str):
         direction = directions[op_type]
         if isinstance(hamiltonian, system.System):
-            operator = hamiltonian.hamiltonian_submatrix(params=params,
-                                                         sparse=True)
-            positions = np.array([site.pos for site in hamiltonian.sites
-                                  for iorb in range(site.family.norbs)])
+            operator, norbs, norbs = hamiltonian.hamiltonian_submatrix(
+                params=params, sparse=True, return_norb=True
+            )
+            positions = np.vstack([[hamiltonian.pos(i)] * norb
+                                   for i, norb in enumerate(norbs)])
         elif positions is not None:
             operator = coo_matrix(hamiltonian, copy=True)
         displacements = positions[operator.col] - positions[operator.row]
diff --git a/kwant/physics/leads.py b/kwant/physics/leads.py
index fc799edc25705cd7c2d483a59a3bec7fce70cfcd..384cc8d0d57b44bfc611985d0093d3b28e6e9a4b 100644
--- a/kwant/physics/leads.py
+++ b/kwant/physics/leads.py
@@ -130,11 +130,10 @@ class PropagatingModes:
     momentum and velocity, an arbitrary orthonormal basis in the subspace of
     these modes is chosen.
 
-    If a conservation law is specified to block diagonalize the Hamiltonian
-    to N blocks, then `block_nmodes[i]` is the number of left or right moving
-    propagating modes in conservation law block `i`. The ordering of blocks
-    is the same as the ordering of the projectors used to block diagonalize
-    the Hamiltonian.
+    If a conservation law is specified to block diagonalize the Hamiltonian,
+    then `block_nmodes[i]` is the number of left or right moving propagating
+    modes in conservation law block `i`. The ordering of blocks is the same as
+    the ordering of the projectors used to block diagonalize the Hamiltonian.
     """
     def __init__(self, wave_functions, velocities, momenta):
         kwargs = locals()
@@ -1046,6 +1045,10 @@ def modes(h_cell, h_hop, tol=1e6, stabilization=None, *,
     Propagating modes with the same momentum are orthogonalized. All the
     propagating modes are normalized by current.
 
+    `projectors`, `time_reversal`, `particle_hole`, and `chiral` affect the
+    basis in which the scattering modes are expressed - see
+    `~kwant.physics.DiscreteSymmetry` for details.
+
     This function uses the most stable and efficient algorithm for calculating
     the mode decomposition that the Kwant authors are aware about. Its details
     are to be published.
diff --git a/kwant/physics/symmetry.py b/kwant/physics/symmetry.py
index 9420f52d4967b66c22cfb971a7b948a10a377581..6c648a95d1687acd2b4c6318277e635ae6e6171a 100644
--- a/kwant/physics/symmetry.py
+++ b/kwant/physics/symmetry.py
@@ -37,7 +37,7 @@ _names = ['Time reversal', 'Particle-hole', 'Chiral']
 _signs = [1, -1, -1]
 
 class DiscreteSymmetry:
-    """A collection of discrete symmetries and conservation laws.
+    r"""A collection of discrete symmetries and conservation laws.
 
     Parameters
     ----------
@@ -52,22 +52,46 @@ class DiscreteSymmetry:
 
     Notes
     -----
-    Whenever one or more discrete symmetry is declared in conjunction with a
-    conservation law, the symmetry operators and projectors must be declared
-    in canonical form. This means that each block of the Hamiltonian is
-    transformed either to itself by a discrete symmetry or to a single
-    other block.
-
-    More formally, consider a discrete symmetry S. The symmetry projection
-    that maps from block i to block j of the Hamiltonian with projectors
-    :math:`P_i` and :math:`P_j` is :math:`S_{ji} = P_j^+ S P_i`.
-    If :math:`S_{ji}` is nonzero, a symmetry relation exists between
-    blocks i and j. Canonical form means that for each j, the block
-    :math:`S_{ji}` is nonzero at most for one i, while all other blocks vanish.
-
-    If the operators are not in canonical form, they can be made so by
-    further splitting the Hamiltonian into smaller blocks, i.e. by adding
-    more projectors.
+
+    When computing scattering modes, the representation of the
+    modes is chosen to reflect declared discrete symmetries and
+    conservation laws.
+
+    `projectors` block diagonalize the Hamiltonian, and modes are computed
+    separately in each block. The ordering of blocks is the same as of
+    `projectors`. If `conservation_law` is declared in
+    `~kwant.builder.Builder`, `projectors` is computed as the projectors
+    onto its orthogonal eigensubspaces. The projectors are stored in the
+    order of ascending eigenvalues of `conservation_law`.
+
+    Symmetrization using discrete symmetries varies depending on whether
+    a conservation law/projectors are declared. Consider the case with no
+    conservation law declared. With `time_reversal` declared, the outgoing
+    modes are chosen as the time-reversed partners of the incoming modes,
+    i.e. :math:`\psi_{out}(-k) = T \psi_{in}(k)` with k the momentum.
+    `chiral` also relates incoming and outgoing modes, such that
+    :math:`\psi_{out}(k) = C \psi_{in}(k)`. `particle_hole` gives symmetric
+    incoming and outgoing modes separately, such that
+    :math:`\psi_{in/out}(-k) = P \psi_{in/out}(k)`, except when k=-k, at
+    k = 0 or :math:`\pi`. In this case, each mode is chosen as an eigenstate
+    :math:`P \psi = \psi` if :math:`P^2=1`. If :math:`P^2=-1`, we
+    symmetrize the modes by generating pairs of orthogonal modes
+    :math:`\psi` and :math:`P\psi`. Because `chiral` and `particle_hole`
+    flip the sign of energy, they only apply at zero energy.
+
+    Discrete symmetries can be combined with a conservation law if they
+    leave each block invariant or transform it to another block. With S
+    a discrete symmetry and :math:`P_i` and :math:`P_j` projectors onto
+    blocks i and j of the Hamiltonian, :math:`S_{ji} = P_j^+ S P_i` is the
+    symmetry projection that maps from block i to block j.
+    :math:`S_{ji} = P_j^+ S P_i` must for each j be nonzero for exactly
+    one i. If S leaves block i invariant, the modes within block i are
+    symmetrized using the nonzero projection :math:`S_{ii}`, like in the
+    case without a conservation law. If S transforms between blocks i and
+    j, the modes of the block with the larger index are obtained by
+    transforming the modes of the block with the lower index. Thus, with
+    :math:`\psi_i` and :math:`\psi_j` the modes of blocks i and j, we have
+    :math:`\psi_j = S_{ji} \psi_i`.
     """
     def __init__(self, projectors=None, time_reversal=None, particle_hole=None,
                  chiral=None):
diff --git a/kwant/qsymm.py b/kwant/qsymm.py
index bb752e19314a18cbc7ec53e53eb163df5bf17f80..fef2fd6438bd7028ad7dd22f3eaecf65dc2f300d 100644
--- a/kwant/qsymm.py
+++ b/kwant/qsymm.py
@@ -68,8 +68,8 @@ def builder_to_model(syst, momenta=None, real_space=True,
     is used in finalized kwant systems.
     """
     def term_to_model(d, par, matrix):
-        if np.allclose(matrix, 0):
-            result = BlochModel({})
+        if allclose(matrix, 0):
+            result = BlochModel({}, shape=matrix.shape, format=np.ndarray)
         else:
             result = BlochModel({BlochCoeff(d, qsymm.sympify(par)): matrix},
                                 momenta=momenta)
@@ -323,8 +323,8 @@ def model_to_builder(model, norbs, lat_vecs, atom_coords, *, coeffs=None):
 
     # Keep track of the hoppings and onsites by storing those
     # which have already been set.
-    hopping_dict = defaultdict(dict)
-    onsites_dict = defaultdict(dict)
+    hopping_dict = defaultdict(lambda: 0)
+    onsites_dict = defaultdict(lambda: 0)
 
     # Iterate over all terms in the model.
     for key, hop_mat in model.items():
diff --git a/kwant/tests/test_kpm.py b/kwant/tests/test_kpm.py
index 7ae187339e51163549637cf44306643b7f816bf4..524cc57d78a97434ed6605f36b72d5833c5fd410 100644
--- a/kwant/tests/test_kpm.py
+++ b/kwant/tests/test_kpm.py
@@ -80,6 +80,13 @@ def test_conductivity():
         cond = kwant.kpm.conductivity(syst, alpha=alpha, beta=beta,
                                         positions=None, **kpm_params)
 
+    # test when 'norbs' not provided
+    n = lat.norbs
+    lat.norbs = None
+    cond = kwant.kpm.conductivity(syst, alpha=alpha, beta=beta,
+                                  positions=None, **kpm_params)
+    lat.norbs = n
+
     # test system or hamiltonian, no positions, but velocity operators
     cond_xx = kwant.kpm.conductivity(syst, alpha='x', beta='x',
                                      positions=None, **kpm_params)
diff --git a/kwant/tests/test_qsymm.py b/kwant/tests/test_qsymm.py
index bf7e45d31286b30e5dcbd12247db96bbc279b6b0..7714c1c397d49171c8c6699860752ba333ed444c 100644
--- a/kwant/tests/test_qsymm.py
+++ b/kwant/tests/test_qsymm.py
@@ -19,7 +19,7 @@ from qsymm.symmetry_finder import symmetries
 from qsymm.hamiltonian_generator import bloch_family, hamiltonian_from_family
 from qsymm.groups import (hexagonal, PointGroupElement, spin_matrices,
                           spin_rotation, ContinuousGroupGenerator)
-from qsymm.model import Model, e, I, _commutative_momenta
+from qsymm.model import Model, BlochModel, BlochCoeff
 from qsymm.linalg import allclose
 
 import kwant
@@ -41,7 +41,7 @@ def test_honeycomb():
     syst[lat.neighbors(1)] = -1
 
     H = builder_to_model(syst)
-    sg, cs = symmetries(H, hexagonal(sympy_R=False), prettify=True)
+    sg, cs = symmetries(H, hexagonal(sympy_R=False))
     assert len(sg) == 24
     assert len(cs) == 0
 
@@ -52,7 +52,7 @@ def test_honeycomb():
     syst[lat.neighbors(1)] = lambda site1, site2, t: t
 
     H = builder_to_model(syst)
-    sg, cs = symmetries(H, hexagonal(sympy_R=False), prettify=True)
+    sg, cs = symmetries(H, hexagonal(sympy_R=False))
     assert len(sg) == 12
     assert len(cs) == 0
 
@@ -93,7 +93,7 @@ def test_higher_dim():
     syst[lat(0, 0, 0), lat(1, 0, -1)] = -1
 
     H = builder_to_model(syst)
-    sg, cs = symmetries(H, prettify=True)
+    sg, cs = symmetries(H)
     assert len(sg) == 2
     assert len(cs) == 5
 
@@ -107,7 +107,7 @@ def test_higher_dim():
     syst[lat(0, 0, 0), lat(1, 0, -1)] = -1
 
     H = builder_to_model(syst)
-    sg, cs = symmetries(H, hexagonal(sympy_R=False), prettify=True)
+    sg, cs = symmetries(H, hexagonal(sympy_R=False))
     assert len(sg) == 24
     assert len(cs) == 0
 
@@ -137,9 +137,9 @@ def test_graphene_to_kwant():
     family = bloch_family(hopping_vectors, symmetries, norbs)
     syst_from_family = model_to_builder(family, norbs, lat_vecs, atom_coords, coeffs=None)
     # Generate using a single Model object
-    g = sympy.Symbol('g', real=True)
-    ham = hamiltonian_from_family(family, coeffs=[g])
-    ham = Model(hamiltonian=ham, momenta=family[0].momenta)
+    g = sympy.Symbol('g')
+    # tosympy=False to return a BlochModel
+    ham = hamiltonian_from_family(family, coeffs=[g], tosympy=False)
     syst_from_model = model_to_builder(ham, norbs, lat_vecs, atom_coords)
 
     # Make the graphene Hamiltonian using kwant only
@@ -185,9 +185,9 @@ def test_graphene_to_kwant():
                Model({one: np.array([[0, 0], [0, 1]])}, momenta=family[0].momenta)]
     family = family + onsites
     syst_from_family = model_to_builder(family, norbs, lat_vecs, atom_coords, coeffs=None)
-    gs = list(sympy.symbols('g0:%d'%3, real=True))
-    ham = hamiltonian_from_family(family, coeffs=gs)
-    ham = Model(hamiltonian=ham, momenta=family[0].momenta)
+    gs = list(sympy.symbols('g0:3'))
+    # tosympy=False to return a BlochModel
+    ham = hamiltonian_from_family(family, coeffs=gs, tosympy=False)
     syst_from_model = model_to_builder(ham, norbs, lat_vecs, atom_coords)
 
     def onsite_A(site, c1):
@@ -277,16 +277,16 @@ def test_inverse_transform():
     # Hopping to a neighbouring atom one primitive lattice vector away
     hopping_vectors = [('A', 'A', [1, 0])]
     # Make family
-    family = bloch_family(hopping_vectors, symmetries, norbs)
+    family = bloch_family(hopping_vectors, symmetries, norbs, bloch_model=True)
     fam = hamiltonian_from_family(family, tosympy=False)
     # Atomic coordinates within the unit cell
     atom_coords = [(0, 0)]
     lat_vecs = [(1, 0), (0, 1)]
     syst = model_to_builder(fam, norbs, lat_vecs, atom_coords)
     # Convert it back
-    ham2 = builder_to_model(syst).tomodel(nsimplify=True)
+    ham2 = builder_to_model(syst)
     # Check that it's the same as the original
-    assert fam == ham2
+    assert fam.allclose(ham2)
 
     # Check that the Hamiltonians are identical at random points in the Brillouin zone
     sysw = kwant.wraparound.wraparound(syst).finalized()
@@ -314,12 +314,11 @@ def test_consistency_kwant():
     H += H.T.conj()
 
     # Make the 1D Model manually using only qsymm features.
-    c0, c1 = sympy.symbols('c0 c1', real=True)
-    kx = _commutative_momenta[0]
+    c0, c1 = sympy.symbols('c0 c1')
 
-    Ham = Model({c0 * e**(-I*kx): T}, momenta=['k_x'])
+    Ham = BlochModel({BlochCoeff(np.array([-1]), c0): T}, momenta=['k_x'])
     Ham += Ham.T().conj()
-    Ham += Model({c1: H}, momenta=['k_x'])
+    Ham += BlochModel({BlochCoeff(np.array([0]), c1): H}, momenta=['k_x'])
 
     # Two superimposed atoms, same number of orbitals on each
     norbs = OrderedDict([('A', orbs), ('B', orbs)])
@@ -367,8 +366,8 @@ def test_consistency_kwant():
                      np.exp(1j*k)*model_kwant_hop.T.conj()) # As in kwant.Bands
     h_model = Ham.lambdify()
     wsyst = kwant.wraparound.wraparound(model_syst).finalized()
-    for _ in range(20):
-        k = (np.random.rand() - 0.5)*2*np.pi
+    ks = np.linspace(-np.pi, np.pi, 21)
+    for k in ks:
         assert allclose(h_model_kwant(k), h_model(coeffs[0], coeffs[1], k))
         params['k_x'] = k
         h_wrap = wsyst.hamiltonian_submatrix(params=params)
@@ -376,11 +375,11 @@ def test_consistency_kwant():
 
     # Get the model back from the builder
     # From the Kwant builder based on original Model
-    Ham1 = builder_to_model(model_syst, momenta=Ham.momenta).tomodel(nsimplify=True)
+    Ham1 = builder_to_model(model_syst, momenta=Ham.momenta)
     # From the pure Kwant builder
-    Ham2 = builder_to_model(kwant_syst, momenta=Ham.momenta).tomodel(nsimplify=True)
-    assert Ham == Ham1
-    assert Ham == Ham2
+    Ham2 = builder_to_model(kwant_syst, momenta=Ham.momenta)
+    assert Ham.allclose(Ham1)
+    assert Ham.allclose(Ham2)
 
 
 def test_find_builder_discrete_symmetries():
@@ -405,12 +404,15 @@ def test_find_builder_discrete_symmetries():
         bulk[kwant.builder.HoppingKind((1, 0), lat)] = h_hop
         bulk[kwant.builder.HoppingKind((0, 1), lat)] = h_hop
 
+        # We need to specify 'prettify=True' here to ensure that we do not end up with
+        # an overcomplete set of symmetries. In some badly conditioned cases sparse=True
+        # or sparse=False may affect how many symmetries are found.
         builder_symmetries_default = find_builder_symmetries(bulk, spatial_symmetries=True,
                                                              prettify=True)
         builder_symmetries_sparse = find_builder_symmetries(bulk, spatial_symmetries=True,
                                                             prettify=True, sparse=True)
         builder_symmetries_dense = find_builder_symmetries(bulk, spatial_symmetries=True,
-                                                            prettify=True, sparse=False)
+                                                           prettify=True, sparse=False)
 
         assert len(builder_symmetries_default) == len(builder_symmetries_sparse)
         assert len(builder_symmetries_default) == len(builder_symmetries_dense)
@@ -476,7 +478,7 @@ def test_find_cons_law():
     syst[lat(1)] = np.kron(sy, ons)
     syst[lat(1), lat(0)] = np.kron(sy, hop)
 
-    builder_symmetries = find_builder_symmetries(syst, spatial_symmetries=False, prettify=True)
+    builder_symmetries = find_builder_symmetries(syst, spatial_symmetries=False)
     onsites = [symm for symm in builder_symmetries if
                isinstance(symm, qsymm.ContinuousGroupGenerator) and symm.R is None]
     mham = builder_to_model(syst)
@@ -508,8 +510,7 @@ def test_basis_ordering():
 
         # Find the symmetries of the square
         builder_symmetries = find_builder_symmetries(square,
-                                                     spatial_symmetries=False,
-                                                     prettify=True)
+                                                     spatial_symmetries=False)
         # Finalize the square, extract Hamiltonian
         fsquare = square.finalized()
         ham = fsquare.hamiltonian_submatrix()
diff --git a/setup.py b/setup.py
index 5d4c30873c1a420bbf9221ec2c4f239991e3044d..cef8c9f545c81c4eb49868ff125cde816237c2ec 100755
--- a/setup.py
+++ b/setup.py
@@ -588,7 +588,7 @@ def main():
               'plotting': 'matplotlib >= 1.5.1',
               'continuum': 'sympy >= 0.7.6',
               # qsymm is only packaged on PyPI
-              'qsymm': 'qsymm >= 1.1.2',
+              'qsymm': 'qsymm >= 1.2.6',
           },
           classifiers=[c.strip() for c in classifiers.split('\n')])