diff --git a/examples/advection/var-velocity.py b/examples/advection/var-velocity.py
index ddc7535288cd82c1bbc56cb8decd9b0644cea73a..00e4f0c8496dced20a21e808e68986ecec1d5be2 100644
--- a/examples/advection/var-velocity.py
+++ b/examples/advection/var-velocity.py
@@ -1,17 +1,26 @@
-# Copyright (C) 2007 Andreas Kloeckner
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+from __future__ import division, absolute_import
+
+__copyright__ = "Copyright (C) 2017 Bogdan Enache"
+
+__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
@@ -37,20 +46,19 @@ def main(write_output=True, order=4):
 
     dim = 2
 
+    resolution = 15
     from meshmode.mesh.generation import generate_regular_rect_mesh
     mesh = generate_regular_rect_mesh(a=(-0.5, -0.5), b=(0.5, 0.5),
-            n=(15, 15), order=order)
+            n=(resolution, resolution), order=order)
 
-    dt_factor = 4
-    h = 1/20
-
-    discr = DGDiscretizationWithBoundaries(cl_ctx, mesh, order=order)
+    dt_factor = 5
+    h = 1/resolution
 
     sym_x = sym.nodes(2)
 
     advec_v = join_fields(-1*sym_x[1], sym_x[0]) / 2
 
-    flux_type = "central"
+    flux_type = "upwind"
 
     source_center = np.array([0.1, 0.1])
     source_width = 0.05
@@ -58,30 +66,46 @@ def main(write_output=True, order=4):
     sym_x = sym.nodes(2)
     sym_source_center_dist = sym_x - source_center
 
-    def f(x):
+    def f_gaussian(x):
         return sym.exp(
                     -np.dot(sym_source_center_dist, sym_source_center_dist)
                     / source_width**2)
 
+    def f_step(x):
+        return sym.If(
+                sym.Comparison(
+                    np.dot(sym_source_center_dist, sym_source_center_dist),
+                    "<",
+                    (4*source_width)**2),
+                1, 0)
+
     def u_analytic(x):
         return 0
 
     from grudge.models.advection import VariableCoefficientAdvectionOperator
+    from meshmode.discretization.poly_element import QuadratureSimplexGroupFactory  # noqa
+
+    discr = DGDiscretizationWithBoundaries(cl_ctx, mesh, order=order,
+            quad_tag_to_group_factory={
+                #"product": None,
+                "product": QuadratureSimplexGroupFactory(order=4*order)
+                })
 
-    discr = DGDiscretizationWithBoundaries(cl_ctx, mesh, order=order)
     op = VariableCoefficientAdvectionOperator(2, advec_v,
-        inflow_u=u_analytic(sym.nodes(dim, sym.BTAG_ALL)),
+        u_analytic(sym.nodes(dim, sym.BTAG_ALL)), quad_tag="product",
         flux_type=flux_type)
 
-    bound_op = bind(discr, op.sym_operator())
+    bound_op = bind(
+            discr, op.sym_operator(),
+            #debug_flags=["dump_sym_operator_stages"]
+            )
 
-    u = bind(discr, f(sym.nodes(dim)))(queue, t=0)
+    u = bind(discr, f_gaussian(sym.nodes(dim)))(queue, t=0)
 
     def rhs(t, u):
         return bound_op(queue, t=t, u=u)
 
-    final_time = 2
-
+    final_time = 50
     dt = dt_factor * h/order**2
     nsteps = (final_time // dt) + 1
     dt = final_time/nsteps + 1e-15
@@ -90,7 +114,7 @@ def main(write_output=True, order=4):
     dt_stepper = set_up_rk4("u", dt, u, rhs)
 
     from grudge.shortcuts import make_visualizer
-    vis = make_visualizer(discr, vis_order=order)
+    vis = make_visualizer(discr, vis_order=2*order)
 
     step = 0
 
@@ -98,10 +122,11 @@ def main(write_output=True, order=4):
         if isinstance(event, dt_stepper.StateComputed):
 
             step += 1
+            if step % 30 == 0:
+                print(step)
 
-            #print(step, event.t, norm(queue, u=event.state_component[0]))
-            vis.write_vtk_file("fld-%04d.vtu" % step,
-                    [("u", event.state_component)])
+                vis.write_vtk_file("fld-%04d.vtu" % step,
+                        [("u", event.state_component)])
 
 
 if __name__ == "__main__":
diff --git a/grudge/discretization.py b/grudge/discretization.py
index bc59299fefdbc7437a2efd9136c0a83bd4b71c04..74b2060138a888f40f35319df1d983f07e6e019a 100644
--- a/grudge/discretization.py
+++ b/grudge/discretization.py
@@ -1,6 +1,6 @@
 from __future__ import division, absolute_import
 
-__copyright__ = "Copyright (C) 2015 Andreas Kloeckner"
+__copyright__ = "Copyright (C) 2015-2017 Andreas Kloeckner, Bogdan Enache"
 
 __license__ = """
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -49,18 +49,22 @@ class DGDiscretizationWithBoundaries(DiscretizationBase):
     .. automethod :: zeros
     """
 
-    def __init__(self, cl_ctx, mesh, order, quad_min_degrees=None,
+    def __init__(self, cl_ctx, mesh, order, quad_tag_to_group_factory=None,
             mpi_communicator=None):
         """
-        :param quad_min_degrees: A mapping from quadrature tags to the degrees
-            to which the desired quadrature is supposed to be exact.
+        :param quad_tag_to_group_factory: A mapping from quadrature tags (typically
+            strings--but may be any hashable/comparable object) to a
+            :class:`meshmode.discretization.ElementGroupFactory` indicating with
+            which quadrature 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.
         """
 
-        if quad_min_degrees is None:
-            quad_min_degrees = {}
+        if quad_tag_to_group_factory is None:
+            quad_tag_to_group_factory = {}
 
         self.order = order
-        self.quad_min_degrees = quad_min_degrees
+        self.quad_tag_to_group_factory = quad_tag_to_group_factory
 
         from meshmode.discretization import Discretization
 
@@ -129,16 +133,26 @@ class DGDiscretizationWithBoundaries(DiscretizationBase):
 
         if dd.is_volume():
             if qtag is not sym.QTAG_NONE:
-                # FIXME
-                raise NotImplementedError("quadrature")
+                return self._quad_volume_discr(qtag)
             return self._volume_discr
 
-        elif dd.domain_tag is sym.FACE_RESTR_ALL:
-            return self._all_faces_discr(qtag)
+        if qtag is not sym.QTAG_NONE:
+            no_quad_discr = self.discr_from_dd(sym.DOFDesc(dd.domain_tag))
+
+            from meshmode.discretization import Discretization
+            return Discretization(
+                    self._volume_discr.cl_context,
+                    no_quad_discr.mesh,
+                    self.group_factory_for_quadrature_tag(qtag))
+
+        assert qtag is sym.QTAG_NONE
+
+        if dd.domain_tag is sym.FACE_RESTR_ALL:
+            return self._all_faces_volume_connection().to_discr
         elif dd.domain_tag is sym.FACE_RESTR_INTERIOR:
-            return self._interior_faces_discr(qtag)
+            return self._interior_faces_connection().to_discr
         elif dd.is_boundary():
-            return self._boundary_discr(dd.domain_tag, qtag)
+            return self._boundary_connection(dd.domain_tag).to_discr
         else:
             raise ValueError("DOF desc tag not understood: " + str(dd))
 
@@ -147,18 +161,32 @@ class DGDiscretizationWithBoundaries(DiscretizationBase):
         from_dd = sym.as_dofdesc(from_dd)
         to_dd = sym.as_dofdesc(to_dd)
 
-        if from_dd.quadrature_tag is not sym.QTAG_NONE:
-            raise ValueError("cannot interpolate *from* a "
-                    "(non-interpolatory) quadrature grid")
-
         to_qtag = to_dd.quadrature_tag
 
+        if (
+                not from_dd.is_volume()
+                and from_dd.quadrature_tag == to_dd.quadrature_tag
+                and to_dd.domain_tag is sym.FACE_RESTR_ALL):
+            faces_conn = self.connection_from_dds(
+                    sym.DOFDesc("vol"),
+                    sym.DOFDesc(from_dd.domain_tag))
+
+            from meshmode.discretization.connection import \
+                    make_face_to_all_faces_embedding
+
+            return make_face_to_all_faces_embedding(
+                    faces_conn, self.discr_from_dd(to_dd),
+                    self.discr_from_dd(from_dd))
+
         # {{{ simplify domain + qtag change into chained
 
-        if (from_dd.domain_tag != to_dd.domain_tag
-                and from_dd.quadrature_tag != to_dd.quadrature_tag):
+        if (
+                from_dd.domain_tag != to_dd.domain_tag
+                and from_dd.quadrature_tag is sym.QTAG_NONE
+                and to_dd.quadrature_tag is not sym.QTAG_NONE):
 
-            from meshmode.connection import ChainedDiscretizationConnection
+            from meshmode.discretization.connection import \
+                    ChainedDiscretizationConnection
             intermediate_dd = sym.DOFDesc(to_dd.domain_tag)
             return ChainedDiscretizationConnection(
                     [
@@ -177,8 +205,10 @@ class DGDiscretizationWithBoundaries(DiscretizationBase):
 
         # {{{ generic to-quad
 
-        if (from_dd.domain_tag == to_dd.domain_tag
-                and from_dd.quadrature_tag != to_dd.quadrature_tag):
+        if (
+                from_dd.domain_tag == to_dd.domain_tag
+                and from_dd.quadrature_tag is sym.QTAG_NONE
+                and to_dd.quadrature_tag is not sym.QTAG_NONE):
             from meshmode.discretization.connection.same_mesh import \
                     make_same_mesh_connection
 
@@ -188,38 +218,30 @@ class DGDiscretizationWithBoundaries(DiscretizationBase):
 
         # }}}
 
+        if from_dd.quadrature_tag is not sym.QTAG_NONE:
+            raise ValueError("cannot interpolate *from* a "
+                    "(non-interpolatory) quadrature grid")
+
+        assert to_qtag is sym.QTAG_NONE
+
         if from_dd.is_volume():
             if to_dd.domain_tag is sym.FACE_RESTR_ALL:
-                return self._all_faces_volume_connection(to_qtag)
+                return self._all_faces_volume_connection()
             if to_dd.domain_tag is sym.FACE_RESTR_INTERIOR:
-                return self._interior_faces_connection(to_qtag)
+                return self._interior_faces_connection()
             elif to_dd.is_boundary():
                 assert from_dd.quadrature_tag is sym.QTAG_NONE
                 return self._boundary_connection(to_dd.domain_tag)
+            elif to_dd.is_volume():
+                from meshmode.discretization.connection import \
+                        make_same_mesh_connection
+                to_discr = self._quad_volume_discr(to_dd.quadrature_tag)
+                from_discr = self._volume_discr
+                return make_same_mesh_connection(to_discr, from_discr)
 
             else:
                 raise ValueError("cannot interpolate from volume to: " + str(to_dd))
 
-        elif from_dd.domain_tag is sym.FACE_RESTR_INTERIOR:
-            if to_dd.domain_tag is sym.FACE_RESTR_ALL and to_qtag is sym.QTAG_NONE:
-                return self._all_faces_connection(None)
-            else:
-                raise ValueError(
-                        "cannot interpolate from interior faces to: "
-                        + str(to_dd))
-
-        elif from_dd.domain_tag is sym.FACE_RESTR_ALL:
-            if to_dd.domain_tag is sym.FACE_RESTR_ALL and to_qtag is sym.QTAG_NONE:
-                return self._all_faces_connection(None)
-
-        elif from_dd.is_boundary():
-            if to_dd.domain_tag is sym.FACE_RESTR_ALL and to_qtag is sym.QTAG_NONE:
-                return self._all_faces_connection(from_dd.domain_tag)
-            else:
-                raise ValueError(
-                        "cannot interpolate from interior faces to: "
-                        + str(to_dd))
-
         else:
             raise ValueError("cannot interpolate from: " + str(from_dd))
 
@@ -232,11 +254,10 @@ class DGDiscretizationWithBoundaries(DiscretizationBase):
             quadrature_tag = sym.QTAG_NONE
 
         from meshmode.discretization.poly_element import \
-                PolynomialWarpAndBlendGroupFactory, \
-                QuadratureSimplexGroupFactory
+                PolynomialWarpAndBlendGroupFactory
 
         if quadrature_tag is not sym.QTAG_NONE:
-            return QuadratureSimplexGroupFactory(order=self.order)
+            return self.quad_tag_to_group_factory[quadrature_tag]
         else:
             return PolynomialWarpAndBlendGroupFactory(order=self.order)
 
@@ -257,33 +278,17 @@ class DGDiscretizationWithBoundaries(DiscretizationBase):
                         self.group_factory_for_quadrature_tag(sym.QTAG_NONE),
                         boundary_tag=boundary_tag)
 
-    @memoize_method
-    def _boundary_discr(self, boundary_tag, quadrature_tag=None):
-        if quadrature_tag is None:
-            quadrature_tag = sym.QTAG_NONE
-
-        if quadrature_tag is sym.QTAG_NONE:
-            return self._boundary_connection(boundary_tag).to_discr
-        else:
-            no_quad_bdry_discr = self.boundary_discr(boundary_tag, sym.QTAG_NONE)
-
-            from meshmode.discretization import Discretization
-            return Discretization(
-                    self._volume_discr.cl_context,
-                    no_quad_bdry_discr.mesh,
-                    self.group_factory_for_quadrature_tag(quadrature_tag))
-
     # }}}
 
     # {{{ interior faces
 
     @memoize_method
-    def _interior_faces_connection(self, quadrature_tag=None):
+    def _interior_faces_connection(self):
         from meshmode.discretization.connection import (
                 make_face_restriction, FACE_RESTR_INTERIOR)
         return make_face_restriction(
                         self._volume_discr,
-                        self.group_factory_for_quadrature_tag(quadrature_tag),
+                        self.group_factory_for_quadrature_tag(sym.QTAG_NONE),
                         FACE_RESTR_INTERIOR,
 
                         # FIXME: This will need to change as soon as we support
@@ -291,32 +296,24 @@ class DGDiscretizationWithBoundaries(DiscretizationBase):
                         # types.
                         per_face_groups=False)
 
-    def _interior_faces_discr(self, quadrature_tag=None):
-        return self._interior_faces_connection(quadrature_tag).to_discr
-
     @memoize_method
-    def opposite_face_connection(self, quadrature_tag):
-        if quadrature_tag is not sym.QTAG_NONE:
-            # FIXME
-            raise NotImplementedError("quadrature")
-
+    def opposite_face_connection(self):
         from meshmode.discretization.connection import \
                 make_opposite_face_connection
 
-        return make_opposite_face_connection(
-                self._interior_faces_connection(quadrature_tag))
+        return make_opposite_face_connection(self._interior_faces_connection())
 
     # }}}
 
     # {{{ all-faces
 
     @memoize_method
-    def _all_faces_volume_connection(self, quadrature_tag=None):
+    def _all_faces_volume_connection(self):
         from meshmode.discretization.connection import (
                 make_face_restriction, FACE_RESTR_ALL)
         return make_face_restriction(
                         self._volume_discr,
-                        self.group_factory_for_quadrature_tag(quadrature_tag),
+                        self.group_factory_for_quadrature_tag(sym.QTAG_NONE),
                         FACE_RESTR_ALL,
 
                         # FIXME: This will need to change as soon as we support
@@ -324,30 +321,6 @@ class DGDiscretizationWithBoundaries(DiscretizationBase):
                         # types.
                         per_face_groups=False)
 
-    def _all_faces_discr(self, quadrature_tag=None):
-        return self._all_faces_volume_connection(quadrature_tag).to_discr
-
-    @memoize_method
-    def _all_faces_connection(self, boundary_tag):
-        """Return a
-        :class:`meshmode.discretization.connection.DiscretizationConnection`
-        that goes from either
-        :meth:`_interior_faces_discr` (if *boundary_tag* is None)
-        or
-        :meth:`_boundary_discr` (if *boundary_tag* is not None)
-        to a discretization containing all the faces of the volume
-        discretization.
-        """
-        from meshmode.discretization.connection import \
-                make_face_to_all_faces_embedding
-
-        if boundary_tag is None:
-            faces_conn = self._interior_faces_connection()
-        else:
-            faces_conn = self._boundary_connection(boundary_tag)
-
-        return make_face_to_all_faces_embedding(faces_conn, self._all_faces_discr())
-
     # }}}
 
     @property
diff --git a/grudge/execution.py b/grudge/execution.py
index 1fdec1b9aa6ea3942d94e5ce5010996aa6ce2eb6..cc8703dbf75cd84f0908bc2166eca762b1ad5e42 100644
--- a/grudge/execution.py
+++ b/grudge/execution.py
@@ -1,6 +1,6 @@
 from __future__ import division, absolute_import
 
-__copyright__ = "Copyright (C) 2015 Andreas Kloeckner"
+__copyright__ = "Copyright (C) 2015-2017 Andreas Kloeckner, Bogdan Enache"
 
 __license__ = """
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -201,36 +201,37 @@ class ExecutionMapper(mappers.Evaluator,
             knl = lp.make_kernel(
                 """{[k,i,j]:
                     0<=k<nelements and
-                    0<=i,j<ndiscr_nodes}""",
+                    0<=i<ndiscr_nodes_out and
+                    0<=j<ndiscr_nodes_in}""",
                 "result[k,i] = sum(j, mat[i, j] * vec[k, j])",
                 default_offset=lp.auto, name="diff")
 
             knl = lp.split_iname(knl, "i", 16, inner_tag="l.0")
             return lp.tag_inames(knl, dict(k="g.0"))
 
-        discr = self.discrwb.discr_from_dd(op.dd_in)
+        in_discr = self.discrwb.discr_from_dd(op.dd_in)
+        out_discr = self.discrwb.discr_from_dd(op.dd_out)
 
-        # FIXME: This shouldn't really assume that it's dealing with a volume
-        # input. What about quadrature? What about boundaries?
-        result = discr.empty(
+        result = out_discr.empty(
                 queue=self.queue,
                 dtype=field.dtype, allocator=self.bound_op.allocator)
 
-        for grp in discr.groups:
-            cache_key = "elwise_linear", grp, op, field.dtype
+        for in_grp, out_grp in zip(in_discr.groups, out_discr.groups):
+
+            cache_key = "elwise_linear", in_grp, out_grp, op, field.dtype
             try:
                 matrix = self.bound_op.operator_data_cache[cache_key]
             except KeyError:
                 matrix = (
-                        cl.array.to_device(
-                            self.queue,
-                            np.asarray(op.matrix(grp), dtype=field.dtype))
-                        .with_queue(None))
+                    cl.array.to_device(
+                        self.queue,
+                        np.asarray(op.matrix(out_grp, in_grp), dtype=field.dtype))
+                    .with_queue(None))
 
                 self.bound_op.operator_data_cache[cache_key] = matrix
 
-            knl()(self.queue, mat=matrix, result=grp.view(result),
-                    vec=grp.view(field))
+            knl()(self.queue, mat=matrix, result=out_grp.view(result),
+                    vec=in_grp.view(field))
 
         return result
 
@@ -273,14 +274,7 @@ class ExecutionMapper(mappers.Evaluator,
         return shuffled_recv_vec
 
     def map_opposite_interior_face_swap(self, op, field_expr):
-        dd = op.dd_in
-
-        qtag = dd.quadrature_tag
-        if qtag is None:
-            # FIXME: Remove once proper quadrature support arrives
-            qtag = sym.QTAG_NONE
-
-        return self.discrwb.opposite_face_connection(qtag)(
+        return self.discrwb.opposite_face_connection()(
                 self.queue, self.rec(field_expr)).with_queue(self.queue)
 
     # {{{ face mass operator
@@ -316,8 +310,7 @@ class ExecutionMapper(mappers.Evaluator,
 
         assert len(all_faces_discr.groups) == len(vol_discr.groups)
 
-        for cgrp, afgrp, volgrp in zip(all_faces_conn.groups,
-                all_faces_discr.groups, vol_discr.groups):
+        for afgrp, volgrp in zip(all_faces_discr.groups, vol_discr.groups):
             cache_key = "face_mass", afgrp, op, field.dtype
 
             nfaces = volgrp.mesh_el_group.nfaces
@@ -392,10 +385,6 @@ class ExecutionMapper(mappers.Evaluator,
         # This should be unified with map_elementwise_linear, which should
         # be extended to support batching.
 
-        discr = self.discrwb.discr_from_dd(repr_op.dd_in)
-
-        # FIXME: Enable
-        # assert repr_op.dd_in == repr_op.dd_out
         assert repr_op.dd_in.domain_tag == repr_op.dd_out.domain_tag
 
         @memoize_in(self.discrwb, "reference_derivative_knl")
@@ -404,7 +393,8 @@ class ExecutionMapper(mappers.Evaluator,
                 """{[imatrix,k,i,j]:
                     0<=imatrix<nmatrices and
                     0<=k<nelements and
-                    0<=i,j<nunit_nodes}""",
+                    0<=i<nunit_nodes_out and
+                    0<=j<nunit_nodes_in}""",
                 """
                 result[imatrix, k, i] = sum(
                         j, diff_mat[imatrix, i, j] * vec[k, j])
@@ -415,26 +405,31 @@ class ExecutionMapper(mappers.Evaluator,
             return lp.tag_inames(knl, dict(k="g.0"))
 
         noperators = len(insn.operators)
-        result = discr.empty(
+
+        in_discr = self.discrwb.discr_from_dd(repr_op.dd_in)
+        out_discr = self.discrwb.discr_from_dd(repr_op.dd_out)
+
+        result = out_discr.empty(
                 queue=self.queue,
                 dtype=field.dtype, extra_dims=(noperators,),
                 allocator=self.bound_op.allocator)
 
-        for grp in discr.groups:
-            if grp.nelements == 0:
+        for in_grp, out_grp in zip(in_discr.groups, out_discr.groups):
+
+            if in_grp.nelements == 0:
                 continue
 
-            matrices = repr_op.matrices(grp)
+            matrices = repr_op.matrices(out_grp, in_grp)
 
             # FIXME: Should transfer matrices to device and cache them
             matrices_ary = np.empty((
-                noperators, grp.nunit_nodes, grp.nunit_nodes))
+                noperators, out_grp.nunit_nodes, in_grp.nunit_nodes))
             for i, op in enumerate(insn.operators):
                 matrices_ary[i] = matrices[op.rst_axis]
 
             knl()(self.queue,
                     diff_mat=matrices_ary,
-                    result=grp.view(result), vec=grp.view(field))
+                    result=out_grp.view(result), vec=in_grp.view(field))
 
         return [(name, result[i]) for i, name in enumerate(insn.names)], []
 
@@ -522,6 +517,10 @@ def process_sym_operator(discrwb, sym_operator, post_bind_mapper=None,
     dumper("before-cfold", sym_operator)
     sym_operator = mappers.CommutativeConstantFoldingMapper()(sym_operator)
 
+    dumper("before-qcheck", sym_operator)
+    sym_operator = mappers.QuadratureCheckerAndRemover(
+            discrwb.quad_tag_to_group_factory)(sym_operator)
+
     # Work around https://github.com/numpy/numpy/issues/9438
     #
     # The idea is that we need 1j as an expression to survive
@@ -537,26 +536,6 @@ def process_sym_operator(discrwb, sym_operator, post_bind_mapper=None,
             complex_type=discrwb.complex_dtype.type,
             )(sym_operator)
 
-    # Ordering restriction:
-    #
-    # - Must run constant fold before first type inference pass, because zeros,
-    # while allowed, violate typing constraints (because they can't be assigned
-    # a unique type), and need to be killed before the type inferrer sees them.
-
-    # FIXME: Reenable type inference
-
-    # from grudge.symbolic.mappers.type_inference import TypeInferrer
-    # dumper("before-specializer", sym_operator)
-    # sym_operator = mappers.OperatorSpecializer(
-    #         TypeInferrer()(sym_operator)
-    #         )(sym_operator)
-
-    # Ordering restriction:
-    #
-    # - Must run OperatorSpecializer before performing the GlobalToReferenceMapper,
-    # because otherwise it won't differentiate the type of grids (node or quadrature
-    # grids) that the operators will apply on.
-
     dumper("before-global-to-reference", sym_operator)
     sym_operator = mappers.GlobalToReferenceMapper(discrwb.ambient_dim)(sym_operator)
 
@@ -567,12 +546,6 @@ def process_sym_operator(discrwb, sym_operator, post_bind_mapper=None,
     connected_parts = get_connected_partitions(volume_mesh)
     sym_operator = mappers.DistributedMapper(connected_parts)(sym_operator)
 
-    # Ordering restriction:
-    #
-    # - Must specialize quadrature operators before performing inverse mass
-    # contraction, because there are no inverse-mass-contracted variants of the
-    # quadrature operators.
-
     dumper("before-imass", sym_operator)
     sym_operator = mappers.InverseMassContractor()(sym_operator)
 
@@ -598,19 +571,20 @@ def bind(discr, sym_operator, post_bind_mapper=lambda x: x,
 
     stage = [0]
 
-    def dump_optemplate(name, sym_operator):
-        if "dump_optemplate_stages" in debug_flags:
-            from grudge.tools import open_unique_debug_file
-            from grudge.optemplate import pretty
-            open_unique_debug_file("%02d-%s" % (stage[0], name), ".txt").write(
-                    pretty(sym_operator))
+    def dump_sym_operator(name, sym_operator):
+        if "dump_sym_operator_stages" in debug_flags:
+            from pytools.debug import open_unique_debug_file
+            outf, name = open_unique_debug_file("%02d-%s" % (stage[0], name), ".txt")
+            with outf:
+                outf.write(sym.pretty(sym_operator))
+
             stage[0] += 1
 
     sym_operator = process_sym_operator(
             discr,
             sym_operator,
             post_bind_mapper=post_bind_mapper,
-            dumper=dump_optemplate)
+            dumper=dump_sym_operator)
 
     from grudge.symbolic.compiler import OperatorCompiler
     discr_code, eval_code = OperatorCompiler(discr)(sym_operator)
diff --git a/grudge/models/advection.py b/grudge/models/advection.py
index 1c83ed9675d5a45c75b22e3b4d5ca7926acf9133..091b9870c4b82a3fde2fc21b84bf92e5ec62ab17 100644
--- a/grudge/models/advection.py
+++ b/grudge/models/advection.py
@@ -3,7 +3,7 @@
 
 from __future__ import division, absolute_import
 
-__copyright__ = "Copyright (C) 2009 Andreas Kloeckner"
+__copyright__ = "Copyright (C) 2009-2017 Andreas Kloeckner, Bogdan Enache"
 
 __license__ = """
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -132,13 +132,16 @@ class WeakAdvectionOperator(AdvectionOperatorBase):
 # {{{ variable-coefficient advection
 
 class VariableCoefficientAdvectionOperator(HyperbolicOperator):
-    def __init__(self, dim, v, inflow_u, flux_type="central"):
+    def __init__(self, dim, v, inflow_u, flux_type="central", quad_tag="product"):
         self.ambient_dim = dim
         self.v = v
         self.inflow_u = inflow_u
         self.flux_type = flux_type
 
+        self.quad_tag = quad_tag
+
     def flux(self, u):
+
         normal = sym.normal(u. dd, self.ambient_dim)
 
         surf_v = sym.interp("vol", u.dd)(self.v)
@@ -167,15 +170,26 @@ class VariableCoefficientAdvectionOperator(HyperbolicOperator):
         # boundary conditions -------------------------------------------------
         bc_in = self.inflow_u
 
+        all_faces_dd = sym.DOFDesc(sym.FACE_RESTR_ALL, self.quad_tag)
+        boundary_dd = sym.DOFDesc(sym.BTAG_ALL, self.quad_tag)
+
         def flux(pair):
-            return sym.interp(pair.dd, "all_faces")(
+            return sym.interp(pair.dd, all_faces_dd)(
                     self.flux(pair))
 
+        quad_dd = sym.DOFDesc("vol", self.quad_tag)
+
+        to_quad = sym.interp("vol", quad_dd)
+
+        stiff_t = sym.stiffness_t(self.ambient_dim, quad_dd, "vol")
+        quad_v = to_quad(self.v)
+        quad_u = to_quad(u)
+
         return sym.InverseMassOperator()(
-                np.dot(sym.stiffness_t(self.ambient_dim), sym.cse(self.v*u))
-                - sym.FaceMassOperator()(
-                    flux(sym.int_tpair(u))
-                    + flux(sym.bv_tpair(sym.BTAG_ALL, u, bc_in))
+                (stiff_t[0](quad_u * quad_v[0]) + stiff_t[1](quad_u * quad_v[1]))
+                - sym.FaceMassOperator(all_faces_dd, "vol")(
+                    flux(sym.int_tpair(u, self.quad_tag))
+                    + flux(sym.bv_tpair(boundary_dd, u, bc_in))
 
                     # FIXME: Add back support for inflow/outflow tags
                     #+ flux(sym.bv_tpair(self.inflow_tag, u, bc_in))
diff --git a/grudge/symbolic/compiler.py b/grudge/symbolic/compiler.py
index f6e238083266d4ba0c510243b116b2e49b024631..c555cea0a56e947b4a94e0cd1736e8fa2b97ce63 100644
--- a/grudge/symbolic/compiler.py
+++ b/grudge/symbolic/compiler.py
@@ -338,7 +338,7 @@ class Code(object):
         self.static_schedule_attempts = 5
 
     def dump_dataflow_graph(self):
-        from grudge.tools import open_unique_debug_file
+        from pytools.debug import open_unique_debug_file
 
         open_unique_debug_file("dataflow", ".dot")\
                 .write(dot_dataflow_graph(self, max_node_label_length=None))
@@ -792,7 +792,7 @@ def aggregate_assignments(inf_mapper, instructions, result,
 # }}}
 
 
-# {{{
+# {{{ to-loopy mapper
 
 def set_once(d, k, v):
     try:
diff --git a/grudge/symbolic/mappers/__init__.py b/grudge/symbolic/mappers/__init__.py
index a810a335b5ee144ea9fe6fe4aca837781fcace5a..2ddd6f5d41f0c4a6be33c4dddb4a744824d47a86 100644
--- a/grudge/symbolic/mappers/__init__.py
+++ b/grudge/symbolic/mappers/__init__.py
@@ -798,96 +798,67 @@ class NoCSEStringifyMapper(StringifyMapper):
 
 # {{{ quadrature support
 
-class QuadratureUpsamplerRemover(CSECachingMapperMixin, IdentityMapper):
-    def __init__(self, quad_min_degrees, do_warn=True):
+class QuadratureCheckerAndRemover(CSECachingMapperMixin, IdentityMapper):
+    """Checks whether all quadratu
+    """
+
+    def __init__(self, quad_tag_to_group_factory):
         IdentityMapper.__init__(self)
         CSECachingMapperMixin.__init__(self)
-        self.quad_min_degrees = quad_min_degrees
-        self.do_warn = do_warn
+        self.quad_tag_to_group_factory = quad_tag_to_group_factory
 
     map_common_subexpression_uncached = \
             IdentityMapper.map_common_subexpression
 
-    def map_operator_binding(self, expr):
-        if isinstance(expr.op, (op.QuadratureGridUpsampler,
-                op.QuadratureInteriorFacesGridUpsampler,
-                op.QuadratureBoundaryGridUpsampler)):
-            try:
-                min_degree = self.quad_min_degrees[expr.op.quadrature_tag]
-            except KeyError:
-                if self.do_warn:
-                    from warnings import warn
-                    warn("No minimum degree for quadrature tag '%s' specified--"
-                            "falling back to nodal evaluation"
-                            % expr.op.quadrature_tag)
-                return self.rec(expr.field)
-            else:
-                if min_degree is None:
-                    return self.rec(expr.field)
-                else:
-                    return expr.op(self.rec(expr.field))
-        else:
-            return IdentityMapper.map_operator_binding(self, expr)
+    def _process_dd(self, dd, location_descr):
+        from grudge.symbolic.primitives import DOFDesc, QTAG_NONE
+        if dd.quadrature_tag is not QTAG_NONE:
+            if dd.quadrature_tag not in self.quad_tag_to_group_factory:
+                raise ValueError("found unknown quadrature tag '%s' in '%s'"
+                        % (dd.quadrature_tag, location_descr))
 
+            grp_factory = self.quad_tag_to_group_factory[dd.quadrature_tag]
+            if grp_factory is None:
+                dd = DOFDesc(dd.domain_tag, QTAG_NONE)
 
-class QuadratureDetector(CSECachingMapperMixin, CombineMapper):
-    """For a given expression, this mapper returns the upsampling
-    operator in effect at the root of the expression, or *None*
-    if there isn't one.
-    """
-    class QuadStatusNotKnown:
-        pass
-
-    map_common_subexpression_uncached = \
-            CombineMapper.map_common_subexpression
+        return dd
 
-    def combine(self, values):
-        from pytools import single_valued
-        return single_valued([
-            v for v in values if v is not self.QuadStatusNotKnown])
+    def map_operator_binding(self, expr):
+        dd_in_orig = dd_in = expr.op.dd_in
+        dd_out_orig = dd_out = expr.op.dd_out
 
-    def map_variable(self, expr):
-        return None
+        dd_in = self._process_dd(dd_in, "dd_in of %s" % type(expr.op).__name__)
+        dd_out = self._process_dd(dd_out, "dd_out of %s" % type(expr.op).__name__)
 
-    def map_constant(self, expr):
-        return self.QuadStatusNotKnown
+        if dd_in_orig == dd_in and dd_out_orig == dd_out:
+            # unchanged
+            return IdentityMapper.map_operator_binding(self, expr)
 
-    def map_operator_binding(self, expr):
-        if isinstance(expr.op, (
-                op.DiffOperatorBase, op.FluxOperatorBase,
-                op.MassOperatorBase)):
-            return None
-        elif isinstance(expr.op, (op.QuadratureGridUpsampler,
-                op.QuadratureInteriorFacesGridUpsampler)):
-            return expr.op
-        else:
-            return CombineMapper.map_operator_binding(self, expr)
+        import grudge.symbolic.operators as op
+        # changed
 
+        if dd_in == dd_out and isinstance(expr.op, op.InterpolationOperator):
+            # This used to be to-quad interpolation and has become a no-op.
+            # Remove it.
+            return self.rec(expr.field)
 
-class QuadratureUpsamplerChanger(CSECachingMapperMixin, IdentityMapper):
-    """This mapper descends the expression tree, down to each
-    quadrature-consuming operator (diff, mass) along each branch.
-    It then change
-    """
-    def __init__(self, desired_quad_op):
-        IdentityMapper.__init__(self)
-        CSECachingMapperMixin.__init__(self)
+        if isinstance(expr.op, op.StiffnessTOperator):
+            new_op = type(expr.op)(expr.op.xyz_axis, dd_in, dd_out)
+        elif isinstance(expr.op, (op.FaceMassOperator, op.InterpolationOperator)):
+            new_op = type(expr.op)(dd_in, dd_out)
+        else:
+            raise NotImplementedError("do not know how to modify dd_in and dd_out "
+                    "in %s" % type(expr.op).__name__)
 
-        self.desired_quad_op = desired_quad_op
+        return new_op(self.rec(expr.field))
 
-    map_common_subexpression_uncached = \
-            IdentityMapper.map_common_subexpression
+    def map_ones(self, expr):
+        dd = self._process_dd(expr.dd, location_descr=type(expr).__name__)
+        return type(expr)(dd)
 
-    def map_operator_binding(self, expr):
-        if isinstance(expr.op, (
-                op.DiffOperatorBase, op.FluxOperatorBase,
-                op.MassOperatorBase)):
-            return expr
-        elif isinstance(expr.op, (op.QuadratureGridUpsampler,
-                op.QuadratureInteriorFacesGridUpsampler)):
-            return self.desired_quad_op(expr.field)
-        else:
-            return IdentityMapper.map_operator_binding(self, expr)
+    def map_node_coordinate_component(self, expr):
+        dd = self._process_dd(expr.dd, location_descr=type(expr).__name__)
+        return type(expr)(expr.axis, dd)
 
 # }}}
 
diff --git a/grudge/symbolic/operators.py b/grudge/symbolic/operators.py
index 76bd938c941d5b7e0eea5682f164f188f3e2aff0..294c4374e275ac65666c3ae2d6d1442f81e491a7 100644
--- a/grudge/symbolic/operators.py
+++ b/grudge/symbolic/operators.py
@@ -2,7 +2,7 @@
 
 from __future__ import division, absolute_import
 
-__copyright__ = "Copyright (C) 2008 Andreas Kloeckner"
+__copyright__ = "Copyright (C) 2008-2017 Andreas Kloeckner, Bogdan Enache"
 
 __license__ = """
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -97,6 +97,14 @@ class ElementwiseLinearOperator(Operator):
 
 
 class InterpolationOperator(Operator):
+    def __init__(self, dd_in, dd_out):
+        official_dd_in = _sym().as_dofdesc(dd_in)
+        official_dd_out = _sym().as_dofdesc(dd_out)
+        if official_dd_in == official_dd_out:
+            raise ValueError("Interpolating from {} to {}"
+            " does not do anything.".format(official_dd_in, official_dd_out))
+
+        super(InterpolationOperator, self).__init__(dd_in, dd_out)
     mapper_method = intern("map_interpolation")
 
 
@@ -141,8 +149,12 @@ class DiffOperatorBase(Operator):
     def __init__(self, xyz_axis, dd_in=None, dd_out=None):
         if dd_in is None:
             dd_in = _sym().DD_VOLUME
+
         if dd_out is None:
             dd_out = dd_in.with_qtag(_sym().QTAG_NONE)
+        else:
+            dd_out = _sym().as_dofdesc(dd_out)
+
         if dd_out.uses_quadrature():
             raise ValueError("differentiation outputs are not on "
                     "quadrature grids")
@@ -215,18 +227,30 @@ class RefDiffOperator(RefDiffOperatorBase):
     mapper_method = intern("map_ref_diff")
 
     @staticmethod
-    def matrices(element_group):
-        return element_group.diff_matrices()
+    def matrices(out_element_group, in_element_group):
+        assert in_element_group == out_element_group
+        return in_element_group.diff_matrices()
 
 
 class RefStiffnessTOperator(RefDiffOperatorBase):
     mapper_method = intern("map_ref_stiffness_t")
 
     @staticmethod
-    def matrices(element_group):
-        assert element_group.is_orthogonal_basis()
-        mmat = element_group.mass_matrix()
-        return [dmat.T.dot(mmat.T) for dmat in element_group.diff_matrices()]
+    def matrices(out_elem_grp, in_elem_grp):
+        if in_elem_grp == out_elem_grp:
+            assert in_elem_grp.is_orthogonal_basis()
+            mmat = in_elem_grp.mass_matrix()
+            return [dmat.T.dot(mmat.T) for dmat in in_elem_grp.diff_matrices()]
+
+        from modepy import vandermonde
+        vand = vandermonde(out_elem_grp.basis(), out_elem_grp.unit_nodes)
+        grad_vand = vandermonde(out_elem_grp.grad_basis(), in_elem_grp.unit_nodes)
+        vand_inv_t = np.linalg.inv(vand).T
+
+        weights = in_elem_grp.weights
+
+        return np.einsum('c,bz,acz->abc', weights, vand_inv_t, grad_vand)
+
 
 # }}}
 
@@ -322,8 +346,6 @@ class VandermondeOperator(ElementwiseLinearOperator):
 
 # }}}
 
-# }}}
-
 
 # {{{ mass operators
 
@@ -338,9 +360,6 @@ class MassOperatorBase(Operator):
         if dd_out is None:
             dd_out = dd_in
 
-        if dd_out != dd_in:
-            raise ValueError("dd_out and dd_in must be identical")
-
         super(MassOperatorBase, self).__init__(dd_in, dd_out)
 
 
@@ -358,19 +377,30 @@ class RefMassOperatorBase(ElementwiseLinearOperator):
 
 class RefMassOperator(RefMassOperatorBase):
     @staticmethod
-    def matrix(element_group):
-        return element_group.mass_matrix()
+    def matrix(out_element_group, in_element_group):
+        if out_element_group == in_element_group:
+            return in_element_group.mass_matrix()
+
+        from modepy import vandermonde
+        vand = vandermonde(out_element_group.basis(), out_element_group.unit_nodes)
+        o_vand = vandermonde(out_element_group.basis(), in_element_group.unit_nodes)
+        vand_inv_t = np.linalg.inv(vand).T
+
+        weights = in_element_group.weights
+
+        return np.einsum('c,bz,acz->abc', weights, vand_inv_t, o_vand)
 
     mapper_method = intern("map_ref_mass")
 
 
 class RefInverseMassOperator(RefMassOperatorBase):
     @staticmethod
-    def matrix(element_group):
+    def matrix(in_element_group, out_element_group):
+        assert in_element_group == out_element_group
         import modepy as mp
         return mp.inverse_mass_matrix(
-                element_group.basis(),
-                element_group.unit_nodes)
+                in_element_group.basis(),
+                in_element_group.unit_nodes)
 
     mapper_method = intern("map_ref_inverse_mass")
 
@@ -426,7 +456,7 @@ class FaceMassOperatorBase(ElementwiseLinearOperator):
         if dd_in is None:
             dd_in = sym.DOFDesc(sym.FACE_RESTR_ALL, None)
 
-        if dd_out is None:
+        if dd_out is None or dd_out == "vol":
             dd_out = sym.DOFDesc("vol", sym.QTAG_NONE)
         if dd_out.uses_quadrature():
             raise ValueError("face mass operator outputs are not on "
@@ -497,10 +527,10 @@ def stiffness(dim):
         [StiffnessOperator(i) for i in range(dim)])
 
 
-def stiffness_t(dim):
+def stiffness_t(dim, dd_in=None, dd_out=None):
     from pytools.obj_array import make_obj_array
     return make_obj_array(
-        [StiffnessTOperator(i) for i in range(dim)])
+        [StiffnessTOperator(i, dd_in, dd_out) for i in range(dim)])
 
 
 def integral(arg, dd=None):
diff --git a/grudge/symbolic/primitives.py b/grudge/symbolic/primitives.py
index 3281abef525697ee692aacba76a18ba39a574664..974833c2a1e49587817fdf03c3a129210b9562c7 100644
--- a/grudge/symbolic/primitives.py
+++ b/grudge/symbolic/primitives.py
@@ -2,7 +2,7 @@
 
 from __future__ import division, absolute_import
 
-__copyright__ = "Copyright (C) 2008 Andreas Kloeckner"
+__copyright__ = "Copyright (C) 2008-2017 Andreas Kloeckner, Bogdan Enache"
 
 __license__ = """
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -657,10 +657,18 @@ class TracePair:
         return 0.5*(self.int + self.ext)
 
 
-def int_tpair(expression):
-    i = cse(_sym().interp("vol", "int_faces")(expression))
+def int_tpair(expression, qtag=None):
+    i = _sym().interp("vol", "int_faces")(expression)
     e = cse(_sym().OppositeInteriorFaceSwap()(i))
-    return TracePair("int_faces", i, e)
+
+    if qtag is not None and qtag != _sym().QTAG_NONE:
+        q_dd = _sym().DOFDesc("int_faces", qtag)
+        i = cse(_sym().interp("int_faces", q_dd)(i))
+        e = cse(_sym().interp("int_faces", q_dd)(e))
+    else:
+        q_dd = "int_faces"
+
+    return TracePair(q_dd, i, e)
 
 
 def bdry_tpair(dd, interior, exterior):
diff --git a/test/test_grudge.py b/test/test_grudge.py
index 84c46168ede26efe1a42935501d69fba99b7700a..34532bfc80371e29bde28ea9674c48e36ab889aa 100644
--- a/test/test_grudge.py
+++ b/test/test_grudge.py
@@ -334,8 +334,7 @@ def test_convergence_advec(ctx_factory, mesh_name, mesh_pars, op_type, flux_type
 
 
 @pytest.mark.parametrize("order", [3, 4, 5])
-# test: 'test_convergence_advec(cl._csc, "disk", [0.1, 0.05], "strong", "upwind", 3)'
-def test_convergence_maxwell(ctx_factory,  order, visualize=False):
+def test_convergence_maxwell(ctx_factory,  order):
     """Test whether 3D maxwells actually converges"""
 
     cl_ctx = cl.create_some_context()
@@ -403,6 +402,73 @@ def test_convergence_maxwell(ctx_factory,  order, visualize=False):
     assert eoc_rec.order_estimate() > order
 
 
+@pytest.mark.parametrize("order", [2, 3, 4])
+def test_improvement_quadrature(ctx_factory, order):
+    """Test whether quadrature improves things and converges"""
+    from meshmode.mesh.generation import generate_regular_rect_mesh
+    from grudge.models.advection import VariableCoefficientAdvectionOperator
+    from pytools.convergence import EOCRecorder
+    from pytools.obj_array import join_fields
+    from meshmode.discretization.poly_element import QuadratureSimplexGroupFactory
+
+    cl_ctx = cl.create_some_context()
+    queue = cl.CommandQueue(cl_ctx)
+
+    dims = 2
+    sym_nds = sym.nodes(dims)
+    advec_v = join_fields(-1*sym_nds[1], sym_nds[0])
+
+    flux = "upwind"
+    op = VariableCoefficientAdvectionOperator(2, advec_v, 0, flux_type=flux)
+
+    def gaussian_mode():
+        source_width = 0.1
+        sym_x = sym.nodes(2)
+        return sym.exp(-np.dot(sym_x, sym_x) / source_width**2)
+
+    def conv_test(descr, use_quad):
+        print("-"*75)
+        print(descr)
+        print("-"*75)
+        eoc_rec = EOCRecorder()
+
+        ns = [20, 25]
+        for n in ns:
+            mesh = generate_regular_rect_mesh(
+                a=(-0.5,)*dims,
+                b=(0.5,)*dims,
+                n=(n,)*dims,
+                order=order)
+
+            if use_quad:
+                quad_tag_to_group_factory = {
+                    "product": QuadratureSimplexGroupFactory(order=4*order)
+                    }
+            else:
+                quad_tag_to_group_factory = {"product": None}
+
+            discr = DGDiscretizationWithBoundaries(cl_ctx, mesh, order=order,
+                    quad_tag_to_group_factory=quad_tag_to_group_factory)
+
+            bound_op = bind(discr, op.sym_operator())
+            fields = bind(discr, gaussian_mode())(queue, t=0)
+            norm = bind(discr, sym.norm(2, sym.var("u")))
+
+            esc = bound_op(queue, u=fields)
+            total_error = norm(queue, u=esc)
+            eoc_rec.add_data_point(1.0/n, total_error)
+
+        print(eoc_rec.pretty_print(abscissa_label="h", error_label="LInf Error"))
+
+        return eoc_rec.order_estimate(), np.array([x[1] for x in eoc_rec.history])
+
+    eoc, errs = conv_test("no quadrature", False)
+    q_eoc, q_errs = conv_test("with quadrature", True)
+    assert q_eoc > eoc
+    assert (q_errs < errs).all()
+    assert q_eoc > order
+
+
 def test_foreign_points(ctx_factory):
     pytest.importorskip("sumpy")
     import sumpy.point_calculus as pc