diff --git a/examples/advection/surface.py b/examples/advection/surface.py
index df179a7615f1e643f8e25bdde656cfed4edfc3e7..d528f5f3eee29366d9c963c3dbd5fe410e1819fa 100644
--- a/examples/advection/surface.py
+++ b/examples/advection/surface.py
@@ -136,24 +136,28 @@ def main(ctx_factory, dim=2, order=4, product_tag=None, visualize=False):
     else:
         raise ValueError("unsupported dimension")
 
-    quad_tag_to_group_factory = {}
+    discr_tag_to_group_factory = {}
     if product_tag == "none":
         product_tag = None
+    else:
+        product_tag = dof_desc.DISCR_TAG_QUAD
 
     from meshmode.discretization.poly_element import \
             PolynomialWarpAndBlendGroupFactory, \
             QuadratureSimplexGroupFactory
 
-    quad_tag_to_group_factory[dof_desc.QTAG_NONE] = \
-            PolynomialWarpAndBlendGroupFactory(order)
+    discr_tag_to_group_factory[dof_desc.DISCR_TAG_BASE] = \
+        PolynomialWarpAndBlendGroupFactory(order)
 
     if product_tag:
-        quad_tag_to_group_factory[product_tag] = \
-                QuadratureSimplexGroupFactory(order=4*order)
+        discr_tag_to_group_factory[product_tag] = \
+            QuadratureSimplexGroupFactory(order=4*order)
 
     from grudge import DiscretizationCollection
-    discr = DiscretizationCollection(actx, mesh,
-            quad_tag_to_group_factory=quad_tag_to_group_factory)
+    discr = DiscretizationCollection(
+        actx, mesh,
+        discr_tag_to_group_factory=discr_tag_to_group_factory
+    )
 
     volume_discr = discr.discr_from_dd(dof_desc.DD_VOLUME)
     logger.info("ndofs:     %d", volume_discr.ndofs)
diff --git a/examples/advection/var-velocity.py b/examples/advection/var-velocity.py
index b99e21ad5056ed466524f9c9eafeb4f5ab0c756e..5843b99318cb65de77e53e9a78669a2c001dee2c 100644
--- a/examples/advection/var-velocity.py
+++ b/examples/advection/var-velocity.py
@@ -141,15 +141,17 @@ def main(ctx_factory, dim=2, order=4, product_tag=None, visualize=False):
             QuadratureSimplexGroupFactory
 
     if product_tag:
-        quad_tag_to_group_factory = {
-                product_tag: QuadratureSimplexGroupFactory(order=4*order)
-                }
+        discr_tag_to_group_factory = {
+            product_tag: QuadratureSimplexGroupFactory(order=4*order)
+        }
     else:
-        quad_tag_to_group_factory = {}
+        discr_tag_to_group_factory = {}
 
     from grudge import DiscretizationCollection
-    discr = DiscretizationCollection(actx, mesh, order=order,
-            quad_tag_to_group_factory=quad_tag_to_group_factory)
+    discr = DiscretizationCollection(
+        actx, mesh, order=order,
+        discr_tag_to_group_factory=discr_tag_to_group_factory
+    )
 
     # }}}
 
diff --git a/examples/dagrt-fusion.py b/examples/dagrt-fusion.py
index cb51964f6442192ed30ea953fe1dc1be2720bdab..bd399c3a38542280f49cb66b00d6979541085a31 100755
--- a/examples/dagrt-fusion.py
+++ b/examples/dagrt-fusion.py
@@ -523,7 +523,7 @@ def test_stepper_equivalence(ctx_factory, order=4):
 
     fused_steps = fused_stepper.run(ic, t_start, dt, t_end)
 
-    for t_ref, (u_ref, v_ref) in stepper.run(ic, t_start, dt, t_end):
+    for t_ref, (u_ref, _v_ref) in stepper.run(ic, t_start, dt, t_end):
         step += 1
         logger.debug("step %d/%d", step, nsteps)
         t, (u, v) = next(fused_steps)
@@ -620,7 +620,7 @@ class ExecutionMapperWithMemOpCounting(ExecutionMapperWrapper):
                         expr.op, expr.field, profile_data)
 
             else:
-                assert False, ("unknown operator: %s" % expr.op)
+                raise AssertionError("unknown operator: %s" % expr.op)
 
         else:
             logger.debug("assignment not profiled: %s", expr)
@@ -811,7 +811,7 @@ def test_stepper_mem_ops(ctx_factory, use_fusion):
     step = 0
 
     nsteps = int(np.ceil((t_end + 1e-9) / dt))
-    for (_, _, profile_data) in stepper.run(
+    for (_, _, profile_data) in stepper.run(  # noqa: B007
             ic, t_start, dt, t_end, return_profile_data=True):
         step += 1
         logger.info("step %d/%d", step, nsteps)
@@ -984,7 +984,7 @@ def test_stepper_timing(ctx_factory, use_fusion):
     import time
     t = time.time()
     nsteps = int(np.ceil((t_end + 1e-9) / dt))
-    for (_, _, profile_data) in stepper.run(
+    for (_, _, profile_data) in stepper.run(  # noqa: B007
             ic, t_start, dt, t_end, return_profile_data=True):
         step += 1
         tn = time.time()
@@ -1146,7 +1146,7 @@ def mem_ops_results(actx, dims):
 
     result = {}
 
-    for (_, _, profile_data) in stepper.run(
+    for (_, _, profile_data) in stepper.run(  # noqa: B007
             ic, t_start, dt, t_end, return_profile_data=True):
         pass
 
@@ -1164,7 +1164,7 @@ def mem_ops_results(actx, dims):
             result["nonfused_bytes_read_by_scalar_assignments"] \
             + result["nonfused_bytes_written_by_scalar_assignments"]
 
-    for (_, _, profile_data) in fused_stepper.run(
+    for (_, _, profile_data) in fused_stepper.run(  # noqa: B007
             ic, t_start, dt, t_end, return_profile_data=True):
         pass
 
diff --git a/examples/wave/wave-op-var-velocity.py b/examples/wave/wave-op-var-velocity.py
index 799ee2ed371c3172f4a16a65dbe6bc22a1277989..5ac1233d34e59f6fc7073307c4adb2ff3bb2c494 100644
--- a/examples/wave/wave-op-var-velocity.py
+++ b/examples/wave/wave-op-var-velocity.py
@@ -33,7 +33,7 @@ from meshmode.dof_array import thaw
 from meshmode.mesh import BTAG_ALL, BTAG_NONE  # noqa
 
 from grudge.discretization import DiscretizationCollection
-from grudge.dof_desc import QTAG_NONE, DOFDesc
+from grudge.dof_desc import DISCR_TAG_BASE, DISCR_TAG_QUAD, DOFDesc
 import grudge.op as op
 from grudge.shortcuts import make_visualizer
 from grudge.symbolic.primitives import TracePair
@@ -43,7 +43,7 @@ from grudge.symbolic.primitives import TracePair
 
 def wave_flux(dcoll, c, w_tpair):
     dd = w_tpair.dd
-    dd_quad = dd.with_qtag("vel_prod")
+    dd_quad = dd.with_discr_tag(DISCR_TAG_QUAD)
 
     u = w_tpair[0]
     v = w_tpair[1:]
@@ -78,13 +78,13 @@ def wave_operator(dcoll, c, w):
     dir_bval = flat_obj_array(dir_u, dir_v)
     dir_bc = flat_obj_array(-dir_u, dir_v)
 
-    dd_quad = DOFDesc("vol", "vel_prod")
+    dd_quad = DOFDesc("vol", DISCR_TAG_QUAD)
     c_quad = op.project(dcoll, "vol", dd_quad, c)
     w_quad = op.project(dcoll, "vol", dd_quad, w)
     u_quad = w_quad[0]
     v_quad = w_quad[1:]
 
-    dd_allfaces_quad = DOFDesc("all_faces", "vel_prod")
+    dd_allfaces_quad = DOFDesc("all_faces", DISCR_TAG_QUAD)
 
     return (
             op.inverse_mass(dcoll,
@@ -161,11 +161,13 @@ def main():
     from meshmode.discretization.poly_element import \
             QuadratureSimplexGroupFactory, \
             PolynomialWarpAndBlendGroupFactory
-    dcoll = DiscretizationCollection(actx, mesh,
-            quad_tag_to_group_factory={
-                QTAG_NONE: PolynomialWarpAndBlendGroupFactory(order),
-                "vel_prod": QuadratureSimplexGroupFactory(3*order),
-                })
+    dcoll = DiscretizationCollection(
+        actx, mesh,
+        discr_tag_to_group_factory={
+            DISCR_TAG_BASE: PolynomialWarpAndBlendGroupFactory(order),
+            DISCR_TAG_QUAD: QuadratureSimplexGroupFactory(3*order),
+        }
+    )
 
     # bounded above by 1
     c = 0.2 + 0.8*bump(actx, dcoll, center=np.zeros(3), width=0.5)
diff --git a/grudge/discretization.py b/grudge/discretization.py
index a46981c1b1f8e813f3159af5215d99f75216dd7e..2b86c994ec8fa58728f1bb0ef4df255f62549e20 100644
--- a/grudge/discretization.py
+++ b/grudge/discretization.py
@@ -21,14 +21,18 @@ THE SOFTWARE.
 """
 
 from pytools import memoize_method
-from grudge.dof_desc import \
-    QTAG_NONE, QTAG_MODAL, DTAG_BOUNDARY, DOFDesc, as_dofdesc
+from grudge.dof_desc import (
+    DISCR_TAG_BASE, DISCR_TAG_MODAL,
+    DTAG_BOUNDARY, DOFDesc, as_dofdesc
+)
 import numpy as np  # noqa: F401
 from meshmode.array_context import ArrayContext
 from meshmode.discretization.connection import \
     FACE_RESTR_INTERIOR, FACE_RESTR_ALL, make_face_restriction
 from meshmode.mesh import BTAG_PARTITION
 
+from warnings import warn
+
 
 __doc__ = """
 .. autoclass:: DiscretizationCollection
@@ -37,6 +41,8 @@ __doc__ = """
 
 class DiscretizationCollection:
     """
+    .. automethod :: __init__
+
     .. automethod :: discr_from_dd
     .. automethod :: connection_from_dds
 
@@ -49,52 +55,76 @@ class DiscretizationCollection:
     """
 
     def __init__(self, array_context, mesh, order=None,
-            quad_tag_to_group_factory=None, mpi_communicator=None):
+            discr_tag_to_group_factory=None, mpi_communicator=None,
+            # FIXME: `quad_tag_to_group_factory` is deprecated
+            quad_tag_to_group_factory=None):
         """
-        :param quad_tag_to_group_factory: A mapping from quadrature tags (typically
-            strings--but may be any hashable/comparable object) to a
+        :param discr_tag_to_group_factory: A mapping from discretization tags
+            (typically one of: :class:`grudge.dof_desc.DISCR_TAG_BASE`,
+            :class:`grudge.dof_desc.DISCR_TAG_MODAL`, or
+            :class:`grudge.dof_desc.DISCR_TAG_QUAD`) to a
             :class:`~meshmode.discretization.poly_element.ElementGroupFactory`
-            indicating with which quadrature discretization the operations are
+            indicating with which type of discretization the operations are
             to be carried out, or *None* to indicate that operations with this
-            quadrature tag should be carried out with the standard volume
+            discretization tag should be carried out with the standard volume
             discretization.
         """
 
+        if (quad_tag_to_group_factory is not None
+                and discr_tag_to_group_factory is not None):
+            raise ValueError(
+                "Both `quad_tag_to_group_factory` and `discr_tag_to_group_factory` "
+                "are specified. Use `discr_tag_to_group_factory` instead."
+            )
+
+        # FIXME: `quad_tag_to_group_factory` is deprecated
+        if (quad_tag_to_group_factory is not None
+                and discr_tag_to_group_factory is None):
+            warn("`quad_tag_to_group_factory` is a deprecated kwarg and will "
+                 "be dropped in version 2022.x. Use `discr_tag_to_group_factory` "
+                 "instead.",
+                 DeprecationWarning, stacklevel=2)
+            discr_tag_to_group_factory = quad_tag_to_group_factory
+
         self._setup_actx = array_context
 
         from meshmode.discretization.poly_element import \
                 PolynomialWarpAndBlendGroupFactory
 
-        if quad_tag_to_group_factory is None:
+        if discr_tag_to_group_factory is None:
             if order is None:
-                raise TypeError("one of 'order' and "
-                        "'quad_tag_to_group_factory' must be given")
+                raise TypeError(
+                    "one of 'order' and 'discr_tag_to_group_factory' must be given"
+                )
 
-            quad_tag_to_group_factory = {
-                    QTAG_NONE: PolynomialWarpAndBlendGroupFactory(order=order)}
+            discr_tag_to_group_factory = {
+                    DISCR_TAG_BASE: PolynomialWarpAndBlendGroupFactory(order=order)}
         else:
             if order is not None:
-                quad_tag_to_group_factory = quad_tag_to_group_factory.copy()
-                if QTAG_NONE in quad_tag_to_group_factory:
-                    raise ValueError("if 'order' is given, "
-                            "'quad_tag_to_group_factory' must not have a "
-                            "key of QTAG_NONE")
-
-                quad_tag_to_group_factory[QTAG_NONE] = \
+                discr_tag_to_group_factory = discr_tag_to_group_factory.copy()
+                if DISCR_TAG_BASE in discr_tag_to_group_factory:
+                    raise ValueError(
+                        "if 'order' is given, 'discr_tag_to_group_factory' must "
+                        "not have a key of DISCR_TAG_BASE"
+                    )
+
+                discr_tag_to_group_factory[DISCR_TAG_BASE] = \
                         PolynomialWarpAndBlendGroupFactory(order=order)
 
         # Modal discr should always comes from the base discretization
-        quad_tag_to_group_factory[QTAG_MODAL] = \
+        discr_tag_to_group_factory[DISCR_TAG_MODAL] = \
             _generate_modal_group_factory(
-                quad_tag_to_group_factory[QTAG_NONE]
+                discr_tag_to_group_factory[DISCR_TAG_BASE]
             )
 
-        self.quad_tag_to_group_factory = quad_tag_to_group_factory
+        self.discr_tag_to_group_factory = discr_tag_to_group_factory
 
         from meshmode.discretization import Discretization
 
-        self._volume_discr = Discretization(array_context, mesh,
-                self.group_factory_for_quadrature_tag(QTAG_NONE))
+        self._volume_discr = Discretization(
+            array_context, mesh,
+            self.group_factory_for_discretization_tag(DISCR_TAG_BASE)
+        )
 
         # {{{ management of discretization-scoped common subexpressions
 
@@ -112,6 +142,16 @@ class DiscretizationCollection:
 
         self.mpi_communicator = mpi_communicator
 
+    @property
+    def quad_tag_to_group_factory(self):
+        warn("`DiscretizationCollection.quad_tag_to_group_factory` "
+             "is deprecated and will go away in 2022. Use "
+             "`DiscretizationCollection.discr_tag_to_group_factory` "
+             "instead.",
+             DeprecationWarning, stacklevel=2)
+
+        return self.discr_tag_to_group_factory
+
     def get_management_rank_index(self):
         return 0
 
@@ -123,7 +163,7 @@ class DiscretizationCollection:
                     == self.get_management_rank_index()
 
     def _set_up_distributed_communication(self, mpi_communicator, array_context):
-        from_dd = DOFDesc("vol", QTAG_NONE)
+        from_dd = DOFDesc("vol", DISCR_TAG_BASE)
 
         boundary_connections = {}
 
@@ -135,13 +175,14 @@ class DiscretizationCollection:
                 raise RuntimeError("must supply an MPI communicator when using a "
                     "distributed mesh")
 
-            grp_factory = self.group_factory_for_quadrature_tag(QTAG_NONE)
+            grp_factory = \
+                self.group_factory_for_discretization_tag(DISCR_TAG_BASE)
 
             local_boundary_connections = {}
             for i_remote_part in connected_parts:
                 local_boundary_connections[i_remote_part] = self.connection_from_dds(
                         from_dd, DOFDesc(BTAG_PARTITION(i_remote_part),
-                        QTAG_NONE))
+                        DISCR_TAG_BASE))
 
             from meshmode.distributed import MPIBoundaryCommSetupHelper
             with MPIBoundaryCommSetupHelper(mpi_communicator, array_context,
@@ -156,9 +197,12 @@ class DiscretizationCollection:
         return boundary_connections
 
     def get_distributed_boundary_swap_connection(self, dd):
-        if dd.quadrature_tag != QTAG_NONE:
+        if dd.discretization_tag is not DISCR_TAG_BASE:
             # FIXME
-            raise NotImplementedError("Distributed communication with quadrature")
+            raise NotImplementedError(
+                "Distributed communication with discretization tag "
+                f"{dd.discretization_tag} is not implemented."
+            )
 
         assert isinstance(dd.domain_tag, DTAG_BOUNDARY)
         assert isinstance(dd.domain_tag.tag, BTAG_PARTITION)
@@ -169,26 +213,27 @@ class DiscretizationCollection:
     def discr_from_dd(self, dd):
         dd = as_dofdesc(dd)
 
-        qtag = dd.quadrature_tag
+        discr_tag = dd.discretization_tag
 
-        if qtag is QTAG_MODAL:
+        if discr_tag is DISCR_TAG_MODAL:
             return self._modal_discr(dd.domain_tag)
 
         if dd.is_volume():
-            if qtag is not QTAG_NONE:
-                return self._quad_volume_discr(qtag)
+            if discr_tag is not DISCR_TAG_BASE:
+                return self._discr_tag_volume_discr(discr_tag)
             return self._volume_discr
 
-        if qtag is not QTAG_NONE:
+        if discr_tag is not DISCR_TAG_BASE:
             no_quad_discr = self.discr_from_dd(DOFDesc(dd.domain_tag))
 
             from meshmode.discretization import Discretization
             return Discretization(
-                    self._setup_actx,
-                    no_quad_discr.mesh,
-                    self.group_factory_for_quadrature_tag(qtag))
+                self._setup_actx,
+                no_quad_discr.mesh,
+                self.group_factory_for_discretization_tag(discr_tag)
+            )
 
-        assert qtag is QTAG_NONE
+        assert discr_tag is DISCR_TAG_BASE
 
         if dd.domain_tag is FACE_RESTR_ALL:
             return self._all_faces_volume_connection().to_discr
@@ -204,24 +249,26 @@ class DiscretizationCollection:
         from_dd = as_dofdesc(from_dd)
         to_dd = as_dofdesc(to_dd)
 
-        to_qtag = to_dd.quadrature_tag
-        from_qtag = from_dd.quadrature_tag
+        to_discr_tag = to_dd.discretization_tag
+        from_discr_tag = from_dd.discretization_tag
 
         # {{{ mapping between modal and nodal representations
 
-        if (from_qtag is QTAG_MODAL and to_qtag is not QTAG_MODAL):
+        if (from_discr_tag is DISCR_TAG_MODAL
+                and to_discr_tag is not DISCR_TAG_MODAL):
             return self._modal_to_nodal_connection(to_dd)
 
-        if (to_qtag is QTAG_MODAL and from_qtag is not QTAG_MODAL):
+        if (to_discr_tag is DISCR_TAG_MODAL
+                and from_discr_tag is not DISCR_TAG_MODAL):
             return self._nodal_to_modal_connection(from_dd)
 
         # }}}
 
-        assert (to_qtag is not QTAG_MODAL and from_qtag is not QTAG_MODAL)
+        assert (to_discr_tag is not DISCR_TAG_MODAL
+                    and from_discr_tag is not DISCR_TAG_MODAL)
 
-        if (
-                not from_dd.is_volume()
-                and from_qtag == to_qtag
+        if (not from_dd.is_volume()
+                and from_discr_tag == to_discr_tag
                 and to_dd.domain_tag is FACE_RESTR_ALL):
             faces_conn = self.connection_from_dds(
                     DOFDesc("vol"),
@@ -235,11 +282,11 @@ class DiscretizationCollection:
                     faces_conn, self.discr_from_dd(to_dd),
                     self.discr_from_dd(from_dd))
 
-        # {{{ simplify domain + qtag change into chained
+        # {{{ simplify domain + discr_tag change into chained
 
         if (from_dd.domain_tag != to_dd.domain_tag
-                and from_qtag is QTAG_NONE
-                and to_qtag is not QTAG_NONE):
+                and from_discr_tag is DISCR_TAG_BASE
+                and to_discr_tag is not DISCR_TAG_BASE):
 
             from meshmode.discretization.connection import \
                     ChainedDiscretizationConnection
@@ -261,10 +308,10 @@ class DiscretizationCollection:
 
         # {{{ generic to-quad
 
-        # QTAG_MODAL is handled above
+        # DISCR_TAG_MODAL is handled above
         if (from_dd.domain_tag == to_dd.domain_tag
-                and from_qtag is QTAG_NONE
-                and to_qtag is not QTAG_NONE):
+                and from_discr_tag is DISCR_TAG_BASE
+                and to_discr_tag is not DISCR_TAG_BASE):
 
             from meshmode.discretization.connection.same_mesh import \
                     make_same_mesh_connection
@@ -276,11 +323,11 @@ class DiscretizationCollection:
 
         # }}}
 
-        if from_qtag is not QTAG_NONE:
+        if from_discr_tag is not DISCR_TAG_BASE:
             raise ValueError("cannot interpolate *from* a "
                     "(non-interpolatory) quadrature grid")
 
-        assert to_qtag is QTAG_NONE
+        assert to_discr_tag is DISCR_TAG_BASE
 
         if from_dd.is_volume():
             if to_dd.domain_tag is FACE_RESTR_ALL:
@@ -288,12 +335,12 @@ class DiscretizationCollection:
             if to_dd.domain_tag is FACE_RESTR_INTERIOR:
                 return self._interior_faces_connection()
             elif to_dd.is_boundary_or_partition_interface():
-                assert from_qtag is QTAG_NONE
+                assert from_discr_tag is DISCR_TAG_BASE
                 return self._boundary_connection(to_dd.domain_tag.tag)
             elif to_dd.is_volume():
                 from meshmode.discretization.connection import \
                         make_same_mesh_connection
-                to_discr = self._quad_volume_discr(to_qtag)
+                to_discr = self._discr_tag_volume_discr(to_discr_tag)
                 from_discr = self._volume_discr
                 return make_same_mesh_connection(self._setup_actx, to_discr,
                             from_discr)
@@ -304,22 +351,32 @@ class DiscretizationCollection:
         else:
             raise ValueError("cannot interpolate from: " + str(from_dd))
 
-    def group_factory_for_quadrature_tag(self, quadrature_tag):
+    def group_factory_for_quadrature_tag(self, discretization_tag):
+        warn("`DiscretizationCollection.group_factory_for_quadrature_tag` "
+             "is deprecated and will go away in 2022. Use "
+             "`DiscretizationCollection.group_factory_for_discretization_tag` "
+             "instead.",
+             DeprecationWarning, stacklevel=2)
+
+        return self.group_factory_for_discretization_tag(discretization_tag)
+
+    def group_factory_for_discretization_tag(self, discretization_tag):
         """
         OK to override in user code to control mode/node choice.
         """
+        if discretization_tag is None:
+            discretization_tag = DISCR_TAG_BASE
 
-        if quadrature_tag is None:
-            quadrature_tag = QTAG_NONE
-
-        return self.quad_tag_to_group_factory[quadrature_tag]
+        return self.discr_tag_to_group_factory[discretization_tag]
 
     @memoize_method
-    def _quad_volume_discr(self, quadrature_tag):
+    def _discr_tag_volume_discr(self, discretization_tag):
         from meshmode.discretization import Discretization
 
-        return Discretization(self._setup_actx, self._volume_discr.mesh,
-                self.group_factory_for_quadrature_tag(quadrature_tag))
+        return Discretization(
+            self._setup_actx, self._volume_discr.mesh,
+            self.group_factory_for_discretization_tag(discretization_tag)
+        )
 
     # {{{ modal to nodal connections
 
@@ -327,10 +384,10 @@ class DiscretizationCollection:
     def _modal_discr(self, domain_tag):
         from meshmode.discretization import Discretization
 
-        discr_base = self.discr_from_dd(DOFDesc(domain_tag, QTAG_NONE))
+        discr_base = self.discr_from_dd(DOFDesc(domain_tag, DISCR_TAG_BASE))
         return Discretization(
             self._setup_actx, discr_base.mesh,
-            self.group_factory_for_quadrature_tag(QTAG_MODAL)
+            self.group_factory_for_discretization_tag(DISCR_TAG_MODAL)
         )
 
     @memoize_method
@@ -370,10 +427,11 @@ class DiscretizationCollection:
     @memoize_method
     def _boundary_connection(self, boundary_tag):
         return make_face_restriction(
-                self._setup_actx,
-                self._volume_discr,
-                self.group_factory_for_quadrature_tag(QTAG_NONE),
-                boundary_tag=boundary_tag)
+            self._setup_actx,
+            self._volume_discr,
+            self.group_factory_for_discretization_tag(DISCR_TAG_BASE),
+            boundary_tag=boundary_tag
+        )
 
     # }}}
 
@@ -382,15 +440,16 @@ class DiscretizationCollection:
     @memoize_method
     def _interior_faces_connection(self):
         return make_face_restriction(
-                self._setup_actx,
-                self._volume_discr,
-                self.group_factory_for_quadrature_tag(QTAG_NONE),
-                FACE_RESTR_INTERIOR,
-
-                # FIXME: This will need to change as soon as we support
-                # pyramids or other elements with non-identical face
-                # types.
-                per_face_groups=False)
+            self._setup_actx,
+            self._volume_discr,
+            self.group_factory_for_discretization_tag(DISCR_TAG_BASE),
+            FACE_RESTR_INTERIOR,
+
+            # FIXME: This will need to change as soon as we support
+            # pyramids or other elements with non-identical face
+            # types.
+            per_face_groups=False
+        )
 
     @memoize_method
     def opposite_face_connection(self):
@@ -408,15 +467,16 @@ class DiscretizationCollection:
     @memoize_method
     def _all_faces_volume_connection(self):
         return make_face_restriction(
-                self._setup_actx,
-                self._volume_discr,
-                self.group_factory_for_quadrature_tag(QTAG_NONE),
-                FACE_RESTR_ALL,
-
-                # FIXME: This will need to change as soon as we support
-                # pyramids or other elements with non-identical face
-                # types.
-                per_face_groups=False)
+            self._setup_actx,
+            self._volume_discr,
+            self.group_factory_for_discretization_tag(DISCR_TAG_BASE),
+            FACE_RESTR_ALL,
+
+            # FIXME: This will need to change as soon as we support
+            # pyramids or other elements with non-identical face
+            # types.
+            per_face_groups=False
+        )
 
     # }}}
 
@@ -451,7 +511,6 @@ class DiscretizationCollection:
 
     @property
     def order(self):
-        from warnings import warn
         warn("DiscretizationCollection.order is deprecated, "
                 "consider using the orders of element groups instead. "
                 "'order' will go away in 2021.",
@@ -463,7 +522,6 @@ class DiscretizationCollection:
 
 class DGDiscretizationWithBoundaries(DiscretizationCollection):
     def __init__(self, *args, **kwargs):
-        from warnings import warn
         warn("DGDiscretizationWithBoundaries is deprecated and will go away "
                 "in 2022. Use DiscretizationCollection instead.",
                 DeprecationWarning, stacklevel=2)
diff --git a/grudge/dof_desc.py b/grudge/dof_desc.py
index 0d83c37b91275a421c879d0315eba864f7ce5432..621f245e72ecc384e913bf81a78b20b5fc2f1345 100644
--- a/grudge/dof_desc.py
+++ b/grudge/dof_desc.py
@@ -29,14 +29,18 @@ from meshmode.discretization.connection import \
     FACE_RESTR_INTERIOR, FACE_RESTR_ALL
 from meshmode.mesh import \
     BTAG_PARTITION, BTAG_ALL, BTAG_REALLY_ALL, BTAG_NONE
+from warnings import warn
+import sys
 
 
 __doc__ = """
 .. autoclass:: DTAG_SCALAR
 .. autoclass:: DTAG_VOLUME_ALL
 .. autoclass:: DTAG_BOUNDARY
-.. autoclass:: QTAG_NONE
-.. autoclass:: QTAG_MODAL
+
+.. autoclass:: DISCR_TAG_BASE
+.. autoclass:: DISCR_TAG_QUAD
+.. autoclass:: DISCR_TAG_MODAL
 
 .. autoclass:: DOFDesc
 .. autofunction:: as_dofdesc
@@ -50,19 +54,19 @@ __doc__ = """
 # {{{ DOF description
 
 class DTAG_SCALAR:  # noqa: N801
-    """A tag denoting scalar values."""
+    """A domain tag denoting scalar values."""
 
 
 class DTAG_VOLUME_ALL:  # noqa: N801
     """
-    A tag denoting degrees of freedom defined
+    A domain tag denoting values defined
     in all cell volumes.
     """
 
 
 class DTAG_BOUNDARY:  # noqa: N801
-    """A tag describing the meaning of degrees of freedom
-    on element boundaries which are adjacent to elements
+    """A domain tag describing the values on element
+    boundaries which are adjacent to elements
     of another :class:`~meshmode.mesh.Mesh`.
 
     .. attribute:: tag
@@ -96,21 +100,45 @@ class DTAG_BOUNDARY:  # noqa: N801
         return "<{}({})>".format(type(self).__name__, repr(self.tag))
 
 
-class QTAG_NONE:  # noqa: N801
-    """A quadrature tag indicating the use of a
+class DISCR_TAG_BASE:  # noqa: N801
+    """A discretization tag indicating the use of a
     basic discretization grid. This tag is used
-    to distinguish the base discretization (`QTAG_NONE`)
-    from quadrature (e.g. overintegration) grids.
+    to distinguish the base discretization from quadrature
+    (e.g. overintegration) or modal (:class:`DISCR_TAG_MODAL`)
+    discretizations.
     """
 
 
-class QTAG_MODAL:  # noqa: N801
-    """A quadrature tag indicating the use of a
+class DISCR_TAG_QUAD:  # noqa: N801
+    """A discretization tag indicating the use of a
+    quadrature discretization grid. This tag is used
+    to distinguish the quadrature discretization
+    (e.g. overintegration) from modal (:class:`DISCR_TAG_MODAL`)
+    or base (:class:`DISCR_TAG_BASE`) discretizations.
+
+    For working with multiple quadrature grids, it is
+    recommended to create appropriate subclasses of
+    :class:`DISCR_TAG_QUAD` and define appropriate
+    :class:`DOFDesc` objects corresponding to each
+    subclass. For example:
+
+    .. code-block:: python
+
+        class CustomQuadTag(DISCR_TAG_QUAD):
+            "A custom quadrature discretization tag."
+
+        dd = DOFDesc(DTAG_VOLUME_ALL, CustomQuadTag)
+
+    """
+
+
+class DISCR_TAG_MODAL:  # noqa: N801
+    """A discretization tag indicating the use of a
     basic discretization grid with modal degrees of
     freedom. This tag is used to distinguish the
-    modal discretization (`QTAG_MODAL`) from
-    the base (nodal) discretization (e.g. `QTAG_NONE`)
-    or discretizations on quadrature grids.
+    modal discretization from the base (nodal)
+    discretization (e.g. :class:`DISCR_TAG_BASE`) or
+    discretizations on quadrature grids (:class:`DISCR_TAG_QUAD`).
     """
 
 
@@ -118,7 +146,7 @@ class DOFDesc:
     """Describes the meaning of degrees of freedom.
 
     .. attribute:: domain_tag
-    .. attribute:: quadrature_tag
+    .. attribute:: discretization_tag
 
     .. automethod:: __init__
 
@@ -130,7 +158,7 @@ class DOFDesc:
 
     .. automethod:: uses_quadrature
 
-    .. automethod:: with_qtag
+    .. automethod:: with_discr_tag
     .. automethod:: with_dtag
 
     .. automethod:: __eq__
@@ -138,7 +166,9 @@ class DOFDesc:
     .. automethod:: __hash__
     """
 
-    def __init__(self, domain_tag, quadrature_tag=None):
+    def __init__(self, domain_tag, discretization_tag=None,
+                 # FIXME: `quadrature_tag` is deprecated
+                 quadrature_tag=None):
         """
         :arg domain_tag: One of the following:
             :class:`DTAG_SCALAR` (or the string ``"scalar"``),
@@ -154,11 +184,11 @@ class DOFDesc:
             :class:`~meshmode.mesh.BTAG_PARTITION`,
             or *None* to indicate that the geometry is not yet known.
 
-        :arg quadrature_tag:
-            *None* to indicate that the quadrature grid is not known, or
-            :class:`QTAG_NONE` to indicate the use of the basic discretization
-            grid, or a string to indicate the use of the thus-tagged quadratue
-            grid.
+        :arg discretization_tag:
+            *None* or :class:`DISCR_TAG_BASE` to indicate the use of the basic
+            discretization grid, :class:`DISCR_TAG_MODAL` to indicate a
+            modal discretization, or :class:`DISCR_TAG_QUAD` to indicate
+            the use of a quadrature grid.
         """
 
         if domain_tag is None:
@@ -180,14 +210,41 @@ class DOFDesc:
         else:
             raise ValueError("domain tag not understood: %s" % domain_tag)
 
-        if domain_tag is DTAG_SCALAR and quadrature_tag is not None:
-            raise ValueError("cannot have nontrivial quadrature tag on scalar")
+        if (quadrature_tag is not None and discretization_tag is not None):
+            raise ValueError(
+                "Both `quadrature_tag` and `discretization_tag` are specified. "
+                "Use `discretization_tag` instead."
+            )
+
+        # FIXME: `quadrature_tag` is deprecated
+        if (quadrature_tag is not None and discretization_tag is None):
+            warn("`quadrature_tag` is a deprecated kwarg and will be dropped "
+                 "in version 2022.x. Use `discretization_tag` instead.",
+                 DeprecationWarning, stacklevel=2)
+            discretization_tag = quadrature_tag
 
-        if quadrature_tag is None:
-            quadrature_tag = QTAG_NONE
+        if domain_tag is DTAG_SCALAR and discretization_tag is not None:
+            raise ValueError("cannot have nontrivial discretization tag on scalar")
+
+        if discretization_tag is None:
+            discretization_tag = DISCR_TAG_BASE
+
+        # FIXME: String tags are deprecated
+        if isinstance(discretization_tag, str):
+            warn("Support for string values of `discretization_tag` will "
+                 "be dropped in version 2022.x. Use one of the `DISCR_TAG_` "
+                 "tags instead.",
+                 DeprecationWarning, stacklevel=2)
 
         self.domain_tag = domain_tag
-        self.quadrature_tag = quadrature_tag
+        self.discretization_tag = discretization_tag
+
+    @property
+    def quadrature_tag(self):
+        warn("`DOFDesc.quadrature_tag` is deprecated and will be dropped "
+             "in version 2022.x. Use `DOFDesc.discretization_tag` instead.",
+             DeprecationWarning, stacklevel=2)
+        return self.discretization_tag
 
     def is_scalar(self):
         return self.domain_tag is DTAG_SCALAR
@@ -208,29 +265,49 @@ class DOFDesc:
                     FACE_RESTR_INTERIOR])
 
     def uses_quadrature(self):
-        if self.quadrature_tag is None:
-            return False
-        if self.quadrature_tag is QTAG_NONE:
+        # FIXME: String tags are deprecated
+        # Check for string first, otherwise
+        # `issubclass` will raise an exception whenever
+        # its first argument is not a class.
+        # This can go away once support for strings is dropped
+        # completely.
+        if isinstance(self.discretization_tag, str):
+            # All strings are interpreted as quadrature-related tags
+            return True
+        elif issubclass(self.discretization_tag, DISCR_TAG_QUAD):
+            return True
+        elif issubclass(self.discretization_tag,
+                        (DISCR_TAG_BASE, DISCR_TAG_MODAL)):
             return False
+        else:
+            raise ValueError(
+                f"Unsure how to interpret tag: {self.discretization_tag}"
+            )
 
-        return True
+    def with_qtag(self, discr_tag):
+        warn("`DOFDesc.with_qtag` is deprecated and will be dropped "
+             "in version 2022.x. Use `DOFDesc.with_discr_tag` instead.",
+             DeprecationWarning, stacklevel=2)
+        return self.with_discr_tag(discr_tag)
 
-    def with_qtag(self, qtag):
-        return type(self)(domain_tag=self.domain_tag, quadrature_tag=qtag)
+    def with_discr_tag(self, discr_tag):
+        return type(self)(domain_tag=self.domain_tag,
+                          discretization_tag=discr_tag)
 
     def with_dtag(self, dtag):
-        return type(self)(domain_tag=dtag, quadrature_tag=self.quadrature_tag)
+        return type(self)(domain_tag=dtag,
+                          discretization_tag=self.discretization_tag)
 
     def __eq__(self, other):
         return (type(self) == type(other)
                 and self.domain_tag == other.domain_tag
-                and self.quadrature_tag == other.quadrature_tag)
+                and self.discretization_tag == other.discretization_tag)
 
     def __ne__(self, other):
         return not self.__eq__(other)
 
     def __hash__(self):
-        return hash((type(self), self.domain_tag, self.quadrature_tag))
+        return hash((type(self), self.domain_tag, self.discretization_tag))
 
     def __repr__(self):
         def fmt(s):
@@ -241,20 +318,44 @@ class DOFDesc:
 
         return "DOFDesc({}, {})".format(
                 fmt(self.domain_tag),
-                fmt(self.quadrature_tag))
+                fmt(self.discretization_tag))
 
 
 DD_SCALAR = DOFDesc(DTAG_SCALAR, None)
 
 DD_VOLUME = DOFDesc(DTAG_VOLUME_ALL, None)
 
-DD_VOLUME_MODAL = DOFDesc(DTAG_VOLUME_ALL, QTAG_MODAL)
+DD_VOLUME_MODAL = DOFDesc(DTAG_VOLUME_ALL, DISCR_TAG_MODAL)
 
 
 def as_dofdesc(dd):
     if isinstance(dd, DOFDesc):
         return dd
-    return DOFDesc(dd, quadrature_tag=None)
+    return DOFDesc(dd, discretization_tag=None)
+
+# }}}
+
+
+# {{{ Deprecated tags
+
+_deprecated_name_to_new_name = {"QTAG_NONE": "DISCR_TAG_BASE",
+                                "QTAG_MODAL": "DISCR_TAG_MODAL"}
+
+
+def __getattr__(name):
+    if name in _deprecated_name_to_new_name:
+        warn(f"'{name}' is deprecated and will be dropped "
+             f"in version 2022.x. Use '{_deprecated_name_to_new_name[name]}' "
+             "instead.",
+             DeprecationWarning, stacklevel=2)
+        return globals()[_deprecated_name_to_new_name[name]]
+
+    raise AttributeError(f"module {__name__} has no attribute {name}")
+
+
+if sys.version_info < (3, 7):
+    for name in _deprecated_name_to_new_name:
+        globals()[name] = globals()[_deprecated_name_to_new_name[name]]
 
 # }}}
 
diff --git a/grudge/execution.py b/grudge/execution.py
index a0624479210f84b72ccbec58b9372e6282025211..4b86fcb9e359c828a99f8f6d94a4714a999cd705 100644
--- a/grudge/execution.py
+++ b/grudge/execution.py
@@ -628,7 +628,7 @@ class BoundOperator:
             else:
                 pass
 
-        for key, val in context.items():
+        for val in context.values():
             look_for_array_contexts(val)
 
         if array_contexts:
@@ -719,7 +719,7 @@ def process_sym_operator(dcoll, sym_operator, post_bind_mapper=None, dumper=None
 
     dumper("before-qcheck", sym_operator)
     sym_operator = mappers.QuadratureCheckerAndRemover(
-            dcoll.quad_tag_to_group_factory)(sym_operator)
+            dcoll.discr_tag_to_group_factory)(sym_operator)
 
     # Work around https://github.com/numpy/numpy/issues/9438
     #
diff --git a/grudge/models/advection.py b/grudge/models/advection.py
index 55ad4fc70add026ee43c25b8a2e6993183985e1f..980425e5be60079d46bd2067936cdad83f164ec7 100644
--- a/grudge/models/advection.py
+++ b/grudge/models/advection.py
@@ -200,11 +200,12 @@ def v_dot_n_tpair(velocity, dd=None):
         dd = DOFDesc(FACE_RESTR_INTERIOR)
 
     ambient_dim = len(velocity)
-    normal = sym.normal(dd.with_qtag(None), ambient_dim, dim=ambient_dim - 2)
+    normal = sym.normal(dd.with_discr_tag(None),
+                        ambient_dim, dim=ambient_dim - 2)
 
     return sym.int_tpair(velocity.dot(normal),
-            qtag=dd.quadrature_tag,
-            from_dd=dd.with_qtag(None))
+            qtag=dd.discretization_tag,
+            from_dd=dd.with_discr_tag(None))
 
 
 def surface_advection_weak_flux(flux_type, u, velocity):
@@ -231,7 +232,7 @@ class SurfaceAdvectionOperator(AdvectionOperatorBase):
     def flux(self, u):
         from grudge.dof_desc import DD_VOLUME
 
-        surf_v = sym.project(DD_VOLUME, u.dd.with_qtag(None))(self.v)
+        surf_v = sym.project(DD_VOLUME, u.dd.with_discr_tag(None))(self.v)
         return surface_advection_weak_flux(self.flux_type, u, surf_v)
 
     def sym_operator(self):
diff --git a/grudge/models/em.py b/grudge/models/em.py
index 244d324e0749ef48d19a67af9ab0be5c3156aa0c..3f4bc13d42a017fcee36ba7b05237c512ca11451 100644
--- a/grudge/models/em.py
+++ b/grudge/models/em.py
@@ -327,7 +327,9 @@ class MaxwellOperator(HyperbolicOperator):
                     1 / sym.sqrt(self.epsilon * self.mu)
                     )
 
-    def max_eigenvalue(self, t, fields=None, discr=None, context={}):
+    def max_eigenvalue(self, t, fields=None, discr=None, context=None):
+        if context is None:
+            context = {}
         if self.fixed_material:
             return self.max_eigenvalue_expr()
         else:
diff --git a/grudge/models/poisson.py b/grudge/models/poisson.py
deleted file mode 100644
index 6636b94fe4d7ea90894bf92a86d30189fe93348e..0000000000000000000000000000000000000000
--- a/grudge/models/poisson.py
+++ /dev/null
@@ -1,261 +0,0 @@
-"""Operators for Poisson problems."""
-
-__copyright__ = "Copyright (C) 2007 Andreas Kloeckner"
-
-__license__ = """
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-"""
-
-import numpy as np
-
-from grudge.models import Operator
-from grudge.second_order import LDGSecondDerivative
-import grudge.data
-import grudge.iterative
-
-
-class LaplacianOperatorBase:
-    def sym_operator(self, apply_minv, u=None, dir_bc=None, neu_bc=None):
-        """
-        :param apply_minv: :class:`bool` specifying whether to compute a complete
-          divergence operator. If False, the final application of the inverse
-          mass operator is skipped. This is used in :meth:`op` in order to
-          reduce the scheme :math:`M^{-1} S u = f` to :math:`S u = M f`, so
-          that the mass operator only needs to be applied once, when preparing
-          the right hand side in :meth:`prepare_rhs`.
-
-          :class:`grudge.models.diffusion.DiffusionOperator` needs this.
-        """
-
-        from grudge.symbolic import Field, make_sym_vector
-        from grudge.second_order import SecondDerivativeTarget
-
-        if u is None:
-            u = Field("u")
-        if dir_bc is None:
-            dir_bc = Field("dir_bc")
-        if neu_bc is None:
-            neu_bc = Field("neu_bc")
-
-        # strong_form here allows IPDG to reuse the value of grad u.
-        grad_tgt = SecondDerivativeTarget(
-                self.dimensions, strong_form=True,
-                operand=u)
-
-        def grad_bc_getter(tag, expr):
-            assert tag == self.dirichlet_tag
-            return dir_bc
-        self.scheme.grad(grad_tgt,
-                bc_getter=grad_bc_getter,
-                dirichlet_tags=[self.dirichlet_tag],
-                neumann_tags=[self.neumann_tag])
-
-        def apply_diff_tensor(v):
-            if isinstance(self.diffusion_tensor, np.ndarray):
-                sym_diff_tensor = self.diffusion_tensor
-            else:
-                sym_diff_tensor = (make_sym_vector(
-                        "diffusion", self.dimensions**2)
-                        .reshape(self.dimensions, self.dimensions))
-
-            return np.dot(sym_diff_tensor, v)
-
-        div_tgt = SecondDerivativeTarget(
-                self.dimensions, strong_form=False,
-                operand=apply_diff_tensor(grad_tgt.minv_all))
-
-        def div_bc_getter(tag, expr):
-            if tag == self.dirichlet_tag:
-                return dir_bc
-            elif tag == self.neumann_tag:
-                return neu_bc
-            else:
-                assert False, "divergence bc getter " \
-                        "asked for '%s' BC for '%s'" % (tag, expr)
-
-        self.scheme.div(div_tgt,
-                div_bc_getter,
-                dirichlet_tags=[self.dirichlet_tag],
-                neumann_tags=[self.neumann_tag])
-
-        if apply_minv:
-            return div_tgt.minv_all
-        else:
-            return div_tgt.all
-
-
-class PoissonOperator(Operator, LaplacianOperatorBase):
-    """Implements the Local Discontinuous Galerkin (LDG) Method for elliptic
-    operators.
-
-    See P. Castillo et al.,
-    Local discontinuous Galerkin methods for elliptic problems",
-    Communications in Numerical Methods in Engineering 18, no. 1 (2002): 69-75.
-    """
-
-    def __init__(self, dimensions, diffusion_tensor=None,
-            dirichlet_bc=grudge.data.ConstantGivenFunction(),
-            dirichlet_tag="dirichlet",
-            neumann_bc=grudge.data.ConstantGivenFunction(),
-            neumann_tag="neumann",
-            scheme=LDGSecondDerivative()):
-        self.dimensions = dimensions
-
-        self.scheme = scheme
-
-        self.dirichlet_bc = dirichlet_bc
-        self.dirichlet_tag = dirichlet_tag
-        self.neumann_bc = neumann_bc
-        self.neumann_tag = neumann_tag
-
-        if diffusion_tensor is None:
-            diffusion_tensor = np.eye(dimensions)
-        self.diffusion_tensor = diffusion_tensor
-
-    # bound operator ----------------------------------------------------------
-    def bind(self, discr):
-        """Return a :class:`BoundPoissonOperator`."""
-
-        assert self.dimensions == discr.dimensions
-
-        from grudge.mesh import check_bc_coverage
-        check_bc_coverage(discr.mesh, [self.dirichlet_tag, self.neumann_tag])
-
-        return BoundPoissonOperator(self, discr)
-
-
-class BoundPoissonOperator(grudge.iterative.OperatorBase):
-    """Returned by :meth:`PoissonOperator.bind`."""
-
-    def __init__(self, poisson_op, discr):
-        grudge.iterative.OperatorBase.__init__(self)
-        self.discr = discr
-
-        pop = self.poisson_op = poisson_op
-
-        op = pop.sym_operator(
-            apply_minv=False, dir_bc=0, neu_bc=0)
-        bc_op = pop.sym_operator(apply_minv=False)
-
-        self.compiled_op = discr.compile(op)
-        self.compiled_bc_op = discr.compile(bc_op)
-
-        if not isinstance(pop.diffusion_tensor, np.ndarray):
-            self.diffusion = pop.diffusion_tensor.volume_interpolant(discr)
-
-        # Check whether use of Poincaré mean-value method is required.
-        # (for pure Neumann or pure periodic)
-
-        from grudge.mesh import BTAG_ALL
-        self.poincare_mean_value_hack = (
-                len(self.discr.get_boundary(BTAG_ALL).nodes)
-                == len(self.discr.get_boundary(poisson_op.neumann_tag).nodes))
-
-    @property
-    def dtype(self):
-        return self.discr.default_scalar_type
-
-    @property
-    def shape(self):
-        nodes = len(self.discr)
-        return nodes, nodes
-
-    def op(self, u):
-        context = {"u": u}
-        if not isinstance(self.poisson_op.diffusion_tensor, np.ndarray):
-            context["diffusion"] = self.diffusion
-
-        result = self.compiled_op(**context)
-
-        if self.poincare_mean_value_hack:
-            state_int = self.discr.integral(u)
-            mean_state = state_int / self.discr.mesh_volume()
-            return result - mean_state * self.discr._mass_ones()
-        else:
-            return result
-
-    __call__ = op
-
-    def prepare_rhs(self, rhs):
-        """Prepare the right-hand side for the linear system op(u)=rhs(f).
-
-        In matrix form, LDG looks like this:
-
-        .. math::
-            Mv = Cu + g
-            Mf = Av + Bu + h
-
-        where v is the auxiliary vector, u is the argument of the operator, f
-        is the result of the grad operator, g and h are inhom boundary data, and
-        A,B,C are some operator+lifting matrices.
-
-        .. math::
-
-            M f = A M^{-1}(Cu + g) + Bu + h
-
-        so the linear system looks like
-
-        .. math::
-
-            M f = A M^{-1} Cu + A M^{-1} g + Bu + h
-            M f - A M^{-1} g - h = (A M^{-1} C + B)u (*)
-
-        So the right hand side we're putting together here is really
-
-        .. math::
-
-            M f - A M^{-1} g - h
-
-        .. note::
-
-            Resist the temptation to left-multiply by the inverse
-            mass matrix, as this will result in a non-symmetric
-            matrix, which (e.g.) a conjugate gradient Krylov
-            solver will not take well.
-        """
-        pop = self.poisson_op
-
-        from grudge.symbolic import MassOperator
-        return (MassOperator().apply(self.discr, rhs)
-            - self.compiled_bc_op(
-                u=self.discr.volume_zeros(),
-                dir_bc=pop.dirichlet_bc.boundary_interpolant(
-                    self.discr, pop.dirichlet_tag),
-                neu_bc=pop.neumann_bc.boundary_interpolant(
-                    self.discr, pop.neumann_tag)))
-
-
-class HelmholtzOperator(PoissonOperator):
-    def __init__(self, k, *args, **kwargs):
-        PoissonOperator.__init__(self, *args, **kwargs)
-        self.k = k
-
-    def sym_operator(self, apply_minv, u=None, dir_bc=None, neu_bc=None):
-        from grudge.symbolic import Field
-        if u is None:
-            u = Field("u")
-
-        result = PoissonOperator.sym_operator(self,
-                apply_minv, u, dir_bc, neu_bc)
-
-        if apply_minv:
-            return result + self.k**2 * u
-        else:
-            from grudge.symbolic import MassOperator
-            return result + self.k**2 * MassOperator()(u)
diff --git a/grudge/models/second_order.py b/grudge/models/second_order.py
deleted file mode 100644
index 8f7251d329ea1cbbb960e368fe3c272f481ca36a..0000000000000000000000000000000000000000
--- a/grudge/models/second_order.py
+++ /dev/null
@@ -1,505 +0,0 @@
-"""Schemes for second-order derivatives."""
-
-__copyright__ = "Copyright (C) 2009 Andreas Kloeckner"
-
-__license__ = """
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-"""
-
-import numpy as np
-import grudge.symbolic.mappers as mappers
-from grudge import sym
-
-
-# {{{ stabilization term generator
-
-class StabilizationTermGenerator(mappers.IdentityMapper):
-    def __init__(self, flux_args):
-        super().__init__()
-        self.flux_args = flux_args
-        self.flux_arg_lookup = {
-                flux_arg: i for i, flux_arg in enumerate(flux_args)}
-
-    def get_flux_arg_idx(self, expr, quad_above):
-        from grudge.symbolic.mappers import QuadratureDetector
-
-        quad_below = QuadratureDetector()(expr)
-        if quad_above:
-            if quad_below is not None:
-                # Both the part of the expression above and below the
-                # differentiation operator had quadrature upsamplers in it.
-                # Since we're removing the differentiation operator, there are
-                # now two layers of quadrature operators. We need to change the
-                # inner layer to be the only layer.
-
-                from grudge.symbolic.mappers import QuadratureUpsamplerChanger
-                expr = QuadratureUpsamplerChanger(quad_above[0])(expr)
-            else:
-                # Only the part of the expression above the differentiation
-                # operator had quadrature. Insert quadrature here, be done.
-                expr = quad_above[0](expr)
-        else:
-            if quad_below is not None:
-                # Only the part of the expression below the differentiation
-                # operator had quadrature--the stuff above doesn't want it.
-                # Get rid of it.
-                from grudge.symbolic.mappers import QuadratureUpsamplerRemover
-                expr = QuadratureUpsamplerRemover({}, do_warn=False)(expr)
-            else:
-                # No quadrature, no headaches.
-                pass
-
-        try:
-            return self.flux_arg_lookup[expr]
-        except KeyError:
-            flux_arg_idx = len(self.flux_args)
-            self.flux_arg_lookup[expr] = flux_arg_idx
-            self.flux_args.append(expr)
-            return flux_arg_idx
-
-    def map_operator_binding(self, expr, quad_above=[]):
-        from grudge.symbolic.operators import (
-                DiffOperatorBase, FluxOperatorBase,
-                InverseMassOperator,
-                QuadratureInteriorFacesGridUpsampler)
-
-        if isinstance(expr.op, DiffOperatorBase):
-            flux_arg_idx = self.get_flux_arg_idx(expr.field, quad_above=quad_above)
-
-            from grudge.symbolic import \
-                    WeakFormDiffOperatorBase, \
-                    StrongFormDiffOperatorBase
-            if isinstance(expr.op, WeakFormDiffOperatorBase):
-                factor = -1
-            elif isinstance(expr.op, StrongFormDiffOperatorBase):
-                factor = 1
-            else:
-                raise RuntimeError("unknown type of differentiation "
-                        "operator encountered by stab term generator")
-
-            from grudge.flux import Normal, FluxScalarPlaceholder
-            sph = FluxScalarPlaceholder(flux_arg_idx)
-            return (factor
-                    * Normal(expr.op.xyz_axis)
-                    * (sph.int - sph.ext))
-
-        elif isinstance(expr.op, FluxOperatorBase):
-            return 0
-        elif isinstance(expr.op, InverseMassOperator):
-            return self.rec(expr.field, quad_above)
-        elif isinstance(expr.op, QuadratureInteriorFacesGridUpsampler):
-            if quad_above:
-                raise RuntimeError("double quadrature upsampler found "
-                        "when generating stabilization term")
-            return self.rec(expr.field, [expr.op])
-        else:
-            from grudge.symbolic.tools import pretty
-            raise ValueError("stabilization term generator doesn't know "
-                    "what to do with '%s'" % pretty(expr))
-
-    def map_variable(self, expr, quad_above=[]):
-        from grudge.flux import FieldComponent
-        return FieldComponent(
-                self.get_flux_arg_idx(expr, quad_above),
-                is_interior=True)
-
-    def map_subscript(self, expr, quad_above=[]):
-        from pymbolic.primitives import Variable
-        assert isinstance(expr.aggregate, Variable)
-        from grudge.flux import FieldComponent
-        return FieldComponent(
-                self.get_flux_arg_idx(expr, quad_above),
-                is_interior=True)
-
-# }}}
-
-
-# {{{ neumann bc generator
-
-class NeumannBCGenerator(mappers.IdentityMapper):
-    def __init__(self, tag, bc):
-        super().__init__()
-        self.tag = tag
-        self.bc = bc
-
-    def map_operator_binding(self, expr):
-        if isinstance(expr.op, sym.DiffOperatorBase):
-            from grudge.symbolic import \
-                    WeakFormDiffOperatorBase, \
-                    StrongFormDiffOperatorBase
-            if isinstance(expr.op, WeakFormDiffOperatorBase):
-                factor = -1
-            elif isinstance(expr.op, StrongFormDiffOperatorBase):
-                factor = 1
-            else:
-                raise RuntimeError("unknown type of differentiation "
-                        "operator encountered by stab term generator")
-
-            from grudge.symbolic import BoundaryNormalComponent
-            return (self.bc * factor
-                    * BoundaryNormalComponent(self.tag, expr.op.xyz_axis))
-
-        elif isinstance(expr.op, sym.FluxOperatorBase):
-            return 0
-        elif isinstance(expr.op, sym.InverseMassOperator):
-            return self.rec(expr.field)
-        else:
-            raise ValueError("neumann normal direction generator doesn't know "
-                    "what to do with '%s'" % expr)
-
-# }}}
-
-
-class IPDGDerivativeGenerator(mappers.IdentityMapper):
-    def map_operator_binding(self, expr):
-        if isinstance(expr.op, sym.DiffOperatorBase):
-            from grudge.symbolic import (
-                    WeakFormDiffOperatorBase,
-                    StrongFormDiffOperatorBase)
-
-            if isinstance(expr.op, WeakFormDiffOperatorBase):
-                factor = -1
-            elif isinstance(expr.op, StrongFormDiffOperatorBase):
-                factor = 1
-            else:
-                raise RuntimeError("unknown type of differentiation "
-                        "operator encountered by stab term generator")
-
-            from grudge.symbolic import DifferentiationOperator
-            return factor*DifferentiationOperator(expr.op.xyz_axis)(expr.field)
-
-        elif isinstance(expr.op, sym.FluxOperatorBase):
-            return 0
-        elif isinstance(expr.op, sym.InverseMassOperator):
-            return self.rec(expr.field)
-        elif isinstance(expr.op,
-                sym.QuadratureInteriorFacesGridUpsampler):
-            return super().map_operator_binding(expr)
-        else:
-            from grudge.symbolic.tools import pretty
-            raise ValueError("IPDG derivative generator doesn't know "
-                    "what to do with '%s'" % pretty(expr))
-
-
-# {{{ second derivative target
-
-class SecondDerivativeTarget:
-    def __init__(self, dimensions, strong_form, operand,
-            int_flux_operand=None,
-            bdry_flux_int_operand=None):
-        """
-        :param int_flux_operand: if not None, is used as the interior
-          argument to the interior fluxes. This is useful e.g. if the boundary
-          values are on a quadrature grid--in this case, *bdry_flux_int_operand*
-          can be passed to also be on a boundary grid. If it is None, it defaults
-          to *operand*.
-
-        :param bdry_flux_int_operand: if not None, is used as the interior
-          argument to the boundary fluxes. This is useful e.g. if the boundary
-          values are on a quadrature grid--in this case, *bdry_flux_int_operand*
-          can be passed to also be on a boundary grid. If it is None, it defaults
-          to *int_flux_operand*.
-        """
-        self.dimensions = dimensions
-        self.operand = operand
-
-        if int_flux_operand is None:
-            int_flux_operand = operand
-        if bdry_flux_int_operand is None:
-            bdry_flux_int_operand = int_flux_operand
-
-        self.int_flux_operand = int_flux_operand
-        self.bdry_flux_int_operand = bdry_flux_int_operand
-
-        self.strong_form = strong_form
-        if strong_form:
-            self.strong_neg = -1
-        else:
-            self.strong_neg = 1
-
-        self.local_derivatives = 0
-        self.inner_fluxes = 0
-        self.boundary_fluxes = 0
-
-    def add_local_derivatives(self, expr):
-        self.local_derivatives = self.local_derivatives \
-                + (-self.strong_neg)*expr
-
-    def vec_times(self, vec, operand):
-        from pytools.obj_array import is_obj_array
-
-        if is_obj_array(operand):
-            if len(operand) != self.dimensions:
-                raise ValueError("operand of vec_times must have %d dimensions"
-                        % self.dimensions)
-
-            return np.dot(vec, operand)
-        else:
-            return vec*operand
-
-    def normal_times_flux(self, flux):
-        from grudge.flux import make_normal
-        return self.vec_times(make_normal(self.dimensions), flux)
-
-    def apply_diff(self, nabla, operand):
-        from pytools.obj_array import make_obj_array, is_obj_array
-        if is_obj_array(operand):
-            if len(operand) != self.dimensions:
-                raise ValueError("operand of apply_diff must have %d dimensions"
-                        % self.dimensions)
-
-            return sum(nabla[i](operand[i]) for i in range(self.dimensions))
-        else:
-            return make_obj_array(
-                [nabla[i](operand) for i in range(self.dimensions)])
-
-    def _local_nabla(self):
-        if self.strong_form:
-            from grudge.symbolic import make_stiffness
-            return make_stiffness(self.dimensions)
-        else:
-            from grudge.symbolic import make_stiffness_t
-            return make_stiffness_t(self.dimensions)
-
-    def add_derivative(self, operand=None):
-        if operand is None:
-            operand = self.operand
-
-        self.add_local_derivatives(
-                self.apply_diff(self._local_nabla(), operand))
-
-    def add_inner_fluxes(self, flux, expr):
-        from grudge.symbolic import get_flux_operator
-        self.inner_fluxes = self.inner_fluxes \
-                + get_flux_operator(self.strong_neg*flux)(expr)
-
-    def add_boundary_flux(self, flux, volume_expr, bdry_expr, tag):
-        from grudge.symbolic import BoundaryPair, get_flux_operator
-        self.boundary_fluxes = self.boundary_fluxes + \
-                get_flux_operator(self.strong_neg*flux)(BoundaryPair(
-                        volume_expr, bdry_expr, tag))
-
-    @property
-    def fluxes(self):
-        return self.inner_fluxes + self.boundary_fluxes
-
-    @property
-    def all(self):
-        return self.local_derivatives + self.fluxes
-
-    @property
-    def minv_all(self):
-        from grudge.symbolic.primitives import make_common_subexpression as cse
-        from grudge.symbolic.operators import InverseMassOperator
-        return (cse(InverseMassOperator()(self.local_derivatives), "grad_loc")
-                + cse(InverseMassOperator()(self.fluxes), "grad_flux"))
-
-# }}}
-
-
-# {{{ second derivative schemes
-
-class SecondDerivativeBase:
-    def grad(self, tgt, bc_getter, dirichlet_tags, neumann_tags):
-        """
-        :param bc_getter: a function (tag, volume_expr) -> boundary expr.
-          *volume_expr* will be None to query the Neumann condition.
-        """
-        n_times = tgt.normal_times_flux
-
-        if tgt.strong_form:
-            def adjust_flux(f):
-                return n_times(u.int) - f
-        else:
-            def adjust_flux(f):
-                return f
-
-        from grudge.flux import FluxScalarPlaceholder
-        u = FluxScalarPlaceholder()
-
-        tgt.add_derivative()
-        tgt.add_inner_fluxes(
-                adjust_flux(self.grad_interior_flux(tgt, u)),
-                tgt.int_flux_operand)
-
-        for tag in dirichlet_tags:
-            tgt.add_boundary_flux(
-                    adjust_flux(n_times(u.ext)),
-                    tgt.bdry_flux_int_operand, bc_getter(tag, tgt.operand), tag)
-
-        for tag in neumann_tags:
-            tgt.add_boundary_flux(
-                    adjust_flux(n_times(u.int)),
-                    tgt.bdry_flux_int_operand, 0, tag)
-
-    def add_div_bcs(self, tgt, bc_getter, dirichlet_tags, neumann_tags,
-            stab_term, adjust_flux, flux_v, flux_arg_int,
-            grad_flux_arg_count):
-        from pytools.obj_array import make_obj_array, join_fields
-        n_times = tgt.normal_times_flux
-
-        def unwrap_cse(expr):
-            from pymbolic.primitives import CommonSubexpression
-            if isinstance(expr, CommonSubexpression):
-                return expr.child
-            else:
-                return expr
-
-        for tag in dirichlet_tags:
-            dir_bc_w = join_fields(
-                    [0]*grad_flux_arg_count,
-                    [bc_getter(tag, unwrap_cse(vol_expr)) for vol_expr in
-                        flux_arg_int[grad_flux_arg_count:]])
-            tgt.add_boundary_flux(
-                    adjust_flux(n_times(flux_v.int-stab_term)),
-                    flux_arg_int, dir_bc_w, tag)
-
-        loc_bc_vec = make_obj_array([0]*len(flux_arg_int))
-
-        for tag in neumann_tags:
-            neu_bc_w = join_fields(
-                    NeumannBCGenerator(tag, bc_getter(tag, None))(tgt.operand),
-                    [0]*len(flux_arg_int))
-
-            tgt.add_boundary_flux(
-                    adjust_flux(n_times(flux_v.ext)),
-                    loc_bc_vec, neu_bc_w, tag)
-
-
-class LDGSecondDerivative(SecondDerivativeBase):
-    def __init__(self, beta_value=0.5, stab_coefficient=1):
-        self.beta_value = beta_value
-        self.stab_coefficient = stab_coefficient
-
-    def beta(self, tgt):
-        return np.array([self.beta_value]*tgt.dimensions, dtype=np.float64)
-
-    def grad_interior_flux(self, tgt, u):
-        from grudge.symbolic.primitives import make_common_subexpression as cse
-        n_times = tgt.normal_times_flux
-        v_times = tgt.vec_times
-
-        return n_times(
-                cse(u.avg, "u_avg")
-                - v_times(self.beta(tgt), n_times(u.int-u.ext)))
-
-    def div(self, tgt, bc_getter, dirichlet_tags, neumann_tags):
-        """
-        :param bc_getter: a function (tag, volume_expr) -> boundary expr.
-          *volume_expr* will be None to query the Neumann condition.
-        """
-
-        from grudge.symbolic.primitives import make_common_subexpression as cse
-        from grudge.flux import FluxVectorPlaceholder, PenaltyTerm
-
-        n_times = tgt.normal_times_flux
-        v_times = tgt.vec_times
-
-        if tgt.strong_form:
-            def adjust_flux(f):
-                return n_times(flux_v.int) - f
-        else:
-            def adjust_flux(f):
-                return f
-
-        flux_v = FluxVectorPlaceholder(tgt.dimensions)
-
-        stab_term_generator = StabilizationTermGenerator(
-                list(tgt.int_flux_operand))
-        stab_term = (self.stab_coefficient * PenaltyTerm()
-                * stab_term_generator(tgt.int_flux_operand))
-
-        flux = n_times(flux_v.avg
-                + v_times(self.beta(tgt),
-                    cse(n_times(flux_v.int - flux_v.ext), "jump_v"))
-                - stab_term)
-
-        from pytools.obj_array import make_obj_array
-        flux_arg_int = cse(make_obj_array(stab_term_generator.flux_args))
-
-        tgt.add_derivative(cse(tgt.operand))
-        tgt.add_inner_fluxes(adjust_flux(flux), flux_arg_int)
-
-        self.add_div_bcs(tgt, bc_getter, dirichlet_tags, neumann_tags,
-                stab_term, adjust_flux, flux_v, flux_arg_int, tgt.dimensions)
-
-
-class StabilizedCentralSecondDerivative(LDGSecondDerivative):
-    def __init__(self, stab_coefficient=1):
-        LDGSecondDerivative.__init__(self, 0, stab_coefficient=stab_coefficient)
-
-
-class CentralSecondDerivative(LDGSecondDerivative):
-    def __init__(self):
-        LDGSecondDerivative.__init__(self, 0, 0)
-
-
-class IPDGSecondDerivative(SecondDerivativeBase):
-    def __init__(self, stab_coefficient=1):
-        self.stab_coefficient = stab_coefficient
-
-    def grad_interior_flux(self, tgt, u):
-        from grudge.symbolic.primitives import make_common_subexpression as cse
-        n_times = tgt.normal_times_flux
-        return n_times(cse(u.avg, "u_avg"))
-
-    def div(self, tgt, bc_getter, dirichlet_tags, neumann_tags):
-        """
-        :param bc_getter: a function (tag, volume_expr) -> boundary expr.
-          *volume_expr* will be None to query the Neumann condition.
-        """
-
-        from grudge.symbolic.primitives import make_common_subexpression as cse
-        from grudge.flux import FluxVectorPlaceholder, PenaltyTerm
-
-        n_times = tgt.normal_times_flux
-
-        if tgt.strong_form:
-            def adjust_flux(f):
-                return n_times(flux_v.int) - f
-        else:
-            def adjust_flux(f):
-                return f
-
-        dim = tgt.dimensions
-
-        flux_w = FluxVectorPlaceholder(2*tgt.dimensions)
-        flux_v = flux_w[:dim]
-        pure_diff_v = flux_w[dim:]
-        flux_args = (
-                list(tgt.int_flux_operand)
-                + list(IPDGDerivativeGenerator()(tgt.int_flux_operand)))
-
-        stab_term_generator = StabilizationTermGenerator(flux_args)
-        stab_term = (self.stab_coefficient * PenaltyTerm()
-                * stab_term_generator(tgt.int_flux_operand))
-        flux = n_times(pure_diff_v.avg - stab_term)
-
-        from pytools.obj_array import make_obj_array
-        flux_arg_int = cse(make_obj_array(stab_term_generator.flux_args))
-
-        tgt.add_derivative(cse(tgt.operand))
-        tgt.add_inner_fluxes(adjust_flux(flux), flux_arg_int)
-
-        self.add_div_bcs(tgt, bc_getter, dirichlet_tags, neumann_tags,
-                stab_term, adjust_flux, flux_v, flux_arg_int, 2*tgt.dimensions)
-
-# }}}
-
-# vim: fdm=marker
diff --git a/grudge/op.py b/grudge/op.py
index c64c51b1d7a1dfb2576bdd06e07bab36ac5138ba..b3c402ca9c3f3c739f4e80378d5a284ad769f0d9 100644
--- a/grudge/op.py
+++ b/grudge/op.py
@@ -218,7 +218,7 @@ def weak_local_grad(dcoll, *args):
     """
     if len(args) == 1:
         vec, = args
-        dd = dof_desc.DOFDesc("vol", dof_desc.QTAG_NONE)
+        dd = dof_desc.DOFDesc("vol", dof_desc.DISCR_TAG_BASE)
     elif len(args) == 2:
         dd, vec = args
     else:
@@ -248,7 +248,7 @@ def weak_local_d_dx(dcoll, *args):
     """
     if len(args) == 2:
         xyz_axis, vec = args
-        dd = dof_desc.DOFDesc("vol", dof_desc.QTAG_NONE)
+        dd = dof_desc.DOFDesc("vol", dof_desc.DISCR_TAG_BASE)
     elif len(args) == 3:
         dd, xyz_axis, vec = args
     else:
@@ -273,7 +273,7 @@ def weak_local_div(dcoll, *args):
     """
     if len(args) == 1:
         vecs, = args
-        dd = dof_desc.DOFDesc("vol", dof_desc.QTAG_NONE)
+        dd = dof_desc.DOFDesc("vol", dof_desc.DISCR_TAG_BASE)
     elif len(args) == 2:
         dd, vecs = args
     else:
@@ -297,7 +297,7 @@ def _bound_mass(dcoll, dd):
 def mass(dcoll, *args):
     if len(args) == 1:
         vec, = args
-        dd = dof_desc.DOFDesc("vol", dof_desc.QTAG_NONE)
+        dd = dof_desc.DOFDesc("vol", dof_desc.DISCR_TAG_BASE)
     elif len(args) == 2:
         dd, vec = args
     else:
@@ -333,7 +333,7 @@ def _bound_face_mass(dcoll, dd):
 def face_mass(dcoll, *args):
     if len(args) == 1:
         vec, = args
-        dd = dof_desc.DOFDesc("all_faces", dof_desc.QTAG_NONE)
+        dd = dof_desc.DOFDesc("all_faces", dof_desc.DISCR_TAG_BASE)
     elif len(args) == 2:
         dd, vec = args
     else:
diff --git a/grudge/symbolic/compiler.py b/grudge/symbolic/compiler.py
index 742483aed9e02264c5ac65cfb40ee8ef0dbb8ee5..f5f7c66499b2adeadc470b9be416a06d5074048a 100644
--- a/grudge/symbolic/compiler.py
+++ b/grudge/symbolic/compiler.py
@@ -796,7 +796,7 @@ def aggregate_assignments(inf_mapper, instructions, result,
             schedulable.sort()
 
             if schedulable:
-                for key, name, expr in schedulable:
+                for _key, name, expr in schedulable:
                     ordered_names_exprs.append((name, expr))
                     available_names.add(name)
             else:
diff --git a/grudge/symbolic/dofdesc_inference.py b/grudge/symbolic/dofdesc_inference.py
index 9b5466190d9e7a303ea4ed967932e9a68ace1bdb..894a9bfc0e779e780b5da78e7a0aabb74cc925b2 100644
--- a/grudge/symbolic/dofdesc_inference.py
+++ b/grudge/symbolic/dofdesc_inference.py
@@ -48,8 +48,8 @@ def unify_dofdescs(dd_a, dd_b, expr=None):
             raise ValueError("mismatched domain tags " + loc_str)
 
     # domain tags match
-    if dd_a.quadrature_tag != dd_b.quadrature_tag:
-        raise ValueError("mismatched quadrature tags " + loc_str)
+    if dd_a.discretization_tag != dd_b.discretization_tag:
+        raise ValueError("mismatched discretization tags " + loc_str)
 
     return dd_a
 
diff --git a/grudge/symbolic/mappers/__init__.py b/grudge/symbolic/mappers/__init__.py
index 99b4d0897f22c112e3c3af67a3b65f08bb4de2dc..84c11feb883db70b63bcac293c6b672c6e1af605 100644
--- a/grudge/symbolic/mappers/__init__.py
+++ b/grudge/symbolic/mappers/__init__.py
@@ -537,21 +537,21 @@ class OperatorSpecializer(CSECachingMapperMixin, IdentityMapper):
 
         if isinstance(expr.op, op.MassOperator) and has_quad_operand:
             return op.QuadratureMassOperator(
-                    field_repr_tag.quadrature_tag)(self.rec(expr.field))
+                    field_repr_tag.discretization_tag)(self.rec(expr.field))
 
         elif isinstance(expr.op, op.RefMassOperator) and has_quad_operand:
             return op.RefQuadratureMassOperator(
-                    field_repr_tag.quadrature_tag)(self.rec(expr.field))
+                    field_repr_tag.discretization_tag)(self.rec(expr.field))
 
         elif (isinstance(expr.op, op.StiffnessTOperator) and has_quad_operand):
             return op.QuadratureStiffnessTOperator(
-                    expr.op.xyz_axis, field_repr_tag.quadrature_tag)(
+                    expr.op.xyz_axis, field_repr_tag.discretization_tag)(
                             self.rec(expr.field))
 
         elif (isinstance(expr.op, op.RefStiffnessTOperator)
                 and has_quad_operand):
             return op.RefQuadratureStiffnessTOperator(
-                    expr.op.xyz_axis, field_repr_tag.quadrature_tag)(
+                    expr.op.xyz_axis, field_repr_tag.discretization_tag)(
                             self.rec(expr.field))
 
         elif (isinstance(expr.op, op.QuadratureGridUpsampler)
@@ -560,11 +560,11 @@ class OperatorSpecializer(CSECachingMapperMixin, IdentityMapper):
             # if (isinstance(expr.field, OperatorBinding)
             #        and isinstance(expr.field.op, RestrictToBoundary)):
             #    return QuadratureRestrictToBoundary(
-            #            expr.field.op.tag, expr.op.quadrature_tag)(
+            #            expr.field.op.tag, expr.op.discretization_tag)(
             #                    self.rec(expr.field.field))
 
             return op.QuadratureBoundaryGridUpsampler(
-                    expr.op.quadrature_tag, field_type.boundary_tag)(expr.field)
+                expr.op.discretization_tag, field_type.boundary_tag)(expr.field)
         # }}}
 
         elif isinstance(expr.op, op.RestrictToBoundary) and has_quad_operand:
@@ -612,7 +612,7 @@ class GlobalToReferenceMapper(CSECachingMapperMixin, IdentityMapper):
 
         jac_in = sym.area_element(self.ambient_dim, dim, dd=dd_in)
         jac_noquad = sym.area_element(self.ambient_dim, dim,
-                dd=dd_in.with_qtag(dof_desc.QTAG_NONE))
+                dd=dd_in.with_discr_tag(dof_desc.DISCR_TAG_BASE))
 
         def rewrite_derivative(ref_class, field,  dd_in, with_jacobian=True):
             def imd(rst):
@@ -713,12 +713,12 @@ class StringifyMapper(pymbolic.mapper.stringifier.StringifyMapper):
         else:
             result = fmt(dd.domain_tag)
 
-        if dd.quadrature_tag is None:
+        if dd.discretization_tag is None:
             pass
-        elif dd.quadrature_tag is dof_desc.QTAG_NONE:
+        elif dd.discretization_tag is dof_desc.DISCR_TAG_BASE:
             result += "q"
         else:
-            result += "Q"+fmt(dd.quadrature_tag)
+            result += "Q"+fmt(dd.discretization_tag)
 
         return result
 
@@ -868,23 +868,24 @@ class QuadratureCheckerAndRemover(CSECachingMapperMixin, IdentityMapper):
     """Checks whether all quadratu
     """
 
-    def __init__(self, quad_tag_to_group_factory):
+    def __init__(self, discr_tag_to_group_factory):
         IdentityMapper.__init__(self)
         CSECachingMapperMixin.__init__(self)
-        self.quad_tag_to_group_factory = quad_tag_to_group_factory
+        self.discr_tag_to_group_factory = discr_tag_to_group_factory
 
     map_common_subexpression_uncached = \
             IdentityMapper.map_common_subexpression
 
     def _process_dd(self, dd, location_descr):
-        if dd.quadrature_tag is not dof_desc.QTAG_NONE:
-            if dd.quadrature_tag not in self.quad_tag_to_group_factory:
+
+        if dd.discretization_tag is not dof_desc.DISCR_TAG_BASE:
+            if dd.discretization_tag not in self.discr_tag_to_group_factory:
                 raise ValueError("found unknown quadrature tag '%s' in '%s'"
-                        % (dd.quadrature_tag, location_descr))
+                        % (dd.discretization_tag, location_descr))
 
-            grp_factory = self.quad_tag_to_group_factory[dd.quadrature_tag]
+            grp_factory = self.discr_tag_to_group_factory[dd.discretization_tag]
             if grp_factory is None:
-                dd = dof_desc.DOFDesc(dd.domain_tag, dof_desc.QTAG_NONE)
+                dd = dof_desc.DOFDesc(dd.domain_tag, dof_desc.DISCR_TAG_BASE)
 
         return dd
 
diff --git a/grudge/symbolic/operators.py b/grudge/symbolic/operators.py
index 701661ca9b09a99da74bd2319b7139c43fcfc782..031ec60e0fab4ea8847ea4a4c6929269d98658d5 100644
--- a/grudge/symbolic/operators.py
+++ b/grudge/symbolic/operators.py
@@ -264,7 +264,7 @@ class DiffOperatorBase(Operator):
             dd_in = dof_desc.DD_VOLUME
 
         if dd_out is None:
-            dd_out = dd_in.with_qtag(dof_desc.QTAG_NONE)
+            dd_out = dd_in.with_discr_tag(dof_desc.DISCR_TAG_BASE)
         else:
             dd_out = dof_desc.as_dofdesc(dd_out)
 
@@ -322,7 +322,7 @@ class RefDiffOperatorBase(ElementwiseLinearOperator):
             dd_in = dof_desc.DD_VOLUME
 
         if dd_out is None:
-            dd_out = dd_in.with_qtag(dof_desc.QTAG_NONE)
+            dd_out = dd_in.with_discr_tag(dof_desc.DISCR_TAG_BASE)
 
         if dd_out.uses_quadrature():
             raise ValueError("differentiation outputs are not on "
@@ -633,7 +633,7 @@ class FaceMassOperatorBase(ElementwiseLinearOperator):
             dd_in = dof_desc.DOFDesc(FACE_RESTR_ALL, None)
 
         if dd_out is None or dd_out == "vol":
-            dd_out = dof_desc.DOFDesc("vol", dof_desc.QTAG_NONE)
+            dd_out = dof_desc.DOFDesc("vol", dof_desc.DISCR_TAG_BASE)
 
         if dd_out.uses_quadrature():
             raise ValueError("face mass operator outputs are not on "
diff --git a/grudge/symbolic/primitives.py b/grudge/symbolic/primitives.py
index 4fc28a733daa9d5578395f14f9c622b86516173a..c880d9d0925d0b8c48a01b788cf81249160865e9 100644
--- a/grudge/symbolic/primitives.py
+++ b/grudge/symbolic/primitives.py
@@ -35,12 +35,6 @@ from pymbolic.primitives import (
         make_common_subexpression as cse)
 from pymbolic.geometric_algebra import MultiVector
 
-# FIXME: Need to import these for backwards compatibility with
-# Mirgecom. Need to remove importing the following from
-# `grudge.symbolic.primitives` in https://github.com/illinois-ceesd/mirgecom
-from grudge.dof_desc import \
-    DOFDesc, as_dofdesc, DTAG_BOUNDARY, QTAG_NONE  # noqa: F401
-
 
 class ExpressionBase(prim.Expression):
     def make_stringifier(self, originating_stringifier=None):
@@ -368,7 +362,7 @@ def forward_metric_nth_derivative(xyz_axis, ref_axes, dd=None):
 
     if dd is None:
         dd = dof_desc.DD_VOLUME
-    inner_dd = dd.with_qtag(dof_desc.QTAG_NONE)
+    inner_dd = dd.with_discr_tag(dof_desc.DISCR_TAG_BASE)
 
     from pytools import flatten
     flat_ref_axes = flatten([rst_axis] * n for rst_axis, n in ref_axes)
diff --git a/setup.cfg b/setup.cfg
index e8791cc48f86a67e726046a2f7510f32a39a98a5..38dcd9de2c01e21191ca1612388f36ecbc251b70 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,11 +9,12 @@ exclude=
   grudge/models/nd_calculus.py,
   grudge/dt_finding.py
 
-
 inline-quotes = "
 docstring-quotes = """
 multiline-quotes = """
 
+# enable-flake8-bugbear
+
 [mypy]
 ignore_missing_imports = True
 
diff --git a/test/test_grudge.py b/test/test_grudge.py
index 1b375bdfe7241422142b4d8511235738b214253c..f246a895c2bf2b1229986c52c152d405aebf68b4 100644
--- a/test/test_grudge.py
+++ b/test/test_grudge.py
@@ -93,8 +93,9 @@ def test_inverse_metric(actx_factory, dim):
 # {{{ mass operator trig integration
 
 @pytest.mark.parametrize("ambient_dim", [1, 2, 3])
-@pytest.mark.parametrize("quad_tag", [dof_desc.QTAG_NONE, "OVSMP"])
-def test_mass_mat_trig(actx_factory, ambient_dim, quad_tag):
+@pytest.mark.parametrize("discr_tag", [dof_desc.DISCR_TAG_BASE,
+                                       dof_desc.DISCR_TAG_QUAD])
+def test_mass_mat_trig(actx_factory, ambient_dim, discr_tag):
     """Check the integral of some trig functions on an interval using the mass
     matrix.
     """
@@ -108,19 +109,21 @@ def test_mass_mat_trig(actx_factory, ambient_dim, quad_tag):
     true_integral = 13*np.pi/2 * (b - a)**(ambient_dim - 1)
 
     from meshmode.discretization.poly_element import QuadratureSimplexGroupFactory
-    dd_quad = dof_desc.DOFDesc(dof_desc.DTAG_VOLUME_ALL, quad_tag)
-    if quad_tag is dof_desc.QTAG_NONE:
-        quad_tag_to_group_factory = {}
+    dd_quad = dof_desc.DOFDesc(dof_desc.DTAG_VOLUME_ALL, discr_tag)
+    if discr_tag is dof_desc.DISCR_TAG_BASE:
+        discr_tag_to_group_factory = {}
     else:
-        quad_tag_to_group_factory = {
-                quad_tag: QuadratureSimplexGroupFactory(order=2*order)
-                }
+        discr_tag_to_group_factory = {
+            discr_tag: QuadratureSimplexGroupFactory(order=2*order)
+        }
 
     mesh = mgen.generate_regular_rect_mesh(
             a=(a,)*ambient_dim, b=(b,)*ambient_dim,
             nelements_per_axis=(nel_1d,)*ambient_dim, order=1)
-    discr = DiscretizationCollection(actx, mesh, order=order,
-            quad_tag_to_group_factory=quad_tag_to_group_factory)
+    discr = DiscretizationCollection(
+        actx, mesh, order=order,
+        discr_tag_to_group_factory=discr_tag_to_group_factory
+    )
 
     def _get_variables_on(dd):
         sym_f = sym.var("f", dd=dd)
@@ -147,7 +150,7 @@ def test_mass_mat_trig(actx_factory, ambient_dim, quad_tag):
     err_2 = abs(num_integral_2 - true_integral)
     assert err_2 < 1.0e-9, err_2
 
-    if quad_tag is dof_desc.QTAG_NONE:
+    if discr_tag is dof_desc.DISCR_TAG_BASE:
         # NOTE: `integral` always makes a square mass matrix and
         # `QuadratureSimplexGroupFactory` does not have a `mass_matrix` method.
         num_integral_3 = bind(discr,
@@ -568,17 +571,19 @@ def test_surface_divergence_theorem(actx_factory, mesh_name, visualize=False):
 
         from meshmode.discretization.poly_element import \
                 QuadratureSimplexGroupFactory
-        discr = DiscretizationCollection(actx, mesh, order=builder.order,
-                quad_tag_to_group_factory={
+        discr = DiscretizationCollection(
+            actx, mesh, order=builder.order,
+            discr_tag_to_group_factory={
                     "product": QuadratureSimplexGroupFactory(2 * builder.order)
-                    })
+            }
+        )
 
         volume = discr.discr_from_dd(dof_desc.DD_VOLUME)
         logger.info("ndofs:     %d", volume.ndofs)
         logger.info("nelements: %d", volume.mesh.nelements)
 
         dd = dof_desc.DD_VOLUME
-        dq = dd.with_qtag("product")
+        dq = dd.with_discr_tag("product")
         df = dof_desc.as_dofdesc(FACE_RESTR_ALL)
         ambient_dim = discr.ambient_dim
         dim = discr.dim
@@ -910,14 +915,16 @@ def test_improvement_quadrature(actx_factory, order):
                 order=order)
 
             if use_quad:
-                quad_tag_to_group_factory = {
+                discr_tag_to_group_factory = {
                     "product": QuadratureSimplexGroupFactory(order=4*order)
-                    }
+                }
             else:
-                quad_tag_to_group_factory = {"product": None}
+                discr_tag_to_group_factory = {"product": None}
 
-            discr = DiscretizationCollection(actx, mesh, order=order,
-                    quad_tag_to_group_factory=quad_tag_to_group_factory)
+            discr = DiscretizationCollection(
+                actx, mesh, order=order,
+                discr_tag_to_group_factory=discr_tag_to_group_factory
+            )
 
             bound_op = bind(discr, op.sym_operator())
             fields = bind(discr, gaussian_mode())(actx, t=0)
diff --git a/test/test_modal_connections.py b/test/test_modal_connections.py
index 539a78069a9363323d07d1f26c1f3dcebf5c88eb..eeb9a9782f71ffc99cc41093ed84296a3155f599 100644
--- a/test/test_modal_connections.py
+++ b/test/test_modal_connections.py
@@ -66,8 +66,8 @@ def test_inverse_modal_connections(actx_factory, nodal_group_factory):
 
     dcoll = DiscretizationCollection(
         actx, mesh,
-        quad_tag_to_group_factory={
-            dof_desc.QTAG_NONE: nodal_group_factory(order)
+        discr_tag_to_group_factory={
+            dof_desc.DISCR_TAG_BASE: nodal_group_factory(order)
         }
     )
 
@@ -106,15 +106,16 @@ def test_inverse_modal_connections_quadgrid(actx_factory):
 
     dcoll = DiscretizationCollection(
         actx, mesh,
-        quad_tag_to_group_factory={
-            dof_desc.QTAG_NONE: PolynomialWarpAndBlendGroupFactory(order),
-            "quad": QuadratureSimplexGroupFactory(2*order)
+        discr_tag_to_group_factory={
+            dof_desc.DISCR_TAG_BASE: PolynomialWarpAndBlendGroupFactory(order),
+            dof_desc.DISCR_TAG_QUAD: QuadratureSimplexGroupFactory(2*order)
         }
     )
 
     # Use dof descriptors on the quadrature grid
     dd_modal = dof_desc.DD_VOLUME_MODAL
-    dd_quad = dof_desc.DOFDesc(dof_desc.DTAG_VOLUME_ALL, "quad")
+    dd_quad = dof_desc.DOFDesc(dof_desc.DTAG_VOLUME_ALL,
+                               dof_desc.DISCR_TAG_QUAD)
 
     x_quad = thaw(actx, dcoll.discr_from_dd(dd_quad).nodes()[0])
     quad_f = f(x_quad)