diff --git a/examples/algebraicSolver.m b/examples/algebraicSolver.m
index 15084f31b955b2fc67e5442a9a104c4c26881c7e..3ef04b1eb3a6f50650b74ff9ad1cf3380613f7b9 100644
--- a/examples/algebraicSolver.m
+++ b/examples/algebraicSolver.m
@@ -33,7 +33,7 @@ for p = 1:pmax
     % choose the iterative solver of choice: multigrid (with the variants
     % lowOrderVcycle and highOrderVcycle), pcg (with jacobi and additive
     % Schwarz preconditioner), and the cg solver
-    [solver, P] = chooseIterativeSolver(fes, blf, 'multigrid', 'lowOrderVcycle');
+    [solver, P] = IterativeSolver.choose(fes, blf, 'multigrid', 'lowOrderVcycle');
     solver.tol = 1e-6;
     solver.maxIter = 100;
 
diff --git a/examples/goafemIterativeSolver.m b/examples/goafemIterativeSolver.m
index 801bbfb0ba1caaa3660e4ee777db7969d4fcc842..50a7df3835d03592ed0f894e33081a62b8935177 100644
--- a/examples/goafemIterativeSolver.m
+++ b/examples/goafemIterativeSolver.m
@@ -37,7 +37,7 @@ for p = 1:pmax
     lfG.qrf = QuadratureRule.ofOrder(2*p);
 
     %% set up solver and lifting operator for nested iteration
-    [solver, P] = chooseIterativeSolver(fes, blf, 'multigrid', 'lowOrderVcycle');
+    [solver, P] = IterativeSolver.choose(fes, blf, 'multigrid', 'lowOrderVcycle');
     solver.tol = 1e-6;
     solver.maxIter = 1000;
 
diff --git a/lib/solvers/IterativeSolver.m b/lib/solvers/@IterativeSolver/IterativeSolver.m
similarity index 92%
rename from lib/solvers/IterativeSolver.m
rename to lib/solvers/@IterativeSolver/IterativeSolver.m
index 191676e17144f503abcdefd31db89cafbba62aec..be5b90b45bbca63d72b0cb500295acc8cb234787 100644
--- a/lib/solvers/IterativeSolver.m
+++ b/lib/solvers/@IterativeSolver/IterativeSolver.m
@@ -25,6 +25,10 @@
 %
 % tf = solver.isConverged(solver) returns state of convergence of the solver
 %   for each column of the right-hand side.
+%
+% [solver, P] = IterativeSolver.choose(fes, blf, class, variant) convenience
+%   factory method. Returns instances of IterativeSolver and Prolongation
+%   accoding to the given parameters.
 
 
 classdef IterativeSolver < handle
@@ -102,6 +106,11 @@ classdef IterativeSolver < handle
             obj.activeComponents = obj.activeComponents & ~tf;
         end
     end
+
+    %% convenience factory function
+    methods (Static, Access=public)
+        [solver, P] = choose(fes, blf, class, variant)
+    end
     
     %% validation functions to use within this class
     methods (Static, Access=protected)
diff --git a/lib/solvers/@IterativeSolver/choose.m b/lib/solvers/@IterativeSolver/choose.m
new file mode 100644
index 0000000000000000000000000000000000000000..ef5d4fa6dbc32592f5c496f354ff839080dbd3a7
--- /dev/null
+++ b/lib/solvers/@IterativeSolver/choose.m
@@ -0,0 +1,91 @@
+% choose Return suitable solver (instance of subclass of IterativeSolver) and
+%   suitable Prolongation P.
+%
+%   solver = choose(fes, blf, class) returns a solver of class 'class' for given
+%       finite element space fes and bilinear form blf. For each class, sensible
+%       default variants are chosen.
+%
+%   solver = choose(fes, blf, class, variant) returns a solver of class 'class'
+%       of specific variant 'variant'.
+%
+%   [solver, P] = choose(__) additionally to the solver, returns a suitable
+%       Prolongation P, that can be used to prolongate the solution of the
+%       solver to the next finer mesh.
+
+function [solver, P] = choose(fes, blf, class, variant)
+arguments
+    fes FeSpace
+    blf BilinearForm
+    class (1,1) string {mustBeMember(class, ["cg", "pcg", "multigrid", "direct"])}
+    variant (1,1) string {mustBeMember(variant, [ "", ...
+        "iChol", "jacobi", ...
+        "additiveSchwarzLowOrder", "additiveSchwarzHighOrder" ...
+        "lowOrderVcycle", "highOrderVcycle"])} = ""
+end
+
+order = fes.finiteElement.order;
+P = Prolongation.chooseFor(fes);
+
+switch class
+    % non-preconditioned CG
+    case "cg"
+        solver = PcgSolver(NoPreconditioner());
+        
+    % preconditioned CG family
+    case "pcg"
+        switch variant
+            case "iChol"
+                preconditioner = IncompleteCholesky();
+            case "jacobi"
+                if order == 1
+                    preconditioner = DiagonalJacobi();
+                else
+                    preconditioner = BlockJacobi(fes, blf);
+                end
+            case {"", "additiveSchwarzLowOrder"}
+                if order == 1
+                    preconditioner = OptimalMLAdditiveSchwarz(P1JacobiCascade(fes, blf, P));
+                else
+                    preconditioner = OptimalMLAdditiveSchwarz(JacobiLowOrderCascade(fes, blf));
+                end
+            case "additiveSchwarzHighOrder"
+                if order == 1
+                    preconditioner = OptimalMLAdditiveSchwarz(P1JacobiCascade(fes, blf, P));
+                else
+                    preconditioner = OptimalMLAdditiveSchwarz(JacobiHighOrderCascade(fes, blf, P));
+                end
+            otherwise
+                error('No PCG variant %s!', variant)
+        end
+        solver = PcgSolver(preconditioner);
+        
+    % geometric , Pmultigrid family
+    case "multigrid"
+        switch variant
+            case {"", "lowOrderVcycle"}
+                if order == 1
+                    smoother = P1JacobiCascade(fes, blf, P);
+                else
+                    smoother = JacobiLowOrderCascade(fes, blf);
+                end
+            case "highOrderVcycle"
+                if order == 1
+                    smoother = P1JacobiCascade(fes, blf, P);
+                else
+                    smoother = JacobiHighOrderCascade(fes, blf, P);
+                end
+            otherwise
+                error('No multigrid variant %s!', variant)
+        end
+        solver = OptimalVcycleMultigridSolver(smoother);
+        
+    % direct solve (for testing purposes)
+    case "direct"
+        solver = DirectSolver();
+   
+    % default
+    otherwise
+        error('No iterative solver of class %s!', class)
+end
+    
+end
diff --git a/lib/solvers/AdditiveSchwarzHighOrderPcg.m b/lib/solvers/AdditiveSchwarzHighOrderPcg.m
deleted file mode 100644
index 076f2fdd50e58d4262e85366e13f98763f060221..0000000000000000000000000000000000000000
--- a/lib/solvers/AdditiveSchwarzHighOrderPcg.m
+++ /dev/null
@@ -1,161 +0,0 @@
-% AdditiveSchwarzHighOrderPcg (subclass of PcgSolver) Solves linear equations
-%   iteratively using the CG method with optimal multilevel additive
-%   Schwartz preconditioner.
-
-classdef AdditiveSchwarzHighOrderPcg < PcgSolver
-    %% properties
-    properties (GetAccess=public,SetAccess=protected)
-        nLevels
-    end
-    
-    properties (Access=protected)
-        blf
-        hoFes
-        loFes
-        coarseLoFes
-        coarseHoFes
-        p1Acoarse
-        smoother
-        P
-        intergridMatrix
-        freeVertices
-        freeVerticesOld
-        freeDofs
-        freeDofsOld
-        changedPatches
-        patchwiseA
-    end
-
-    properties (Access=private)
-        listenerHandle
-    end
-    
-    %% methods
-    methods (Access=public)
-        function obj = AdditiveSchwarzHighOrderPcg(fes, blf, P)
-            arguments
-                fes FeSpace
-                blf BilinearForm
-                P Prolongation
-            end
-            obj = obj@PcgSolver();
-            
-            assert(fes.finiteElement.order > 1, ...
-                'AdditiveSchwartzPcg only works for higher order finite elements.')
-            assert(isempty(blf.b), ...
-                'Additive Schwarz PCG solvers only tested for symmetric problems.')
-
-            obj.nLevels = 0;
-
-            mesh = fes.mesh;
-            obj.loFes = FeSpace(mesh, LowestOrderH1Fe, 'dirichlet', ':');
-            obj.hoFes = fes;
-            obj.P = P;
-            obj.blf = blf;
-
-            obj.listenerHandle = mesh.listener('IsAboutToRefine', @obj.getChangedPatches);
-        end
-        
-        function setupSystemMatrix(obj, A)
-            obj.nLevels = obj.nLevels + 1;
-            
-            L = obj.nLevels;
-            obj.freeVerticesOld = obj.freeVertices;
-            obj.freeVertices = getFreeDofs(obj.loFes);
-
-            obj.freeDofsOld = obj.freeDofs;
-            obj.freeDofs = getFreeDofs(obj.hoFes);
-
-            ppMatrix = assemble(obj.blf, obj.hoFes);
-            ppMatrix = ppMatrix(getFreeDofs(obj.hoFes), getFreeDofs(obj.hoFes));
-
-            p1Matrix = assemble(obj.blf, obj.loFes);
-            p1Matrix = p1Matrix(obj.freeVertices, obj.freeVertices);
-            
-            if L == 1
-                obj.p1Acoarse = p1Matrix;
-                fes = obj.hoFes;
-                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);
-            else
-                obj.smoother{L} = full(diag(ppMatrix)).^(-1);
-                obj.intergridMatrix{L} = obj.P.matrix(obj.freeDofs, obj.freeDofsOld);
-                obj.changedPatches{L} = obj.changedPatches{L}(obj.freeVertices);
-                obj.patchwiseA{L} = assemblePatchwise(obj.blf, obj.hoFes, obj.changedPatches{L});
-            end
-            
-            setupSystemMatrix@PcgSolver(obj, A);
-        end
-           
-        function setupRhs(obj, b, varargin)
-            setupRhs@PcgSolver(obj, b, varargin{:});
-        end
-        
-        % preconditioner: inverse of diagonal on each level
-        function Cx = preconditionAction(obj, residual)
-            assert(~isempty(obj.nLevels), 'Data for multilevel iteration not given!')
-
-            L = obj.nLevels;
-            rho = cell(L, 1);
-
-            if L == 1
-                Cx = obj.A \ residual;
-                return
-            end
-
-            % descending cascade
-            for k = L:-1:2
-                rho{k} = localSmoothing(obj, k, residual);
-                residual = obj.intergridMatrix{k}'*residual;
-            end
-
-            % exact solve on coarsest level
-            residual = interpolateFreeData(residual, obj.coarseHoFes, obj.coarseLoFes);
-            sigma = obj.p1Acoarse \ residual;
-            sigma = interpolateFreeData(sigma, obj.coarseLoFes, obj.coarseHoFes);
-            
-            % ascending cascade
-            for k = 2:L
-                sigma = obj.intergridMatrix{k}*sigma;
-                sigma = sigma + rho{k};
-            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)
-            % Selects vertices of bisected edges and all newly created
-            % vertices
-            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 = [bisectedEdgeNodes; ((nCOld+1):nCNew)'];
-            obj.changedPatches{obj.nLevels+1}(idx) = true;
-        end
-        
-        function rho = localSmoothing(obj, k, res)
-            rho = obj.patchwiseA{k} \ res;
-        end
-    end
-end
-
- %% auxiliary functions
-function interpolatedData = interpolateFreeData(data, fromFes, toFes)
-    freeDofs = getFreeDofs(toFes);
-    nComponents = size(data, 2);
-    interpolatedData = zeros(numel(freeDofs), nComponents);
-    feFunctionWrapper = FeFunction(fromFes);
-    for k = 1:nComponents
-        feFunctionWrapper.setFreeData(data(:,k));
-        wholeData = nodalInterpolation(feFunctionWrapper, toFes);
-        interpolatedData(:,k) = wholeData(freeDofs);
-    end
-end
\ No newline at end of file
diff --git a/lib/solvers/AdditiveSchwarzLowOrderPcg.m b/lib/solvers/AdditiveSchwarzLowOrderPcg.m
deleted file mode 100644
index 448e069da4be9e6797e538c9896ae7c2107ba1de..0000000000000000000000000000000000000000
--- a/lib/solvers/AdditiveSchwarzLowOrderPcg.m
+++ /dev/null
@@ -1,152 +0,0 @@
-% AdditiveSchwartzPcg (subclass of PcgSolver) Solves linear equations
-%   iteratively using the CG method with optimal multilevel additive
-%   Schwartz preconditioner.
-
-classdef AdditiveSchwarzLowOrderPcg < PcgSolver
-    %% properties
-    properties (GetAccess=public,SetAccess=protected)
-        nLevels
-    end
-    
-    properties (Access=protected)
-        blf
-        hoFes
-        loFes
-        p1Acoarse
-        p1Smoother
-        P
-        inclusionMatrix
-        intergridMatrix
-        freeVertices
-        freeVerticesOld
-        changedPatches
-        patchwiseA
-        patchwiseP1Matrix
-    end
-
-    properties (Access=private)
-        listenerHandle
-    end
-    
-    %% methods
-    methods (Access=public)
-        function obj = AdditiveSchwarzLowOrderPcg(fes, blf)
-            arguments
-                fes FeSpace
-                blf BilinearForm
-            end
-            obj = obj@PcgSolver();
-            
-            assert(fes.finiteElement.order > 1, ...
-                'AdditiveSchwartzPcg only works for higher order finite elements.')
-            assert(isempty(blf.b), ...
-                'Additive Schwarz PCG solvers only tested for symmetric problems.')
-
-            obj.nLevels = 0;
-
-            mesh = fes.mesh;
-            obj.loFes = FeSpace(mesh, LowestOrderH1Fe, 'dirichlet', ':');
-            obj.hoFes = fes;
-            obj.P = Prolongation.chooseFor(obj.loFes);
-            obj.blf = blf;
-
-            obj.listenerHandle = mesh.listener('IsAboutToRefine', @obj.getChangedPatches);
-        end
-        
-        function setupSystemMatrix(obj, A)
-            obj.nLevels = obj.nLevels + 1;
-            
-            L = obj.nLevels;
-            obj.freeVerticesOld = obj.freeVertices;
-            obj.freeVertices = getFreeDofs(obj.loFes);
-
-            p1Matrix = assemble(obj.blf, obj.loFes);
-            p1Matrix = p1Matrix(obj.freeVertices, obj.freeVertices);
-            
-            if L == 1
-                obj.p1Acoarse = p1Matrix;
-            else
-                obj.p1Smoother{L} = full(diag(p1Matrix)).^(-1);
-                obj.intergridMatrix{L} = obj.P.matrix(obj.freeVertices, obj.freeVerticesOld);
-                obj.changedPatches{L} = obj.changedPatches{L}(obj.freeVertices);
-                obj.patchwiseA = assemblePatchwise(obj.blf, obj.hoFes);
-                obj.inclusionMatrix = SpaceProlongation(obj.loFes, obj.hoFes) ...
-                    .matrix(getFreeDofs(obj.loFes), getFreeDofs(obj.hoFes));
-            end
-            
-            setupSystemMatrix@PcgSolver(obj, A);
-        end
-           
-        function setupRhs(obj, b, varargin)
-            setupRhs@PcgSolver(obj, b, varargin{:});
-        end
-        
-        % preconditioner: inverse of diagonal on each level
-        function Cx = preconditionAction(obj, residual)
-            assert(~isempty(obj.nLevels), 'Data for multilevel iteration not given!')
-
-            L = obj.nLevels;
-            rho = cell(L, 1);
-
-            if L == 1
-                Cx = obj.A \ residual;
-                return
-            end
-
-            % descending cascade
-            rho{L} = hoGlobalSmoothing(obj, residual);
-
-            residual = obj.inclusionMatrix * residual;
-
-            for k = L:-1:3
-                residual = obj.intergridMatrix{k}'*residual;
-                rho{k-1} = p1LocalSmoothing(obj, k-1, residual);
-            end
-
-            % exact solve on coarsest level
-            residual = obj.intergridMatrix{2}'*residual;
-            sigma = obj.p1Acoarse \ residual;
-            sigma = obj.intergridMatrix{2}*sigma;
-
-            % ascending cascade
-            for k = 3:L
-                sigma = sigma + rho{k-1};
-                sigma = obj.intergridMatrix{k}*sigma;
-            end
-
-            sigma = obj.inclusionMatrix' * sigma;
-            sigma = sigma + rho{L};
-
-            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)
-            % Selects vertices of bisected edges and all newly created
-            % vertices
-            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 = [bisectedEdgeNodes; ((nCOld+1):nCNew)'];
-            obj.changedPatches{obj.nLevels+1}(idx) = true;
-        end
-
-        function rho = p1LocalSmoothing(obj, k, res)
-            idx = obj.changedPatches{k};
-            rho = zeros(size(res));
-            rho(idx,:) = obj.p1Smoother{k}(idx).*res(idx,:);
-            % DEBUG: Local patchwise smoothing in P1
-            % rho = obj.patchwiseP1Matrix{k} \ res;
-        end
-        
-        function rho = hoGlobalSmoothing(obj, res)
-            rho = obj.patchwiseA \ res;
-        end
-    end
-end
\ No newline at end of file
diff --git a/lib/solvers/BlockJacobiPcgSolver.m b/lib/solvers/BlockJacobiPcgSolver.m
deleted file mode 100644
index 71cbd08b19cc02f0f64c52d8be600c365408b470..0000000000000000000000000000000000000000
--- a/lib/solvers/BlockJacobiPcgSolver.m
+++ /dev/null
@@ -1,42 +0,0 @@
-% BlockJacobiPcgSolver (subclass of PcgSolver) Solves linear equations
-%   iteratively using the CG method with block-diagonal preconditioner.
-
-classdef BlockJacobiPcgSolver < PcgSolver
-    %% properties
-    properties (GetAccess=public,SetAccess=protected)
-        fes
-    end
-
-    properties (Access=private)
-        C
-        blf
-    end
-    
-    %% methods
-    methods (Access=public)
-        % preconditioner action: patchwise inverse of diagonal
-        function obj = BlockJacobiPcgSolver(fes, blf)
-            arguments
-                fes FeSpace
-                blf BilinearForm
-            end
-            
-            obj = obj@PcgSolver();
-            obj.fes = fes;
-            obj.blf = blf;
-        end
-
-        function setupSystemMatrix(obj, A)
-            obj.C = assemblePatchwise(obj.blf, obj.fes, ':');
-            setupSystemMatrix@PcgSolver(obj, A);
-        end
-           
-        function setupRhs(obj, b, varargin)
-            setupRhs@PcgSolver(obj, b, varargin{:});
-        end
-        
-        function Cx = preconditionAction(obj, x)
-            Cx = obj.C \ x;
-        end
-    end
-end
\ No newline at end of file
diff --git a/lib/solvers/ICholPcgSolver.m b/lib/solvers/ICholPcgSolver.m
deleted file mode 100644
index 58a300faaf4ad73d33336da2f8b4ccbfa093025c..0000000000000000000000000000000000000000
--- a/lib/solvers/ICholPcgSolver.m
+++ /dev/null
@@ -1,39 +0,0 @@
-% ICholPcgSolver (subclass of PcgSolver) Solves linear equations iteratively
-%   using the CG method with incomplete Cholesky decomposition preconditioner.
-
-classdef ICholPcgSolver < PcgSolver
-    %% properties
-    properties (GetAccess=public,SetAccess=protected)
-        fes
-    end
-
-    properties (Access=private)
-        C
-    end
-    
-    %% methods
-    methods (Access=public)
-        % preconditioner action: inverse of diagonal
-        function obj = ICholPcgSolver(fes)
-            arguments
-                fes FeSpace
-            end
-            
-            obj = obj@PcgSolver();
-            obj.fes = fes;
-        end
-
-        function setupSystemMatrix(obj, A)
-            obj.C = ichol(A);
-            setupSystemMatrix@PcgSolver(obj, A);
-        end
-           
-        function setupRhs(obj, b, varargin)
-            setupRhs@PcgSolver(obj, b, varargin{:});
-        end
-        
-        function Cx = preconditionAction(obj, x)
-            Cx = obj.C' \ (obj.C \ x);
-        end
-    end
-end
\ No newline at end of file
diff --git a/lib/solvers/JacobiPcgSolver.m b/lib/solvers/JacobiPcgSolver.m
deleted file mode 100644
index 0b54610008a77e2cc73a6f0630da6addb03b760f..0000000000000000000000000000000000000000
--- a/lib/solvers/JacobiPcgSolver.m
+++ /dev/null
@@ -1,39 +0,0 @@
-% JacobiPcgSolver (subclass of PcgSolver) Solves linear equations
-%   iteratively using the CG method with diagonal preconditioner.
-
-classdef JacobiPcgSolver < PcgSolver
-    %% properties
-    properties (GetAccess=public,SetAccess=protected)
-        fes
-    end
-
-    properties (Access=private)
-        C
-    end
-    
-    %% methods
-    methods (Access=public)
-        % preconditioner action: inverse of diagonal
-        function obj = JacobiPcgSolver(fes)
-            arguments
-                fes FeSpace
-            end
-            
-            obj = obj@PcgSolver();
-            obj.fes = fes;
-        end
-
-        function setupSystemMatrix(obj, A)
-            obj.C = full(diag(A)).^(-1);
-            setupSystemMatrix@PcgSolver(obj, A);
-        end
-           
-        function setupRhs(obj, b, varargin)
-            setupRhs@PcgSolver(obj, b, varargin{:});
-        end
-        
-        function Cx = preconditionAction(obj, x)
-            Cx = obj.C .* x;
-        end
-    end
-end
\ No newline at end of file
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/LocalMgLowOrderVcycle.m b/lib/solvers/LocalMgLowOrderVcycle.m
deleted file mode 100644
index f8920c03d1057f10ed21b3f411112f4e4c83abd5..0000000000000000000000000000000000000000
--- a/lib/solvers/LocalMgLowOrderVcycle.m
+++ /dev/null
@@ -1,179 +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 LocalMgLowOrderVcycle < MGSolver
-    %% properties
-    properties (GetAccess=public,SetAccess=protected)
-        nLevels
-    end
-
-    properties (Access=protected)
-        blf
-        hoFes
-        loFes
-        p1Matrix
-        p1Smoother
-        P
-        intergridMatrix
-        inclusionMatrix
-        freeVertices
-        freeVerticesOld
-        changedPatches
-        patchwiseA
-    end
-    
-    %% event data
-    properties (Access=private)
-        listenerHandle
-    end
-
-    %% methods
-    methods (Access=public)
-        function obj = LocalMgLowOrderVcycle(fes, blf)
-            arguments
-                fes FeSpace
-                blf BilinearForm
-            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.loFes = FeSpace(mesh, LowestOrderH1Fe(), 'dirichlet', ':');
-            obj.hoFes = fes;
-            obj.P = Prolongation.chooseFor(obj.loFes);
-            obj.blf = blf;
-            
-            obj.listenerHandle = mesh.listener('IsAboutToRefine', @obj.getChangedPatches);
-        end
-
-        function setupSystemMatrix(obj, A)
-            obj.nLevels = obj.nLevels + 1;
-            
-            L = obj.nLevels;
-            obj.freeVerticesOld = obj.freeVertices;
-            obj.freeVertices = getFreeDofs(obj.loFes);
-            freeVerticesHigher = getFreeDofs(obj.hoFes);
-            
-            obj.p1Matrix{L} = assemble(obj.blf, obj.loFes);
-            obj.p1Matrix{L} = obj.p1Matrix{L}(obj.freeVertices, obj.freeVertices);
-            obj.p1Smoother{L} = full(diag(obj.p1Matrix{L})).^(-1);
-            
-            if L >= 2
-                obj.intergridMatrix{L} = obj.P.matrix(obj.freeVertices, obj.freeVerticesOld);
-                obj.changedPatches{L} = find(obj.changedPatches{L}(obj.freeVertices));
-                obj.patchwiseA = assemblePatchwise(obj.blf, obj.hoFes);
-                obj.inclusionMatrix = SpaceProlongation(obj.loFes, obj.hoFes) ...
-                    .matrix(getFreeDofs(obj.loFes), getFreeDofs(obj.hoFes));
-            end
-            setupSystemMatrix@MGSolver(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 P1: no smoothing
-            residual{L} = obj.inclusionMatrix * res; %interpolateFreeData(res, obj.hoFes, obj.loFes);
-            for k = L:-1:2
-                residual{k-1}  = obj.intergridMatrix{k}'*residual{k};
-            end
-
-            % exact solve on coarsest level to compute accumulative lifting
-            % of the residual (sigma)
-            sigma = obj.p1Matrix{1} \ residual{1};
-            algError2 = algError2 + scalarProduct(sigma, obj.p1Matrix{1}*sigma);
-
-            % ascending cascade in P1 AND local smoothing
-            for k = 2:(L-1)
-                sigma = obj.intergridMatrix{k}*sigma;
-                uptres = residual{k} - obj.p1Matrix{k}*sigma; %updated residual 
-                rho = p1LocalSmoothing(obj, k, uptres);
-                [aeUpd, sUpd] = computeOptimalUpdate(obj.p1Matrix{k}, uptres, rho);
-                sigma = sigma + sUpd;
-                algError2 = algError2 + aeUpd;
-            end
-            
-            % smoothing on finest level dependent on p
-            sigma = obj.intergridMatrix{L}*sigma;
-            sigma = obj.inclusionMatrix' * sigma; %interpolateFreeData(sigma, obj.loFes, obj.hoFes);
-            uptres = res - obj.A*sigma;
-            rho = hoGlobalSmoothing(obj, uptres);
-            [aeUpd, sUpd] = computeOptimalUpdate(obj.A, uptres, rho);
-            sigma = sigma + sUpd;
-            algError2 = algError2 + aeUpd;
-
-            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
-        
-        function rho = p1LocalSmoothing(obj, k, res)
-            idx = obj.changedPatches{k};
-            rho = zeros(size(res));
-            rho(idx,:) = obj.p1Smoother{k}(idx).*res(idx,:);
-        end
-        
-        function rho = hoGlobalSmoothing(obj, res)
-            rho = obj.patchwiseA \ res;
-        end
-    end
-end
-
-%% auxiliary functions
-% error correction with optimal stepsize 
-function [etaUpdate, sigmaUpdate] = computeOptimalUpdate(A, res, rho)
-    rhoArho = scalarProduct(rho, A*rho);
-    if max(abs(rho)) < eps
-        lambda = 1; 
-    else
-        lambda = scalarProduct(res, rho) ./ rhoArho;
-    end
-    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
diff --git a/lib/solvers/LowestOrderAdditiveSchwarzPcg.m b/lib/solvers/LowestOrderAdditiveSchwarzPcg.m
deleted file mode 100644
index 9e19823e6c22b4141f296dd8ae4d903e33d143c3..0000000000000000000000000000000000000000
--- a/lib/solvers/LowestOrderAdditiveSchwarzPcg.m
+++ /dev/null
@@ -1,130 +0,0 @@
-% LowestOrderAdditiveSchwartzPcg (subclass of PcgSolver) Solves linear
-%   equations iteratively using the CG method with optimal multilevel
-%   additive Schwartz preconditioner for lowest order finite elements.
-
-classdef LowestOrderAdditiveSchwarzPcg < PcgSolver
-    %% properties
-    properties (GetAccess=public,SetAccess=protected)
-        nLevels
-    end
-    
-    properties (Access=protected)
-        blf
-        fes
-        matrix
-        smoother
-        P
-        intergridMatrix
-        freeDofs
-        freeDofsOld
-        changedPatches
-        highestOrderIsOne
-    end
-
-    properties (Access=private)
-        listenerHandle
-    end
-    
-    %% methods
-    methods (Access=public)
-        function obj = LowestOrderAdditiveSchwarzPcg(fes, blf, P)
-            arguments
-                fes FeSpace
-                blf BilinearForm
-                P Prolongation
-            end
-            obj = obj@PcgSolver();
-            
-            assert(fes.finiteElement.order == 1, ...
-                'LowestOrderAdditiveSchwartzPcg only works for lowest order finite elements.')
-            assert(isempty(blf.b), ...
-                'Additive Schwarz PCG solvers only tested for symmetric problems.')
-            
-            obj.nLevels = 0;
-            
-            mesh = fes.mesh;
-            obj.fes = fes;
-            obj.P = P;
-            obj.blf = blf;
-            
-            obj.listenerHandle = mesh.listener('IsAboutToRefine', @obj.getChangedPatches);
-        end
-        
-        function setupSystemMatrix(obj, A)
-            obj.nLevels = obj.nLevels + 1;
-            
-            L = obj.nLevels;
-            obj.freeDofsOld = obj.freeDofs;
-            obj.freeDofs = getFreeDofs(obj.fes);
-            
-            obj.matrix{L} = A;
-            obj.smoother{L} = full(diag(obj.matrix{L})).^(-1);
-            
-            if L >= 2
-                obj.intergridMatrix{L} = obj.P.matrix(obj.freeDofs, obj.freeDofsOld);
-                obj.changedPatches{L} = find(obj.changedPatches{L}(obj.freeDofs));
-            end
-            
-            setupSystemMatrix@PcgSolver(obj, A);
-        end
-           
-        function setupRhs(obj, b, varargin)
-            setupRhs@PcgSolver(obj, b, varargin{:});
-        end
-        
-        % preconditioner: inverse of diagonal on each level
-        function Cx = preconditionAction(obj, res)
-            assert(~isempty(obj.nLevels), 'Data for multilevel iteration not given!')
-
-            L = obj.nLevels;
-            rho = cell(L, 1);
-
-            if L == 1
-                Cx = obj.A \ res;
-                return
-            end
-
-            % descending cascade
-            rho{L} = localSmoothing(obj, L, res);
-            residual = res;
-            for k = L-1:-1:2
-                residual = obj.intergridMatrix{k+1}'*residual;
-                rho{k} = localSmoothing(obj, k, residual);
-            end
-            
-            % exact solve on coarsest level
-            residual = obj.intergridMatrix{2}'*residual;
-            sigma = obj.matrix{1} \ residual;
-            
-            % ascending cascade
-            for k = 2:L-1
-                sigma = obj.intergridMatrix{k}*sigma;
-                sigma = sigma + rho{k};
-            end
-            sigma = obj.intergridMatrix{L}*sigma + rho{L};
-           
-            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
-        
-        function rho = localSmoothing(obj, k, res)
-            idx = obj.changedPatches{k};
-            rho = zeros(size(res));
-            rho(idx,:) = obj.smoother{k}(idx).*res(idx,:);
-        end
-    end
-end
\ No newline at end of file
diff --git a/lib/solvers/LowestOrderLocalMg.m b/lib/solvers/LowestOrderLocalMg.m
deleted file mode 100644
index cde8fb1d573b76a5d2de1c0927f0222c1637b3a9..0000000000000000000000000000000000000000
--- a/lib/solvers/LowestOrderLocalMg.m
+++ /dev/null
@@ -1,162 +0,0 @@
-% LowestOrderLocalMg (subclass of MGSolver) Solves linear equations
-%   stemming from lowest order AFEM computations iteratively using a
-%   multigrid solver where one iteration is characterized by Vcycle(0,1)
-%   (no pre-/one post-smoothing step).
-%   On every level, local Jacobi smoothing is employed on changed patches
-%   and the smoothed update is weighted by an optimal step size.
-%
-%   See also [Innerberger, Miraçi, Praetorius, Streitberger; Optimal
-%   computational costs of AFEM with optimal local hp-robust multigrid
-%   solver].
-
-classdef LowestOrderLocalMg < MGSolver
-    %% properties
-    properties (GetAccess=public,SetAccess=protected)
-        nLevels
-    end
-
-    properties (Access=protected)
-        fes
-        blf
-        matrix
-        smoother
-        P
-        intergridMatrix
-        freeVertices
-        freeVerticesOld
-        changedPatches
-    end
-    
-    %% event data
-    properties (Access=private)
-        listenerHandle
-    end
-
-    %% methods
-    methods (Access=public)
-        function obj = LowestOrderLocalMg(fes, blf, P)
-            arguments
-                fes FeSpace
-                blf BilinearForm
-                P Prolongation
-            end
-            obj = obj@MGSolver();
-            
-            assert(fes.finiteElement.order == 1, ...
-                'LowestOrderLocalMg only works for lowest order finite elements.')
-            assert(isempty(blf.b), ...
-                'Multigrid solvers only tested for symmetric problems.')
-            
-            obj.nLevels = 0;
-            
-            mesh = fes.mesh;
-            obj.fes = fes;
-            obj.P = P;
-            obj.blf = blf;
-            
-            obj.listenerHandle = mesh.listener('IsAboutToRefine', @obj.getChangedPatches);
-        end
-
-        function setupSystemMatrix(obj, A)
-            obj.nLevels = obj.nLevels + 1;
-            
-            L = obj.nLevels;
-            obj.freeVerticesOld = obj.freeVertices;
-            obj.freeVertices = getFreeDofs(obj.fes);
-            
-            obj.matrix{L} = A;
-            obj.smoother{L} = full(diag(obj.matrix{L})).^(-1);
-            
-            if L >= 2
-                obj.intergridMatrix{L} = obj.P.matrix(obj.freeVertices, obj.freeVerticesOld);
-                obj.changedPatches{L} = find(obj.changedPatches{L}(obj.freeVertices));
-            end
-            setupSystemMatrix@MGSolver(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 P1: no smoothing
-            residual{L} = res;
-            for k = L:-1:2
-                residual{k-1}  = obj.intergridMatrix{k}'*residual{k};
-            end
-
-            % exact solve on coarsest level to compute accumulative lifting
-            % of the residual (sigma)
-            sigma = obj.matrix{1} \ residual{1};
-            algError2 = algError2 + scalarProduct(sigma, obj.matrix{1}*sigma);
-
-            % ascending cascade in P1 AND local smoothing
-            for k = 2:L
-                sigma = obj.intergridMatrix{k}*sigma;
-                uptres = residual{k} - obj.matrix{k}*sigma; %updated residual 
-                rho = localSmoothing(obj, k, uptres);
-                [aeUpd, sUpd] = computeOptimalUpdate(obj.matrix{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
-        
-        function rho = localSmoothing(obj, k, res)
-            idx = obj.changedPatches{k};
-            rho = zeros(size(res));
-            rho(idx,:) = obj.smoother{k}(idx).*res(idx,:);
-        end
-    end
-end
-
-%% auxiliary functions
-% error correction with optimal stepsize 
-function [etaUpdate, sigmaUpdate] = computeOptimalUpdate(A, res, rho)
-    rhoArho = scalarProduct(rho, A*rho);
-    if max(abs(rho)) < eps
-        lambda = 1; 
-    else
-        lambda = scalarProduct(res, rho) ./ rhoArho;
-    end
-    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
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/OptimalVcycleMultigridSolver.m b/lib/solvers/OptimalVcycleMultigridSolver.m
new file mode 100644
index 0000000000000000000000000000000000000000..496c2a73082af19af51919bf7ebb1c0ae04f8572
--- /dev/null
+++ b/lib/solvers/OptimalVcycleMultigridSolver.m
@@ -0,0 +1,144 @@
+% OptimalVcycleMultigridSolver (subclass of IterativeSolver) Solves linear
+%   equations iteratively, using a geometric multigrid solver with specified
+%   MultiLevelSmoother.
+%   One iteration = Vcycle(0,1) (no pre-/one post-smoothing step).
+%
+%   To obtain the solver described in [Innerberger, Miraçi, Praetorius,
+%   Streitberger; Optimal computational costs of AFEM with optimal local
+%   hp-robust multigrid solver], choose the smoother as DiagonalJacobi (p=1),
+%   LowOrderBlockJacobi (p > 1, 1->1->p iteration), or HighOrderBlockJacobi
+%   (p > 1, 1->p->p iteration).
+%   This solver also comes with built-in estimator for the algebraic error,
+%   which is also returned by this multigrid solver.
+%
+% See also IterativeSolver, MultiLevelSmoother, DiagonalJacobi,
+%   LowOrderBlockJacobi, HighOrderBlockJacobi
+
+classdef OptimalVcycleMultigridSolver < IterativeSolver
+    %% properties 
+    properties (SetAccess=protected, GetAccess=public)
+        residualCNorm
+        algEstimator
+        residual
+        Cresidual
+    end
+
+    properties (Dependent, Access=public)
+        nLevels
+    end
+    
+    properties (Access=protected)
+        smoother
+        projectedMatrix
+        lhsNorm
+    end
+    
+    methods
+        function obj = OptimalVcycleMultigridSolver(smoother)
+            obj = obj@IterativeSolver();
+            obj.smoother = smoother;
+        end
+
+        function n = get.nLevels(obj)
+            n = obj.smoother.nLevels;
+        end
+
+        function setupSystemMatrix(obj, A, varargin)
+            % override superclass method, because ALL matrices are needed
+            setupSystemMatrix@IterativeSolver(obj, A, varargin{:});
+
+            projectedA = obj.smoother.setup(A, varargin{:});
+            obj.projectedMatrix{obj.nLevels} = projectedA;
+        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 = sqrt(dot(obj.residual, obj.Cresidual, Dim.Vector));
+            obj.lhsNorm = vecnorm(b, 2, Dim.Vector);
+        end
+        
+        % stopping criterion
+        function tf = isConverged(obj)
+            tf = ((obj.iterationCount >= obj.maxIter) ...
+                | (obj.residualCNorm < obj.tol) ...
+                | (obj.residualCNorm ./ obj.lhsNorm < 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) = sqrt(dot(obj.residual(:,idx), obj.Cresidual(:,idx), Dim.Vector));
+        end
+
+        function [Cx, algEstimator] = Vcycle(obj, x)
+            assert(isequal(size(x, 1), size(obj.A, 1)), ...
+                'Setup for multilevel iteration not complete!')
+            
+            % if there is only one coarse level: exact solve
+            L = obj.nLevels;
+            if L == 1
+                Cx = obj.A \ x;
+                algEstimator = sqrt(dot(Cx, obj.A * Cx, Dim.Vector));
+                return
+            end
+
+            % descending cascade: no smoothing
+            res{L} = x;
+            for k = L:-1:2
+                res{k-1} = obj.smoother.restrict(res{k}, k);
+            end
+
+            % exact solve on coarsest level to compute accumulative lifting of x
+            Cx = obj.projectedMatrix{1} \ res{1};
+            eta2 = dot(Cx, obj.projectedMatrix{1} * Cx, Dim.Vector);
+
+            % ascending cascade: (local) smoothing and compute error estimator
+            for k = 2:(L-1)
+                Cx = obj.smoother.prolongate(Cx, k);
+                updatedResidual = res{k} - obj.projectedMatrix{k} * Cx;
+                rho = obj.smoother.smooth(updatedResidual, k);
+                [etaUpdate2, xUpdate] = computeOptimalUpdate(obj.projectedMatrix{k}, updatedResidual, rho);
+                Cx = Cx + xUpdate;
+                eta2 = eta2 + etaUpdate2;
+            end
+
+            % final smoothing and residual update (with non-projected matrix)
+            Cx = obj.smoother.prolongate(Cx, L);
+            updatedResidual = res{L} - obj.A * Cx;
+            rho = obj.smoother.smooth(updatedResidual, L);
+            [etaUpdate2, xUpdate] = computeOptimalUpdate(obj.A, updatedResidual, rho);
+            Cx = Cx + xUpdate;
+            algEstimator = sqrt(eta2 + etaUpdate2);
+        end
+    end
+end
+
+%% auxiliary functions
+% error correction with optimal stepsize 
+function [etaUpdate2, resUpdate] = computeOptimalUpdate(A, res, rho)
+    rhoArho = dot(rho, A*rho, Dim.Vector);
+    if max(abs(rho)) < eps
+        lambda = 1; 
+    else
+        lambda = dot(res, rho, Dim.Vector) ./ rhoArho;
+    end
+    resUpdate = lambda .* rho;
+    etaUpdate2 = 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
diff --git a/lib/solvers/PcgSolver.m b/lib/solvers/PcgSolver.m
index 3a21ecc58737624493b4679cdb3dbfe9f211c153..1a278c82dda3a969218f6e1d6f730baa11c4bdcc 100644
--- a/lib/solvers/PcgSolver.m
+++ b/lib/solvers/PcgSolver.m
@@ -10,27 +10,41 @@ classdef PcgSolver < IterativeSolver
     end
     
     properties (Access=protected)
+        C
         residual
         Cresidual
         searchDirection
         normb
     end
+
+    %% constructor
+    methods (Access=public)
+        function obj = PcgSolver(preconditioner)
+            arguments
+                preconditioner (1,1) Preconditioner
+            end
+
+            obj = obj@IterativeSolver();
+            obj.C = preconditioner;
+        end
+    end
     
     %% extend superclass methods
     methods (Access=public)
         function setupSystemMatrix(obj, A)
             setupSystemMatrix@IterativeSolver(obj, A);
+            obj.C.setup(A);
         end
         
         function setupRhs(obj, b, varargin)
             setupRhs@IterativeSolver(obj, b, varargin{:});
             
             % initialize residual & search direction
-            obj.residual = b - obj.A*obj.x;
-            obj.Cresidual = obj.preconditionAction(obj.residual);
+            obj.residual = b - obj.A * obj.x;
+            obj.Cresidual = obj.C.apply(obj.residual);
             obj.searchDirection = obj.Cresidual;
-            obj.residualCNorm = sum(obj.residual.*obj.Cresidual, 1);
-            obj.normb = sqrt(sum(b.^2, 1));
+            obj.residualCNorm = sqrt(dot(obj.residual, obj.Cresidual, 1));
+            obj.normb = vecnorm(b, 2, 1);
         end
     end
     
@@ -38,8 +52,8 @@ classdef PcgSolver < IterativeSolver
     methods (Access=public)
         function tf = isConverged(obj)
             tf = ((obj.iterationCount >= obj.maxIter) ...
-                        | (sqrt(obj.residualCNorm) < obj.tol) ...
-                        | (sqrt(obj.residualCNorm)./obj.normb < obj.tol));
+                | (obj.residualCNorm < obj.tol) ...
+                | (obj.residualCNorm ./ obj.normb < obj.tol));
         end
     end
     
@@ -50,30 +64,23 @@ classdef PcgSolver < IterativeSolver
             
             % update solution
             AsearchDirection = obj.A * obj.searchDirection(:,idx);
-            if sum(obj.searchDirection(:,idx).*AsearchDirection, 1) < eps
+            dAd = dot(obj.searchDirection(:,idx), AsearchDirection, 1);
+            if dAd < eps
                 alpha = 1;
             else
-                alpha = obj.residualCNorm(:,idx) ./ sum(obj.searchDirection(:,idx).*AsearchDirection, 1);
+                alpha = obj.residualCNorm(:,idx).^2 ./ dAd;
             end
             obj.x(:,idx) = obj.x(:,idx) + alpha .* obj.searchDirection(:,idx);
 
-            % DEBUG:
-            % disp(['alpha = ', num2str(alpha)])
-            
             % update residual
             obj.residual(:,idx) = obj.residual(:,idx) - alpha .* AsearchDirection;
-            obj.Cresidual(:,idx) = obj.preconditionAction(obj.residual(:,idx));
+            obj.Cresidual(:,idx) = obj.C.apply(obj.residual(:,idx));
             residualCNormOld = obj.residualCNorm(:,idx);
-            obj.residualCNorm(:,idx) = sum(obj.residual(:,idx).*obj.Cresidual(:,idx), 1);
+            obj.residualCNorm(:,idx) = sqrt(dot(obj.residual(:,idx), obj.Cresidual(:,idx), 1));
             
             % update search direction
-            beta = obj.residualCNorm(:,idx) ./ residualCNormOld;
+            beta = (obj.residualCNorm(:,idx) ./ residualCNormOld).^2;
             obj.searchDirection(:,idx) = obj.Cresidual(:,idx) + beta .* obj.searchDirection(:,idx);
         end
     end
-    
-    %% abstract preconditioning
-    methods (Abstract, Access=public)
-        preconditionAction(obj, x)
-    end
 end
\ No newline at end of file
diff --git a/lib/solvers/chooseIterativeSolver.m b/lib/solvers/chooseIterativeSolver.m
deleted file mode 100644
index c0d3bc50f1223bd9d51b41151084f8a2bb9f3aea..0000000000000000000000000000000000000000
--- a/lib/solvers/chooseIterativeSolver.m
+++ /dev/null
@@ -1,66 +0,0 @@
-% chooseIterativeSolver Return suitable solver (instance of subclass of
-%   IterativeSolver) and suitable Prolongation P.
-
-function [solver, P] = chooseIterativeSolver(fes, blf, class, variant)
-arguments
-    fes FeSpace
-    blf BilinearForm
-    class (1,1) string {mustBeMember(class, ["cg", "pcg", "multigrid", "direct"])}
-    variant (1,1) string {mustBeMember(variant, [ "", ...
-        "iChol", "jacobi", ...
-        "additiveSchwarzLowOrder", "additiveSchwarzHighOrder" ...
-        "lowOrderVcycle", "highOrderVcycle"])} = ""
-end
-
-order = fes.finiteElement.order;
-P = Prolongation.chooseFor(fes);
-
-switch class
-    % non-preconditioned CG
-    case "cg"
-        solver = CgSolver();
-        
-    % preconditioned CG family
-    case "pcg"
-        switch variant
-            case "iChol"
-                solver = ICholPcgSolver(fes);
-            case "jacobi"
-                if order == 1, solver = JacobiPcgSolver(fes);
-                else, solver = BlockJacobiPcgSolver(fes, blf); end
-            case {"", "additiveSchwarzLowOrder"}
-                if order == 1, solver = LowestOrderAdditiveSchwarzPcg(fes, blf, P);
-                else
-                    solver = AdditiveSchwarzLowOrderPcg(fes, blf);
-                end
-            case "additiveSchwarzHighOrder"
-                solver = AdditiveSchwarzHighOrderPcg(fes, blf, P);
-            otherwise
-                error('No PCG variant %s!', variant)
-        end
-        
-    % geometric multigrid family
-    case "multigrid"
-        switch variant
-            case {"", "lowOrderVcycle"}
-                if order == 1, solver = LowestOrderLocalMg(fes, blf, P);
-                else, solver = LocalMgLowOrderVcycle(fes, blf); end
-            case "highOrderVcycle"
-                if order == 1, solver = LowestOrderLocalMg(fes, blf, P);
-                else, solver = LocalMgHighOrderVcycle(fes, blf, P); end
-            otherwise
-                error('No multigrid variant %s!', variant)
-        end
-        
-    % direct solve (for testing purposes)
-    case "direct"
-        solver = DirectSolver();
-   
-    % default
-    otherwise
-        error('No iterative solver of class %s!', class)
-end
-    
-end
-
-
diff --git a/lib/solvers/preconditioners/BlockJacobi.m b/lib/solvers/preconditioners/BlockJacobi.m
new file mode 100644
index 0000000000000000000000000000000000000000..d4bf3ec1cffaab76bcea9d48ed8d4f036e92a34c
--- /dev/null
+++ b/lib/solvers/preconditioners/BlockJacobi.m
@@ -0,0 +1,34 @@
+% BlockJacobi (subclass of Preconditioner) block-diagonal preconditioner.
+%
+% See also: Preconditioner, PcgSolver
+
+classdef BlockJacobi < Preconditioner
+    %% properties
+    properties (Access=private)
+        C
+        fes
+        blf
+    end
+    
+    %% methods
+    methods (Access=public)
+        % preconditioner action: patchwise inverse of diagonal
+        function obj = BlockJacobi(fes, blf)
+            arguments
+                fes FeSpace
+                blf BilinearForm
+            end
+            
+            obj.fes = fes;
+            obj.blf = blf;
+        end
+
+        function setup(obj, A)
+            obj.C = assemblePatchwise(obj.blf, obj.fes, ':');
+        end
+           
+        function Cx = apply(obj, x)
+            Cx = obj.C \ x;
+        end
+    end
+end
\ No newline at end of file
diff --git a/lib/solvers/preconditioners/DiagonalJacobi.m b/lib/solvers/preconditioners/DiagonalJacobi.m
new file mode 100644
index 0000000000000000000000000000000000000000..9ade2832478bd8c204fcb6c3c99b89e5c1132500
--- /dev/null
+++ b/lib/solvers/preconditioners/DiagonalJacobi.m
@@ -0,0 +1,22 @@
+% DiagonalJacobi (subclass of Preconditioner) diagonal preconditioner.
+%
+% See also: Preconditioner, PcgSolver
+
+classdef DiagonalJacobi < Preconditioner
+    %% properties
+    properties (Access=private)
+        C
+    end
+    
+    %% methods
+    methods (Access=public)
+        % preconditioner action: inverse of diagonal
+        function setup(obj, A)
+            obj.C = full(diag(A)).^(-1);
+        end
+           
+        function Cx = apply(obj, x)
+            Cx = obj.C .* x;
+        end
+    end
+end
\ No newline at end of file
diff --git a/lib/solvers/preconditioners/IncompleteCholesky.m b/lib/solvers/preconditioners/IncompleteCholesky.m
new file mode 100644
index 0000000000000000000000000000000000000000..6a04a2577a04519a146d399cbe4ecbeb9b242583
--- /dev/null
+++ b/lib/solvers/preconditioners/IncompleteCholesky.m
@@ -0,0 +1,22 @@
+% IncompleteCholesky (subclass of Preconditioner) incomplete Cholesky
+%   decomposition preconditioner.
+%
+% See also: Preconditioner, PcgSolver
+
+classdef IncompleteCholesky < Preconditioner
+    %% properties
+    properties (Access=private)
+        C
+    end
+    
+    %% methods
+    methods (Access=public)
+        function setup(obj, A)
+            obj.C = ichol(A);
+        end
+
+        function Cx = apply(obj, x)
+            Cx = obj.C' \ (obj.C \ x);
+        end
+    end
+end
\ No newline at end of file
diff --git a/lib/solvers/preconditioners/NoPreconditioner.m b/lib/solvers/preconditioners/NoPreconditioner.m
new file mode 100644
index 0000000000000000000000000000000000000000..fa413b5e0f21d730a0c570740656d09f603c7d5d
--- /dev/null
+++ b/lib/solvers/preconditioners/NoPreconditioner.m
@@ -0,0 +1,12 @@
+% NoPreconditioner (subclass of Preconditioner) identity preconditioner (=no-op).
+%
+% See also: Preconditioner, PcgSolver
+
+
+classdef NoPreconditioner < Preconditioner
+    methods (Access=public)
+        function Cx = apply(~, x)
+            Cx = x;
+        end
+    end
+end
\ No newline at end of file
diff --git a/lib/solvers/preconditioners/OptimalMLAdditiveSchwarz.m b/lib/solvers/preconditioners/OptimalMLAdditiveSchwarz.m
new file mode 100644
index 0000000000000000000000000000000000000000..c7499bdc6d56acac17c2531fd484ba6ac3c603b9
--- /dev/null
+++ b/lib/solvers/preconditioners/OptimalMLAdditiveSchwarz.m
@@ -0,0 +1,76 @@
+% OptimalMLAdditiveSchwarz (subclass of Preconditioner) optimal multilevel
+%   additive Schwarz preconditioner with given smoother.
+%
+% See also: Preconditioner, PcgSolver, MultilevelSmoother
+
+classdef OptimalMLAdditiveSchwarz < Preconditioner
+    %% properties
+    properties (Access=protected)
+        smoother
+        Acoarse
+        projectedAcoarse
+    end
+
+    properties (Dependent)
+        nLevels
+    end
+
+    properties (Access=private)
+        listenerHandle
+    end
+    
+    %% methods
+    methods (Access=public)
+        function obj = OptimalMLAdditiveSchwarz(smoother)
+            arguments
+                smoother MultilevelSmoother
+            end
+            
+            obj = obj@Preconditioner();
+            obj.smoother = smoother;
+        end
+        
+        function setup(obj, A)
+            if obj.nLevels == 0
+                obj.Acoarse = A;
+                obj.projectedAcoarse = obj.smoother.setup(A);
+            else
+                obj.smoother.setup(A);
+            end
+        end
+           
+        function Cx = apply(obj, x)
+            assert(~isempty(obj.nLevels), 'Data for multilevel iteration not given!')
+
+            L = obj.nLevels;
+            rho = cell(L, 1);
+
+            if L == 1
+                Cx = obj.Acoarse \ x;
+                return
+            end
+
+            % descending cascade
+            residual = x;
+            for k = L:-1:2
+                rho{k} = obj.smoother.smooth(residual, k);
+                residual = obj.smoother.restrict(residual, k);
+            end
+            
+            % exact solve on coarsest level
+            Cx = obj.projectedAcoarse \ residual;
+            
+            % ascending cascade
+            for k = 2:L
+                Cx = obj.smoother.prolongate(Cx, k);
+                Cx = Cx + rho{k};
+            end
+        end
+    end
+
+    methods
+        function n = get.nLevels(obj)
+            n = obj.smoother.nLevels;
+        end
+    end
+end
\ No newline at end of file
diff --git a/lib/solvers/preconditioners/Preconditioner.m b/lib/solvers/preconditioners/Preconditioner.m
new file mode 100644
index 0000000000000000000000000000000000000000..88833d8618a7f99c5c25dc8e83edf8e46f840a16
--- /dev/null
+++ b/lib/solvers/preconditioners/Preconditioner.m
@@ -0,0 +1,23 @@
+% Preconditioner (abstract handle class) Interface for preconditioners.
+%
+% pc.setup(A, ...) hook to set up the preconditioner for the matrix A.
+%   Default implementation does nothing.
+%
+% Abstract methods to implement:
+%
+% Cx = pc.apply(x) applies the preconditioner to the vector x.
+%
+% See also: PcgSolver
+
+
+classdef Preconditioner < handle
+    methods (Access=public)
+        function setup(~, ~)
+            % Default implementation does nothing.
+        end
+    end
+    
+    methods (Abstract, Access=public)
+        apply(obj, x)
+    end
+end
\ No newline at end of file
diff --git a/lib/solvers/smoothers/JacobiHighOrderCascade.m b/lib/solvers/smoothers/JacobiHighOrderCascade.m
new file mode 100644
index 0000000000000000000000000000000000000000..d58ab6995e87d15155a6a8aac5e58fe7a68779a2
--- /dev/null
+++ b/lib/solvers/smoothers/JacobiHighOrderCascade.m
@@ -0,0 +1,85 @@
+% JacobiHighOrderCascade (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 JacobiHighOrderCascade < MultilevelSmoother
+    properties (Access=protected)
+        P
+        loFes
+        patchwiseA
+        inclusionMatrix
+        intergridMatrix
+        freeDofs
+        freeDofsOld
+    end
+
+    %% methods
+    methods (Access=public)
+        function obj = JacobiHighOrderCascade(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/JacobiLowOrderCascade.m b/lib/solvers/smoothers/JacobiLowOrderCascade.m
new file mode 100644
index 0000000000000000000000000000000000000000..b0b9e5545ee9e79ae3e8420dedf94b822bca81f3
--- /dev/null
+++ b/lib/solvers/smoothers/JacobiLowOrderCascade.m
@@ -0,0 +1,90 @@
+% JacobiLowOrderCascade (subclass of MultilevelSmoother) multilevel
+%   Jacobi smoother for higher order finite elements: smooth locally with
+%   diagonal of P1 stiffness matrix on changed patches on every level, except
+%   for finest level, where global patchwise higher order smoothing is done.
+%   
+%   Reference: [Innerberger, Miraçi, Praetorius, Streitberger; Optimal
+%   computational costs of AFEM with optimal local hp-robust multigrid solver].
+%
+% See also: MultilevelSmoother, OptimalVcycleMultigridSolver
+
+
+classdef JacobiLowOrderCascade < MultilevelSmoother
+    properties (Access=protected)
+        P
+        loFes
+        p1Smoother
+        hoSmoother
+        inclusionMatrix
+        intergridMatrix
+        freeVertices
+        freeVerticesOld
+    end
+
+    %% methods
+    methods (Access=public)
+        function obj = JacobiLowOrderCascade(fes, blf)
+            obj = obj@MultilevelSmoother(fes, blf);
+            
+            assert(fes.finiteElement.order > 1, ...
+                'JacobiLowOrderCascadeSmoother only works for higher order finite elements.')
+
+            mesh = fes.mesh;
+            obj.loFes = FeSpace(mesh, LowestOrderH1Fe(), 'dirichlet', fes.bnd.dirichlet);
+            obj.P = Prolongation.chooseFor(obj.loFes);
+            
+        end
+
+        function nonInvertedSmootherMatrix = setup(obj, A)
+            setup@MultilevelSmoother(obj, A);
+            
+            L = obj.nLevels;
+            obj.freeVerticesOld = obj.freeVertices;
+            obj.freeVertices = getFreeDofs(obj.loFes);
+            
+            p1Matrix = assemble(obj.blf, obj.loFes);
+            p1Matrix = p1Matrix(obj.freeVertices, obj.freeVertices);
+            obj.p1Smoother{L} = full(diag(p1Matrix)).^(-1);
+            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.p1Smoother{L} = obj.p1Smoother{L}(obj.changedPatches{L});
+                % finest level smoother and transfer operator
+                obj.hoSmoother = assemblePatchwise(obj.blf, obj.fes, ':');
+                obj.inclusionMatrix = SpaceProlongation(obj.loFes, obj.fes) ...
+                    .matrix(getFreeDofs(obj.loFes), getFreeDofs(obj.fes));
+            end
+        end
+
+        function Cx = smooth(obj, x, k)
+            if k == obj.nLevels
+                % higher order global patchwise smooting
+                Cx = obj.hoSmoother \ x;
+            else
+                % local p1 patchwise smoothing
+                idx = obj.changedPatches{k};
+                Cx = zeros(size(x));
+                Cx(idx,:) = obj.p1Smoother{k} .* x(idx,:);
+            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
+                Px = obj.inclusionMatrix' * Px;
+            end
+        end
+
+        function Px = restrict(obj, x, k)
+            if k == obj.nLevels
+                x = obj.inclusionMatrix * x;
+            end
+            Px = obj.intergridMatrix{k}' * x;
+        end
+    end
+end
\ No newline at end of file
diff --git a/lib/solvers/smoothers/MultilevelSmoother.m b/lib/solvers/smoothers/MultilevelSmoother.m
new file mode 100644
index 0000000000000000000000000000000000000000..ca7271db059c51bb0e23f923b20bd0e54110dc7c
--- /dev/null
+++ b/lib/solvers/smoothers/MultilevelSmoother.m
@@ -0,0 +1,84 @@
+% MultilevelSmoother (abstract handle class) Interface for smoothers to use in a
+%   multigrid solver.
+%
+% Abstract methods to implement:
+%
+% smoother.setup(A) hook to set up the smoother for the matrix A.
+%
+% nonInvertedSmootherMatrix = smoother.setup(A) set up the smoother and also
+%   return the non-inverted matrix, based on which the smoother is computed.
+%
+% Cx = smoother.smooth(x, k) applies the smoother to the vector x on level k.
+%
+% Px = smoother.prolongate(x, k) prolongate vector x from level k to level k+1.
+%
+% Px = smoother.restrict(x, k) restrict vector x from level k+1 to level k.
+%
+% See also: OptimalVcycleMultigridSolver
+
+
+classdef MultilevelSmoother < handle
+    %% properties
+    properties (GetAccess=public, SetAccess=protected)
+        nLevels
+    end
+
+    properties (Access=protected)
+        fes
+        blf
+        changedPatches
+    end
+    
+    properties (Access=private)
+        listenerHandle
+    end
+
+    %% methods
+    methods (Access=public)
+        function obj = MultilevelSmoother(fes, blf)
+            arguments
+                fes FeSpace
+                blf BilinearForm
+            end
+
+            assert(isempty(blf.b), ...
+                'Multilevel smoothers only tested for symmetric problems.')
+
+            obj.nLevels = 0;
+            obj.fes = fes;
+            obj.blf = blf;
+
+            mesh = fes.mesh;
+            obj.listenerHandle = mesh.listener('IsAboutToRefine', @obj.getChangedPatches);
+        end
+
+        function nonInvertedSmootherMatrix = setup(obj, ~)
+            obj.nLevels = obj.nLevels + 1;
+
+            % should be implemented by subclasses
+            nonInvertedSmootherMatrix = [];
+        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
+
+    %% abstract methods
+    methods (Abstract, Access=public)
+        smooth(obj, x, k)
+        prolongate(obj, x, k)
+        restrict(obj, x, k)
+    end
+end
\ No newline at end of file
diff --git a/lib/solvers/smoothers/P1JacobiCascade.m b/lib/solvers/smoothers/P1JacobiCascade.m
new file mode 100644
index 0000000000000000000000000000000000000000..93d15999bc92e42a7ebcb05955c717d93cdd4844
--- /dev/null
+++ b/lib/solvers/smoothers/P1JacobiCascade.m
@@ -0,0 +1,69 @@
+% P1JacobiCascade (subclass of MultilevelSmoother) multilevel Jacobi smoother
+%   for lowest order finite elements: smooth with diagonal of stiffness matrix
+%   on changed patches on every level.
+%
+% See also: MultilevelSmoother, OptimalVcycleMultigridSolver
+
+
+classdef P1JacobiCascade < MultilevelSmoother
+    properties (Access=protected)
+        P
+        inverseDiagonal
+        intergridMatrix
+        freeVertices
+        freeVerticesOld
+    end
+    
+    %% event data
+    properties (Access=private)
+        listenerHandle
+    end
+
+    %% methods
+    methods (Access=public)
+        function obj = P1JacobiCascade(fes, blf, P)
+            arguments
+                fes
+                blf
+                P Prolongation
+            end
+
+            assert(fes.finiteElement.order == 1, ...
+                'P1JacobiSmoother only works for lowest order finite elements.')
+            
+            obj = obj@MultilevelSmoother(fes, blf);
+            obj.P = P;
+        end
+
+        function nonInvertedSmootherMatrix = setup(obj, A)
+            setup@MultilevelSmoother(obj, A);
+            
+            L = obj.nLevels;
+            obj.freeVerticesOld = obj.freeVertices;
+            obj.freeVertices = getFreeDofs(obj.fes);
+            
+            nonInvertedSmootherMatrix = A;
+            obj.inverseDiagonal{L} = full(diag(A)).^(-1);
+            
+            if L >= 2
+                obj.intergridMatrix{L} = obj.P.matrix(obj.freeVertices, obj.freeVerticesOld);
+                obj.changedPatches{L} = find(obj.changedPatches{L}(obj.freeVertices));
+                obj.inverseDiagonal{L} = obj.inverseDiagonal{L}(obj.changedPatches{L});
+            end
+        end
+
+        function Cx = smooth(obj, x, k)
+            idx = obj.changedPatches{k};
+            Cx = zeros(size(x));
+            Cx(idx,:) = obj.inverseDiagonal{k} .* x(idx,:);
+        end
+
+        function Px = prolongate(obj, x, k)
+            Px = obj.intergridMatrix{k} * x;
+        end
+
+        function Px = restrict(obj, x, k)
+            Px = obj.intergridMatrix{k}' * x;
+        end
+    end
+end
\ No newline at end of file
diff --git a/runAdditiveSchwarzPreconditioner.m b/runAdditiveSchwarzPreconditioner.m
index 3e6d606bd1631a6eaad60923c6a2d12516e26ae9..971ffd8d1f8979c73c28c1216fafa76b00187343 100644
--- a/runAdditiveSchwarzPreconditioner.m
+++ b/runAdditiveSchwarzPreconditioner.m
@@ -32,10 +32,10 @@ function leveldata = runAdditiveSchwarzPreconditioner(pMax)
         lf.f = Constant(mesh, 1);
     
         % Choose iterative solver
-        solver = chooseIterativeSolver(fes, blf, "pcg", "additiveSchwarzLowOrder");
-        % solver = chooseIterativeSolver(fes, blf, "pcg", "additiveSchwarzHighOrder");
-        % solver = chooseIterativeSolver(fes, blf, "pcg", "iChol");
-        % solver = chooseIterativeSolver(fes, blf, "multigrid", "lowOrderVcycle");
+        solver = IterativeSolver.choose(fes, blf, "pcg", "additiveSchwarzLowOrder");
+        % solver = IterativeSolver.choose(fes, blf, "pcg", "additiveSchwarzHighOrder");
+        % solver = IterativeSolver.choose(fes, blf, "pcg", "iChol");
+        % solver = IterativeSolver.choose(fes, blf, "multigrid", "lowOrderVcycle");
     
         for k = 1:nLevels-1
             % Assemble matrices on intermediate meshes
diff --git a/runIterativeSolver.m b/runIterativeSolver.m
index 350c6b1f2801128319ed64ee03192f39facac566..ce2bcf8b6d8cd180066937d7c5843e07e05907f6 100644
--- a/runIterativeSolver.m
+++ b/runIterativeSolver.m
@@ -28,10 +28,10 @@ function leveldata = runIterativeSolver(maxNiter)
     lf.f = Constant(mesh, 1);
 
     % Choose iterative solver
-    solver = chooseIterativeSolver(fes, blf, "pcg", "additiveSchwarzLowOrder");
-    % solver = chooseIterativeSolver(fes, blf, "pcg", "additiveSchwarzHighOrder");
-    % solver = chooseIterativeSolver(fes, blf, "pcg", "iChol");
-    % solver = chooseIterativeSolver(fes, blf, "multigrid", "lowOrderVcycle");
+    solver = IterativeSolver.choose(fes, blf, "pcg", "additiveSchwarzLowOrder");
+    % solver = IterativeSolver.choose(fes, blf, "pcg", "additiveSchwarzHighOrder");
+    % solver = IterativeSolver.choose(fes, blf, "pcg", "iChol");
+    % solver = IterativeSolver.choose(fes, blf, "multigrid", "lowOrderVcycle");
 
     % Initialize LevelData
     leveldata = LevelData('results');
diff --git a/tests/TestIterativeSolver.m b/tests/TestIterativeSolver.m
index 043d4e7fb5ea8ed03f1e85ecd84491f71a9575fc..6a32e6af2f72e24d59a80be2ceb5d94690187682 100644
--- a/tests/TestIterativeSolver.m
+++ b/tests/TestIterativeSolver.m
@@ -6,16 +6,33 @@ properties (TestParameter)
         'direct', ["direct", ""], ...
         'jacobiPCG', ["pcg", "jacobi"], ...
         'iChol', ["pcg", "iChol"], ...
-        'additiveSchwarzPCG', ["pcg", "additiveSchwarzLowOrder"], ...
+        'loAdditiveSchwarzPCG', ["pcg", "additiveSchwarzLowOrder"], ...
+        'hoAdditiveSchwarzPCG', ["pcg", "additiveSchwarzHighOrder"], ...
         'lowMG', ["multigrid", "lowOrderVcycle"], ...
         'highMG', ["multigrid", "highOrderVcycle"]);
 end
 
 methods (Test)
-    function oneStepDecreasesNorm(testCase, p, variant)
+    function firstStepDecreasesNorm(testCase, p, variant)
+        [~, fes, blf, lf] = setupProblem(testCase, p);
+        s = variant(1); v = variant(2);
+        solver = IterativeSolver.choose(fes, blf, s, v);
+        
+        [xstar, A, F] = assembleData(testCase, blf, lf, fes);
+        solver.setupSystemMatrix(A);
+        solver.setupRhs(F);
+
+        normBefore = sqrt((xstar - solver.x)' * A * (xstar - solver.x));
+        solver.step();
+        normAfterwards = sqrt((xstar - solver.x)' * A * (xstar - solver.x));
+        
+        testCase.verifyGreaterThan(normBefore, normAfterwards);
+    end
+
+    function laterStepDecreasesNorm(testCase, p, variant)
         [mesh, fes, blf, lf] = setupProblem(testCase, p);
         s = variant(1); v = variant(2);
-        solver = chooseIterativeSolver(fes, blf, s, v);
+        solver = IterativeSolver.choose(fes, blf, s, v);
         
         for k = 1:3
             [xstar, A, F] = assembleData(testCase, blf, lf, fes);
@@ -30,11 +47,11 @@ methods (Test)
         
         testCase.verifyGreaterThan(normBefore, normAfterwards);
     end
-    
+
     function solverIsLinear(testCase, p, variant)
         [mesh, fes, blf, lf] = setupProblem(testCase, p);
         s = variant(1); v = variant(2);
-        solver = chooseIterativeSolver(fes, blf, s, v);
+        solver = IterativeSolver.choose(fes, blf, s, v);
         
         for k = 1:3
             [xstar, A, F] = assembleData(testCase, blf, lf, fes);
@@ -44,15 +61,9 @@ methods (Test)
         
         solver.setupRhs([F, -pi*F], 0*[F, F]);
         solver.solve();
-%         for i = 1:60
-%             solver.step();
-%         end
-        %xmat = cgs(A, F, 1e-16, 100);
-        
-         testCase.verifyEqual(-pi*solver.x(:,1), solver.x(:,2), 'RelTol', 2e-5);
-         testCase.verifyEqual(solver.x(:,1), xstar, 'RelTol', 2e-5);
-         %testCase.verifyEqual(solver.x(:, 1), xmat(:, 1), 'RelTol', 2e-5)
-        %testCase.verifyEqual(xmat(:,1), xstar, 'RelTol', 2e-5);
+
+        testCase.verifyEqual(-pi*solver.x(:,1), solver.x(:,2), 'RelTol', 2e-5);
+        testCase.verifyEqual(solver.x(:,1), xstar, 'RelTol', 2e-5);
     end
 end