diff --git a/grudge/operators.py b/grudge/operators.py
deleted file mode 100644
index 22d290129316e02346fa2dfb601c6960e46c42a7..0000000000000000000000000000000000000000
--- a/grudge/operators.py
+++ /dev/null
@@ -1,598 +0,0 @@
-__copyright__ = """
-Copyright (C) 2021 University of Illinois Board of Trustees
-"""
-
-__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.
-"""
-
-from numbers import Number
-from pytools import (
-    memoize_in,
-    memoize_on_first_arg,
-    # keyed_memoize_in
-)
-from pytools.obj_array import obj_array_vectorize
-from meshmode.array_context import (
-    FirstAxisIsElementsTag, make_loopy_program
-)
-from meshmode.dof_array import DOFArray
-
-import numpy as np
-import grudge.dof_desc as dof_desc
-
-
-# {{{ tags
-
-class HasElementwiseMatvecTag(FirstAxisIsElementsTag):
-    pass
-
-
-class MassOperatorTag(HasElementwiseMatvecTag):
-    pass
-
-# }}}
-
-
-def project(dcoll, src, tgt, vec):
-    """Project from one discretization to another, e.g. from the
-    volume to the boundary, or from the base to the an overintegrated
-    quadrature discretization.
-
-    :arg src: a :class:`~grudge.dof_desc.DOFDesc`, or a value convertible to one
-    :arg tgt: a :class:`~grudge.dof_desc.DOFDesc`, or a value convertible to one
-    :arg vec: a :class:`~meshmode.dof_array.DOFArray`
-    """
-    src = dof_desc.as_dofdesc(src)
-    tgt = dof_desc.as_dofdesc(tgt)
-    if src == tgt:
-        return vec
-
-    if isinstance(vec, np.ndarray):
-        return obj_array_vectorize(
-                lambda el: project(dcoll, src, tgt, el), vec)
-
-    if isinstance(vec, Number):
-        return vec
-
-    return dcoll.connection_from_dds(src, tgt)(vec)
-
-
-# {{{ geometric properties
-
-def nodes(dcoll, dd=None):
-    r"""Return the nodes of a discretization.
-
-    :arg dd: a :class:`~grudge.dof_desc.DOFDesc`, or a value convertible to one.
-        Defaults to the base volume discretization.
-    :returns: an object array of :class:`~meshmode.dof_array.DOFArray`\ s
-    """
-    if dd is None:
-        return dcoll._volume_discr.nodes()
-    else:
-        return dcoll.discr_from_dd(dd).nodes()
-
-
-@memoize_on_first_arg
-def normal(dcoll, dd):
-    """Get unit normal to specified surface discretization, *dd*.
-
-    :arg dd: a :class:`~grudge.dof_desc.DOFDesc` as the surface discretization.
-    :returns: an object array of :class:`~meshmode.dof_array.DOFArray`.
-    """
-    from grudge.geometry import normal
-    surface_discr = dcoll.discr_from_dd(dd)
-    actx = surface_discr._setup_actx
-    return actx.freeze(normal(actx, dcoll, dd))
-
-# }}}
-
-
-# {{{ Derivative operator
-
-def reference_derivative_matrices(actx, element_group):
-    # @keyed_memoize_in(
-    #     actx, reference_derivative_matrices,
-    #     lambda grp: grp.discretization_key())
-    def get_ref_derivative_mats(grp):
-        from meshmode.discretization.poly_element import diff_matrices
-        return tuple(
-            actx.freeze(actx.from_numpy(dfmat))
-            for dfmat in diff_matrices(grp)
-        )
-
-    return get_ref_derivative_mats(element_group)
-
-
-def _compute_local_gradient(dcoll, vec, xyz_axis):
-    from grudge.geometry import \
-        inverse_surface_metric_derivative
-
-    discr = dcoll.discr_from_dd(dof_desc.DD_VOLUME)
-    actx = vec.array_context
-
-    # FIXME: Each individual term comes out as (result,)
-    inverse_jac_t = [
-        inverse_surface_metric_derivative(
-            actx, dcoll, rst_axis, xyz_axis
-        )[0] for rst_axis in range(dcoll.dim)
-    ]
-
-    data = []
-    for grp, vec_i in zip(discr.groups, vec):
-        data.append(sum(
-            actx.einsum(
-                "ij,ej,ej->ei",
-                reference_derivative_matrices(actx, grp)[rst_axis],
-                vec_i,
-                inverse_jac_t[rst_axis],
-                arg_names=("ref_diff_mat", "vec", "inv_jac_t"),
-                tagged=(HasElementwiseMatvecTag(),)
-            ) for rst_axis in range(dcoll.dim))
-        )
-    return DOFArray(actx, data=tuple(data))
-
-
-def local_gradient(dcoll, vec):
-    from pytools.obj_array import make_obj_array
-    return make_obj_array(
-        [_compute_local_gradient(dcoll, vec, xyz_axis)
-         for xyz_axis in range(dcoll.dim)]
-    )
-
-# }}}
-
-
-# {{{ Mass operator
-
-def reference_mass_matrix(actx, out_element_group, in_element_group):
-    # @keyed_memoize_in(
-    #     actx, reference_mass_matrix,
-    #     lambda out_grp, in_grp: (out_grp.discretization_key(),
-    #                              in_grp.discretization_key()))
-    def get_ref_mass_mat(out_grp, in_grp):
-        if out_grp == in_grp:
-            from meshmode.discretization.poly_element import mass_matrix
-
-            return actx.freeze(
-                actx.from_numpy(
-                    np.asarray(
-                        mass_matrix(out_grp),
-                        order="C"
-                    )
-                )
-            )
-
-        from modepy import vandermonde
-        basis = out_grp.basis_obj()
-        vand = vandermonde(basis.functions, out_grp.unit_nodes)
-        o_vand = vandermonde(basis.functions, in_grp.unit_nodes)
-        vand_inv_t = np.linalg.inv(vand).T
-
-        weights = in_grp.quadrature_rule().weights
-        return actx.freeze(
-            actx.from_numpy(
-                np.asarray(
-                    np.einsum("j,ik,jk->ij", weights, vand_inv_t, o_vand),
-                    order="C"
-                )
-            )
-        )
-
-    return get_ref_mass_mat(out_element_group, in_element_group)
-
-
-def _apply_mass_operator(dcoll, dd_out, dd_in, vec):
-    from grudge.geometry import area_element
-
-    in_discr = dcoll.discr_from_dd(dd_in)
-    out_discr = dcoll.discr_from_dd(dd_out)
-
-    actx = vec.array_context
-    area_elements = area_element(actx, dcoll, dd=dd_in)
-    return DOFArray(
-        actx,
-        tuple(
-            actx.einsum(
-                "ij,ej,ej->ei",
-                reference_mass_matrix(
-                    actx,
-                    out_element_group=out_grp,
-                    in_element_group=in_grp
-                ),
-                ae_i,
-                vec_i,
-                arg_names=("mass_mat", "jac_det", "vec"),
-                tagged=(MassOperatorTag(),))
-
-            for in_grp, out_grp, ae_i, vec_i in zip(
-                    in_discr.groups, out_discr.groups, area_elements, vec)
-        )
-    )
-
-
-def mass_operator(dcoll, *args):
-    if len(args) == 1:
-        vec, = args
-        dd = dof_desc.DOFDesc("vol", dof_desc.QTAG_NONE)
-    elif len(args) == 2:
-        dd, vec = args
-    else:
-        raise TypeError("invalid number of arguments")
-
-    if isinstance(vec, np.ndarray):
-        return obj_array_vectorize(
-            lambda el: mass_operator(dcoll, dd, el), vec
-        )
-
-    return _apply_mass_operator(dcoll, dof_desc.DD_VOLUME, dd, vec)
-
-# }}}
-
-
-# {{{ Stiffness transpose operator
-
-def reference_stiffness_transpose_matrix(actx, out_element_group, in_element_group):
-
-    # FIXME: Think about how to compose this better with existing functions
-    # @keyed_memoize_in(
-    #     actx, reference_stiffness_transpose_matrix,
-    #     lambda out_grp, in_grp: (out_grp.discretization_key(),
-    #                              in_grp.discretization_key()))
-    def get_ref_stiffness_transpose_mat(out_grp, in_grp):
-        if in_grp == out_grp:
-            mmat = reference_mass_matrix(actx, out_grp, in_grp)
-            return [dmat.T.dot(mmat.T)
-                    for dmat in reference_derivative_matrices(actx, in_grp)]
-
-        from modepy import vandermonde
-        basis = out_grp.basis_obj()
-        vand = vandermonde(basis.functions, out_grp.unit_nodes)
-        grad_vand = vandermonde(basis.gradients, in_grp.unit_nodes)
-        vand_inv_t = np.linalg.inv(vand).T
-
-        if not isinstance(grad_vand, tuple):
-            # NOTE: special case for 1d
-            grad_vand = (grad_vand,)
-
-        weights = in_grp.quadrature_rule().weights
-        return actx.freeze(
-            actx.from_numpy(
-                np.einsum(
-                    "c,bz,acz->abc",
-                    weights,
-                    vand_inv_t,
-                    grad_vand
-                )
-            )
-        )
-    return get_ref_stiffness_transpose_mat(out_element_group,
-                                           in_element_group)
-
-
-def _apply_stiffness_transpose_operator(dcoll, dd_out, dd_in, vec, xyz_axis):
-    from grudge.geometry import \
-        inverse_surface_metric_derivative, area_element
-
-    in_discr = dcoll.discr_from_dd(dd_in)
-    out_discr = dcoll.discr_from_dd(dd_out)
-
-    actx = vec.array_context
-    area_elements = area_element(actx, dcoll, dd=dd_in)
-    # FIXME: Each individual term comes out as (result,)
-    inverse_jac_t = [
-        inverse_surface_metric_derivative(
-            actx, dcoll, rst_axis, xyz_axis
-        )[0] for rst_axis in range(dcoll.dim)
-    ]
-    data = []
-    for out_grp, in_grp, vec_i, ae_i in zip(out_discr.groups,
-                                            in_discr.groups,
-                                            vec,
-                                            area_elements):
-        ref_stiffness_t = reference_stiffness_transpose_matrix(
-            actx,
-            out_element_group=out_grp,
-            in_element_group=in_grp
-        )
-        data.append(sum(
-            actx.einsum(
-                "ej,ij,ej,ej->ei",
-                inverse_jac_t[rst_axis],
-                ref_stiffness_t[rst_axis],
-                vec_i,
-                ae_i,
-                arg_names=("inv_jac_t", "ref_stiffT_mat", "vec", "jac"),
-                tagged=(HasElementwiseMatvecTag(),)
-            ) for rst_axis in range(dcoll.dim))
-        )
-    return DOFArray(actx, data=tuple(data))
-
-
-def stiffness_transpose_operator(dcoll, *args):
-    if len(args) == 1:
-        vec, = args
-        dd = dof_desc.DOFDesc("vol", dof_desc.QTAG_NONE)
-    elif len(args) == 2:
-        dd, vec = args
-    else:
-        raise TypeError("invalid number of arguments")
-
-    from pytools.obj_array import make_obj_array
-
-    return make_obj_array(
-        [_apply_stiffness_transpose_operator(dcoll,
-                                             dof_desc.DD_VOLUME,
-                                             dd, vec, xyz_axis)
-         for xyz_axis in range(dcoll.dim)]
-    )
-
-# }}}
-
-
-# {{{ Mass inverse operator
-
-def reference_inverse_mass_matrix(actx, element_group):
-    # @keyed_memoize_in(
-    #     actx, reference_inverse_mass_matrix,
-    #     lambda grp: grp.discretization_key())
-    def get_ref_inv_mass_mat(grp):
-        from modepy import inverse_mass_matrix
-        basis = grp.basis_obj()
-
-        return actx.freeze(
-            actx.from_numpy(
-                np.asarray(
-                    inverse_mass_matrix(basis.functions, grp.unit_nodes),
-                    order="C"
-                )
-            )
-        )
-
-    return get_ref_inv_mass_mat(element_group)
-
-
-def _apply_inverse_mass_operator(dcoll, dd_out, dd_in, vec):
-    from grudge.geometry import area_element
-
-    if dd_out != dd_in:
-        raise ValueError(
-            "Cannot compute inverse of a mass matrix mapping "
-            "between different element groups; inverse is not "
-            "guaranteed to be well-defined"
-        )
-    discr = dcoll.discr_from_dd(dd_in)
-    use_wadg = not all(grp.is_affine for grp in discr.groups)
-
-    actx = vec.array_context
-    inv_area_elements = 1./area_element(actx, dcoll, dd=dd_in)
-    if use_wadg:
-        # FIXME: Think of how to compose existing functions here...
-        # NOTE: Rewritten for readability/debuggability
-        grps = discr.groups
-        data = []
-        for grp, jac_inv, x in zip(grps, inv_area_elements, vec):
-            ref_mass = reference_mass_matrix(actx,
-                                             out_element_group=grp,
-                                             in_element_group=grp)
-            ref_mass_inv = reference_inverse_mass_matrix(actx,
-                                                         element_group=grp)
-            data.append(
-                # Based on https://arxiv.org/pdf/1608.03836.pdf
-                # true_Minv ~ ref_Minv * ref_M * (1/jac_det) * ref_Minv
-                actx.einsum("ik,km,em,mj,ej->ei",
-                            ref_mass_inv, ref_mass, jac_inv, ref_mass_inv, x,
-                            tagged=(MassOperatorTag(),))
-            )
-        return DOFArray(actx, data=tuple(data))
-    else:
-        return DOFArray(
-            actx,
-            tuple(
-                actx.einsum("ij,ej,ej->ei",
-                            reference_inverse_mass_matrix(
-                                actx,
-                                element_group=grp
-                            ),
-                            iae_i,
-                            vec_i,
-                            arg_names=("mass_inv_mat", "jac_det_inv", "vec"),
-                            tagged=(MassOperatorTag(),))
-
-                for grp, iae_i, vec_i in zip(discr.groups,
-                                             inv_area_elements, vec)
-            )
-        )
-
-
-def inverse_mass_operator(dcoll, vec):
-    if isinstance(vec, np.ndarray):
-        return obj_array_vectorize(
-            lambda el: inverse_mass_operator(dcoll, el), vec
-        )
-
-    return _apply_inverse_mass_operator(
-        dcoll, dof_desc.DD_VOLUME, dof_desc.DD_VOLUME, vec
-    )
-
-# }}}
-
-
-# {{{ Face mass operator
-
-def reference_face_mass_matrix(actx, face_element_group, vol_element_group, dtype):
-
-    # @keyed_memoize_in(
-    #     actx, reference_mass_matrix,
-    #     lambda face_grp, vol_grp: (face_grp.discretization_key(),
-    #                                vol_grp.discretization_key()))
-    def get_ref_face_mass_mat(face_grp, vol_grp):
-        nfaces = vol_grp.mesh_el_group.nfaces
-        assert (face_grp.nelements
-                == nfaces * vol_grp.nelements)
-
-        matrix = np.empty(
-            (vol_grp.nunit_dofs,
-            nfaces,
-            face_grp.nunit_dofs),
-            dtype=dtype
-        )
-
-        import modepy as mp
-        from meshmode.discretization import ElementGroupWithBasis
-        from meshmode.discretization.poly_element import \
-            QuadratureSimplexElementGroup
-
-        n = vol_grp.order
-        m = face_grp.order
-        vol_basis = vol_grp.basis_obj()
-        faces = mp.faces_for_shape(vol_grp.shape)
-
-        for iface, face in enumerate(faces):
-            # If the face group is defined on a higher-order
-            # quadrature grid, use the underlying quadrature rule
-            if isinstance(face_grp, QuadratureSimplexElementGroup):
-                face_quadrature = face_grp.quadrature_rule()
-                if face_quadrature.exact_to < m:
-                    raise ValueError(
-                        "The face quadrature rule is only exact for polynomials "
-                        f"of total degree {face_quadrature.exact_to}. Please "
-                        "ensure a quadrature rule is used that is at least "
-                        f"exact for degree {m}."
-                    )
-            else:
-                # NOTE: This handles the general case where
-                # volume and surface quadrature rules may have different
-                # integration orders
-                face_quadrature = mp.quadrature_for_space(
-                    mp.space_for_shape(face, 2*max(n, m)),
-                    face
-                )
-
-            # If the group has a nodal basis and is unisolvent,
-            # we use the basis on the face to compute the face mass matrix
-            if (isinstance(face_grp, ElementGroupWithBasis)
-                    and face_grp.space.space_dim
-                    == face_grp.nunit_dofs):
-
-                face_basis = face_grp.basis_obj()
-
-                # Sanity check for face quadrature accuracy. Not integrating
-                # degree N + M polynomials here is asking for a bad time.
-                if face_quadrature.exact_to < m + n:
-                    raise ValueError(
-                        "The face quadrature rule is only exact for polynomials "
-                        f"of total degree {face_quadrature.exact_to}. Please "
-                        "ensure a quadrature rule is used that is at least "
-                        f"exact for degree {n+m}."
-                    )
-
-                matrix[:, iface, :] = mp.nodal_mass_matrix_for_face(
-                    face, face_quadrature,
-                    face_basis.functions, vol_basis.functions,
-                    vol_grp.unit_nodes,
-                    face_grp.unit_nodes,
-                )
-            else:
-                # Otherwise, we use a routine that is purely quadrature-based
-                # (no need for explicit face basis functions)
-                matrix[:, iface, :] = mp.nodal_quad_mass_matrix_for_face(
-                    face,
-                    face_quadrature,
-                    vol_basis.functions,
-                    vol_grp.unit_nodes,
-                )
-
-        return actx.freeze(actx.from_numpy(matrix))
-
-    return get_ref_face_mass_mat(face_element_group, vol_element_group)
-
-
-def _apply_face_mass_operator(dcoll, dd, vec):
-    from grudge.geometry import area_element
-
-    volm_discr = dcoll.discr_from_dd(dof_desc.DD_VOLUME)
-    face_discr = dcoll.discr_from_dd(dd)
-    dtype = vec.entry_dtype
-    actx = vec.array_context
-    surf_area_elements = area_element(actx, dcoll, dd=dd)
-
-    @memoize_in(actx, (_apply_face_mass_operator, "face_mass_knl"))
-    def prg():
-        return make_loopy_program(
-            """{[iel,idof,f,j]:
-                0<=iel<nelements and
-                0<=f<nfaces and
-                0<=idof<nvol_nodes and
-                0<=j<nface_nodes}""",
-            """
-            result[iel,idof] = sum(f, sum(j, mat[idof, f, j] \
-                                             * jac_surf[f, iel, j] \
-                                             * vec[f, iel, j]))
-            """,
-            name="face_mass"
-        )
-
-    result = volm_discr.empty(actx, dtype=dtype)
-    assert len(face_discr.groups) == len(volm_discr.groups)
-
-    for afgrp, volgrp in zip(face_discr.groups, volm_discr.groups):
-
-        nfaces = volgrp.mesh_el_group.nfaces
-        matrix = reference_face_mass_matrix(
-            actx,
-            face_element_group=afgrp,
-            vol_element_group=volgrp,
-            dtype=dtype
-        )
-        input_view = result[afgrp.index].reshape(
-            nfaces, volgrp.nelements, afgrp.nunit_dofs
-        )
-        jac_surf = surf_area_elements[afgrp.index].reshape(
-            nfaces, volgrp.nelements, afgrp.nunit_dofs
-        )
-        actx.call_loopy(
-            prg(),
-            mat=matrix,
-            result=result[volgrp.index],
-            jac_surf=jac_surf,
-            vec=input_view
-        )
-    return result
-
-
-def face_mass_operator(dcoll, *args):
-    if len(args) == 1:
-        vec, = args
-        dd = dof_desc.DOFDesc("all_faces", dof_desc.QTAG_NONE)
-    elif len(args) == 2:
-        dd, vec = args
-    else:
-        raise TypeError("invalid number of arguments")
-
-    if isinstance(vec, np.ndarray):
-        return obj_array_vectorize(
-                lambda el: face_mass_operator(dcoll, dd, el), vec)
-
-    return _apply_face_mass_operator(dcoll, dd, vec)
-
-# }}}
-
-
-# vim: foldmethod=marker
diff --git a/test/test_new_world_grudge.py b/test/test_new_world_grudge.py
index d7719a247ea30d4fc32596dd01d4e607898a1434..30b2d08241bac910ffb0a690523466dd79a9a0cc 100644
--- a/test/test_new_world_grudge.py
+++ b/test/test_new_world_grudge.py
@@ -31,7 +31,7 @@ from meshmode.array_context import (  # noqa
         as pytest_generate_tests)
 import meshmode.mesh.generation as mgen
 
-import grudge.operators as op
+import grudge.op as op
 
 from grudge import DiscretizationCollection
 import grudge.dof_desc as dof_desc
@@ -140,14 +140,14 @@ def test_mass_mat_trig(actx_factory, ambient_dim, quad_tag):
     f_quad = f(x_quad)
     ones_quad = quad_disc.zeros(actx) + 1
 
-    mop_1 = op.mass_operator(dcoll, dd_quad, f_quad)
+    mop_1 = op.mass(dcoll, dd_quad, f_quad)
     num_integral_1 = np.dot(actx.to_numpy(flatten(ones_volm)),
                             actx.to_numpy(flatten(mop_1)))
 
     err_1 = abs(num_integral_1 - true_integral)
     assert err_1 < 1e-9, err_1
 
-    mop_2 = op.mass_operator(dcoll, dd_quad, ones_quad)
+    mop_2 = op.mass(dcoll, dd_quad, ones_quad)
     num_integral_2 = np.dot(actx.to_numpy(flatten(f_volm)),
                             actx.to_numpy(flatten(mop_2)))
 
@@ -232,7 +232,7 @@ def test_mass_surface_area(actx_factory, name):
 
         dd = dof_desc.DD_VOLUME
         ones_volm = volume_discr.zeros(actx) + 1
-        flattened_mass_weights = flatten(op.mass_operator(dcoll, dd, ones_volm))
+        flattened_mass_weights = flatten(op.mass(dcoll, dd, ones_volm))
         approx_surface_area = np.dot(actx.to_numpy(flatten(ones_volm)),
                                      actx.to_numpy(flattened_mass_weights))
 
@@ -300,8 +300,8 @@ def test_surface_mass_operator_inverse(actx_factory, name):
         x_volm = thaw(actx, volume_discr.nodes())
         f_volm = f(x_volm)
 
-        res = op.inverse_mass_operator(
-            dcoll, op.mass_operator(dcoll, dd, f_volm)
+        res = op.inverse_mass(
+            dcoll, op.mass(dcoll, dd, f_volm)
         )
 
         inv_error = actx.np.linalg.norm(res - f_volm, ord=2)
@@ -311,7 +311,7 @@ def test_surface_mass_operator_inverse(actx_factory, name):
         # {{{ compute h_max from mass weights
 
         ones_volm = volume_discr.zeros(actx) + 1
-        flattened_mass_weights = flatten(op.mass_operator(dcoll, dd, ones_volm))
+        flattened_mass_weights = flatten(op.mass(dcoll, dd, ones_volm))
         h_max = actx.np.max(flattened_mass_weights) ** (1/dcoll.dim)
 
         # }}}
@@ -359,7 +359,7 @@ def test_tri_diff_mat(actx_factory, dim, order=4):
 
         for axis in range(dim):
 
-            df_num = op.local_gradient(dcoll, f(x, axis))[axis]
+            df_num = op.local_grad(dcoll, f(x, axis))[axis]
             df_volm = df(x, axis)
 
             linf_error = actx.np.linalg.norm(df_num - df_volm, ord=np.inf)