diff --git a/lib/solvers/LocalMgHighOrderVcycle.m b/lib/solvers/LocalMgHighOrderVcycle.m
deleted file mode 100644
index 1ac07b7c4b4b43fd3bf136c2fd9d11c58ed03dd3..0000000000000000000000000000000000000000
--- a/lib/solvers/LocalMgHighOrderVcycle.m
+++ /dev/null
@@ -1,172 +0,0 @@
-% OptimalLocalMGSolver (subclass of MGSolver) Solves linear equations
-%   iteratively using high-order p-optimal multilevel local smoothing (additive
-%   Schwarz/block Jacobi).
-%   One iteration = Vcycle(0,1) (no pre-/one post-smoothing step).
-%
-% With the solver comes an 
-
-classdef LocalMgHighOrderVcycle < MGSolver
-    %% properties
-    properties (GetAccess=public,SetAccess=protected)
-        nLevels
-    end
-
-    properties (Access=protected)
-        blf
-        hoFes
-        loFes
-        P
-        intergridMatrix
-        inclusionMatrix
-        freeDofs
-        freeDofsOld
-        changedPatches
-        patchwiseA
-        Acoarse
-        Afine
-        coarseLoFes
-        coarseHoFes
-    end
-    
-    %% event data
-    properties (Access=private)
-        listenerHandle
-    end
-
-    %% methods
-    methods (Access=public)
-        function obj = LocalMgHighOrderVcycle(fes, blf, P)
-            arguments
-                fes FeSpace
-                blf BilinearForm
-                P Prolongation
-            end
-            obj = obj@MGSolver();
-            
-            assert(fes.finiteElement.order > 1, ...
-                'LocalMgLowOrderVcycle only works for higher order finite elements.')
-            assert(isempty(blf.b), ...
-                'Multigrid solvers only tested for symmetric problems.')
-            
-            obj.nLevels = 0;
-            
-            mesh = fes.mesh;
-            obj.P = P;
-            obj.loFes = FeSpace(mesh, LowestOrderH1Fe(), 'dirichlet', fes.bnd.dirichlet);
-            obj.hoFes = fes;
-            obj.blf = blf;
-            obj.Afine = cell(1,1);
-            obj.Acoarse = assemble(obj.blf, obj.loFes);
-            obj.Acoarse = obj.Acoarse(getFreeDofs(obj.loFes), getFreeDofs(obj.loFes));
-            
-            % set up FE spaces on coarsest level for projection
-            coarseMesh = clone(fes.mesh);
-            obj.coarseLoFes = FeSpace(coarseMesh, LowestOrderH1Fe, 'dirichlet', fes.bnd.dirichlet);
-            obj.coarseHoFes = FeSpace(coarseMesh, HigherOrderH1Fe(fes.finiteElement.order), 'dirichlet', fes.bnd.dirichlet);
-            
-            obj.listenerHandle = mesh.listener('IsAboutToRefine', @obj.getChangedPatches);
-        end
-
-        function setupSystemMatrix(obj, A)
-            obj.nLevels = obj.nLevels + 1;
-            
-            L = obj.nLevels;
-            freeVertices = getFreeDofs(obj.loFes);
-
-            obj.freeDofsOld = obj.freeDofs;
-            obj.freeDofs = getFreeDofs(obj.hoFes);
-            
-            if L >= 2
-                obj.intergridMatrix{L} = obj.P.matrix(obj.freeDofs, obj.freeDofsOld);
-                obj.changedPatches{L} = freeVertices(obj.changedPatches{L}(freeVertices));
-                obj.inclusionMatrix = SpaceProlongation(obj.coarseLoFes, obj.coarseHoFes) ...
-                    .matrix(getFreeDofs(obj.coarseLoFes), getFreeDofs(obj.coarseHoFes));
-                if L == 2
-                    obj.patchwiseA{L} = assemblePatchwise(obj.blf, obj.hoFes, ':');
-                else
-                    obj.patchwiseA{L} = assemblePatchwise(obj.blf, obj.hoFes, obj.changedPatches{L});
-                end
-            end
-            
-            setupSystemMatrix@MGSolver(obj, A);
-            obj.Afine{L} = obj.A;
-        end
-
-        function setupRhs(obj, b, varargin)
-            setupRhs@MGSolver(obj, b, varargin{:});
-        end
-
-        % Geometric MultiGrid
-        function [Cx, algError2] = Vcycle(obj, res)
-            assert(isequal(size(res, 1), size(obj.A, 1)), ...
-                'Setup for multilevel iteration not complete!')
-            
-            % if there is only one coarse level: exact solve
-            if obj.nLevels == 1
-                Cx = obj.A \ res;
-                algError2 = scalarProduct(Cx, obj.A*Cx);
-                return
-            end
-
-            L = obj.nLevels;
-            algError2 = zeros(1, size(res, 2)); % built-in estimator of the algebraic error
-            
-            % descending cascade in p: no smoothing
-            residual{L} = res;
-            for k = L:-1:2
-                residual{k-1} = obj.intergridMatrix{k}'*residual{k};
-            end
-
-            % coarse level in P1
-            residual{1} = obj.inclusionMatrix * residual{1};
-            sigma = obj.Acoarse \ residual{1};
-            algError2 = algError2 + scalarProduct(sigma, obj.Acoarse*sigma);
-            
-            % ascending cascade in Pp 
-            sigma = obj.inclusionMatrix' * sigma;
-            
-            for k = 2:L
-                sigma = obj.intergridMatrix{k}*sigma;
-                uptres = residual{k} - obj.Afine{k}*sigma; %updated residual 
-                rho = obj.patchwiseA{k} \ uptres;
-                [aeUpd, sUpd] = computeOptimalUpdate(obj.Afine{k}, uptres, rho);
-                sigma = sigma + sUpd;
-                algError2 = algError2 + aeUpd;
-            end
-            
-            Cx = sigma;
-        end
-    end
-    
-    methods (Access=protected)
-        % callback function to be executed before mesh refinement:
-        % -> patches change for all vertices adjacent to refined edges and
-        %    all new vertices
-        function getChangedPatches(obj, mesh, bisecData)
-            nCOld = mesh.nCoordinates;
-            nCNew = mesh.nCoordinates + nnz(bisecData.bisectedEdges) + ...
-                bisecData.nRefinedElements'*bisecData.nInnerNodes;
-            bisectedEdgeNodes = unique(mesh.edges(:,bisecData.bisectedEdges));
-            obj.changedPatches{obj.nLevels+1} = false(nCNew, 1);
-            idx = [unique(bisectedEdgeNodes); ((nCOld+1):nCNew)'];
-            obj.changedPatches{obj.nLevels+1}(idx) = true;
-        end
-    end
-end
-
-%% auxiliary functions
-% error correction with optimal stepsize 
-function [etaUpdate, sigmaUpdate] = computeOptimalUpdate(A, res, rho)
-    rhoArho = scalarProduct(rho, A*rho);
-    lambda = scalarProduct(res, rho) ./ rhoArho;
-    sigmaUpdate = lambda.*rho;
-    etaUpdate = rhoArho.*(lambda.^2);
-
-    if any(lambda > 3)
-       warning('MG step-sizes no longer bound by d+1. Optimality of step size cannot be guaranteed!')
-    end
-end
-
-function a = scalarProduct(x, y)
-    a = sum(x.*y, 1);
-end
\ No newline at end of file
diff --git a/lib/solvers/MGSolver.m b/lib/solvers/MGSolver.m
deleted file mode 100644
index 036213e537cb8e4ec83c1aaa521c959c2eb66f4a..0000000000000000000000000000000000000000
--- a/lib/solvers/MGSolver.m
+++ /dev/null
@@ -1,74 +0,0 @@
-% MGSolver (abstract subclass of IterativeSolver) Solves linear equations
-%   iteratively, using a Vcycle geometric multigrid solver.
-%
-% Abstract methods to be implemented by subclasses:
-%
-%   solver.Vcycle(x) performs one Vcycle iteration.
-%
-% See also IterativeSolver
-
-classdef MGSolver < IterativeSolver
-    %% properties    
-    properties (SetAccess=protected, GetAccess=public)
-        residualCNorm
-        algEstimator
-    end
-    
-    properties (Access=protected)
-        residual
-        Cresidual
-        normb
-    end
-    
-    %% methods
-    methods (Access=public)        
-        % initialize iteration
-        function setupSystemMatrix(obj, A, varargin)
-            setupSystemMatrix@IterativeSolver(obj, A);
-        end
-
-        function setupRhs(obj, b, varargin)
-            setupRhs@IterativeSolver(obj, b, varargin{:});
-            % initialize residual & hierarchy
-            obj.residual = b - obj.A*obj.x;
-            [obj.Cresidual, obj.algEstimator] = obj.Vcycle(obj.residual);
-            obj.residualCNorm = sum(obj.residual.*obj.Cresidual, 1);
-            obj.normb = sqrt(sum(b.^2, 1));
-        end
-        
-        % stopping criterion
-        function tf = isConverged(obj)
-            tf = ((obj.iterationCount >= obj.maxIter) ...
-                        | (sqrt(obj.residualCNorm) < obj.tol) ...
-                        | (sqrt(obj.residualCNorm)./obj.normb < obj.tol)); 
-        end
-    end
-        
-    methods (Access=protected)   
-        % one step
-        function computeUpdate(obj)
-            % only iterate on active components
-            idx = find(obj.activeComponents);
-            
-            % update solution & residual
-            obj.x(:,idx) = obj.x(:,idx) + obj.Cresidual(:,idx);
-            obj.residual(:,idx) = obj.residual(:,idx) - obj.A * obj.Cresidual(:,idx);
-
-            % residual & update error correction
-            [obj.Cresidual(:,idx), obj.algEstimator(idx)] = obj.Vcycle(obj.residual(:,idx));
-            obj.residualCNorm(:,idx) = sum(obj.residual(:,idx).*obj.Cresidual(:,idx), 1);
-        end
-    end
-    
-    methods (Abstract,Access=public)
-        Vcycle(obj, x)
-    end
-end
-
-
-
-
-
-
-
-
diff --git a/lib/solvers/chooseIterativeSolver.m b/lib/solvers/chooseIterativeSolver.m
index 9bc2cf95231e39f832f7b21faad1791314702795..5058d490f878596961034050428649b48499a063 100644
--- a/lib/solvers/chooseIterativeSolver.m
+++ b/lib/solvers/chooseIterativeSolver.m
@@ -53,19 +53,20 @@ switch class
         switch variant
             case {"", "lowOrderVcycle"}
                 if order == 1
-                    solver = OptimalVcycleMultigridSolver(P1JacobiSmoother(fes, blf, P));
+                    smoother = P1JacobiSmoother(fes, blf, P);
                 else
-                    solver = OptimalVcycleMultigridSolver(JacobiLowOrderCascadeSmoother(fes, blf));
+                    smoother = JacobiLowOrderCascadeSmoother(fes, blf);
                 end
             case "highOrderVcycle"
                 if order == 1
-                    solver = OptimalVcycleMultigridSolver(P1JacobiSmoother(fes, blf, P));
+                    smoother = P1JacobiSmoother(fes, blf, P);
                 else
-                    solver = LocalMgHighOrderVcycle(fes, blf, P);
+                    smoother = JacobiHighOrderCascadeSmoother(fes, blf, P);
                 end
             otherwise
                 error('No multigrid variant %s!', variant)
         end
+        solver = OptimalVcycleMultigridSolver(smoother);
         
     % direct solve (for testing purposes)
     case "direct"
diff --git a/lib/solvers/smoothers/JacobiHighOrderCascadeSmoother.m b/lib/solvers/smoothers/JacobiHighOrderCascadeSmoother.m
new file mode 100644
index 0000000000000000000000000000000000000000..793261b86d04ada5761991f725058557e73274cf
--- /dev/null
+++ b/lib/solvers/smoothers/JacobiHighOrderCascadeSmoother.m
@@ -0,0 +1,85 @@
+% JacobiHighOrderCascadeSmoother (subclass of MultilevelSmoother) multilevel
+%   Jacobi smoother for higher order finite elements: smooth locally with
+%   patchwise stiffness matrix on changed patches on every level, except for
+%   coarsest level, where global p1-solving is done. Because of the lifting from
+%   p1 to higher order between the coarsest and the second level, all patches
+%   are assumed to be changed on the second level
+%   
+%   Reference: [Innerberger, Miraçi, Praetorius, Streitberger; Optimal
+%   computational costs of AFEM with optimal local hp-robust multigrid solver].
+%
+% See also: MultilevelSmoother, OptimalVcycleMultigridSolver
+
+
+classdef JacobiHighOrderCascadeSmoother < MultilevelSmoother
+    properties (Access=protected)
+        P
+        loFes
+        patchwiseA
+        inclusionMatrix
+        intergridMatrix
+        freeDofs
+        freeDofsOld
+    end
+
+    %% methods
+    methods (Access=public)
+        function obj = JacobiHighOrderCascadeSmoother(fes, blf, P)
+            obj = obj@MultilevelSmoother(fes, blf);
+            
+            assert(fes.finiteElement.order > 1, ...
+                'JacobiHighOrderCascadeSmoother only works for higher order finite elements.')
+
+            obj.P = P;
+            obj.loFes = FeSpace(fes.mesh, LowestOrderH1Fe(), 'dirichlet', fes.bnd.dirichlet);
+        end
+
+        function nonInvertedSmootherMatrix = setup(obj, A)
+            setup@MultilevelSmoother(obj, A);
+            
+            L = obj.nLevels;
+            obj.freeDofsOld = obj.freeDofs;
+            obj.freeDofs = getFreeDofs(obj.fes);
+            freeVertices = getFreeDofs(obj.loFes);
+            
+            hoFes = obj.fes;
+            if L == 1
+                % set up FE inclusion and lowest order matrix on coarsest level
+                obj.inclusionMatrix = SpaceProlongation(obj.loFes, hoFes) ...
+                    .matrix(getFreeDofs(obj.loFes), getFreeDofs(hoFes));
+                nonInvertedSmootherMatrix = assemble(obj.blf, obj.loFes);
+                nonInvertedSmootherMatrix = nonInvertedSmootherMatrix(freeVertices, freeVertices);
+            else
+                % note: patchwiseA is not already indexed by free vertices -> use freeVertices here
+                obj.changedPatches{L} = freeVertices(obj.changedPatches{L}(freeVertices));
+                obj.intergridMatrix{L} = obj.P.matrix(obj.freeDofs, obj.freeDofsOld);
+                nonInvertedSmootherMatrix = A;
+                if L == 2
+                    obj.patchwiseA{L} = assemblePatchwise(obj.blf, hoFes, ':');
+                else
+                    obj.patchwiseA{L} = assemblePatchwise(obj.blf, hoFes, obj.changedPatches{L});
+                end
+            end
+        end
+
+        function Cx = smooth(obj, x, k)
+            % higher order global (k=2) or local (else) patchwise smooting
+            Cx = obj.patchwiseA{k} \ x;
+        end
+
+        % order of FESpace per level: 1 -> p -> ... -> p
+        function Px = prolongate(obj, x, k)
+            if k == 2
+                x = obj.inclusionMatrix' * x;
+            end
+            Px = obj.intergridMatrix{k} * x;
+        end
+
+        function Px = restrict(obj, x, k)
+            Px = obj.intergridMatrix{k}' * x;
+            if k == 2
+                Px = obj.inclusionMatrix * Px;
+            end
+        end
+    end
+end
\ No newline at end of file
diff --git a/lib/solvers/smoothers/JacobiLowOrderCascadeSmoother.m b/lib/solvers/smoothers/JacobiLowOrderCascadeSmoother.m
index ad5dd1e038e06072b08b35a03d78f9514ef3af4b..10541f2f7db960a80826b9f75230c74d7172f3e8 100644
--- a/lib/solvers/smoothers/JacobiLowOrderCascadeSmoother.m
+++ b/lib/solvers/smoothers/JacobiLowOrderCascadeSmoother.m
@@ -48,9 +48,10 @@ classdef JacobiLowOrderCascadeSmoother < MultilevelSmoother
             nonInvertedSmootherMatrix = p1Matrix;
             
             if L >= 2
+                % note: p1 smoother is already indexed by free vertices -> use find here
+                obj.changedPatches{L} = find(obj.changedPatches{L}(obj.freeVertices));
                 % intermediate level smoothers and transfer operators
                 obj.intergridMatrix{L} = obj.P.matrix(obj.freeVertices, obj.freeVerticesOld);
-                obj.changedPatches{L} = find(obj.changedPatches{L}(obj.freeVertices));
                 obj.p1Smoother{L} = obj.p1Smoother{L}(obj.changedPatches{L});
                 % finest level smoother and transfer operator
                 obj.hoSmoother = assemblePatchwise(obj.blf, obj.fes, ':');
@@ -71,6 +72,7 @@ classdef JacobiLowOrderCascadeSmoother < MultilevelSmoother
             end
         end
 
+        % order of FESpace per level: 1 -> ... -> 1 -> p
         function Px = prolongate(obj, x, k)
             Px = obj.intergridMatrix{k} * x;
             if k == obj.nLevels