diff --git a/doc/index.rst b/doc/index.rst
index d113cce65741f2d97f0f89b9248587ab4a2a4c5a..39ae5ec611b5fb40ac70a21dc554001782f329a9 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -10,6 +10,7 @@ Contents:
     dof_desc
     geometry
     operators
+    utils
     misc
     🚀 Github <https://github.com/inducer/grudge>
     💾 Download Releases <https://pypi.org/project/grudge>
diff --git a/doc/misc.rst b/doc/misc.rst
index 9f1f28b10899961adc6f92acfb50e3157080a99c..79eb5de443dc14e05b71b720bf24d23f55d49754 100644
--- a/doc/misc.rst
+++ b/doc/misc.rst
@@ -104,7 +104,7 @@ Licensing
 
 :mod:`grudge` is licensed to you under the MIT/X Consortium license:
 
-Copyright (c) 2014-16 Andreas Klöckner and Contributors.
+Copyright (c) 2014-21 Andreas Klöckner and Contributors.
 
 Permission is hereby granted, free of charge, to any person
 obtaining a copy of this software and associated documentation
diff --git a/doc/utils.rst b/doc/utils.rst
new file mode 100644
index 0000000000000000000000000000000000000000..d269517a0f785142bf466745bf3bd8172f365cc3
--- /dev/null
+++ b/doc/utils.rst
@@ -0,0 +1,7 @@
+Helper functions
+================
+
+Estimating stable time-steps
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. automodule:: grudge.dt_utils
diff --git a/grudge/dt_utils.py b/grudge/dt_utils.py
index 94c1f81e2d33062f85b32769653da85ef4a21195..19487c19a7620176052186109c050c18bdea2456 100644
--- a/grudge/dt_utils.py
+++ b/grudge/dt_utils.py
@@ -1,6 +1,8 @@
 """Helper functions for estimating stable time steps for RKDG methods.
 
 .. autofunction:: dt_non_geometric_factor
+.. autofunction:: symmetric_eigenvalues
+.. autofunction:: dt_geometric_factor
 """
 
 __copyright__ = """
@@ -30,12 +32,12 @@ THE SOFTWARE.
 
 import numpy as np
 
-from arraycontext import rec_map_array_container
+from arraycontext import ArrayContext, rec_map_array_container
 
 from functools import reduce
 
 from grudge.dof_desc import DD_VOLUME
-from grudge.geometry import forward_metric_derivative_mat
+from grudge.geometry import first_fundamental_form
 from grudge.discretization import DiscretizationCollection
 
 from pytools import memoize_on_first_arg
@@ -85,9 +87,17 @@ def dt_non_geometric_factor(dcoll: DiscretizationCollection, dd=None) -> float:
     return min(min_delta_rs)
 
 
-def symmetric_eigenvalues(actx, amat):
-    """*amat* must be complex-valued, or ``actx.np.sqrt`` must automatically
-    up-cast to complex data.
+def symmetric_eigenvalues(actx: ArrayContext, amat):
+    """Analytically computes the eigenvalues of a self-adjoint matrix, up
+    to matrices of size 3 by 3.
+
+    :arg amat: a square array-like object.
+    :returns: a :class:`list` of the eigenvalues of *amat*.
+
+    .. note::
+
+        *amat* must be complex-valued, or ``actx.np.sqrt`` must automatically
+        up-cast to complex data.
     """
 
     # https://gist.github.com/inducer/75ede170638c389c387e72e0ef1f0ef4
@@ -122,11 +132,11 @@ def symmetric_eigenvalues(actx, amat):
         x12 = d*f
         x13 = (-9*a - 9*d - 9*f)*(x0 + x11 + x12 - x3 - x5 - x7)
         x14 = -3*x0 - 3*x11 - 3*x12 + 3*x3 + 3*x5 + 3*x7 + x9**2
-        x15_0 = (-4*x14**3 + (-27*x1 + 2*x10 - x13 - 54*x2 + 27*x4 + 27*x6
-                    + 27*x8)**2)
+        x15_0 = (-4*x14**3
+                 + (-27*x1 + 2*x10 - x13 - 54*x2 + 27*x4 + 27*x6 + 27*x8)**2)
         x15_1 = sqrt(x15_0)
-        x15_2 =(-27*x1/2 + x10 - x13/2 - 27*x2 + 27*x4/2 + 27*x6/2 + 27*x8/2
-                + x15_1/2)
+        x15_2 = (-27*x1/2 + x10 - x13/2 - 27*x2 + 27*x4/2 + 27*x6/2 + 27*x8/2
+                 + x15_1/2)
         x15 = x15_2**(1/3)
         x16 = x15/3
         x17 = x14/(3*x15)
@@ -146,7 +156,10 @@ def symmetric_eigenvalues(actx, amat):
 
 @memoize_on_first_arg
 def dt_geometric_factor(dcoll: DiscretizationCollection, dd=None) -> float:
-    """
+    """Computes a geometric scaling factor, determined by taking the minimum
+    singular value of the coordinate transformation from reference to physical
+    cells.
+
     :arg dd: a :class:`~grudge.dof_desc.DOFDesc`, or a value convertible to one.
         Defaults to the base volume discretization if not provided.
     :returns: a :class:`float` denoting the geometric scaling factor.
@@ -155,8 +168,7 @@ def dt_geometric_factor(dcoll: DiscretizationCollection, dd=None) -> float:
         dd = DD_VOLUME
 
     actx = dcoll._setup_actx
-    fmd = forward_metric_derivative_mat(actx, dcoll, dd=dd)
-    ata = fmd @ fmd.T
+    ata = first_fundamental_form(actx, dcoll, dd=dd)
 
     complex_dtype = dcoll.discr_from_dd(dd).complex_dtype