diff --git a/grudge/geometry/metrics.py b/grudge/geometry/metrics.py
new file mode 100644
index 0000000000000000000000000000000000000000..67bc93784072277b0e341af746d6fd4b048716a5
--- /dev/null
+++ b/grudge/geometry/metrics.py
@@ -0,0 +1,52 @@
+
+from grudge import sym
+from pymbolic.geometric_algebra import MultiVector
+from pytools.obj_array import make_obj_array
+
+
+def forward_metric_nth_derivative(dcoll, xyz_axis, ref_axes, vec, dd=None):
+    r"""
+    Pointwise metric derivatives representing repeated derivatives to *vec*
+
+    .. math::
+
+        \frac{\partial^n x_{\mathrm{xyz\_axis}} }{\partial r_{\mathrm{ref\_axes}}}
+
+    where *ref_axes* is a multi-index description.
+    """
+    if dd is None:
+        dd = sym.DD_VOLUME
+
+    if isinstance(ref_axes, int):
+        ref_axes = ((ref_axes, 1),)
+
+    if not isinstance(ref_axes, tuple):
+        raise ValueError("ref_axes must be a tuple")
+
+    if tuple(sorted(ref_axes)) != ref_axes:
+        raise ValueError("ref_axes must be sorted")
+
+    if len(dict(ref_axes)) != len(ref_axes):
+        raise ValueError("ref_axes must not contain an axis more than once")
+
+    from meshmode.discretization import num_reference_derivative
+
+    vol_discr = dcoll.discr_from_dd(sym.DD_VOLUME)
+    vec = num_reference_derivative(vol_discr, ref_axes, vec)
+
+    if dd.uses_quadrature():
+        vec = dcoll.connection_from_dds(sym.DD_VOLUME, dd)(vec)
+
+    return vec
+
+
+def forward_metric_derivative_vector(dcoll, rst_axis, vec, dd=None):
+    return make_obj_array([
+            forward_metric_nth_derivative(dcoll, i, rst_axis, vec[i], dd=dd)
+            for i in range(dcoll.ambient_dim)])
+
+
+def forward_metric_derivative_mv(dcoll, rst_axis, vec, dd=None):
+    return MultiVector(
+        forward_metric_derivative_vector(dcoll, rst_axis, vec, dd=dd)
+    )