diff --git a/examples/goalOrientedAFEM.m b/examples/goalOrientedAFEM.m
index c270eba74963aad09d60c3e803885ae2a59e6af8..e966ffa0bc7ff92cf2f609649d2436c66ea0e58c 100644
--- a/examples/goalOrientedAFEM.m
+++ b/examples/goalOrientedAFEM.m
@@ -39,8 +39,7 @@ for p = 1:pmax
     lfG.qrfvec = QuadratureRule.ofOrder(max(p-1, 1));
     
     %% set up lifting operators for rhs FEM-data
-    Pv = LoL2Prolongation(v);
-    Pw = LoL2Prolongation(w);
+    P = LoFeProlongation(ncFes);
 
     %% adaptive loop
     i = 1;
@@ -70,8 +69,8 @@ for p = 1:pmax
         marked = markGoafemMS(eta2, zeta2, theta);
         mesh.refineLocally(marked, 'NVB');
         i = i+1;
-        v.setData(Pv.interpolatedData);
-        w.setData(Pw.interpolatedData);
+        v.setData(prolongate(P, v));
+        w.setData(prolongate(P, w));
     end
 end
 
diff --git a/examples/iterativeLinearization.m b/examples/iterativeLinearization.m
index 7e199187ec144f0b0ec32084bb59f5c9ea6072bb..9c9ea5efaf0902140faec7b93cc08eefcb30ef3e 100644
--- a/examples/iterativeLinearization.m
+++ b/examples/iterativeLinearization.m
@@ -24,7 +24,10 @@ for k = 1:length(linearizations)
     g = Constant(mesh, 1);
     [blf, lf] = setProblemData(mesh, fes, Du, g, linearizations(k));
     eDensity = CompositeFunction(@(p) muIntegral(vectorProduct(p, p)), Du);
-    u.setData(0);
+    u.setData(0);    
+
+    %% nested iteration
+    P = LoFeProlongation(fes);
 
     %% adaptive algorithm
     i = 1;
@@ -67,13 +70,10 @@ for k = 1:length(linearizations)
             break
         end
 
-        %% nested iteration
-        uold = LoH1Prolongation(u);
-
         %% refine mesh
         marked = markDoerflerSorting(eta2, theta);
         mesh.refineLocally(marked, 'NVB');
-        u.setData(uold.interpolatedData);
+        u.setData(prolongate(P, u));
         i = i+1;
     end
 end
diff --git a/lib/assembly/@BilinearForm/BilinearForm.m b/lib/assembly/@BilinearForm/BilinearForm.m
index 866ad7269fd88aa6d68171b2ea6c34f87fe0dc7d..fbd4f0e807b96750ef9c6f8bf92fb44a780a6f50 100644
--- a/lib/assembly/@BilinearForm/BilinearForm.m
+++ b/lib/assembly/@BilinearForm/BilinearForm.m
@@ -4,11 +4,11 @@
 
 classdef BilinearForm < handle    
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         fes
     end
     
-    properties (Access='public')
+    properties (Access=public)
         a {mustBeEvaluableOrEmpty} = []     % Diffusion matrix (or scalar)
         b {mustBeEvaluableOrEmpty} = []     % Convection vector field
         c {mustBeEvaluableOrEmpty} = []     % Reaction coefficient
@@ -33,11 +33,11 @@ classdef BilinearForm < handle
         end
     end
     
-    methods (Access='public')
+    methods (Access=public)
         mat = assemble(obj);
     end
     
-    methods (Static, Access='protected')
+    methods (Static, Access=protected)
         val = diffusionPart(a, Dphi);
         val = convectionPart(b, Dphi, phi);
         val = reactionPart(c, phi);
diff --git a/lib/assembly/@LinearForm/LinearForm.m b/lib/assembly/@LinearForm/LinearForm.m
index c79c0efd22c6a3066a5d6045d7cc24f5f4c909d0..7fc9ddc830b59b8297d949a37b77d16d9a0d2b9b 100644
--- a/lib/assembly/@LinearForm/LinearForm.m
+++ b/lib/assembly/@LinearForm/LinearForm.m
@@ -4,11 +4,11 @@
 
 classdef LinearForm < handle
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         fes
     end
     
-    properties (Access='public')
+    properties (Access=public)
         f {mustBeEvaluableOrEmpty} = []         % Volume load
         fvec {mustBeEvaluableOrEmpty} = []      % Generalized volume load
         robin {mustBeEvaluableOrEmpty} = []     % Robin boundary load
@@ -33,11 +33,11 @@ classdef LinearForm < handle
         end
     end
     
-    methods (Access='public')
+    methods (Access=public)
         vec = assemble(obj);
     end
     
-    methods (Static, Access='protected')
+    methods (Static, Access=protected)
         val = scalarPart(f, phi);
         val = vectorPart(fvec, Dphi);
         val = neumannPart(neumann, phi);
diff --git a/lib/functions/@Evaluable/Evaluable.m b/lib/functions/@Evaluable/Evaluable.m
index f42571dad89a4d5fec6899694c6e1d80504a2ad7..683d3516f77e94165b7c7c7e454f41d3b0d1e613 100644
--- a/lib/functions/@Evaluable/Evaluable.m
+++ b/lib/functions/@Evaluable/Evaluable.m
@@ -8,16 +8,16 @@
 
 classdef Evaluable < handle
     %% properties
-    properties (Abstract, GetAccess='public', SetAccess='protected')
+    properties (Abstract, GetAccess=public, SetAccess=protected)
         mesh
     end
     
     %% methods
-    methods (Abstract, Access='public')
+    methods (Abstract, Access=public)
         eval(obj, bary, idx)
     end
     
-    methods (Access='public')
+    methods (Access=public)
         plot(obj, plotOptions)
         evalEdge(obj, bary, idx)
     end
diff --git a/lib/functions/CompositeFunction.m b/lib/functions/CompositeFunction.m
index a62ed3e0eb396a640eef55a6eefbcc3a137c49ef..5b5ef2f5160c390acb6df31e22cf98803d44e2a7 100644
--- a/lib/functions/CompositeFunction.m
+++ b/lib/functions/CompositeFunction.m
@@ -13,14 +13,14 @@
 
 classdef CompositeFunction < Evaluable
     %% properties
-    properties (SetAccess='protected', GetAccess='public')
+    properties (SetAccess=protected, GetAccess=public)
         mesh
         functionHandle
         functionArguments
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = CompositeFunction(functionHandle, functionArguments)
             arguments
                 functionHandle (1,1) function_handle
@@ -44,7 +44,7 @@ classdef CompositeFunction < Evaluable
         end
     end
     
-    methods (Access='private')
+    methods (Access=private)
         function val = compositeEval(obj, evalMethod, bary, idx)
             arguments
                 obj
diff --git a/lib/functions/Constant.m b/lib/functions/Constant.m
index 324a00750020ca5f8fcd865567d834bc1798b74a..28c902afe17aa69fc4c3761f0cd494e93147864c 100644
--- a/lib/functions/Constant.m
+++ b/lib/functions/Constant.m
@@ -6,13 +6,13 @@
 
 classdef Constant < Evaluable
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         mesh
         constantValue (:,1) double
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = Constant(mesh, value)
             obj.mesh = mesh;
             obj.constantValue = value;
diff --git a/lib/functions/FeFunction.m b/lib/functions/FeFunction.m
index d47d0c91a7aa2e4a6ae4cc25e3662b7070330f11..510a0239132f4f1c69ecf5776fb2a47999cf320b 100644
--- a/lib/functions/FeFunction.m
+++ b/lib/functions/FeFunction.m
@@ -19,19 +19,19 @@
 
 classdef FeFunction < Evaluable
     %% properties
-    properties (SetAccess='protected', GetAccess='public')
+    properties (SetAccess=protected, GetAccess=public)
         mesh
         fes
         data (1,:) double
         dataIsValid (1,1) logical
     end
     
-    properties (Access = 'private')
+    properties (Access = private)
         listenerHandle
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = FeFunction(fes)
             arguments
                 fes (1,1) FeSpace
@@ -43,7 +43,7 @@ classdef FeFunction < Evaluable
             obj.dataIsValid = true;
             
             % register FeFunction as an observer of mesh
-            obj.listenerHandle = fes.mesh.listener('HasChanged', @obj.handleChangedMesh);
+            obj.listenerHandle = fes.mesh.listener('JustRefined', @obj.handleChangedMesh);
         end
         
         function setData(obj, data)
@@ -91,7 +91,7 @@ classdef FeFunction < Evaluable
         end
     end
     
-    methods (Access = 'private')
+    methods (Access = private)
         function handleChangedMesh(obj, ~, ~)
             obj.dataIsValid = false;
         end
diff --git a/lib/functions/Gradient.m b/lib/functions/Gradient.m
index 8eae3d8357beee37b70f6c29f8ff10d5e734d291..46b34ae96ff394dabc3e4004d2297fa99a8fcd33 100644
--- a/lib/functions/Gradient.m
+++ b/lib/functions/Gradient.m
@@ -8,13 +8,13 @@
 
 classdef Gradient < Evaluable
     %% properties
-    properties (SetAccess='protected', GetAccess='public')
+    properties (SetAccess=protected, GetAccess=public)
         u
         mesh
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = Gradient(u)
             arguments
                 u (1,1) FeFunction
diff --git a/lib/functions/Hessian.m b/lib/functions/Hessian.m
index 74d69d22dfcf1cf761894189e48f3447e2d4f1de..35a75fc2ee6cd2ba20aa6107c3fb1b5e806ac1f9 100644
--- a/lib/functions/Hessian.m
+++ b/lib/functions/Hessian.m
@@ -8,13 +8,13 @@
 
 classdef Hessian < Evaluable
     %% properties
-    properties (SetAccess='protected', GetAccess='public')
+    properties (SetAccess=protected, GetAccess=public)
         u
         mesh
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = Hessian(u)
             arguments
                 u (1,1) FeFunction
diff --git a/lib/functions/MeshFunction.m b/lib/functions/MeshFunction.m
index 6f52fb3a2ec49fc78732a49a9c81f3a3c97a311d..df0d0028cba8914c677724479d8c419eefe0c874 100644
--- a/lib/functions/MeshFunction.m
+++ b/lib/functions/MeshFunction.m
@@ -8,13 +8,13 @@
 
 classdef MeshFunction < Evaluable
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         mesh
         functionHandle
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = MeshFunction(mesh, functionHandle)
             arguments
                 mesh (1,1) Mesh
diff --git a/lib/functions/NormalVector.m b/lib/functions/NormalVector.m
index c19d24a1ded1d5454840da9578413a18a57d4cdc..2c728cc7bdff59a440139ef69ab5abe76865d278 100644
--- a/lib/functions/NormalVector.m
+++ b/lib/functions/NormalVector.m
@@ -8,12 +8,12 @@
 
 classdef NormalVector < Evaluable
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         mesh
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = NormalVector(mesh)
             obj.mesh = mesh;
         end
diff --git a/lib/functions/TestFunction.m b/lib/functions/TestFunction.m
index 7e2cbeeea44f4a6f7486a475fc37b1e2b1b6bbae..15a636c0fd1ff85f22cac60fb209530956b14540 100644
--- a/lib/functions/TestFunction.m
+++ b/lib/functions/TestFunction.m
@@ -9,13 +9,13 @@
 
 classdef TestFunction < Evaluable
     %% properties
-    properties (SetAccess='protected', GetAccess='public')
+    properties (SetAccess=protected, GetAccess=public)
         fes
         mesh
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = TestFunction(fes)
             arguments
                 fes (1,1) FeSpace
diff --git a/lib/functions/TestFunctionGradient.m b/lib/functions/TestFunctionGradient.m
index 1ee153976e67724b6a9ba10b44c9447265dab0e4..8e9a94462cdc21ab3f6fc44c4136d1ee9610d629 100644
--- a/lib/functions/TestFunctionGradient.m
+++ b/lib/functions/TestFunctionGradient.m
@@ -10,13 +10,13 @@
 
 classdef TestFunctionGradient < Evaluable
     %% properties
-    properties (SetAccess='protected', GetAccess='public')
+    properties (SetAccess=protected, GetAccess=public)
         fes
         mesh
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = TestFunctionGradient(fes)
             arguments
                 fes (1,1) FeSpace
diff --git a/lib/mesh/@Mesh/Mesh.m b/lib/mesh/@Mesh/Mesh.m
index 3305d23e1cfd6e3b4fe1b7657cc28d58c6e04be2..02333f174279cfbad7b491361d1ec0ab8b35d636 100644
--- a/lib/mesh/@Mesh/Mesh.m
+++ b/lib/mesh/@Mesh/Mesh.m
@@ -6,7 +6,7 @@
 
 classdef Mesh < handle
     %% properties
-    properties (GetAccess = 'public', SetAccess = 'private')
+    properties (GetAccess = public, SetAccess = private)
         coordinates (2,:) double
         elements (3,:) double
         edges (2,:) double
@@ -15,11 +15,6 @@ classdef Mesh < handle
         boundaries (:,1) cell
     end
     
-    properties (GetAccess = 'public', SetAccess = 'private', Hidden = true)
-        markedEdges (:,1) logical
-        bisecGroups (:,1) cell
-    end
-    
     %% dependent properties (computed from data)
     properties (Dependent)
         nCoordinates
@@ -29,18 +24,19 @@ classdef Mesh < handle
     end
     
     %% private properties for caching purposes
-    properties (Access = 'private')
+    properties (Access = private)
         trafo
     end
     
     %% events
     events
         IsAboutToRefine
-        HasChanged
+        JustRefined
+        RefineCompleted
     end
     
     %% public methods
-    methods (Access = 'public')
+    methods (Access = public)
         function obj = Mesh(coordinates, elements, boundaries)
             % Construct Mesh object from given coordinate, element, and boundary
             % arrays.
@@ -75,8 +71,8 @@ classdef Mesh < handle
     end
     
     %% protected methods
-    methods (Access='protected')
-        updateData(obj)
+    methods (Access=protected)
+        updateData(obj, bisecData)
     end
     
     %% get methods for dependent data
diff --git a/lib/mesh/@Mesh/refineLocally.m b/lib/mesh/@Mesh/refineLocally.m
index 5fb042bcb45a33e2e60fe739397c8b466b1453ed..972ac8217456506865d55d3e6170f0c924c5ea2e 100644
--- a/lib/mesh/@Mesh/refineLocally.m
+++ b/lib/mesh/@Mesh/refineLocally.m
@@ -22,14 +22,14 @@ end
 
 %% prepare data for refinement and make it available to listeners
 refinementAlgorithm = feval(method, obj);
-[obj.markedEdges, obj.bisecGroups] = refinementAlgorithm.prepareRefinementData(marked);
-obj.notify('IsAboutToRefine')
+bisecData = refinementAlgorithm.prepareRefinementData(marked);
+obj.notify('IsAboutToRefine', bisecData)
 
 %%  do actual refinement and clear data of old mesh
-obj.updateData();
-obj.markedEdges = [];
-obj.bisecGroups = [];
+obj.updateData(bisecData);
 obj.trafo = [];
-obj.notify('HasChanged');
+obj.notify('JustRefined');
+
+obj.notify('RefineCompleted')
 
 end
\ No newline at end of file
diff --git a/lib/mesh/@Mesh/updateData.m b/lib/mesh/@Mesh/updateData.m
index 2756179f204255ecc375d63a604149ce116b6a3f..7cdad5624b01c306a8bbaed11847efa18633b69f 100644
--- a/lib/mesh/@Mesh/updateData.m
+++ b/lib/mesh/@Mesh/updateData.m
@@ -5,80 +5,86 @@
 %
 %   See also Mesh
 
-function updateData(obj)
-%% create indices of ...
-% new nodes
-edgeNodes = obj.nCoordinates + (1:nnz(obj.markedEdges))';
-innerNodes = cumsum([edgeNodes(end); cellfun(@(x) x.nMembers*x.nInnerNodes, obj.bisecGroups)]);
-
-% new edges
-innerEdges = cumsum([obj.nEdges + nnz(obj.markedEdges); cellfun(@(x) x.nMembers*x.nInnerEdges, obj.bisecGroups)]);
-nChildEdges = 1 + obj.markedEdges;
-oldEdge2parent = cumsum([1; nChildEdges]);
-
-% new elements
-nChildElems = ones(obj.nElements, 1);
-for k = 1:length(obj.bisecGroups)
-    nChildElems(obj.bisecGroups{k}.elementIdx) = obj.bisecGroups{k}.nDescendants;
-end
-oldElement2parent = cumsum([1; nChildElems]);
+function updateData(obj, bisecData)
+%% create old-to-new connectivity
+oldEdge2parent = cumsum([1; 1+bisecData.bisectedEdges]);
+oldElement2parent = cumsum([1; getNChildrenPerElement(bisecData)]);
 
 %% create new coordinates
-newCoordinates = [obj.coordinates, ...
-    edgewiseCoordinates(obj, [1;1]/2, obj.markedEdges), ...
-    zeros(2, innerNodes(end)-innerNodes(1))];
-
-for k = 1:numel(obj.bisecGroups)
-    if obj.bisecGroups{k}.nInnerNodes ~= 0
-        newCoordinates(:,innerNodes(k)+1:innerNodes(k+1)) = ...
-            elementwiseCoordinates(obj, obj.bisecGroups{k}.innerNodes, obj.bisecGroups{k}.elementIdx);
-    end
+% new nodes = old nodes | nodes on edges | interior nodes
+idxN = cumsum([1; obj.nCoordinates; nnz(bisecData.bisectedEdges); ...
+    bisecData.nRefinedElements.*bisecData.nInnerNodes]);
+
+newCoordinates = zeros(2, idxN(end)-1);
+newCoordinates(:,idxN(1):idxN(2)-1) = obj.coordinates;
+newCoordinates(:,idxN(2):idxN(3)-1) = edgewiseCoordinates(obj, [1;1]/2, bisecData.bisectedEdges);
+
+for k = find(bisecData.nInnerNodes ~= 0)'
+    newCoordinates(:,idxN(k+2):idxN(k+3)-1) = ...
+        elementwiseCoordinates(obj, bisecData.bisection{k}.innerNodes, getRefinedElementIdx(bisecData, k));
 end
 
 %% refine existing edges
-newEdges = [moveParents(obj.edges, oldEdge2parent), ...
-    zeros(2, innerEdges(end) - innerEdges(1))];
+% new edges = old edges + children | interior edges
+idxS = cumsum([1; obj.nEdges + nnz(bisecData.bisectedEdges); ...
+    bisecData.nRefinedElements.*bisecData.nInnerEdges]);
 
-children = getChildIndices(oldEdge2parent(obj.markedEdges), 2);
-newEdges(:,children) = [obj.edges(1,obj.markedEdges), edgeNodes'; ...
-    edgeNodes', obj.edges(2,obj.markedEdges)];
+newEdges = zeros(2, idxS(end)-1);
+newEdges(:,oldEdge2parent(1:(end-1))) = obj.edges;
+
+edgeNodes = (idxN(2):idxN(3)-1)';
+children = getChildIndices(oldEdge2parent(bisecData.bisectedEdges), 2);
+newEdges(:,children) = [obj.edges(1,bisecData.bisectedEdges), edgeNodes'; ...
+    edgeNodes', obj.edges(2,bisecData.bisectedEdges)];
 
 %% refine boundaries
 newBoundaries = cell(obj.nBoundaries, 1);
 for k = 1:obj.nBoundaries
     newBoundaries{k} = oldEdge2parent(obj.boundaries{k});
-    bndChildren = newBoundaries{k}(obj.markedEdges(obj.boundaries{k})) + 1;
+    bndChildren = newBoundaries{k}(bisecData.bisectedEdges(obj.boundaries{k})) + 1;
     newBoundaries{k} = [newBoundaries{k}; bndChildren];
 end
 
-%% allocate new element-wise arrays
-oldEdgeNewIdx = oldEdge2parent(obj.element2edges);
-newElements = moveParents(obj.elements, oldElement2parent);
-newE2E = moveParents(oldEdgeNewIdx, oldElement2parent);
-newFlip = moveParents(obj.flipEdges, oldElement2parent);
+%% refine element data
+newElements = zeros(3, oldElement2parent(end)-1);
+newE2E = zeros(3, oldElement2parent(end)-1);
+newFlip = false(3, oldElement2parent(end)-1);
 
-%% generate bisection rule dependent data
-[left, right] = getLeftAndRightEdgePerElement(obj, oldEdgeNewIdx);
+oldEdgeNewIdx = oldEdge2parent(obj.element2edges);
+elems = bisecData.getRefinedElementIdx(0);
+newElements(:,oldElement2parent(elems)) = obj.elements(:,elems);
+newE2E(:,oldElement2parent(elems)) = oldEdgeNewIdx(:,elems);
+newFlip(:,oldElement2parent(elems)) = obj.flipEdges(:,elems);
+
+%% get left and right edge per element
+% here, we use the fact that left and right edge index only differ by 1
+bisectedEdges = bisecData.bisectedEdges(obj.element2edges);
+left = oldEdgeNewIdx.*bisectedEdges;
+right = left + bisectedEdges;
+left = left + obj.flipEdges;
+right = right - obj.flipEdges;
+
+%% refine elements + add inner edges
 edgeMidPts = zeros(obj.nEdges,1);
-edgeMidPts(obj.markedEdges) = edgeNodes;
+edgeMidPts(bisecData.bisectedEdges) = edgeNodes;
 edgeMidPts = edgeMidPts(obj.element2edges);
 
-for k = 1:length(obj.bisecGroups)
-    idx = (innerEdges(k)+1):innerEdges(k+1);
-    intEdges = reshape(idx, [], obj.bisecGroups{k}.nInnerEdges)';
-    intNodes = reshape((innerNodes(k)+1:innerNodes(k+1))', obj.bisecGroups{k}.nInnerNodes, []);
+for k = find(bisecData.nRefinedElements)'
+    idx = idxS(k+1):idxS(k+2)-1;
+    elems = getRefinedElementIdx(bisecData, k);
+    intEdges = reshape(idx, [], bisecData.nInnerEdges(k))';
+    intNodes = reshape((idxN(k+2):idxN(k+3)-1)', bisecData.nInnerNodes(k), []);
     
-    parentEdges = oldEdgeNewIdx(:, obj.bisecGroups{k}.elementIdx);
-    parentElements = oldElement2parent(obj.bisecGroups{k}.elementIdx);
-    children = getChildIndices(parentElements, obj.bisecGroups{k}.nDescendants);
+    parentEdges = oldEdgeNewIdx(:, elems);
+    parentElements = oldElement2parent(elems);
+    children = getChildIndices(parentElements, bisecData.nDescendants(k));
     
-    arg = restrictTo(obj.bisecGroups{k}.elementIdx, ...
-        obj.elements, edgeMidPts, left, right, obj.flipEdges);
+    arg = restrictTo(elems, obj.elements, edgeMidPts, left, right, obj.flipEdges);
     
-    newEdges(:,idx) = obj.bisecGroups{k}.createNewEdges(arg{1}, arg{2}, intNodes);
-    newElements(:,children) = obj.bisecGroups{k}.refineElement(arg{1}, arg{2}, intNodes);
-    newE2E(:,children) = obj.bisecGroups{k}.refineEdgeConnectivity(parentEdges, arg{3}, arg{4}, intEdges);
-    newFlip(:,children) = obj.bisecGroups{k}.refineEdgeOrientation(arg{5});
+    newEdges(:,idx) = bisecData.bisection{k}.createNewEdges(arg{1}, arg{2}, intNodes);
+    newElements(:,children) = bisecData.bisection{k}.refineElement(arg{1}, arg{2}, intNodes);
+    newE2E(:,children) = bisecData.bisection{k}.refineEdgeConnectivity(parentEdges, arg{3}, arg{4}, intEdges);
+    newFlip(:,children) = bisecData.bisection{k}.refineEdgeOrientation(arg{5});
 end
 
 %% assign new arrays
@@ -96,20 +102,6 @@ function children = getChildIndices(parents, nDescendants)
     children = reshape((0:(nDescendants-1)) + parents, [], 1);
 end
 
-function newArray = moveParents(oldArray, parentPositions)
-    newArray = zeros(size(oldArray, Dim.Vector), parentPositions(end)-1, class(oldArray));
-    newArray(:,parentPositions(1:(end-1))) = oldArray;
-end
-
 function restrictedArrays = restrictTo(idx, varargin)
     restrictedArrays = cellfun(@(x) x(:,idx), varargin, 'UniformOutput', false);
-end
-
-function [left, right] = getLeftAndRightEdgePerElement(mesh, oldEdgeNewIdx)
-    % here, we use the fact that left and right edge index only differ by 1
-    bisectedEdges = mesh.markedEdges(mesh.element2edges);
-    left = oldEdgeNewIdx.*bisectedEdges;
-    right = left + bisectedEdges;
-    left = left + mesh.flipEdges;
-    right = right - mesh.flipEdges;
-end
+end
\ No newline at end of file
diff --git a/lib/mesh/refinements/NVB.m b/lib/mesh/refinements/NVB.m
index 9a7384395d2e67e4ecdef01c707c486b42824c97..ae819ae629cedc8942fbd9a76156c5b0e6b4eb51 100644
--- a/lib/mesh/refinements/NVB.m
+++ b/lib/mesh/refinements/NVB.m
@@ -7,24 +7,24 @@
 
 classdef NVB
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         mesh
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = NVB(mesh)
             obj.mesh = mesh;
         end
         
-        function [markedEdges, bisecGroups] = prepareRefinementData(obj, markedElements)
+        function bisecData = prepareRefinementData(obj, markedElements)
             markedEdges = obj.markedElementsToEdges(markedElements);
             markedEdges = obj.meshClosure(markedEdges);
-            bisecGroups = obj.groupElements(markedEdges, markedElements);
+            bisecData = obj.groupElements(markedEdges, markedElements);
         end
     end
     
-    methods (Access='protected')
+    methods (Access=protected)
         function markedEdges = markedElementsToEdges(obj, markedElements)
             markedEdges = false(obj.mesh.nEdges,1);
             markedEdges(obj.mesh.element2edges(:,markedElements)) = true;
@@ -39,13 +39,14 @@ classdef NVB
             end
         end
         
-        function bisecGroups = groupElements(obj, markedEdges, ~)
+        function bisecData = groupElements(obj, markedEdges, ~)
             % group elements according to their refinement pattern
             edge = reshape(markedEdges(obj.mesh.element2edges), size(obj.mesh.element2edges));
-            bisecGroups{1} = Bisec1  ( find( edge(1,:) & ~edge(2,:) & ~edge(3,:) ) );
-            bisecGroups{2} = Bisec12 ( find( edge(1,:) &  edge(2,:) & ~edge(3,:) ) );
-            bisecGroups{3} = Bisec13 ( find( edge(1,:) & ~edge(2,:) &  edge(3,:) ) );
-            bisecGroups{4} = Bisec123( find( edge(1,:) &  edge(2,:) &  edge(3,:) ) );
+            bisecData = BisectionEventData(obj.mesh.nElements, markedEdges, ...
+                Bisec1,   find(edge(1,:) & ~edge(2,:) & ~edge(3,:)), ...
+            	Bisec12,  find(edge(1,:) &  edge(2,:) & ~edge(3,:)), ...
+            	Bisec13,  find(edge(1,:) & ~edge(2,:) &  edge(3,:)), ...
+            	Bisec123, find(edge(1,:) &  edge(2,:) &  edge(3,:)));
         end
     end
 end
diff --git a/lib/mesh/refinements/NVB1.m b/lib/mesh/refinements/NVB1.m
index 7af4bd8794401e163ea71a2fc0f7d92cb73aa7c1..59bf6bfcc9d3b698121a60e6455c1e1ea56c80eb 100644
--- a/lib/mesh/refinements/NVB1.m
+++ b/lib/mesh/refinements/NVB1.m
@@ -3,13 +3,13 @@
 
 classdef NVB1 < NVB
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = NVB1(mesh)
             obj = obj@NVB(mesh);
         end
     end
     
-    methods (Access='protected')
+    methods (Access=protected)
         function markedEdges = markedElementsToEdges(obj, markedElements)
             markedEdges = false(1,obj.mesh.nEdges);
             markedEdges(obj.mesh.element2edges(1,markedElements)) = true;
diff --git a/lib/mesh/refinements/NVB3.m b/lib/mesh/refinements/NVB3.m
index a6a14a8ddfef1b66873485a1b8702b45451c98f4..7ab6e12a98f9b166f183cdd40776fac2983f2db5 100644
--- a/lib/mesh/refinements/NVB3.m
+++ b/lib/mesh/refinements/NVB3.m
@@ -3,9 +3,9 @@
 
 classdef NVB3 < NVB   
     %% public methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = NVB3(mesh)
             obj = obj@NVB(mesh);
         end
     end
-end
\ No newline at end of file
+end
diff --git a/lib/mesh/refinements/NVB5.m b/lib/mesh/refinements/NVB5.m
index 15ff8de243e4aaa9bde81a5e8d95555b838e5123..aa8f4116cfb6cd8b31fd588a96df308e81417634 100644
--- a/lib/mesh/refinements/NVB5.m
+++ b/lib/mesh/refinements/NVB5.m
@@ -4,27 +4,30 @@
 
 classdef NVB5 < NVB
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = NVB5(mesh)
             obj = obj@NVB(mesh);
         end
     end
     
     %% methods
-    methods (Access='protected')
+    methods (Access=protected)
         function markedEdges = markedElementsToEdges(obj, markedElements)
             markedEdges = markedElementsToEdges@NVB(obj, markedElements);
         end
         
-        function bisecGroups = groupElements(obj, markedEdges, markedElements)
-            bisecGroups = groupElements@NVB(obj, markedEdges);
-            
+        function bisecData = groupElements(obj, markedEdges, markedElements)
             % split elements with 3 marked edges into 'has interior node' and 'doesnt'
-            noInteriorNode = false(1,obj.mesh.nElements);
-            noInteriorNode(bisecGroups{4}.elementIdx) = true;
-            noInteriorNode(markedElements) = false;
-            bisecGroups{4} = Bisec123(find(noInteriorNode));
-            bisecGroups{5} = Bisec5(markedElements);
+            hasInteriorNode = false(1,obj.mesh.nElements);
+            hasInteriorNode(markedElements) = true;
+            
+            edge = reshape(markedEdges(obj.mesh.element2edges), size(obj.mesh.element2edges));
+            bisecData = BisectionEventData(obj.mesh.nElements, markedEdges, ...
+                Bisec1,   find(edge(1,:) & ~edge(2,:) & ~edge(3,:)), ...
+            	Bisec12,  find(edge(1,:) &  edge(2,:) & ~edge(3,:)), ...
+            	Bisec13,  find(edge(1,:) & ~edge(2,:) &  edge(3,:)), ...
+            	Bisec123, find(edge(1,:) &  edge(2,:) &  edge(3,:) & ~hasInteriorNode), ...
+            	Bisec5,   find(edge(1,:) &  edge(2,:) &  edge(3,:) &  hasInteriorNode));
         end
     end
 end
diff --git a/lib/mesh/refinements/NVBEdge.m b/lib/mesh/refinements/NVBEdge.m
index 735c87c5fb942be8a87f18fac05dc01fef179fc8..cf1734737f42152fd15b82354aab833d54914f34 100644
--- a/lib/mesh/refinements/NVBEdge.m
+++ b/lib/mesh/refinements/NVBEdge.m
@@ -4,14 +4,14 @@
 
 classdef NVBEdge < NVB
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = NVBEdge(mesh)
             obj = obj@NVB(mesh);
         end
     end
     
     %% internal methods
-    methods (Access='protected')
+    methods (Access=protected)
         function markedEdges = markedElementsToEdges(obj, markedElements)
             markedEdges = false(1,obj.mesh.nEdges);
             markedEdges(markedElements) = true;
diff --git a/lib/mesh/refinements/RGB.m b/lib/mesh/refinements/RGB.m
index aa894349ed16c1da921cf57cef712a07a4f5e22c..cf8de10cb1234d927581efbeaf9fc71f4da78881 100644
--- a/lib/mesh/refinements/RGB.m
+++ b/lib/mesh/refinements/RGB.m
@@ -4,14 +4,14 @@
 
 classdef RGB < NVB
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = RGB(mesh)
             obj = obj@NVB(mesh);
         end
     end
     
     %% methods
-    methods (Access='protected')
+    methods (Access=protected)
         function markedEdges = markedElementsToEdges(obj, markedElements)
             % sort edges by length (while still retaining the orientation of the element)
             mesh = obj.mesh;
@@ -23,9 +23,13 @@ classdef RGB < NVB
             markedEdges(mesh.element2edges(:,markedElements)) = true;
         end
         
-        function bisecGroups = groupElements(obj, markedEdges, markedElements)
-            bisecGroups = groupElements@NVB(obj, markedEdges, markedElements);
-            bisecGroups{4} = BisecRed(bisecGroups{4}.elementIdx) ;
+        function bisecData = groupElements(obj, markedEdges, ~)
+            edge = reshape(markedEdges(obj.mesh.element2edges), size(obj.mesh.element2edges));
+            bisecData = BisectionEventData(obj.mesh.nElements, markedEdges, ...
+                Bisec1,   find(edge(1,:) & ~edge(2,:) & ~edge(3,:)), ...
+            	Bisec12,  find(edge(1,:) &  edge(2,:) & ~edge(3,:)), ...
+            	Bisec13,  find(edge(1,:) & ~edge(2,:) &  edge(3,:)), ...
+            	BisecRed, find(edge(1,:) &  edge(2,:) &  edge(3,:)));
         end
     end
 end
diff --git a/lib/mesh/refinements/bisection/AbstractBisection.m b/lib/mesh/refinements/bisection/AbstractBisection.m
index 7cd69ee80d5e3fa5f27abcb6c61b27afa6020453..11eed9282a363a6985bf1ebade8ffc7b7bb48a56 100644
--- a/lib/mesh/refinements/bisection/AbstractBisection.m
+++ b/lib/mesh/refinements/bisection/AbstractBisection.m
@@ -3,46 +3,21 @@
 
 classdef AbstractBisection
     %% properties
-    properties (Abstract, GetAccess='public', SetAccess='protected')
+    properties (Abstract, GetAccess=public, SetAccess=protected)
         innerNodes (3,:) double
         nInnerEdges (1,1) double
         nDescendants (1,1) double
     end
     
-    properties (Dependent)
-        nInnerNodes
-        nMembers
-    end
-    
-    properties (GetAccess='public', SetAccess='protected')
-        elementIdx (:,1) double
-    end
-    
     %% methods
-    methods (Access='public')
-        function obj = AbstractBisection(elementIndices)
-            obj.elementIdx = elementIndices;
-        end
-    end
-    
-    methods
-        function n = get.nInnerNodes(obj)
-            n = size(obj.innerNodes, Dim.Elements);
-        end
-        
-        function n = get.nMembers(obj)
-            n = length(obj.elementIdx);
-        end
-    end
-    
-    methods (Access='protected')
-        function [T, F] = getTrueFalseArrays(obj)
-            T = true(1, obj.nMembers);
-            F = false(1, obj.nMembers);
+    methods (Access=protected)
+        function [T, F] = getTrueFalseArrays(obj, n)
+            T = true(1, n);
+            F = false(1, n);
         end
     end
     
-    methods (Abstract, Access='public')
+    methods (Abstract, Access=public)
         newElements = refineElement(obj, elements, edge2newNodes, innerNodes)
         newEdges = createNewEdges(obj, elements, edge2newNodes, innerNodes)
         newElement2Edges = refineEdgeConnectivity(obj, element2edges, left, right, innerEdges)
diff --git a/lib/mesh/refinements/bisection/Bisec1.m b/lib/mesh/refinements/bisection/Bisec1.m
index 0f371ff69ea4752d22e69c4e2bad8afcd55875f6..a600835b33e4637a558bc542d37d5211d0c12ef2 100644
--- a/lib/mesh/refinements/bisection/Bisec1.m
+++ b/lib/mesh/refinements/bisection/Bisec1.m
@@ -3,14 +3,14 @@
 
 classdef Bisec1 < AbstractBisection
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         innerNodes = []
         nInnerEdges = 1
         nDescendants = 2
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function newElements = refineElement(~, oldNodes, edgeMidPts, ~)
             newElements = [oldNodes(3,:),   oldNodes(2,:); ...
                 oldNodes(1,:),   oldNodes(3,:); ...
@@ -28,7 +28,7 @@ classdef Bisec1 < AbstractBisection
         end
         
         function newOrientation = refineEdgeOrientation(obj, old)
-            [T, F] = getTrueFalseArrays(obj);
+            [T, F] = getTrueFalseArrays(obj, size(old,2));
             newOrientation = [old(3,:), old(2,:); ...
                 old(1,:),        T; ...
                 F, old(1,:)];
diff --git a/lib/mesh/refinements/bisection/Bisec12.m b/lib/mesh/refinements/bisection/Bisec12.m
index 01ddf9dadb1b548797b19a51c11d151487e54543..d39225ed6b17b70aa590aa89676e1968695fec09 100644
--- a/lib/mesh/refinements/bisection/Bisec12.m
+++ b/lib/mesh/refinements/bisection/Bisec12.m
@@ -3,14 +3,14 @@
 
 classdef Bisec12 < AbstractBisection
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         innerNodes = []
         nInnerEdges = 2
         nDescendants = 3
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function newElements = refineElement(~, oldNodes, edgeMidPts, ~)
             newElements = [oldNodes(3,:), edgeMidPts(1,:),   oldNodes(3,:); ...
                 oldNodes(1,:),   oldNodes(2,:), edgeMidPts(1,:); ...
@@ -30,7 +30,7 @@ classdef Bisec12 < AbstractBisection
         end
         
         function newOrientation = refineEdgeOrientation(obj, old)
-            [T, F] = getTrueFalseArrays(obj);
+            [T, F] = getTrueFalseArrays(obj, size(old,2));
             newOrientation = [old(3,:), old(1,:),       T; ...
                 old(1,:), old(2,:),       T; ...
                 F,        F, old(2,:)];
diff --git a/lib/mesh/refinements/bisection/Bisec123.m b/lib/mesh/refinements/bisection/Bisec123.m
index 49ce4036efbc5c14b5adfc987481db7765972878..25de0c7419061efb60634cc8248490c637d3c325 100644
--- a/lib/mesh/refinements/bisection/Bisec123.m
+++ b/lib/mesh/refinements/bisection/Bisec123.m
@@ -3,14 +3,14 @@
 
 classdef Bisec123 < AbstractBisection
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         innerNodes = []
         nInnerEdges = 3
         nDescendants = 4
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function newElements = refineElement(~, oldNodes, edgeMidPts, ~)
             newElements = [edgeMidPts(1,:),   oldNodes(1,:), edgeMidPts(1,:),   oldNodes(3,:); ...
                 oldNodes(3,:), edgeMidPts(1,:),   oldNodes(2,:), edgeMidPts(1,:); ...
@@ -29,7 +29,7 @@ classdef Bisec123 < AbstractBisection
         end
         
         function newOrientation = refineEdgeOrientation(obj, old)
-            [T, F] = getTrueFalseArrays(obj);
+            [T, F] = getTrueFalseArrays(obj, size(old,2));
             newOrientation = [F, old(1,:), old(1,:),        T; ...
                 old(3,:),        T, old(2,:),        T; ...
                 F, old(3,:),        F, old(2,:)];
diff --git a/lib/mesh/refinements/bisection/Bisec13.m b/lib/mesh/refinements/bisection/Bisec13.m
index 94418f917914195460d8506a8f431a27ad17a825..f67888fca295828d2881854e894d9a078b143237 100644
--- a/lib/mesh/refinements/bisection/Bisec13.m
+++ b/lib/mesh/refinements/bisection/Bisec13.m
@@ -3,14 +3,14 @@
 
 classdef Bisec13 < AbstractBisection
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         innerNodes = []
         nInnerEdges = 2
         nDescendants = 3
     end
     
     %% public methods
-    methods (Access='public')
+    methods (Access=public)
         function newElements = refineElement(~, oldNodes, edgeMidPts, ~)
             newElements = [edgeMidPts(1,:),   oldNodes(1,:),   oldNodes(2,:); ...
                 oldNodes(3,:), edgeMidPts(1,:),   oldNodes(3,:); ...
@@ -29,7 +29,7 @@ classdef Bisec13 < AbstractBisection
         end
         
         function newOrientation = refineEdgeOrientation(obj, old)
-            [T, F] = getTrueFalseArrays(obj);
+            [T, F] = getTrueFalseArrays(obj, size(old,2));
             newOrientation = [F, old(1,:), old(2,:); ...
                 old(3,:),        T,        T; ...
                 F, old(3,:), old(1,:)];
diff --git a/lib/mesh/refinements/bisection/Bisec5.m b/lib/mesh/refinements/bisection/Bisec5.m
index 412b348547146664b3c2628edc7cdec0d1dac3bb..2800f8ed9af6c46c94111aca22305a31a9a1db2c 100644
--- a/lib/mesh/refinements/bisection/Bisec5.m
+++ b/lib/mesh/refinements/bisection/Bisec5.m
@@ -3,7 +3,7 @@
 
 classdef Bisec5 < AbstractBisection
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         innerNodes = [1;1;2]/4;
         nInnerEdges = 6
         nDescendants = 6
@@ -11,7 +11,7 @@ classdef Bisec5 < AbstractBisection
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function newElements = refineElement(~, oldNodes, edgeMidPts, innerNodes)
             newElements = [edgeMidPts(3,:),   oldNodes(3,:),   oldNodes(1,:), edgeMidPts(1,:), edgeMidPts(2,:), edgeMidPts(1,:); ...
                 edgeMidPts(1,:), edgeMidPts(3,:), edgeMidPts(1,:),   oldNodes(2,:),   oldNodes(3,:), edgeMidPts(2,:); ...
@@ -30,7 +30,7 @@ classdef Bisec5 < AbstractBisection
         end
         
         function newOrientation = refineEdgeOrientation(obj, old)
-            [T, F] = getTrueFalseArrays(obj);
+            [T, F] = getTrueFalseArrays(obj, size(old,2));
             newOrientation = [T, old(3,:), old(1,:), old(1,:), old(2,:), T; ...
                 F,        F,        F, old(2,:),        F, F; ...
                 T,        T, old(3,:),        F,        T, T];
diff --git a/lib/mesh/refinements/bisection/BisecRed.m b/lib/mesh/refinements/bisection/BisecRed.m
index 0125fdb2dca4ac8f6c98e365cde7ad2823975123..7435ac39a021d1389010da12a3537ba017cb394a 100644
--- a/lib/mesh/refinements/bisection/BisecRed.m
+++ b/lib/mesh/refinements/bisection/BisecRed.m
@@ -3,14 +3,14 @@
 
 classdef BisecRed < AbstractBisection
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         innerNodes = []
         nInnerEdges = 3
         nDescendants = 4
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function newElements = refineElement(~, oldNodes, edgeMidPts, ~)
             newElements = [oldNodes(1,:), edgeMidPts(1,:), edgeMidPts(3,:), edgeMidPts(2,:); ...
                 edgeMidPts(1,:),   oldNodes(2,:), edgeMidPts(2,:), edgeMidPts(3,:); ...
@@ -29,7 +29,7 @@ classdef BisecRed < AbstractBisection
         end
         
         function newOrientation = refineEdgeOrientation(obj, old)
-            [T, F] = getTrueFalseArrays(obj);
+            [T, F] = getTrueFalseArrays(obj, size(old,2));
             newOrientation = [old(1,:), old(1,:),        F, T; ...
                 F, old(2,:), old(2,:), T; ...
                 old(3,:),        F, old(3,:), T];
diff --git a/lib/mesh/utils/AffineTransformation.m b/lib/mesh/utils/AffineTransformation.m
index a4026e71aaf4c2868ebd614771217f810f9e5f81..2a93023c97983dc44dbce58db1e59e8d3f7577d5 100644
--- a/lib/mesh/utils/AffineTransformation.m
+++ b/lib/mesh/utils/AffineTransformation.m
@@ -9,7 +9,7 @@
 
 classdef AffineTransformation < handle
     %% properties
-    properties (GetAccess = 'public', SetAccess = 'private')
+    properties (GetAccess=public, SetAccess=private)
         DFinv (4,:) double
         area (1,:) double
         ds (1,:) double
diff --git a/lib/mesh/utils/BisectionEventData.m b/lib/mesh/utils/BisectionEventData.m
new file mode 100644
index 0000000000000000000000000000000000000000..35d39cc1f0b7bcf542813b53f28aa1dc88f21f03
--- /dev/null
+++ b/lib/mesh/utils/BisectionEventData.m
@@ -0,0 +1,75 @@
+% BisectionEventData (subclass of event.EventData) Encapsulates data used for
+%   bisection.
+
+classdef (ConstructOnLoad) BisectionEventData < event.EventData
+    properties (GetAccess=public, SetAccess=protected)
+        nElements (1,1) double
+        bisectedEdges (:,1) logical
+        bisection (:,1) cell
+        nNonRefinedElements (1,1) double
+        nRefinedElements (:,1) double
+        nDescendants (:,1) double
+        nInnerNodes (:,1) double
+        nInnerEdges (:,1) double
+    end
+    
+    properties (Access=protected)
+        nonRefinedElements
+        refinedElements
+    end
+    
+    properties (Dependent)
+        nBisecMethods
+    end
+    
+    methods (Access=public)
+        function obj = BisectionEventData(nElements, bisectedEdges, bisecMethods, refinedElements)
+            arguments
+                nElements
+                bisectedEdges
+            end
+            arguments (Repeating)
+                bisecMethods
+                refinedElements
+            end
+            
+            obj.nElements = nElements;
+            obj.bisectedEdges = bisectedEdges;
+            obj.bisection = bisecMethods;
+            obj.refinedElements = refinedElements;
+            
+            obj.nRefinedElements = cellfun(@(x) numel(x), refinedElements);
+            obj.nDescendants = cellfun(@(x) x.nDescendants, bisecMethods);
+            obj.nInnerNodes = cellfun(@(x) size(x.innerNodes,2), bisecMethods);
+            obj.nInnerEdges = cellfun(@(x) x.nInnerEdges, bisecMethods);
+            
+            elements = true(nElements, 1);
+            elements(horzcat(obj.refinedElements{:})) = false;
+            obj.nonRefinedElements = find(elements);
+            obj.nNonRefinedElements = numel(obj.nonRefinedElements);
+        end
+        
+        function children = getNChildrenPerElement(obj)
+            children = accumarray(horzcat(obj.refinedElements{:})', ...
+                repelem(obj.nDescendants, obj.nRefinedElements), ...
+                [obj.nElements, 1], [], 1);
+        end
+        
+        % 0 = non-refined, 1,... = refined according to bisection{k}
+        function idx = getRefinedElementIdx(obj, k)
+            assert((0 <= k) && (k <= obj.nBisecMethods))
+            if k == 0
+                idx = obj.nonRefinedElements;
+            else
+                idx = obj.refinedElements{k};
+            end
+        end
+    end
+    
+    methods    
+        function val = get.nBisecMethods(obj)
+            val = numel(obj.bisection);
+        end
+    end
+end
+
diff --git a/lib/quadrature/barycentric/Barycentric.m b/lib/quadrature/barycentric/Barycentric.m
index 670652e44fa1c41a6151a57d9c5a3c79542497d2..9cc4f4a171b9cc7c6a1abf46c3789b352b2c885f 100644
--- a/lib/quadrature/barycentric/Barycentric.m
+++ b/lib/quadrature/barycentric/Barycentric.m
@@ -10,17 +10,17 @@
 
 classdef Barycentric
     %% properties
-    properties (GetAccess = 'public', SetAccess = 'protected')
+    properties (GetAccess=public, SetAccess=protected)
         coordinates
         nNodes
     end
     
-    properties (Abstract, GetAccess = 'public', SetAccess = 'protected')
+    properties (Abstract, GetAccess=public, SetAccess=protected)
         dim
     end
     
     %% methods
-    methods (Access = 'public')
+    methods (Access=public)
         function obj = Barycentric(coordinates)
             arguments
                 coordinates {Barycentric.verify}
diff --git a/lib/quadrature/barycentric/Barycentric1D.m b/lib/quadrature/barycentric/Barycentric1D.m
index 77379fe1de0c2867c1c569ee4ec8681ba841b100..d2cadd38404643669c95f52c978b2a4692d2ac8c 100644
--- a/lib/quadrature/barycentric/Barycentric1D.m
+++ b/lib/quadrature/barycentric/Barycentric1D.m
@@ -2,7 +2,7 @@
 %   coordinates on edges.
 
 classdef Barycentric1D < Barycentric
-    properties (GetAccess = 'public', SetAccess = 'protected')
+    properties (GetAccess=public, SetAccess=protected)
         dim = 1;
     end
-end
\ No newline at end of file
+end
diff --git a/lib/quadrature/barycentric/Barycentric2D.m b/lib/quadrature/barycentric/Barycentric2D.m
index b47e38d9fcbcfa0e00bffe8a30b523e9d78d7d30..9fa6444d00cb92448790a193774670a0059bb212 100644
--- a/lib/quadrature/barycentric/Barycentric2D.m
+++ b/lib/quadrature/barycentric/Barycentric2D.m
@@ -5,7 +5,7 @@
 %       Barycentric1D on edge given by edgeInd.
 
 classdef Barycentric2D < Barycentric
-    properties (GetAccess = 'public', SetAccess = 'protected')
+    properties (GetAccess=public, SetAccess=protected)
         dim = 2;
     end
     
@@ -24,4 +24,4 @@ classdef Barycentric2D < Barycentric
             bary2D = Barycentric2D(newNodes);
         end
     end
-end
\ No newline at end of file
+end
diff --git a/lib/quadrature/quadratureRules/QuadratureRule.m b/lib/quadrature/quadratureRules/QuadratureRule.m
index 3fafec1c1a2ad26265b29947cc272e3d875e4305..38d2340340b1e091e0ce185a3ff0d45e65f630ad 100644
--- a/lib/quadrature/quadratureRules/QuadratureRule.m
+++ b/lib/quadrature/quadratureRules/QuadratureRule.m
@@ -14,7 +14,7 @@
 
 classdef QuadratureRule
     %% properties
-    properties (GetAccess = 'public', SetAccess = 'protected')
+    properties (GetAccess=public, SetAccess=protected)
         bary
         weights
         nNodes
@@ -22,7 +22,7 @@ classdef QuadratureRule
     end
     
     %% methods
-    methods (Access = 'public')
+    methods (Access=public)
         function obj = QuadratureRule(bary, weights)            
             arguments
                 bary (1,1) Barycentric
diff --git a/lib/spaces/@FeSpace/FeSpace.m b/lib/spaces/@FeSpace/FeSpace.m
index 1b8b72280172ad67311df65002b4d9a7c9eb628e..d0fbc2da0753f1f10c823febdc99020bd0488930 100644
--- a/lib/spaces/@FeSpace/FeSpace.m
+++ b/lib/spaces/@FeSpace/FeSpace.m
@@ -4,24 +4,24 @@
 
 classdef FeSpace < handle
     %% properties
-    properties (GetAccess = 'public', SetAccess = 'protected')
+    properties (GetAccess=public, SetAccess=protected)
         mesh
         finiteElement
         dirichlet
     end
     
-    properties (Access = 'private')
+    properties (Access=private)
         dofs
         freeDofs
     end
     
     %% events
-    properties (Access = 'private')
+    properties (Access=private)
         listenerHandle
     end
     
     %% methods
-    methods (Access = 'public')
+    methods (Access=public)
         function obj = FeSpace(mesh, fe, opt)
             % Construct FeSpace from given mesh and finite element.
             %
@@ -39,7 +39,7 @@ classdef FeSpace < handle
             end
             
             % register fespace as an observer of mesh
-            obj.listenerHandle = mesh.listener('HasChanged', @obj.handleChangedMesh);
+            obj.listenerHandle = mesh.listener('JustRefined', @obj.handleChangedMesh);
             
             % initialize lazily evaluated properties
             obj.dofs = [];
@@ -79,12 +79,12 @@ classdef FeSpace < handle
         end
     end
     
-    methods (Access = 'protected')
+    methods (Access=protected)
         dofs = assembleDofs(obj)
         freeDofs = assembleFreeDofs(obj)
     end
     
-    methods (Access = 'private')
+    methods (Access=private)
         handleChangedMesh(obj, src, event)
     end
 end
diff --git a/lib/spaces/finiteElements/BernsteinBezierPoly.m b/lib/spaces/finiteElements/BernsteinBezierPoly.m
index d3bc3e581d46b753e2230a2307857227bcb0c082..e0cde89867db6cfab1442fa41980d243a42f0dcc 100644
--- a/lib/spaces/finiteElements/BernsteinBezierPoly.m
+++ b/lib/spaces/finiteElements/BernsteinBezierPoly.m
@@ -7,14 +7,14 @@
 
 classdef BernsteinBezierPoly
     %% properties
-    properties (Access='protected')
+    properties (Access=protected)
         constants
         exponents
         nFun
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = setExponents(obj, exponents)
             arguments
                 obj
diff --git a/lib/spaces/finiteElements/FiniteElement.m b/lib/spaces/finiteElements/FiniteElement.m
index f8e313ba851aab13150af276fc358f845e34d92b..8ae7891630d9672b4737d681591149b0e751431a 100644
--- a/lib/spaces/finiteElements/FiniteElement.m
+++ b/lib/spaces/finiteElements/FiniteElement.m
@@ -3,7 +3,7 @@
 %   the dofs are connected.
 
 classdef FiniteElement
-    methods (Abstract, Access = 'public')
+    methods (Abstract, Access=public)
         % evalShapeFunctions Evaluate local shapefunctions at barycentric
         %   coordinates
         evalShapeFunctions(obj, bary)
diff --git a/lib/spaces/finiteElements/HigherOrderH1Fe.m b/lib/spaces/finiteElements/HigherOrderH1Fe.m
index cfaa48fdd57d5c1f34ce1c60b54d303a4fceefd2..2ce914d9472d45100fa6d3f5e871fc03eddb284d 100644
--- a/lib/spaces/finiteElements/HigherOrderH1Fe.m
+++ b/lib/spaces/finiteElements/HigherOrderH1Fe.m
@@ -4,7 +4,7 @@
 
 classdef HigherOrderH1Fe < NodalFiniteElement & BernsteinBezierPoly
     %% implementation of abstract superclass properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         order
         nodeDofLocations
         edgeDofLocations
@@ -12,7 +12,7 @@ classdef HigherOrderH1Fe < NodalFiniteElement & BernsteinBezierPoly
     end
     
     %% implementation of abstract superclass methods
-    methods (Access = 'public')
+    methods (Access=public)
         function obj = HigherOrderH1Fe(order)
             arguments
                 order (1,1) double {mustBePositive}
diff --git a/lib/spaces/finiteElements/HigherOrderL2Fe.m b/lib/spaces/finiteElements/HigherOrderL2Fe.m
index 9db6258291fc916f7e4addf71d822b1f5e300bc3..c8dd2e049a36d73138c869f22fad840a7457d2de 100644
--- a/lib/spaces/finiteElements/HigherOrderL2Fe.m
+++ b/lib/spaces/finiteElements/HigherOrderL2Fe.m
@@ -4,7 +4,7 @@
 
 classdef HigherOrderL2Fe < NodalFiniteElement & BernsteinBezierPoly
     %% implementation of abstract superclass properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         order
         nodeDofLocations
         edgeDofLocations
@@ -12,7 +12,7 @@ classdef HigherOrderL2Fe < NodalFiniteElement & BernsteinBezierPoly
     end
     
     %% implementation of abstract superclass methods
-    methods (Access = 'public')
+    methods (Access=public)
         %% constructor
         function obj = HigherOrderL2Fe(order)
             arguments
diff --git a/lib/spaces/finiteElements/LowestOrderH1Fe.m b/lib/spaces/finiteElements/LowestOrderH1Fe.m
index 8cde03cdb4ae2c70e09fa4ed565dddc5cc29309a..7bfe66c2d320c66800a5aa49da3cf7c1c77a3823 100644
--- a/lib/spaces/finiteElements/LowestOrderH1Fe.m
+++ b/lib/spaces/finiteElements/LowestOrderH1Fe.m
@@ -4,7 +4,7 @@
 
 classdef LowestOrderH1Fe < NodalFiniteElement
     %% implementation of abstract superclass properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         order
         nodeDofLocations
         edgeDofLocations
@@ -12,7 +12,7 @@ classdef LowestOrderH1Fe < NodalFiniteElement
     end
     
     %% implementation of abstract superclass methods
-    methods (Access = 'public')
+    methods (Access=public)
         function obj = LowestOrderH1Fe()
             obj.order = 1;
             obj.nodeDofLocations = 1;
diff --git a/lib/spaces/finiteElements/LowestOrderL2Fe.m b/lib/spaces/finiteElements/LowestOrderL2Fe.m
index 7b5002c7a40cb92a00510f64f0d37ba93c12e1df..8c11644e983077cfb1811e65de85f3518f94bf08 100644
--- a/lib/spaces/finiteElements/LowestOrderL2Fe.m
+++ b/lib/spaces/finiteElements/LowestOrderL2Fe.m
@@ -4,7 +4,7 @@
 
 classdef LowestOrderL2Fe < NodalFiniteElement
     %% implementation of abstract superclass properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         order
         nodeDofLocations
         edgeDofLocations
@@ -12,7 +12,7 @@ classdef LowestOrderL2Fe < NodalFiniteElement
     end
     
     %% implementation of abstract superclass methods
-    methods (Access = 'public')
+    methods (Access=public)
         function obj = LowestOrderL2Fe()
             obj.order = 0;
             obj.nodeDofLocations = [];
diff --git a/lib/spaces/finiteElements/NodalFiniteElement.m b/lib/spaces/finiteElements/NodalFiniteElement.m
index 58da69af0cc6e7bc8484dd2a90959244d91f3488..9766fd53e54ffed82cd6687d021c3a2bd6f52be7 100644
--- a/lib/spaces/finiteElements/NodalFiniteElement.m
+++ b/lib/spaces/finiteElements/NodalFiniteElement.m
@@ -5,7 +5,7 @@
 
 classdef NodalFiniteElement < FiniteElement
     %% properties
-    properties (Abstract, GetAccess='public', SetAccess='protected')
+    properties (Abstract, GetAccess=public, SetAccess=protected)
         nodeDofLocations
         edgeDofLocations
         innerDofLocations
diff --git a/lib/spaces/interpolation/FeProlongation.m b/lib/spaces/interpolation/FeProlongation.m
new file mode 100644
index 0000000000000000000000000000000000000000..4dc5d6b97ac4e223403a4794c378ad118f6e3a8a
--- /dev/null
+++ b/lib/spaces/interpolation/FeProlongation.m
@@ -0,0 +1,134 @@
+% FeProlongation (subclass of Prolongation) Provides prolongation operator for
+%   general FeFunction to a refined mesh.
+%
+%   P = FeProlongation(fes) returns a handle to the prolongation object
+%       associated to the finite element space fes. The prolongation matrix
+%       P.matrix is set automatically at mesh refinement.
+%
+%   prolongate(P, u) returns the prolongated data of FeFunction u.
+
+classdef FeProlongation < Prolongation
+    %% properties
+    properties (Access=private)
+        postRefineListener
+    end
+    
+    %% methods
+    methods (Access=public)
+        function obj = FeProlongation(fes)
+            obj = obj@Prolongation(fes);
+            obj.postRefineListener = fes.mesh.listener('RefineCompleted', @obj.connectDofs);
+            
+            assert(isa(obj.fes.finiteElement, 'NodalFiniteElement'), ...
+                'Prolongation defined only for nodal finite elements.')
+        end
+    end
+    
+    methods (Access=protected)
+        function setupMatrix(obj, ~, data)
+            % general idea: compute *all* dofs for each new element and store
+            % them consecutively
+            % those are connected to the actual new dofs when their numbering
+            % per element is available (-> obj.connectDofs)
+            
+            % de-allocate for memory-efficiency
+            obj.matrix = [];
+            
+            % get dof information
+            oldDofs = getDofs(obj.fes);
+            fe = obj.fes.finiteElement;
+            dofLocations = getDofLocations(fe);
+            nLocalDofs = dofLocations.nNodes;
+            
+            % pre-allocate
+            element2newDof = cumsum([1; getNChildrenPerElement(data)*nLocalDofs]);
+            n = data.nNonRefinedElements*nLocalDofs + sum(data.nRefinedElements)*nLocalDofs^2;
+            [I, J, V] = deal(zeros(n, 1));
+            
+            % for non-refined elements, just transfer the dofs
+            n = data.nNonRefinedElements*nLocalDofs;
+            if n ~= 0
+                idx = (1:n)';
+                nonRefinedElems = getRefinedElementIdx(data, 0);
+                I(idx) = getConsecutiveIndices(element2newDof(nonRefinedElems), nLocalDofs);
+                J(idx) = reshape(oldDofs.element2Dofs(:,nonRefinedElems), [], 1);
+                V(idx) = 1;
+            end
+            ptr = n;
+            
+            % handle refined elements (nodal interpolation on new elements)
+            localMatrix = squeeze(evalShapeFunctions(fe, dofLocations));
+            for k = find(data.nRefinedElements ~= 0)'
+                unitTriangle = getBisectedUnitTriangle(class(data.bisection{k}));
+                newDofLocations = getNewLocationsElementwise(unitTriangle, dofLocations);
+                newDofWeights = divideElementwise(...
+                    reshape(evalShapeFunctions(fe, newDofLocations), nLocalDofs, nLocalDofs, []), localMatrix);
+
+                nNewLocs = newDofLocations.nNodes;
+                elems = getRefinedElementIdx(data, k);
+                n = data.nRefinedElements(k) * numel(newDofWeights);
+                idx = ptr + (1:n)';
+                I(idx) = repelem(getConsecutiveIndices(element2newDof(elems), nNewLocs), nLocalDofs);
+                J(idx) = reshape(repelem(oldDofs.element2Dofs(:,elems), 1, nNewLocs), [], 1);
+                V(idx) = reshape(repmat(newDofWeights, 1, data.nRefinedElements(k)), [], 1);
+                ptr = ptr + n;
+            end
+            
+            nNewDofs = element2newDof(end)-1;
+            obj.matrix = sparse(I, J, V, nNewDofs, oldDofs.nDofs);
+        end
+    
+        function connectDofs(obj, ~, ~)
+            dofs = getDofs(obj.fes);
+            nOutput = size(obj.matrix, 1);
+            idx = zeros(dofs.nDofs, 1);
+            
+            idx(dofs.element2Dofs(:)) = 1:nOutput;
+            obj.matrix = obj.matrix(idx,:);
+        end
+    end
+end
+
+%% local functions
+function mesh = getBisectedUnitTriangle(bisecMethod)
+    mesh = Mesh.loadFromGeometry('unittriangle');
+
+    % bisect unit triangle according to given bisection rule
+    % TODO: this should be handled by bisection method itself?
+    switch bisecMethod
+        case 'Bisec1'
+            mesh.refineLocally(3, 'NVBEdge');
+        case 'Bisec12'
+            mesh.refineLocally([2,3], 'NVBEdge');
+        case 'Bisec13'
+            mesh.refineLocally([1,3], 'NVBEdge');
+        case 'Bisec123'
+            mesh.refineUniform(1, 'NVB');
+        case 'Bisec5'
+            mesh.refineUniform(1, 'NVB5');
+        case 'BisecRed'
+            mesh.refineUniform(1, 'RGB');
+        otherwise
+            error(['Bisection method ', bisecMethod, ' not related to refinement of unit triangle.'])
+    end
+end
+
+function elementwiseLocations = getNewLocationsElementwise(mesh, locations)
+    % collect dofs such that all locations of one child appear before those of other children
+    % -> permute dimensions: 2 = children, 3 = barycentric coordinates
+    elementwiseLocations = permute(elementwiseCoordinates(mesh, locations), [1,3,2]);
+    elementwiseLocations = reshape(elementwiseLocations, 2, [], 1);
+    elementwiseLocations = Barycentric2D(...
+        cat(Dim.Vector, elementwiseLocations, 1-sum(elementwiseLocations, Dim.Vector)));
+end
+
+function C = divideElementwise(A, B)
+    % TODO: from 2022a on, this can be replaced by pagemrdivide(A, B)
+    n = size(A,1);
+    C = B' \ reshape(permute(A, [2,1,3]), n, []);
+    C = reshape(permute(reshape(C, n, n, []), [2,1,3]), n, []);
+end
+
+function idx = getConsecutiveIndices(parents, nIndices)
+    idx = reshape(((0:(nIndices-1)) + parents)', [], 1);
+end
diff --git a/lib/spaces/interpolation/LinearFeProlongation.m b/lib/spaces/interpolation/LinearFeProlongation.m
deleted file mode 100644
index b28782363a7472016fa0b430b32aed57f0b24f1a..0000000000000000000000000000000000000000
--- a/lib/spaces/interpolation/LinearFeProlongation.m
+++ /dev/null
@@ -1,43 +0,0 @@
-% LinearFeProlongation (subclass of Prolongation) Provides piecewise linear
-%   interpolant of FeFunction on a refined mesh.
-%
-%   Pu = LinearFeProlongation(u) returns the piecewise linear interpolation of u
-%       that can be prolongated to a finer mesh.
-%
-%   Pu.interpolatedData is the data of the prolongation and is set automatically
-%       at mesh refinement.
-
-classdef LinearFeProlongation < Prolongation
-    %% properties
-    properties (GetAccess='public', SetAccess='protected')
-        interpolatedData
-    end
-    
-    properties (Access='private')
-        interpolant
-    end
-    
-    %% methods
-    methods (Access='public')
-        function obj = LinearFeProlongation(feFunction)
-            obj = obj@Prolongation(feFunction);
-            obj.interpolant = [];
-        end
-    end
-    
-    methods
-        function val = get.interpolatedData(obj)
-            if ~isempty(obj.interpolant)
-                obj.interpolatedData = nodalInterpolation(obj.interpolant, obj.u.fes);
-                obj.interpolant = [];
-            end
-            val = obj.interpolatedData;
-        end
-    end
-    
-    methods (Access='protected')
-        function interpolate(obj, ~, ~)
-            obj.interpolant = LinearInterpolation(obj.u);
-        end
-    end
-end
diff --git a/lib/spaces/interpolation/LinearInterpolation.m b/lib/spaces/interpolation/LinearInterpolation.m
index e5482a882736d1c817fa4ac8b261229082d10286..d5d1e7db815f156d168e6d17cb4d3fe2a0844ab2 100644
--- a/lib/spaces/interpolation/LinearInterpolation.m
+++ b/lib/spaces/interpolation/LinearInterpolation.m
@@ -5,17 +5,17 @@
 
 classdef LinearInterpolation < Evaluable
     %% properties
-    properties (GetAccess='public', SetAccess='protected')
+    properties (GetAccess=public, SetAccess=protected)
         mesh
         u
     end
     
-    properties (Access='private')
+    properties (Access=private)
         interpolant
     end
     
     %% methods
-    methods (Access='public')
+    methods (Access=public)
         function obj = LinearInterpolation(u)
             arguments
                 u Evaluable
diff --git a/lib/spaces/interpolation/LoFeProlongation.m b/lib/spaces/interpolation/LoFeProlongation.m
new file mode 100644
index 0000000000000000000000000000000000000000..2c2603e8cf42a7371dabb010b93874d389047f30
--- /dev/null
+++ b/lib/spaces/interpolation/LoFeProlongation.m
@@ -0,0 +1,91 @@
+% LoFeProlongation (subclass of Prolongation) Prolongate lowest order L^2/H^1
+%   conforming finite element function to refined mesh.
+%
+%   P = LoFeProlongation(fes) returns a handle to the prolongation object
+%       associated to the finite element space fes. The prolongation matrix
+%       P.matrix is set automatically at mesh refinement.
+%
+%   prolongate(P, u) returns the prolongated data of FeFunction u.
+
+classdef LoFeProlongation < Prolongation
+    %% properties
+    properties (Access=protected)
+        feType
+    end
+    %% methods
+    methods (Access=public)
+        function obj = LoFeProlongation(fes)
+            obj = obj@Prolongation(fes);
+            switch class(fes.finiteElement)
+                case 'LowestOrderL2Fe'
+                    obj.feType = 'L2';
+                case 'LowestOrderH1Fe'
+                    obj.feType = 'H1';
+                otherwise
+                    eid = 'LoFeProlongation:wrongFeType';
+                    msg = 'LoFeProlongation needs a lowest order L2 or H1 finite element space.';
+                    throwAsCaller(MException(eid, msg));
+            end
+        end
+    end
+    
+    methods (Access=protected)
+        function setupMatrix(obj, mesh, data)
+            if isequal(obj.feType, 'L2')
+                setupMatrixL2(obj, mesh, data);
+            elseif isequal(obj.feType, 'H1')
+                setupMatrixH1(obj, mesh, data);
+            end
+        end
+        
+        function setupMatrixL2(obj, mesh, data)
+            % use that for lowest order L2 elements the dofs correspond to
+            % elements and indices of new elements is known
+            nChildren = getNChildrenPerElement(data);
+            nNewElements = sum(nChildren);
+            
+            obj.matrix = sparse(1:nNewElements, repelem(1:mesh.nElements, nChildren), ...
+                ones(nNewElements,1), nNewElements, mesh.nElements);
+        end
+        
+        function setupMatrixH1(obj, mesh, data)
+            % use that for lowest order H1 elements the dofs correspond to
+            % coordinates and new coordinates reside on edges or on inner nodes
+            dofs = getDofs(obj.fes);
+            nEntries = mesh.nCoordinates + 2*nnz(data.bisectedEdges) + ...
+                3*sum(data.nInnerNodes.*data.nRefinedElements);
+            [I, J, V] = deal(zeros(nEntries, 1));
+            
+            % node dofs stay the same
+            idx = 1:mesh.nCoordinates; 
+            I(idx) = idx';
+            J(idx) = idx';
+            V(idx) = ones(idx(end), 1);
+            dofNr = mesh.nCoordinates;
+            
+            % edge dofs are mean of edge dofs
+            n = nnz(data.bisectedEdges);
+            idx = idx(end) + (1:2*n);
+            I(idx) = repelem(dofNr + (1:n)', 2);
+            J(idx) = reshape(dofs.edge2Dofs(:,data.bisectedEdges), [], 1);
+            V(idx) = ones(2*n, 1)/2;
+            dofNr = idx(end) + n;
+            
+            % inner dofs are weighted sum of element dofs
+            idxEnd = idx(end);
+            for k = find(data.nRefinedElements)'
+                n = data.nRefinedElements(k)*data.nInnerNodes(k);
+                idx = idxEnd + (1:3*n);
+                I(idx) = repelem(dofNr + (1:n)', 3);
+                J(idx) = reshape(repelem(dofs.element2Dofs, [1;1;1], data.nInnerNodes(k)), [], 1);
+                V(idx) = reshape(repmat(data.bisection{k}.innerNodes, 1, data.nInnerNodes(k)), [], 1);
+                dofNr = dofNr + n;
+                idxEnd = idxEnd + 3*n;
+            end
+            
+            nNewDofs = mesh.nCoordinates + nnz(data.bisectedEdges) + ...
+                sum(data.nInnerNodes.*data.nRefinedElements);
+            obj.matrix = sparse(I, J, V, nNewDofs, dofs.nDofs);
+        end
+    end
+end
diff --git a/lib/spaces/interpolation/LoH1Prolongation.m b/lib/spaces/interpolation/LoH1Prolongation.m
deleted file mode 100644
index 10e505accb35679b03c72e65bd825074140b8c70..0000000000000000000000000000000000000000
--- a/lib/spaces/interpolation/LoH1Prolongation.m
+++ /dev/null
@@ -1,41 +0,0 @@
-% LoH1Prolongation (subclass of Prolongation) Prolongate lowest order H^1
-%   conforming finite element function to refined mesh.
-%
-%   Pu = LoH1Prolongation(u) returns a handle to the prolongation object.
-%
-%   Pu.interpolatedData is the data of the prolongation and is set automatically
-%       at mesh refinement.
-
-classdef LoH1Prolongation < Prolongation
-    %% properties
-    properties (GetAccess='public', SetAccess='protected')
-        interpolatedData
-    end
-    
-    %% methods
-    methods (Access='public')
-        function obj = LoH1Prolongation(feFunction)
-            assert(isa(feFunction.fes.finiteElement, 'LowestOrderH1Fe'), ...
-                'Prolongation only implemented for lowest order H1 conforming finite elements.')
-            obj = obj@Prolongation(feFunction);
-        end
-    end
-    
-    methods (Access='protected')
-        function interpolate(obj, src, ~)
-            % use that for lowest order H1 elements the dofs correspond to
-            % coordinates and new coordinates reside on edges or on inner nodes
-            uInnerNodes = cell(numel(src.bisecGroups), 1);
-            for k = 1:numel(src.bisecGroups)
-                if ~isempty(src.bisecGroups{k}.innerNodes)
-                    uInnerNodes{k} = eval(obj.u, src.bisecGroups{k}.innerNodes, ...
-                        src.bisecGroups{k}.elementIdx);
-                end
-            end
-            midpoint = Barycentric1D([1;1]/2);
-            obj.interpolatedData = [obj.u.data, ...
-                evalEdge(obj.u, midpoint, src.markedEdges), ...
-                horzcat(uInnerNodes{:})];
-        end
-    end
-end
diff --git a/lib/spaces/interpolation/LoL2Prolongation.m b/lib/spaces/interpolation/LoL2Prolongation.m
deleted file mode 100644
index a3928cf7968921e404147eb1248bf0e8411914c6..0000000000000000000000000000000000000000
--- a/lib/spaces/interpolation/LoL2Prolongation.m
+++ /dev/null
@@ -1,35 +0,0 @@
-% LoL2Prolongation (subclass of Prolongation) Prolongate lowest order L^2 finite
-%   element function to refined mesh.
-%
-%   Pu = LoL2Prolongation(u) returns a handle to the prolongation object.
-%
-%   Pu.interpolatedData is the data of the prolongation and is set automatically
-%       at mesh refinement.
-
-classdef LoL2Prolongation < Prolongation
-    %% properties
-    properties (GetAccess='public', SetAccess='protected')
-        interpolatedData
-    end
-    
-    %% methods
-    methods (Access='public')
-        function obj = LoL2Prolongation(feFunction)
-            assert(isa(feFunction.fes.finiteElement, 'LowestOrderL2Fe'), ...
-                'Prolongation only implemented for lowest order L2 finite elements.')
-            obj = obj@Prolongation(feFunction);
-        end
-    end
-    
-    methods (Access='protected')
-        function interpolate(obj, src, ~)
-            % use that for lowest order L2 elements the dofs correspond to elements
-            % and indices of new elements is known
-            nChildElems = ones(src.nElements, 1);
-            for k = 1:length(src.bisecGroups)
-                nChildElems(src.bisecGroups{k}.elementIdx) = src.bisecGroups{k}.nDescendants;
-            end
-            obj.interpolatedData = repelem(obj.u.data, nChildElems);
-        end
-    end
-end
diff --git a/lib/spaces/interpolation/Prolongation.m b/lib/spaces/interpolation/Prolongation.m
index 709b535fe0a0026c662318e91fbd2ffa1f5b0ec2..97adb20868910b445d6a1884181623ff17abeab0 100644
--- a/lib/spaces/interpolation/Prolongation.m
+++ b/lib/spaces/interpolation/Prolongation.m
@@ -3,26 +3,46 @@
 
 classdef Prolongation < handle
     %% properties
-    properties (Abstract, GetAccess='public', SetAccess='protected')
-        interpolatedData
+    properties (GetAccess=public, SetAccess=protected)
+        matrix {mustBeSparse}
     end
     
-    properties (Access='protected')
-        u FeFunction
+    properties (Access=protected)
+        fes FeSpace
         listenerHandle
     end
     
     %% methods
-    methods (Access='public')
-        function obj = Prolongation(feFunction)
-            obj.u = feFunction;
-            mesh = feFunction.fes.mesh;
-            obj.listenerHandle = mesh.listener('IsAboutToRefine', @obj.interpolate);
-            obj.interpolatedData = [];
+    methods (Access=public)
+        function obj = Prolongation(fes)
+            obj.fes = fes;
+            mesh = fes.mesh;
+            obj.matrix = [];
+            obj.listenerHandle = mesh.listener('IsAboutToRefine', @obj.setupMatrix);
+        end
+        
+        function newFeData = prolongate(obj, u)
+            arguments
+                obj
+                u FeFunction
+            end
+            
+            if isempty(obj.matrix)
+                error('Prolongation matrix not set. Are you sure there was mesh refinement?')
+            end
+            newFeData = obj.matrix * u.data';
         end
     end
     
-    methods (Abstract, Access='protected')
-        interpolate(obj, src, event)
+    methods (Abstract, Access=protected)
+        setupMatrix(obj, src, event)
+    end
+end
+
+function mustBeSparse(A)
+    if ~(isempty(A) || issparse(A))
+        eidType = 'Prolongation:matrixNotSparse';
+        msgType = 'Property ''matrix'' must be a sparse matrix.';
+        throwAsCaller(MException(eidType,msgType))
     end
 end
diff --git a/tests/TestFiniteElementFunctions.m b/tests/TestFiniteElementFunctions.m
index 9ae9f7365dced10872448045243e930c00f62494..f208227e5759ddeef015997f8a24a3eea1b81e43 100644
--- a/tests/TestFiniteElementFunctions.m
+++ b/tests/TestFiniteElementFunctions.m
@@ -66,7 +66,7 @@ methods (Test)
         testCase.verifyEqual(nnz(testCase.u.data == scalar), nFreeDofs)
     end
     
-    function linearInterpolationPreservesConstants(testCase)
+    function prolongationPreservesConstants(testCase)
         mesh = Mesh.loadFromGeometry('unitsquare');
         fes = FeSpace(mesh, testCase.u.fes.finiteElement);
         v = FeFunction(fes);
@@ -75,20 +75,42 @@ methods (Test)
         integral = sum(integrateElement(v, qr), Dim.Elements);
         testCase.verifyEqual(integral, pi, 'AbsTol', sqrt(eps))
     end
+    
+    function generalProlongationPreservesBasisFunctions(testCase)
+        mesh = Mesh.loadFromGeometry('unitsquare');
+        fes = FeSpace(mesh, testCase.u.fes.finiteElement);
+        qr = QuadratureRule.ofOrder(max(fes.finiteElement.order,1));
+        v = FeFunction(fes);
+        P = FeProlongation(fes);
+        for k = 1:min(5, size(getDofs(fes).element2Dofs, 1))
+            testCase.setToElementwiseBasisFunction(v, k);
+            before = sum(integrateElement(v, qr));
+            mesh.refineLocally(1, 'NVB');
+            v.setData(prolongate(P, v));
+            after = sum(integrateElement(v, qr));
+            testCase.verifyEqual(before, after, 'AbsTol', sqrt(eps))
+        end
+    end
 end
 
-methods (Access='private')
+methods (Access=private)
     function refineAndInterpolate(~, mesh, fes, v, val)
         v.setData(val);
-        if isa(fes.finiteElement, 'LowestOrderH1Fe')
-            oldVals = LoH1Prolongation(v);
-        elseif isa(fes.finiteElement, 'LowestOrderL2Fe')
-            oldVals = LoL2Prolongation(v);
+        if isa(fes.finiteElement, 'LowestOrderH1Fe') ...
+            || isa(fes.finiteElement, 'LowestOrderL2Fe')
+            P = LoFeProlongation(fes);
         else
-            oldVals = LinearFeProlongation(v);
+            P = FeProlongation(fes);
         end
         mesh.refineUniform();
-        v.setData(oldVals.interpolatedData);
+        v.setData(prolongate(P, v));
+    end
+    
+    function setToElementwiseBasisFunction(~, v, k)
+        v.setData(0);
+        data = v.data;
+        data(getDofs(v.fes).element2Dofs(k,:)) = 1;
+        v.setData(data);
     end
 end
     
diff --git a/tests/TestRefinement.m b/tests/TestRefinement.m
index 10d5b1c687b9e0c97ebe0977fcd8aa048f3f363f..3ce2b4dd249987a775612ee2db1197f7d293fe0c 100644
--- a/tests/TestRefinement.m
+++ b/tests/TestRefinement.m
@@ -30,7 +30,7 @@ methods (Test)
     end
 end
 
-methods (Access='protected')
+methods (Access=protected)
     function verifyEdgeConnectivityIntegrity(testCase, mesh)
         [edgesRef, e2eRef, ~, bndRef] = Mesh.computeEdgeInformation( ...
             mesh.elements, cellfun(@(bnd) mesh.edges(:,bnd), mesh.boundaries, 'UniformOutput', false));
@@ -53,4 +53,4 @@ methods (Access='protected')
     end
 end
 
-end
\ No newline at end of file
+end
diff --git a/tests/TestVectorProduct.m b/tests/TestVectorProduct.m
index b0d55aad91f720be538b33dab725848569b98fb6..ba515e96636bc3a61d4f96bd781d91b9d8940572 100644
--- a/tests/TestVectorProduct.m
+++ b/tests/TestVectorProduct.m
@@ -43,7 +43,7 @@ methods (Test, ParameterCombination='exhaustive')
     end
 end
 
-methods (Access='protected')
+methods (Access=protected)
     function [Aext, dimA] = extendAlongDim(~, A, dim, n)
         dimA = size(A);
         repetitions = ones(1, dim);
@@ -52,4 +52,4 @@ methods (Access='protected')
     end
 end
     
-end
\ No newline at end of file
+end