diff --git a/gusto/time_discretisation.py b/gusto/time_discretisation.py
index ec6428228..4a0dcd4d6 100644
--- a/gusto/time_discretisation.py
+++ b/gusto/time_discretisation.py
@@ -7,7 +7,7 @@
 
 from abc import ABCMeta, abstractmethod, abstractproperty
 from firedrake import (Function, TestFunction, NonlinearVariationalProblem,
-                       NonlinearVariationalSolver, DirichletBC, Constant)
+                       NonlinearVariationalSolver, DirichletBC, split, Constant)
 from firedrake.formmanipulation import split_form
 from firedrake.utils import cached_property
 
@@ -22,9 +22,9 @@
 import numpy as np
 
 
-__all__ = ["ForwardEuler", "BackwardEuler", "ExplicitMultistage", "SSPRK3", "RK4",
-           "Heun", "ThetaMethod", "TrapeziumRule", "BDF2", "TR_BDF2", "Leapfrog",
-           "AdamsMoulton", "AdamsBashforth"]
+__all__ = ["ForwardEuler", "BackwardEuler", "ExplicitMultistage", "ImplicitMultistage",
+           "SSPRK3", "RK4", "Heun", "ThetaMethod", "TrapeziumRule", "BDF2", "TR_BDF2",
+           "Leapfrog", "AdamsMoulton", "AdamsBashforth", "ImplicitMidpoint", "QinZhang"]
 
 
 def wrapper_apply(original_apply):
@@ -243,6 +243,144 @@ def apply(self, x_out, x_in):
         pass
 
 
+class ImplicitMultistage(TimeDiscretisation):
+    """
+    A class for implementing general diagonally implicit multistage (Runge-Kutta)
+    methods based on its Butcher tableau.
+
+    Unlike the explicit method, all upper diagonal a_ij elements are non-zero for implicit methods.
+
+    There are three steps to move from the current solution, y^n, to the new one, y^{n+1}
+
+    For each i = 1, s  in an s stage method
+    we have the intermediate solutions:                                       \n
+    y_i = y^n +  dt*(a_i1*k_1 + a_i2*k_2 + ... + a_ii*k_i)                    \n
+    We compute the gradient at the intermediate location, k_i = F(y_i)        \n
+
+    At the last stage, compute the new solution by:                           \n
+    y^{n+1} = y^n + dt*(b_1*k_1 + b_2*k_2 + .... + b_s*k_s)                   \n
+
+    """
+    # ---------------------------------------------------------------------------
+    # Butcher tableau for a s-th order
+    # diagonally implicit scheme:
+    #  c_0 | a_00  0    .     0
+    #  c_1 | a_10 a_11  .     0
+    #   .  |   .   .    .     .
+    #   .  |   .   .    .     .
+    #  c_s | a_s0 a_s1  .    a_ss
+    #   -------------------------
+    #      |  b_1  b_2  ...  b_s
+    #
+    #
+    # The corresponding square 'butcher_matrix' is:
+    #
+    #  [a_00   0   .       0  ]
+    #  [a_10 a_11  .       0  ]
+    #  [  .    .   .       .  ]
+    #  [ b_0  b_1  .       b_s]
+    # ---------------------------------------------------------------------------
+
+    def __init__(self, domain, butcher_matrix, field_name=None,
+                 solver_parameters=None, limiter=None, options=None,):
+        """
+        Args:
+            domain (:class:`Domain`): the model's domain object, containing the
+                mesh and the compatible function spaces.
+            butcher_matrix (numpy array): A matrix containing the coefficients of
+                a butcher tableau defining a given Runge Kutta time discretisation.
+            field_name (str, optional): name of the field to be evolved.
+                Defaults to None.
+            solver_parameters (dict, optional): dictionary of parameters to
+                pass to the underlying solver. Defaults to None.
+            limiter (:class:`Limiter` object, optional): a limiter to apply to
+                the evolving field to enforce monotonicity. Defaults to None.
+            options (:class:`AdvectionOptions`, optional): an object containing
+                options to either be passed to the spatial discretisation, or
+                to control the "wrapper" methods, such as Embedded DG or a
+                recovery method. Defaults to None.
+        """
+        super().__init__(domain, field_name=field_name,
+                         solver_parameters=solver_parameters,
+                         limiter=limiter, options=options)
+        self.butcher_matrix = butcher_matrix
+        self.nStages = int(np.shape(self.butcher_matrix)[1])
+
+    def setup(self, equation, apply_bcs=True, *active_labels):
+        """
+        Set up the time discretisation based on the equation.
+
+        Args:
+            equation (:class:`PrognosticEquation`): the model's equation.
+            *active_labels (:class:`Label`): labels indicating which terms of
+                the equation to include.
+        """
+
+        super().setup(equation, apply_bcs, *active_labels)
+
+        self.k = [Function(self.fs) for i in range(self.nStages)]
+
+    def lhs(self):
+        return super().lhs
+
+    def rhs(self):
+        return super().rhs
+
+    def solver(self, stage):
+        residual = self.residual.label_map(
+            lambda t: t.has_label(time_derivative),
+            map_if_true=drop,
+            map_if_false=replace_subject(self.xnph, self.idx),
+        )
+        mass_form = self.residual.label_map(
+            lambda t: t.has_label(time_derivative),
+            map_if_false=drop)
+        residual += mass_form.label_map(all_terms,
+                                        replace_subject(self.x_out, self.idx))
+
+        problem = NonlinearVariationalProblem(residual.form, self.x_out, bcs=self.bcs)
+
+        solver_name = self.field_name+self.__class__.__name__ + "%s" % (stage)
+        return NonlinearVariationalSolver(problem, solver_parameters=self.solver_parameters,
+                                          options_prefix=solver_name)
+
+    @cached_property
+    def solvers(self):
+        solvers = []
+        for stage in range(self.nStages):
+            solvers.append(self.solver(stage))
+        return solvers
+
+    def solve_stage(self, x0, stage):
+        self.x1.assign(x0)
+        for i in range(stage):
+            self.x1.assign(self.x1 + self.butcher_matrix[stage, i]*self.dt*self.k[i])
+
+        if self.limiter is not None:
+            self.limiter.apply(self.x1)
+
+        if self.idx is None and len(self.fs) > 1:
+            self.xnph = tuple([self.dt*self.butcher_matrix[stage, stage]*a + b for a, b in zip(split(self.x_out), split(self.x1))])
+        else:
+            self.xnph = self.x1 + self.butcher_matrix[stage, stage]*self.dt*self.x_out
+        solver = self.solvers[stage]
+        solver.solve()
+
+        self.k[stage].assign(self.x_out)
+
+    def apply(self, x_out, x_in):
+
+        for i in range(self.nStages):
+            self.solve_stage(x_in, i)
+
+        x_out.assign(x_in)
+        for i in range(self.nStages):
+            x_out.assign(x_out + self.butcher_matrix[self.nStages, i]*self.dt*self.k[i])
+
+        if self.limiter is not None:
+            self.limiter.apply(x_out)
+
+
 class ExplicitTimeDiscretisation(TimeDiscretisation):
     """Base class for explicit time discretisations."""
 
@@ -365,50 +503,78 @@ class ExplicitMultistage(ExplicitTimeDiscretisation):
     A class for implementing general explicit multistage (Runge-Kutta)
     methods based on its Butcher tableau.
 
-    A Butcher tableau is formed in the following way for a s-th order explicit scheme:
-
-    c_0 | a_00
-    c_1 | a_10 a_11
-     .  | a_20 a_21 a_22
-     .  |   .   .    .    .
-     -------------------------
-    c_s |  b_1  b_2  ...  b_s
+    A Butcher tableau is formed in the following way for a s-th order explicit scheme: \n
 
-    The gradient function has no time-dependence, so c elements are not needed.
-    A square 'butcher_matrix' is defined in each class that uses
-    the ExplicitMultiStage structure,
-
-    [a_00   0   .       0  ]
-    [a_10 a_11  .       0  ]
-    [  .    .   .       .  ]
-    [ b_0  b_1  .   b_{s-1}]
-
-    All upper diagonal a_ij elements are zero for explicit methods.
+    All upper diagonal a_ij elements are zero for explicit methods. We exclude the first
+    row of the butcher tableau from our butcher matrix as the row is always zeros.
 
     There are three steps to move from the current solution, y^n, to the new one, y^{n+1}
 
-    For an s stage method,
-      At iteration i (from 0 to s-1)
-        An intermediate location is computed as y_i = y^n + sum{over j less than i} (dt*a_ij*k_i)
-        Compute the gradient at the intermediate location, k_i = F(y_i)
+    For each i = 1, s  in an s stage method
+    we have the intermediate solutions:                                       \n
+    y_i = y^n +  dt*(a_i1*k_1 + a_i2*k_2 + ... + a_i{i-1}*k_{i-1})            \n
+    We compute the gradient at the intermediate location, k_i = F(y_i)        \n
 
     At the last stage, compute the new solution by:
-    y^{n+1} = y^n + sum_{j from 0 to s-1} (dt*b_i*k_i)
+    y^{n+1} = y^n + dt*(b_1*k_1 + b_2*k_2 + .... + b_s*k_s)                   \n
 
     """
-
-    def __init__(self, domain, field_name=None, fixed_subcycles=None,
+    # ---------------------------------------------------------------------------
+    # Butcher tableau for a s-th order
+    # explicit scheme:
+    #  c_0 |   0   0    .    0
+    #  c_1 | a_10  0    .    0
+    #   .  |   .   .    .    .
+    #   .  |   .   .    .    .
+    #  c_s | a_s0 a_s1  .    0
+    #   -------------------------
+    #      |  b_1  b_2  ...  b_s
+    #
+    #
+    # The corresponding square 'butcher_matrix' is:
+    #
+    #  [a_10   0   .       0  ]
+    #  [a_20  a_21 .       0  ]
+    #  [  .    .   .       .  ]
+    #  [ b_0  b_1  .       b_s]
+    # ---------------------------------------------------------------------------
+
+    def __init__(self, domain, butcher_matrix, field_name=None, fixed_subcycles=None,
                  subcycle_by_courant=None, solver_parameters=None,
-                 limiter=None, options=None, butcher_matrix=None):
-
+                 limiter=None, options=None):
+        """
+        Args:
+            domain (:class:`Domain`): the model's domain object, containing the
+                mesh and the compatible function spaces.
+            butcher_matrix (numpy array): A matrix containing the coefficients of
+                a butcher tableau defining a given Runge Kutta time discretisation.
+            field_name (str, optional): name of the field to be evolved.
+                Defaults to None.
+            fixed_subcycles (int, optional): the fixed number of sub-steps to
+                perform. This option cannot be specified with the
+                `subcycle_by_courant` argument. Defaults to None.
+            subcycle_by_courant (float, optional): specifying this option will
+                make the scheme perform adaptive sub-cycling based on the
+                Courant number. The specified argument is the maximum Courant
+                for one sub-cycle. Defaults to None, in which case adaptive
+                sub-cycling is not used. This option cannot be specified with the
+                `fixed_subcycles` argument.
+            solver_parameters (dict, optional): dictionary of parameters to
+                pass to the underlying solver. Defaults to None.
+            limiter (:class:`Limiter` object, optional): a limiter to apply to
+                the evolving field to enforce monotonicity. Defaults to None.
+            options (:class:`AdvectionOptions`, optional): an object containing
+                options to either be passed to the spatial discretisation, or
+                to control the "wrapper" methods, such as Embedded DG or a
+                recovery method. Defaults to None.
+        """
         super().__init__(domain, field_name=field_name,
                          fixed_subcycles=fixed_subcycles,
                          subcycle_by_courant=subcycle_by_courant,
                          solver_parameters=solver_parameters,
                          limiter=limiter, options=options)
-        if butcher_matrix is not None:
-            self.butcher_matrix = butcher_matrix
-            self.nbutcher = int(np.shape(self.butcher_matrix)[0])
+        self.butcher_matrix = butcher_matrix
+        self.nbutcher = int(np.shape(self.butcher_matrix)[0])
 
     @property
     def nStages(self):
@@ -500,44 +666,88 @@ class ForwardEuler(ExplicitMultistage):
     """
     Implements the forward Euler timestepping scheme.
 
-    The forward Euler method for operator F is the most simple explicit scheme:
-    k0 = F[y^n]
-    y^(n+1) = y^n + dt*k0
+    The forward Euler method for operator F is the most simple explicit scheme: \n
+    k0 = F[y^n]                                                                 \n
+    y^(n+1) = y^n + dt*k0                                                       \n
     """
     def __init__(self, domain, field_name=None, fixed_subcycles=None,
                  subcycle_by_courant=None, solver_parameters=None,
-                 limiter=None, options=None, butcher_matrix=None):
-        super().__init__(domain, field_name=field_name,
+                 limiter=None, options=None):
+        """
+        Args:
+            domain (:class:`Domain`): the model's domain object, containing the
+                mesh and the compatible function spaces.
+            field_name (str, optional): name of the field to be evolved.
+                Defaults to None.
+            fixed_subcycles (int, optional): the fixed number of sub-steps to
+                perform. This option cannot be specified with the
+                `subcycle_by_courant` argument. Defaults to None.
+            subcycle_by_courant (float, optional): specifying this option will
+                make the scheme perform adaptive sub-cycling based on the
+                Courant number. The specified argument is the maximum Courant
+                for one sub-cycle. Defaults to None, in which case adaptive
+                sub-cycling is not used. This option cannot be specified with the
+                `fixed_subcycles` argument.
+            solver_parameters (dict, optional): dictionary of parameters to
+                pass to the underlying solver. Defaults to None.
+            limiter (:class:`Limiter` object, optional): a limiter to apply to
+                the evolving field to enforce monotonicity. Defaults to None.
+            options (:class:`AdvectionOptions`, optional): an object containing
+                options to either be passed to the spatial discretisation, or
+                to control the "wrapper" methods, such as Embedded DG or a
+                recovery method. Defaults to None.
+        """
+        butcher_matrix = np.array([1.]).reshape(1, 1)
+        super().__init__(domain, butcher_matrix, field_name=field_name,
                          fixed_subcycles=fixed_subcycles,
                          subcycle_by_courant=subcycle_by_courant,
                          solver_parameters=solver_parameters,
-                         limiter=limiter, options=options,
-                         butcher_matrix=butcher_matrix)
-        self.butcher_matrix = np.array([1.]).reshape(1, 1)
-        self.nbutcher = int(np.shape(self.butcher_matrix)[0])
+                         limiter=limiter, options=options)
 
 
 class SSPRK3(ExplicitMultistage):
     u"""
     Implements the 3-stage Strong-Stability-Preserving Runge-Kutta method
-    for solving ∂y/∂t = F(y). It can be written as:
+    for solving ∂y/∂t = F(y). It can be written as:                           \n
 
-    k0 = F[y^n]
-    k1 = F[y^n + dt*k1]
-    k2 = F[y^n + (1/4)*dt*(k0+k1)]
-    y^(n+1) = y^n + (1/6)*dt*(k0 + k1 + 4*k2)
+    k0 = F[y^n]                                                               \n
+    k1 = F[y^n + dt*k1]                                                       \n
+    k2 = F[y^n + (1/4)*dt*(k0+k1)]                                            \n
+    y^(n+1) = y^n + (1/6)*dt*(k0 + k1 + 4*k2)                                 \n
     """
     def __init__(self, domain, field_name=None, fixed_subcycles=None,
                  subcycle_by_courant=None, solver_parameters=None,
-                 limiter=None, options=None, butcher_matrix=None):
-        super().__init__(domain, field_name=field_name,
+                 limiter=None, options=None):
+        """
+        Args:
+            domain (:class:`Domain`): the model's domain object, containing the
+                mesh and the compatible function spaces.
+            field_name (str, optional): name of the field to be evolved.
+                Defaults to None.
+            fixed_subcycles (int, optional): the fixed number of sub-steps to
+                perform. This option cannot be specified with the
+                `subcycle_by_courant` argument. Defaults to None.
+            subcycle_by_courant (float, optional): specifying this option will
+                make the scheme perform adaptive sub-cycling based on the
+                Courant number. The specified argument is the maximum Courant
+                for one sub-cycle. Defaults to None, in which case adaptive
+                sub-cycling is not used. This option cannot be specified with the
+                `fixed_subcycles` argument.
+            solver_parameters (dict, optional): dictionary of parameters to
+                pass to the underlying solver. Defaults to None.
+            limiter (:class:`Limiter` object, optional): a limiter to apply to
+                the evolving field to enforce monotonicity. Defaults to None.
+            options (:class:`AdvectionOptions`, optional): an object containing
+                options to either be passed to the spatial discretisation, or
+                to control the "wrapper" methods, such as Embedded DG or a
+                recovery method. Defaults to None.
+        """
+        butcher_matrix = np.array([[1., 0., 0.], [1./4., 1./4., 0.], [1./6., 1./6., 2./3.]])
+        super().__init__(domain, butcher_matrix, field_name=field_name,
                          fixed_subcycles=fixed_subcycles,
                          subcycle_by_courant=subcycle_by_courant,
                          solver_parameters=solver_parameters,
-                         limiter=limiter, options=options,
-                         butcher_matrix=butcher_matrix)
-        self.butcher_matrix = np.array([[1., 0., 0.], [1./4., 1./4., 0.], [1./6., 1./6., 2./3.]])
-        self.nbutcher = int(np.shape(self.butcher_matrix)[0])
+                         limiter=limiter, options=options)
 
 
 class RK4(ExplicitMultistage):
@@ -545,27 +755,49 @@ class RK4(ExplicitMultistage):
     Implements the classic 4-stage Runge-Kutta method.
 
     The classic 4-stage Runge-Kutta method for solving ∂y/∂t = F(y). It can be
-    written as:
+    written as:                                                               \n
 
-    k0 = F[y^n]
-    k1 = F[y^n + 1/2*dt*k1]
-    k2 = F[y^n + 1/2*dt*k2]
-    k3 = F[y^n + dt*k3]
-    y^(n+1) = y^n + (1/6) * dt * (k0 + 2*k1 + 2*k2 + k3)
+    k0 = F[y^n]                                                               \n
+    k1 = F[y^n + 1/2*dt*k1]                                                   \n
+    k2 = F[y^n + 1/2*dt*k2]                                                   \n
+    k3 = F[y^n + dt*k3]                                                       \n
+    y^(n+1) = y^n + (1/6) * dt * (k0 + 2*k1 + 2*k2 + k3)                      \n
 
-    where superscripts indicate the time-level.
+    where superscripts indicate the time-level.                               \n
     """
     def __init__(self, domain, field_name=None, fixed_subcycles=None,
                  subcycle_by_courant=None, solver_parameters=None,
-                 limiter=None, options=None, butcher_matrix=None):
-        super().__init__(domain, field_name=field_name,
+                 limiter=None, options=None):
+        """
+        Args:
+            domain (:class:`Domain`): the model's domain object, containing the
+                mesh and the compatible function spaces.
+            field_name (str, optional): name of the field to be evolved.
+                Defaults to None.
+            fixed_subcycles (int, optional): the fixed number of sub-steps to
+                perform. This option cannot be specified with the
+                `subcycle_by_courant` argument. Defaults to None.
+            subcycle_by_courant (float, optional): specifying this option will
+                make the scheme perform adaptive sub-cycling based on the
+                Courant number. The specified argument is the maximum Courant
+                for one sub-cycle. Defaults to None, in which case adaptive
+                sub-cycling is not used. This option cannot be specified with the
+                `fixed_subcycles` argument.
+            solver_parameters (dict, optional): dictionary of parameters to
+                pass to the underlying solver. Defaults to None.
+            limiter (:class:`Limiter` object, optional): a limiter to apply to
+                the evolving field to enforce monotonicity. Defaults to None.
+            options (:class:`AdvectionOptions`, optional): an object containing
+                options to either be passed to the spatial discretisation, or
+                to control the "wrapper" methods, such as Embedded DG or a
+                recovery method. Defaults to None.
+        """
+        butcher_matrix = np.array([[0.5, 0., 0., 0.], [0., 0.5, 0., 0.], [0., 0., 1., 0.], [1./6., 1./3., 1./3., 1./6.]])
+        super().__init__(domain, butcher_matrix, field_name=field_name,
                          fixed_subcycles=fixed_subcycles,
                          subcycle_by_courant=subcycle_by_courant,
                          solver_parameters=solver_parameters,
-                         limiter=limiter, options=options,
-                         butcher_matrix=butcher_matrix)
-        self.butcher_matrix = np.array([[0.5, 0., 0., 0.], [0., 0.5, 0., 0.], [0., 0., 1., 0.], [1./6., 1./3., 1./3., 1./6.]])
-        self.nbutcher = int(np.shape(self.butcher_matrix)[0])
+                         limiter=limiter, options=options)
 
 
 class Heun(ExplicitMultistage):
@@ -573,31 +805,55 @@ class Heun(ExplicitMultistage):
     Implements Heun's method.
 
     The 2-stage Runge-Kutta scheme known as Heun's method,for solving
-    ∂y/∂t = F(y). It can be written as:
+    ∂y/∂t = F(y). It can be written as:                                       \n
 
-    y_1 = F[y^n]
-    y^(n+1) = (1/2)y^n + (1/2)F[y_1]
+    y_1 = F[y^n]                                                              \n
+    y^(n+1) = (1/2)y^n + (1/2)F[y_1]                                          \n
 
     where superscripts indicate the time-level and subscripts indicate the stage
     number.
     """
     def __init__(self, domain, field_name=None, fixed_subcycles=None,
                  subcycle_by_courant=None, solver_parameters=None,
-                 limiter=None, options=None, butcher_matrix=None):
-        super().__init__(domain, field_name, fixed_subcycles=fixed_subcycles,
+                 limiter=None, options=None):
+        """
+        Args:
+            domain (:class:`Domain`): the model's domain object, containing the
+                mesh and the compatible function spaces.
+            field_name (str, optional): name of the field to be evolved.
+                Defaults to None.
+            fixed_subcycles (int, optional): the fixed number of sub-steps to
+                perform. This option cannot be specified with the
+                `subcycle_by_courant` argument. Defaults to None.
+            subcycle_by_courant (float, optional): specifying this option will
+                make the scheme perform adaptive sub-cycling based on the
+                Courant number. The specified argument is the maximum Courant
+                for one sub-cycle. Defaults to None, in which case adaptive
+                sub-cycling is not used. This option cannot be specified with the
+                `fixed_subcycles` argument.
+            solver_parameters (dict, optional): dictionary of parameters to
+                pass to the underlying solver. Defaults to None.
+            limiter (:class:`Limiter` object, optional): a limiter to apply to
+                the evolving field to enforce monotonicity. Defaults to None.
+            options (:class:`AdvectionOptions`, optional): an object containing
+                options to either be passed to the spatial discretisation, or
+                to control the "wrapper" methods, such as Embedded DG or a
+                recovery method. Defaults to None.
+        """
+        butcher_matrix = np.array([[1., 0.], [0.5, 0.5]])
+        super().__init__(domain, butcher_matrix, field_name=field_name,
+                         fixed_subcycles=fixed_subcycles,
                          subcycle_by_courant=subcycle_by_courant,
                          solver_parameters=solver_parameters,
                          limiter=limiter, options=options)
-        self.butcher_matrix = np.array([[1., 0.], [0.5, 0.5]])
-        self.nbutcher = np.shape(self.butcher_matrix)[0]
 
 
 class BackwardEuler(TimeDiscretisation):
     """
     Implements the backward Euler timestepping scheme.
 
-    The backward Euler method for operator F is the most simple implicit scheme:
-    y^(n+1) = y^n + dt*F[y^(n+1)].
+    The backward Euler method for operator F is the most simple implicit scheme: \n
+    y^(n+1) = y^n + dt*F[y^(n+1)].                                               \n
     """
     def __init__(self, domain, field_name=None, solver_parameters=None,
                  limiter=None, options=None):
@@ -672,9 +928,9 @@ class ThetaMethod(TimeDiscretisation):
     Implements the theta implicit-explicit timestepping method, which can
     be thought as a generalised trapezium rule.
 
-    The theta implicit-explicit timestepping method for operator F is written as
-    y^(n+1) = y^n + dt*(1-theta)*F[y^n] + dt*theta*F[y^(n+1)]
-    for off-centring parameter theta.
+    The theta implicit-explicit timestepping method for operator F is written as: \n
+    y^(n+1) = y^n + dt*(1-theta)*F[y^n] + dt*theta*F[y^(n+1)]                     \n
+    for off-centring parameter theta.                                             \n
     """
 
     def __init__(self, domain, theta, field_name=None,
@@ -754,9 +1010,9 @@ class TrapeziumRule(ThetaMethod):
     Implements the trapezium rule timestepping method, also commonly known as
     Crank Nicholson.
 
-    The trapezium rule timestepping method for operator F is written as
-    y^(n+1) = y^n + dt/2*F[y^n] + dt/2*F[y^(n+1)].
-    It is equivalent to the "theta" method with theta = 1/2.
+    The trapezium rule timestepping method for operator F is written as:      \n
+    y^(n+1) = y^n + dt/2*F[y^n] + dt/2*F[y^(n+1)].                            \n
+    It is equivalent to the "theta" method with theta = 1/2.                  \n
     """
 
     def __init__(self, domain, field_name=None, solver_parameters=None,
@@ -818,10 +1074,10 @@ def setup(self, equation, apply_bcs=True, *active_labels):
 
 class BDF2(MultilevelTimeDiscretisation):
     """
-    Implements the implicit multistep BDF2 timestepping method
+    Implements the implicit multistep BDF2 timestepping method.
 
-    The BDF2 timestepping method for operator F is written as
-    y^(n+1) = (4/3)*y^n - (1/3)*y^(n-1) + (2/3)*dt*F[y^(n+1)]
+    The BDF2 timestepping method for operator F is written as:                \n
+    y^(n+1) = (4/3)*y^n - (1/3)*y^(n-1) + (2/3)*dt*F[y^(n+1)]                 \n
     """
 
     @property
@@ -919,10 +1175,10 @@ class TR_BDF2(TimeDiscretisation):
     Implements the two stage implicit TR-BDF2 time stepping method, with a
     trapezoidal stage (TR) followed by a second order backwards difference stage (BDF2).
 
-    The TR-BDF2 time stepping method for operator F is written as
-    y^(n+g) = y^n + dt*g/2*F[y^n] + dt*g/2*F[y^(n+g)] (TR stage)
-    y^(n+1) = 1/(g(2-g))*y^(n+g) - (1-g)**2/(g(2-g))*y^(n) + (1-g)/(2-g)*dt*F[y^(n+1)] (BDF2 stage)
-    for an off-centring parameter g (gamma).
+    The TR-BDF2 time stepping method for operator F is written as:                                  \n
+    y^(n+g) = y^n + dt*g/2*F[y^n] + dt*g/2*F[y^(n+g)] (TR stage)                                    \n
+    y^(n+1) = 1/(g(2-g))*y^(n+g) - (1-g)**2/(g(2-g))*y^(n) + (1-g)/(2-g)*dt*F[y^(n+1)] (BDF2 stage) \n
+    for an off-centring parameter g (gamma).                                                        \n
     """
     def __init__(self, domain, gamma, field_name=None,
                  solver_parameters=None, options=None):
@@ -1047,8 +1303,8 @@ class Leapfrog(MultilevelTimeDiscretisation):
     """
     Implements the multistep Leapfrog timestepping method.
 
-    The Leapfrog timestepping method for operator F is written as
-    y^(n+1) = y^(n-1)  + 2*dt*F[y^n]
+    The Leapfrog timestepping method for operator F is written as:            \n
+    y^(n+1) = y^(n-1)  + 2*dt*F[y^n]                                          \n
     """
     @property
     def nlevels(self):
@@ -1122,10 +1378,11 @@ def apply(self, x_out, *x_in):
 
 class AdamsBashforth(MultilevelTimeDiscretisation):
     """
-    Implements the explicit multistep Adams-Bashforth timestepping method of general order up to 5
+    Implements the explicit multistep Adams-Bashforth timestepping
+    method of general order up to 5.
 
-    The general AB timestepping method for operator F is written as
-    y^(n+1) = y^n + dt*(b_0*F[y^(n)] + b_1*F[y^(n-1)] + b_2*F[y^(n-2)] + b_3*F[y^(n-3)] + b_4*F[y^(n-4)])
+    The general AB timestepping method for operator F is written as:                                      \n
+    y^(n+1) = y^n + dt*(b_0*F[y^(n)] + b_1*F[y^(n-1)] + b_2*F[y^(n-2)] + b_3*F[y^(n-3)] + b_4*F[y^(n-4)]) \n
     """
     def __init__(self, domain, order, field_name=None,
                  solver_parameters=None, options=None):
@@ -1252,10 +1509,11 @@ def apply(self, x_out, *x_in):
 
 class AdamsMoulton(MultilevelTimeDiscretisation):
     """
-    Implements the implicit multistep Adams-Moulton timestepping method of general order up to 5
+    Implements the implicit multistep Adams-Moulton
+    timestepping method of general order up to 5
 
-    The general AM timestepping method for operator F is written as
-    y^(n+1) = y^n + dt*(b_0*F[y^(n+1)] + b_1*F[y^(n)] + b_2*F[y^(n-1)] + b_3*F[y^(n-2)])
+    The general AM timestepping method for operator F is written as                      \n
+    y^(n+1) = y^n + dt*(b_0*F[y^(n+1)] + b_1*F[y^(n)] + b_2*F[y^(n-1)] + b_3*F[y^(n-2)]) \n
     """
     def __init__(self, domain, order, field_name=None,
                  solver_parameters=None, options=None):
@@ -1397,3 +1655,70 @@ def apply(self, x_out, *x_in):
             self.x[n].assign(x_in[n])
         solver.solve()
         x_out.assign(self.x_out)
+
+
+class ImplicitMidpoint(ImplicitMultistage):
+    u"""
+    Implements the Implicit Midpoint method as a 1-stage Runge Kutta method.
+
+    The method, for solving
+    ∂y/∂t = F(y), can be written as:                                          \n
+
+    k0 = F[y^n + 0.5*dt*k0]                                                   \n
+    y^(n+1) = y^n + dt*k0                                                     \n
+    """
+    def __init__(self, domain, field_name=None, solver_parameters=None,
+                 limiter=None, options=None):
+        """
+        Args:
+            domain (:class:`Domain`): the model's domain object, containing the
+                mesh and the compatible function spaces.
+            field_name (str, optional): name of the field to be evolved.
+                Defaults to None.
+            solver_parameters (dict, optional): dictionary of parameters to
+                pass to the underlying solver. Defaults to None.
+            limiter (:class:`Limiter` object, optional): a limiter to apply to
+                the evolving field to enforce monotonicity. Defaults to None.
+            options (:class:`AdvectionOptions`, optional): an object containing
+                options to either be passed to the spatial discretisation, or
+                to control the "wrapper" methods, such as Embedded DG or a
+                recovery method. Defaults to None.
+        """
+        butcher_matrix = np.array([[0.5], [1.]])
+        super().__init__(domain, butcher_matrix, field_name,
+                         solver_parameters=solver_parameters,
+                         limiter=limiter, options=options)
+
+
+class QinZhang(ImplicitMultistage):
+    u"""
+    Implements Qin and Zhang's two-stage, 2nd order, implicit Runge–Kutta method.
+
+    The method, for solving
+    ∂y/∂t = F(y), can be written as:                                          \n
+
+    k0 = F[y^n + 0.25*dt*k0]                                                  \n
+    k1 = F[y^n + 0.5*dt*k0 + 0.25*dt*k1]                                      \n
+    y^(n+1) = y^n + 0.5*dt*(k0 + k1)                                          \n
+    """
+    def __init__(self, domain, field_name=None, solver_parameters=None,
+                 limiter=None, options=None):
+        """
+        Args:
+            domain (:class:`Domain`): the model's domain object, containing the
+                mesh and the compatible function spaces.
+            field_name (str, optional): name of the field to be evolved.
+                Defaults to None.
+            solver_parameters (dict, optional): dictionary of parameters to
+                pass to the underlying solver. Defaults to None.
+            limiter (:class:`Limiter` object, optional): a limiter to apply to
+                the evolving field to enforce monotonicity. Defaults to None.
+            options (:class:`AdvectionOptions`, optional): an object containing
+                options to either be passed to the spatial discretisation, or
+                to control the "wrapper" methods, such as Embedded DG or a
+                recovery method. Defaults to None.
+        """
+        butcher_matrix = np.array([[0.25, 0], [0.5, 0.25], [0.5, 0.5]])
+        super().__init__(domain, butcher_matrix, field_name,
+                         solver_parameters=solver_parameters,
+                         limiter=limiter, options=options)
diff --git a/integration-tests/model/test_time_discretisation.py b/integration-tests/model/test_time_discretisation.py
index ed882bcd9..6f114fe90 100644
--- a/integration-tests/model/test_time_discretisation.py
+++ b/integration-tests/model/test_time_discretisation.py
@@ -8,7 +8,7 @@ def run(timestepper, tmax, f_end):
     return norm(timestepper.fields("f") - f_end) / norm(f_end)
 
 
-@pytest.mark.parametrize("scheme", ["ssprk", "implicit_midpoint",
+@pytest.mark.parametrize("scheme", ["ssprk", "TrapeziumRule", "ImplicitMidpoint", "QinZhang",
                                     "RK4", "Heun", "BDF2", "TR_BDF2", "AdamsBashforth", "Leapfrog", "AdamsMoulton"])
 def test_time_discretisation(tmpdir, scheme, tracer_setup):
     if (scheme == "AdamsBashforth"):
@@ -27,8 +27,12 @@ def test_time_discretisation(tmpdir, scheme, tracer_setup):
 
     if scheme == "ssprk":
         transport_scheme = SSPRK3(domain)
-    elif scheme == "implicit_midpoint":
+    elif scheme == "TrapeziumRule":
         transport_scheme = TrapeziumRule(domain)
+    elif scheme == "ImplicitMidpoint":
+        transport_scheme = ImplicitMidpoint(domain)
+    elif scheme == "QinZhang":
+        transport_scheme = QinZhang(domain)
     elif scheme == "RK4":
         transport_scheme = RK4(domain)
     elif scheme == "Heun":