diff --git a/grudge/models/euler.py b/grudge/models/euler.py
index e3cc0b6b551f39ee3d9a9cb2c11b8799ea58f97e..98acbc1ef5852792b0adcee3fb8d2b77d5ad98ff 100644
--- a/grudge/models/euler.py
+++ b/grudge/models/euler.py
@@ -318,18 +318,10 @@ class EulerOperator(HyperbolicOperator):
         qtag = self.qtag
         dq = DOFDesc("vol", qtag)
         df = DOFDesc("all_faces", qtag)
-        df_int = DOFDesc("int_faces", qtag)
 
         def interp_to_quad(u):
             return op.project(dcoll, "vol", dq, u)
 
-        def interp_to_quad_surf(u):
-            return TracePair(
-                df_int,
-                interior=op.project(dcoll, "int_faces", df_int, u.int),
-                exterior=op.project(dcoll, "int_faces", df_int, u.ext)
-            )
-
         # Compute volume fluxes
         volume_fluxes = op.weak_local_div(
             dcoll, dq,
@@ -341,7 +333,7 @@ class EulerOperator(HyperbolicOperator):
             sum(
                 euler_numerical_flux(
                     dcoll,
-                    interp_to_quad_surf(tpair),
+                    op.tracepair_with_discr_tag(dcoll, qtag, tpair),
                     gamma=gamma,
                     lf_stabilization=self.lf_stabilization
                 ) for tpair in op.interior_trace_pairs(dcoll, q)
diff --git a/grudge/op.py b/grudge/op.py
index 5ed6e5953791252ce9d19d04f16c095470557710..d2e7a54464e7ef201650e1356c02db26555e3c3d 100644
--- a/grudge/op.py
+++ b/grudge/op.py
@@ -22,6 +22,13 @@ Mass, inverse mass, and face mass operators
 .. autofunction:: mass
 .. autofunction:: inverse_mass
 .. autofunction:: face_mass
+
+Working around documentation tool akwardness
+--------------------------------------------
+
+.. class:: TracePair
+
+    See :class:`grudge.trace_pair.TracePair`.
 """
 
 __copyright__ = """
@@ -86,8 +93,11 @@ from grudge.reductions import (
 )
 
 from grudge.trace_pair import (
+    project_tracepair,
+    tracepair_with_discr_tag,
     interior_trace_pair,
     interior_trace_pairs,
+    local_interior_trace_pair,
     connected_ranks,
     cross_rank_trace_pairs,
     bdry_trace_pair,
@@ -112,8 +122,11 @@ __all__ = (
     "elementwise_min",
     "elementwise_integral",
 
+    "project_tracepair",
+    "tracepair_with_discr_tag",
     "interior_trace_pair",
     "interior_trace_pairs",
+    "local_interior_trace_pair",
     "connected_ranks",
     "cross_rank_trace_pairs",
     "bdry_trace_pair",
diff --git a/grudge/trace_pair.py b/grudge/trace_pair.py
index 872028d743db147620c69df96f530eb584ca41ba..f84dfdc58773c73a0dff01a8babc2e0aa538f7ed 100644
--- a/grudge/trace_pair.py
+++ b/grudge/trace_pair.py
@@ -2,11 +2,16 @@
 Trace Pairs
 ^^^^^^^^^^^
 
-Container class
----------------
+Container class and auxiliary functionality
+-------------------------------------------
 
 .. autoclass:: TracePair
 
+.. currentmodule:: grudge.op
+
+.. autoclass:: project_tracepair
+.. autoclass:: tracepair_with_discr_tag
+
 Boundary trace functions
 ------------------------
 
@@ -530,4 +535,27 @@ def cross_rank_trace_pairs(
 # }}}
 
 
+# {{{ project_tracepair
+
+def project_tracepair(
+        dcoll: DiscretizationCollection, new_dd: dof_desc.DOFDesc,
+        tpair: TracePair) -> TracePair:
+    r"""Return a new :class:`TracePair` :func:`~grudge.op.project`\ 'ed
+    onto *new_dd*.
+    """
+    return TracePair(
+        new_dd,
+        interior=project(dcoll, tpair.dd, new_dd, tpair.int),
+        exterior=project(dcoll, tpair.dd, new_dd, tpair.ext)
+    )
+
+
+def tracepair_with_discr_tag(dcoll, discr_tag, tpair: TracePair) -> TracePair:
+    r"""Return a new :class:`TracePair` :func:`~grudge.op.project`\ 'ed
+    onto a :class:`~grudge.dof_desc.DOFDesc` with discretization tag *discr_tag*.
+    """
+    return project_tracepair(dcoll, tpair.dd.with_discr_tag(discr_tag), tpair)
+
+# }}}
+
 # vim: foldmethod=marker