diff --git a/sumpy/expansion/__init__.py b/sumpy/expansion/__init__.py
index 4f0a3a01482af78dacbfe0635ce45249a610f930..e1da810e632822e67e167470e1f02e87538b5b67 100644
--- a/sumpy/expansion/__init__.py
+++ b/sumpy/expansion/__init__.py
@@ -162,11 +162,12 @@ class ExpansionBase(object):
 
 class ExpansionTermsWrangler(object):
 
-    init_arg_names = ("order", "dim")
+    init_arg_names = ("order", "dim", "max_mi")
 
-    def __init__(self, order, dim):
+    def __init__(self, order, dim, max_mi=None):
         self.order = order
         self.dim = dim
+        self.max_mi = max_mi
 
     def get_coefficient_identifiers(self):
         raise NotImplementedError
@@ -189,6 +190,16 @@ class ExpansionTermsWrangler(object):
                 as gnitstam)
 
         res = sorted(gnitstam(self.order, self.dim), key=sum)
+
+        def filter_tuple(tup):
+            if self.max_mi is None:
+                return True
+            for a, b in zip(tup, self.max_mi):
+                if a > b:
+                    return False
+            return True
+
+        res = list(filter(filter_tuple, res))
         return res
 
     def copy(self, **kwargs):
@@ -196,8 +207,8 @@ class ExpansionTermsWrangler(object):
                 (name, getattr(self, name))
                 for name in self.init_arg_names)
 
-        new_kwargs["order"] = kwargs.pop("order", self.order)
-        new_kwargs["dim"] = kwargs.pop("dim", self.dim)
+        for name in self.init_arg_names:
+            new_kwargs[name] = kwargs.pop(name, getattr(self, name))
 
         if kwargs:
             raise TypeError("unexpected keyword arguments '%s'"
@@ -261,9 +272,9 @@ class LinearRecurrenceBasedExpansionTermsWrangler(ExpansionTermsWrangler):
     .. automethod:: get_reduced_coeffs
     """
 
-    init_arg_names = ("order", "dim", "deriv_multiplier")
+    init_arg_names = ("order", "dim", "deriv_multiplier", "max_mi")
 
-    def __init__(self, order, dim, deriv_multiplier):
+    def __init__(self, order, dim, deriv_multiplier, max_mi=None):
         r"""
         :param order: order of the expansion
         :param dim: number of dimensions
@@ -274,7 +285,7 @@ class LinearRecurrenceBasedExpansionTermsWrangler(ExpansionTermsWrangler):
             that representation of the PDE is :math:`\text{coeff} /
             {\text{deriv\_multiplier}^{|\nu|}}`.
         """
-        super(LinearRecurrenceBasedExpansionTermsWrangler, self).__init__(order, dim)
+        super(LinearRecurrenceBasedExpansionTermsWrangler, self).__init__(order, dim, max_mi)
         self.deriv_multiplier = deriv_multiplier
 
     def get_coefficient_identifiers(self):
@@ -354,7 +365,6 @@ class LinearRecurrenceBasedExpansionTermsWrangler(ExpansionTermsWrangler):
 
         pde_dict = self.get_pde_dict()
         pde_mat = []
-
         mis = self.get_full_coefficient_identifiers()
         coeff_ident_enumerate_dict = dict((tuple(mi), i) for
                                             (i, mi) in enumerate(mis))
@@ -385,11 +395,10 @@ class LinearRecurrenceBasedExpansionTermsWrangler(ExpansionTermsWrangler):
             Then, :math:`S^T = N * N_{[r, :]}^{-1}` which implies,
             :math:`S = N_{[r, :]}^{-T} N^T = N_{[r, :]}^{-T} N^T`.
             """
-            pde_mat = np.array(pde_mat, dtype=np.float64)
-            n = nullspace(pde_mat, atol=tol)
+            n = nullspace(pde_mat)
             idx = self.get_reduced_coeffs()
             assert len(idx) >= n.shape[1]
-            s = np.linalg.solve(n.T[:, idx], n.T)
+            s = n.T[:, idx].solve(n.T)
             stored_identifiers = [mis[i] for i in idx]
         else:
             s = np.eye(len(mis))
@@ -400,8 +409,8 @@ class LinearRecurrenceBasedExpansionTermsWrangler(ExpansionTermsWrangler):
         coeff_matrix = defaultdict(list)
         for i in range(s.shape[0]):
             for j in range(s.shape[1]):
-                if np.abs(s[i][j]) > tol:
-                    coeff_matrix[j].append((i, s[i][j]))
+                if np.abs(s[i, j]) > tol:
+                    coeff_matrix[j].append((i, s[i, j]))
 
         plog.done()
 
@@ -453,10 +462,10 @@ class LinearRecurrenceBasedExpansionTermsWrangler(ExpansionTermsWrangler):
 
 class LaplaceExpansionTermsWrangler(LinearRecurrenceBasedExpansionTermsWrangler):
 
-    init_arg_names = ("order", "dim")
+    init_arg_names = ("order", "dim", "max_mi")
 
-    def __init__(self, order, dim):
-        super(LaplaceExpansionTermsWrangler, self).__init__(order, dim, 1)
+    def __init__(self, order, dim, max_mi=None):
+        super(LaplaceExpansionTermsWrangler, self).__init__(order, dim, 1, max_mi)
 
     def get_pde_dict(self):
         pde_dict = {}
@@ -478,13 +487,13 @@ class LaplaceExpansionTermsWrangler(LinearRecurrenceBasedExpansionTermsWrangler)
 
 class HelmholtzExpansionTermsWrangler(LinearRecurrenceBasedExpansionTermsWrangler):
 
-    init_arg_names = ("order", "dim", "helmholtz_k_name")
+    init_arg_names = ("order", "dim", "helmholtz_k_name", "max_mi")
 
-    def __init__(self, order, dim, helmholtz_k_name):
+    def __init__(self, order, dim, helmholtz_k_name, max_mi=None):
         self.helmholtz_k_name = helmholtz_k_name
 
         multiplier = sym.Symbol(helmholtz_k_name)
-        super(HelmholtzExpansionTermsWrangler, self).__init__(order, dim, multiplier)
+        super(HelmholtzExpansionTermsWrangler, self).__init__(order, dim, multiplier, max_mi)
 
     def get_pde_dict(self, **kwargs):
         pde_dict = {}
diff --git a/sumpy/expansion/local.py b/sumpy/expansion/local.py
index 11b1368a43bd8c57ad51065f84bea3b7dc2a5ca5..cf94ebd674201178047cfee86acca0d74512c1ec 100644
--- a/sumpy/expansion/local.py
+++ b/sumpy/expansion/local.py
@@ -167,11 +167,18 @@ class VolumeTaylorLocalExpansionBase(LocalExpansionBase):
 
             from sumpy.tools import add_mi
 
+            max_mi = [0]*self.dim
+            for i in range(self.dim):
+                max_mi[i] = max(mi[i] for mi in
+                                  src_expansion.get_coefficient_identifiers())
+                max_mi[i] += max(mi[i] for mi in
+                                  self.get_coefficient_identifiers())
+
             # Create a expansion terms wrangler for derivatives up to order
             # (tgt order)+(src order).
             tgtplusderiv_exp_terms_wrangler = \
                 src_expansion.expansion_terms_wrangler.copy(
-                        order=self.order + src_expansion.order)
+                        order=self.order + src_expansion.order, max_mi=tuple(max_mi))
             tgtplusderiv_coeff_ids = \
                 tgtplusderiv_exp_terms_wrangler.get_coefficient_identifiers()
             tgtplusderiv_full_coeff_ids = \
diff --git a/sumpy/tools.py b/sumpy/tools.py
index 91c7605060bd46d071093a9c6131f7a71a7f89d7..8916e33c358702628d97cb03889ecb0f7aa861ec 100644
--- a/sumpy/tools.py
+++ b/sumpy/tools.py
@@ -401,48 +401,55 @@ def my_syntactic_subs(expr, subst_dict):
         return expr
 
 
-# Source: http://scipy-cookbook.readthedocs.io/items/RankNullspace.html
-# Author: SciPy Developers
-# License: BSD-3-Clause
-
-def nullspace(A, atol=1e-13, rtol=0):  # noqa:N803
-    """Compute an approximate basis for the nullspace of A.
-
-    The algorithm used by this function is based on the singular value
-    decomposition of `A`.
-
-    Parameters
-    ----------
-    A : ndarray
-        A should be at most 2-D.  A 1-D array with length k will be treated
-        as a 2-D with shape (1, k)
-    atol : float
-        The absolute tolerance for a zero singular value.  Singular values
-        smaller than `atol` are considered to be zero.
-    rtol : float
-        The relative tolerance.  Singular values less than rtol*smax are
-        considered to be zero, where smax is the largest singular value.
-
-    If both `atol` and `rtol` are positive, the combined tolerance is the
-    maximum of the two; that is::
-        tol = max(atol, rtol * smax)
-    Singular values smaller than `tol` are considered to be zero.
-
-    Return value
-    ------------
-    ns : ndarray
-        If `A` is an array with shape (m, k), then `ns` will be an array
-        with shape (k, n), where n is the estimated dimension of the
-        nullspace of `A`.  The columns of `ns` are a basis for the
-        nullspace; each element in numpy.dot(A, ns) will be approximately
-        zero.
-    """
-    from numpy.linalg import svd
-    A = np.atleast_2d(A)  # noqa:N806
-    u, s, vh = svd(A)
-    tol = max(atol, rtol * s[0])
-    nnz = (s >= tol).sum()
-    ns = vh[nnz:].conj().T
-    return ns
+def rref(mat):
+    rows = len(mat)
+    cols = len(mat[0])
+    col = 0
+    pivot_cols = []
+
+    for row in range(rows):
+        if col >= cols:
+            break
+        i = row
+        while mat[i][col] == 0:
+            i += 1
+            if i == rows:
+                i = row
+                col += 1
+                if col == cols:
+                    return mat, pivot_cols
+
+        pivot_cols.append(col)
+        mat[i], mat[row] = mat[row], mat[i]
+
+        piv = mat[row][col]
+        for c in range(col, cols):
+            mat[row][c] /= piv
+
+        for r in range(rows):
+            if r == row:
+                continue
+            piv = mat[r][col]
+            for c in range(col, cols):
+                mat[r][c] -= piv * mat[row][c]
+        col += 1
+    return mat, pivot_cols
+
+def nullspace(m):
+    m2 = [[sym.sympify(col) for col in row] for row in m]
+    mat, pivot_cols = rref(m2)
+    cols = len(mat[0])
+
+    free_vars = [i for i in range(cols) if i not in pivot_cols]
+
+    n = []
+    for free_var in free_vars:
+        vec = [0]*cols
+        vec[free_var] = 1
+        for piv_row, piv_col in enumerate(pivot_cols):
+            for pos in pivot_cols[piv_row+1:] + [free_var]:
+                vec[piv_col] -= mat[piv_row][pos]
+        n.append(vec)
+    return sym.Matrix(n).T
 
 # vim: fdm=marker