diff --git a/examples/wave/wave-min.py b/examples/wave/wave-min.py
index be95db4bb4846b78d02ef7a4a855a364d80bc77c..3b5f5d32d51cf402b15e34b6a086319f2eb083d5 100644
--- a/examples/wave/wave-min.py
+++ b/examples/wave/wave-min.py
@@ -26,13 +26,14 @@ THE SOFTWARE.
 
 
 import numpy as np
+import pyopencl as cl
+from grudge.shortcuts import make_discretization, set_up_rk4
 
 
 def main(write_output=True):
     from meshmode.mesh.generation import generate_regular_rect_mesh
     mesh = generate_regular_rect_mesh(a=(-0.5, -0.5), b=(0.5, 0.5))
 
-    from grudge.shortcuts import make_discretization
     discr = make_discretization(mesh, order=4)
 
     #from grudge.visualization import VtkVisualizer
@@ -42,16 +43,19 @@ def main(write_output=True):
     source_width = 0.05
     source_omega = 3
 
-    import grudge.symbolic as sym
+    from grudge import sym
     sym_x = sym.nodes(2)
     sym_source_center_dist = sym_x - source_center
+    sym_sin = sym.CFunction("sin")
+    sym_exp = sym.CFunction("sin")
+    sym_t = sym.ScalarParameter("t")
 
     from grudge.models.wave import StrongWaveOperator
     from meshmode.mesh import BTAG_ALL, BTAG_NONE
-    op = StrongWaveOperator(-0.1, discr.dimensions,
+    op = StrongWaveOperator(-0.1, discr.dim,
             source_f=(
-                sym.CFunction("sin")(source_omega*sym.ScalarParameter("t"))
-                * sym.CFunction("exp")(
+                sym_sin(source_omega*sym_t)
+                * sym_exp(
                     -np.dot(sym_source_center_dist, sym_source_center_dist)
                     / source_width**2)),
             dirichlet_tag=BTAG_NONE,
@@ -59,18 +63,16 @@ def main(write_output=True):
             radiation_tag=BTAG_ALL,
             flux_type="upwind")
 
+    queue = cl.CommandQueue(discr.cl_context)
     from pytools.obj_array import join_fields
-    fields = join_fields(discr.volume_zeros(),
-            [discr.volume_zeros() for i in range(discr.dimensions)])
+    fields = join_fields(discr.zeros(queue),
+            [discr.zeros(queue) for i in range(discr.dim)])
 
-    from leap.method.rk import LSRK4TimeStepper
-    from leap.vm.codegen import PythonCodeGenerator
+    # FIXME
+    #dt = op.estimate_rk4_timestep(discr, fields=fields)
 
-    dt_method = LSRK4TimeStepper(component_id="y")
-    dt_stepper = PythonCodeGenerator.get_class(dt_method.generate())
-
-    dt = op.estimate_timestep(discr, fields=fields)
-    dt_stepper.set_up(t_start=0, dt_start=dt, context={"y": fields})
+    dt = 0.001
+    dt_stepper = set_up_rk4(dt, fields)
 
     final_t = 10
     nsteps = int(final_t/dt)
diff --git a/grudge/dt_finding.py b/grudge/dt_finding.py
new file mode 100644
index 0000000000000000000000000000000000000000..86e8f9a4be9210e825c25388e2c62cb3925de58d
--- /dev/null
+++ b/grudge/dt_finding.py
@@ -0,0 +1,94 @@
+"""Helpers for estimating a stable time step."""
+
+from __future__ import division, print_function
+
+__copyright__ = "Copyright (C) 2015 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.
+"""
+
+from pytools import memoize_on_first_arg
+from meshmode.discretization.poly_element import PolynomialWarpAndBlendElementGroup
+import numpy.linalg as la
+
+
+class WarpAndBlendTimestepInfo(object):
+    @staticmethod
+    def dt_non_geometric_factor(discr, grp):
+        if grp.dim == 1:
+            if grp.order == 0:
+                return 1
+            else:
+                unodes = grp.unit_nodes
+                return la.norm(unodes[0] - unodes[1]) * 0.85
+        else:
+            unodes = grp.unit_nodes
+            vertex_indices = grp.vertex_indices()
+            return 2 / 3 * \
+                    min(min(min(
+                        la.norm(unodes[face_node_index]-unodes[vertex_index])
+                        for vertex_index in vertex_indices
+                        if vertex_index != face_node_index)
+                        for face_node_index in face_indices)
+                        for face_indices in self.face_indices())
+
+    @staticmethod
+    def dt_geometric_factor(discr, grp):
+        if grp.dim == 1:
+            return abs(el.map.jacobian())
+
+        elif grp.dim == 2:
+            area = abs(2 * el.map.jacobian())
+            semiperimeter = sum(la.norm(vertices[vi1] - vertices[vi2])
+                    for vi1, vi2 in [(0, 1), (1, 2), (2, 0)])/2
+            return area / semiperimeter
+
+        elif grp.dim == 3:
+            result = abs(el.map.jacobian())/max(abs(fj) for fj in el.face_jacobians)
+            if grp.order in [1, 2]:
+                from warnings import warn
+                warn("cowardly halving timestep for order 1 and 2 tets "
+                        "to avoid CFL issues")
+                result /= 2
+
+            return result
+
+        else:
+            raise NotImplementedError("cannot estimate timestep for "
+                    "%d-dimensional elements" % grp.dim)
+
+
+_GROUP_TYPE_TO_INFO = {
+        PolynomialWarpAndBlendElementGroup: WarpAndBlendTimestepInfo
+        }
+
+
+@memoize_on_first_arg
+def dt_non_geometric_factor(discr):
+    return min(
+            _GROUP_TYPE_TO_INFO[type(grp)].dt_non_geometric_factor(discr, grp)
+            for grp in discr.groups)
+
+
+@memoize_on_first_arg
+def dt_geometric_factor(discr):
+    return min(
+            _GROUP_TYPE_TO_INFO[type(grp)].dt_geometric_factor(discr, grp)
+            for grp in discr.groups)
diff --git a/grudge/models/__init__.py b/grudge/models/__init__.py
index 0b33a6f3d45a6a125311c885fb2f2e2944469eb3..9ccd1ab95a4ace69555c1cb1d030023cdaf85b04 100644
--- a/grudge/models/__init__.py
+++ b/grudge/models/__init__.py
@@ -51,18 +51,13 @@ class TimeDependentOperator(Operator):
 class HyperbolicOperator(Operator):
     """A base class for hyperbolic Discontinuous Galerkin operators."""
 
-    def estimate_timestep(self, discr,
-            stepper=None, stepper_class=None, stepper_args=None,
-            t=None, fields=None):
-        u"""Estimate the largest stable timestep, given a time stepper
-        `stepper_class`. If none is given, RK4 is assumed.
+    def estimate_rk4_timestep(self, discr, t=None, fields=None):
+        u"""Estimate the largest stable timestep for an RK4 method.
         """
 
-        rk4_dt = 1 / self.max_eigenvalue(t, fields, discr) \
-                * (discr.dt_non_geometric_factor()
-                * discr.dt_geometric_factor())
-
-        from grudge.timestep.stability import \
-                approximate_rk4_relative_imag_stability_region
-        return rk4_dt * approximate_rk4_relative_imag_stability_region(
-                stepper, stepper_class, stepper_args)
+        from grudge.dt_finding import (
+                dt_non_geometric_factor,
+                dt_geometric_factor)
+        return 1 / self.max_eigenvalue(t, fields, discr) \
+                * (dt_non_geometric_factor(discr)
+                * dt_geometric_factor(discr))
diff --git a/grudge/models/em.py b/grudge/models/em.py
index 697a388cb549074cf562943425c293776cdff4ad..f821206d513d7b8f920999259031126e3c23b755 100644
--- a/grudge/models/em.py
+++ b/grudge/models/em.py
@@ -29,10 +29,10 @@ THE SOFTWARE.
 
 from pytools import memoize_method
 
-import grudge.mesh
 from grudge.models import HyperbolicOperator
 from grudge.symbolic.primitives import make_common_subexpression as cse
-from grudge.tools import make_obj_array
+from meshmode.mesh import BTAG_ALL, BTAG_NONE
+from pytools.obj_array import join_fields, make_obj_array
 
 # TODO: Check PML
 
@@ -126,7 +126,6 @@ class MaxwellOperator(HyperbolicOperator):
         """
         from grudge.flux import (make_normal, FluxVectorPlaceholder,
                 FluxConstantPlaceholder)
-        from grudge.tools import join_fields
 
         normal = make_normal(self.dimensions)
 
@@ -205,7 +204,6 @@ class MaxwellOperator(HyperbolicOperator):
             return self.space_cross_h(nabla, field)
 
         from grudge.symbolic import make_nabla
-        from grudge.tools import join_fields
 
         nabla = make_nabla(self.dimensions)
 
@@ -229,7 +227,6 @@ class MaxwellOperator(HyperbolicOperator):
         "Construct part of the flux operator template for PEC boundary conditions"
         e, h = self.split_eh(self.field_placeholder(w))
 
-        from grudge.tools import join_fields
         from grudge.symbolic import BoundarizeOperator
         pec_e = BoundarizeOperator(self.pec_tag)(e)
         pec_h = BoundarizeOperator(self.pec_tag)(h)
@@ -240,7 +237,6 @@ class MaxwellOperator(HyperbolicOperator):
         "Construct part of the flux operator template for PMC boundary conditions"
         e, h = self.split_eh(self.field_placeholder(w))
 
-        from grudge.tools import join_fields
         from grudge.symbolic import BoundarizeOperator
         pmc_e = BoundarizeOperator(self.pmc_tag)(e)
         pmc_h = BoundarizeOperator(self.pmc_tag)(h)
@@ -256,7 +252,6 @@ class MaxwellOperator(HyperbolicOperator):
         absorb_normal = normal(self.absorb_tag, self.dimensions)
 
         from grudge.symbolic import BoundarizeOperator, Field
-        from grudge.tools import join_fields
 
         e, h = self.split_eh(self.field_placeholder(w))
 
@@ -315,7 +310,6 @@ class MaxwellOperator(HyperbolicOperator):
         Combines the relevant operator templates for spatial
         derivatives, flux, boundary conditions etc.
         """
-        from grudge.tools import join_fields
         w = self.field_placeholder(w)
 
         if self.fixed_material:
@@ -410,7 +404,6 @@ class MaxwellOperator(HyperbolicOperator):
         e = default_fld(e, e_components)
         h = default_fld(h, h_components)
 
-        from grudge.tools import join_fields
         return join_fields(e, h)
 
     assemble_fields = assemble_eh
diff --git a/grudge/models/second_order.py b/grudge/models/second_order.py
index de8404e51654054a53837d9d2336c5e3e18aeaea..24260c0f7012ab7bbd6f7adc0fc23a0248bbee32 100644
--- a/grudge/models/second_order.py
+++ b/grudge/models/second_order.py
@@ -27,14 +27,15 @@ THE SOFTWARE.
 
 
 import numpy as np
-import grudge.symbolic
+import grudge.symbolic.mappers as mappers
+from grudge import sym
 
 
 # {{{ stabilization term generator
 
-class StabilizationTermGenerator(grudge.symbolic.IdentityMapper):
+class StabilizationTermGenerator(mappers.IdentityMapper):
     def __init__(self, flux_args):
-        grudge.symbolic.IdentityMapper.__init__(self)
+        super(StabilizationTermGenerator, self).__init__()
         self.flux_args = flux_args
         self.flux_arg_lookup = dict(
                 (flux_arg, i) for i, flux_arg in enumerate(flux_args))
@@ -135,14 +136,14 @@ class StabilizationTermGenerator(grudge.symbolic.IdentityMapper):
 
 # {{{ neumann bc generator
 
-class NeumannBCGenerator(grudge.symbolic.IdentityMapper):
+class NeumannBCGenerator(mappers.IdentityMapper):
     def __init__(self, tag, bc):
-        grudge.symbolic.IdentityMapper.__init__(self)
+        super(NeumannBCGenerator, self).__init__()
         self.tag = tag
         self.bc = bc
 
     def map_operator_binding(self, expr):
-        if isinstance(expr.op, grudge.symbolic.DiffOperatorBase):
+        if isinstance(expr.op, sym.DiffOperatorBase):
             from grudge.symbolic import \
                     WeakFormDiffOperatorBase, \
                     StrongFormDiffOperatorBase
@@ -158,9 +159,9 @@ class NeumannBCGenerator(grudge.symbolic.IdentityMapper):
             return (self.bc * factor *
                     BoundaryNormalComponent(self.tag, expr.op.xyz_axis))
 
-        elif isinstance(expr.op, grudge.symbolic.FluxOperatorBase):
+        elif isinstance(expr.op, sym.FluxOperatorBase):
             return 0
-        elif isinstance(expr.op, grudge.symbolic.InverseMassOperator):
+        elif isinstance(expr.op, sym.InverseMassOperator):
             return self.rec(expr.field)
         else:
             raise ValueError("neumann normal direction generator doesn't know "
@@ -169,9 +170,9 @@ class NeumannBCGenerator(grudge.symbolic.IdentityMapper):
 # }}}
 
 
-class IPDGDerivativeGenerator(grudge.symbolic.IdentityMapper):
+class IPDGDerivativeGenerator(mappers.IdentityMapper):
     def map_operator_binding(self, expr):
-        if isinstance(expr.op, grudge.symbolic.DiffOperatorBase):
+        if isinstance(expr.op, sym.DiffOperatorBase):
             from grudge.symbolic import (
                     WeakFormDiffOperatorBase,
                     StrongFormDiffOperatorBase)
@@ -187,14 +188,13 @@ class IPDGDerivativeGenerator(grudge.symbolic.IdentityMapper):
             from grudge.symbolic import DifferentiationOperator
             return factor*DifferentiationOperator(expr.op.xyz_axis)(expr.field)
 
-        elif isinstance(expr.op, grudge.symbolic.FluxOperatorBase):
+        elif isinstance(expr.op, sym.FluxOperatorBase):
             return 0
-        elif isinstance(expr.op, grudge.symbolic.InverseMassOperator):
+        elif isinstance(expr.op, sym.InverseMassOperator):
             return self.rec(expr.field)
         elif isinstance(expr.op,
-                grudge.symbolic.QuadratureInteriorFacesGridUpsampler):
-            return grudge.symbolic.IdentityMapper.map_operator_binding(
-                    self, expr)
+                sym.QuadratureInteriorFacesGridUpsampler):
+            return super(IPDGDerivativeGenerator, self).map_operator_binding(expr)
         else:
             from grudge.symbolic.tools import pretty
             raise ValueError("IPDG derivative generator doesn't know "
diff --git a/grudge/models/wave.py b/grudge/models/wave.py
index a3a9aaf4448127c0bf4889f92adecef821966d9f..53a0a848c25fb5c3650f9cb5d2f1f6cb525bdca5 100644
--- a/grudge/models/wave.py
+++ b/grudge/models/wave.py
@@ -29,9 +29,10 @@ THE SOFTWARE.
 
 import numpy as np
 from grudge.models import HyperbolicOperator
-from grudge.second_order import CentralSecondDerivative
+from grudge.models.second_order import CentralSecondDerivative
 from meshmode.mesh import BTAG_ALL, BTAG_NONE
 from grudge import sym, sym_flux
+from pytools.obj_array import join_fields
 
 
 # {{{ constant-velocity
@@ -122,7 +123,6 @@ class StrongWaveOperator(HyperbolicOperator):
         v = w[1:]
 
         # boundary conditions -------------------------------------------------
-        from grudge.tools import join_fields
 
         # dirichlet BCs -------------------------------------------------------
         from grudge.symbolic import normal, Field
@@ -160,7 +160,6 @@ class StrongWaveOperator(HyperbolicOperator):
         nabla = make_nabla(d)
         flux_op = get_flux_operator(self.flux())
 
-        from grudge.tools import join_fields
         result = (
                 - join_fields(
                     -self.c*np.dot(nabla, v),
@@ -252,7 +251,6 @@ class VariableVelocityStrongWaveOperator(HyperbolicOperator):
         v = w[2:]
         normal = make_normal(dim)
 
-        from grudge.tools import join_fields
         flux = self.time_sign*1/2*join_fields(
                 c.ext * np.dot(v.ext, normal)
                 - c.int * np.dot(v.int, normal),
@@ -299,11 +297,9 @@ class VariableVelocityStrongWaveOperator(HyperbolicOperator):
         u = w[0]
         v = w[1:]
 
-        from grudge.tools import join_fields
         flux_w = join_fields(self.c, w)
 
         # {{{ boundary conditions
-        from grudge.tools import join_fields
 
         # Dirichlet
         dir_c = BoundarizeOperator(self.dirichlet_tag) * self.c
diff --git a/grudge/shortcuts.py b/grudge/shortcuts.py
index 9b20ee4090f17e6179d0358d91a6ae9b18594bd3..6ed54d515b7bebe3b5d34d52eaa10942e188b0bb 100644
--- a/grudge/shortcuts.py
+++ b/grudge/shortcuts.py
@@ -37,3 +37,15 @@ def make_discretization(mesh, order, **kwargs):
 
     return Discretization(cl_ctx, mesh,
             PolynomialWarpAndBlendGroupFactory(order=order))
+
+
+def set_up_rk4(dt, fields, t_start=0):
+    from leap.method.rk import LSRK4Method
+    from leap.vm.codegen import PythonCodeGenerator
+
+    dt_method = LSRK4Method(component_id="y")
+    dt_stepper = PythonCodeGenerator().get_class(dt_method.generate())
+
+    dt_stepper.set_up(t_start=t_start, dt_start=dt, context={"y": fields})
+
+    return dt_stepper
diff --git a/grudge/symbolic/flux/primitives b/grudge/symbolic/flux/primitives.py
similarity index 100%
rename from grudge/symbolic/flux/primitives
rename to grudge/symbolic/flux/primitives.py