diff --git a/pytools/convergence.py b/pytools/convergence.py
index 6dd87b8b7e3faf5459ac6d469212e1d2b9e5fac2..cdedc83641e148f2dc23c6c5b83fe2296bdecc3c 100644
--- a/pytools/convergence.py
+++ b/pytools/convergence.py
@@ -80,4 +80,36 @@ class EOCRecorder(object):
 # }}}
 
 
+# {{{ p convergence verifier
+
+class PConvergenceVerifier(object):
+    def __init__(self):
+        self.orders = []
+        self.errors = []
+
+    def add_data_point(self, order, error):
+        self.orders.append(order)
+        self.errors.append(error)
+
+    def __str__(self):
+        from pytools import Table
+        tbl = Table()
+        tbl.add_row(("p", "error"))
+
+        for p, err in zip(self.orders, self.errors):
+            tbl.add_row((str(p), str(err)))
+
+        return str(tbl)
+
+    def __call__(self):
+        orders = np.array(self.orders, np.float64)
+        errors = np.abs(np.array(self.errors, np.float64))
+
+        rel_change = np.diff(1e-20 + np.log10(errors)) / np.diff(orders)
+
+        assert (rel_change < -0.2).all()
+
+# }}}
+
+
 # vim: foldmethod=marker
diff --git a/test/test_pytools.py b/test/test_pytools.py
index f8d0109c11fb44801a4401642687222a400be626..7f0e3f4cea414a7fa575aa47459d7d6c440ec859 100644
--- a/test/test_pytools.py
+++ b/test/test_pytools.py
@@ -45,3 +45,23 @@ def test_memoize_method_clear():
     assert sc.run_count == 1
 
     sc.f.clear_cache(sc)
+
+
+def test_p_convergence_verifier():
+    from pytools.convergence import PConvergenceVerifier
+
+    pconv_verifier = PConvergenceVerifier()
+    for order in [2, 3, 4, 5]:
+        pconv_verifier.add_data_point(order, 0.1**order)
+    pconv_verifier()
+
+    pconv_verifier = PConvergenceVerifier()
+    for order in [2, 3, 4, 5]:
+        pconv_verifier.add_data_point(order, 0.5**order)
+    pconv_verifier()
+
+    pconv_verifier = PConvergenceVerifier()
+    for order in [2, 3, 4, 5]:
+        pconv_verifier.add_data_point(order, 2)
+    with pytest.raises(AssertionError):
+        pconv_verifier()