diff --git a/.gitignore b/.gitignore index 6e36dd534a1f46e0ea94f2e85cd478a7505a2f21..8ab9f475ea2b6aa42bfa029442d30929a35dccd2 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,14 @@ slprj/ # Simulink autosave extension .autosave +**/.DS_Store + +# Ctags file +tags + +# Vim files +*.swp +*.swo + +# Output files +*.csv diff --git a/README.md b/README.md index ae98e01ca692969c03f285778519350260a8d83a..0e8f478f2b23fff53d5c32be3b8ca0b6fd485627 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ Advantages: To get started, run `setup.m` in the root folder. This adds everything to the path and compiles .mex files if necessary. -Note that, at least, Matlab version R2020b is required. +Note that, at least, Matlab version R2022b is required. ## First steps diff --git a/examples/algebraicSolver.m b/examples/algebraicSolver.m new file mode 100644 index 0000000000000000000000000000000000000000..3ef04b1eb3a6f50650b74ff9ad1cf3380613f7b9 --- /dev/null +++ b/examples/algebraicSolver.m @@ -0,0 +1,143 @@ +% ****************************************************************************** +% Prototypical AFEM example with iterative solver for higher order finite +% elements. +% ****************************************************************************** + +%% parameters +nDofsMax = 1e4; +theta = 0.5; +pmax = 4; +lamalg = 0.1; + +%% initialization for every polynomial degree +[nElem, nDofs, errEst, iterations, time, directSolveTime] = deal(zeros(pmax, 1000)); +for p = 1:pmax + %% setup geometry & spaces + printLogMessage('*** p = %d (of %d) ***', p, pmax) + mesh = Mesh.loadFromGeometry('Lshape'); + fes = FeSpace(mesh, HigherOrderH1Fe(p), 'dirichlet', ':'); + u = FeFunction(fes); + u.setData(0); + + %% set problem data for -\Delta u = 1 on L-shape + blf = BilinearForm(); + lf = LinearForm(); + + blf.a = Constant(mesh, 1); + blf.qra = QuadratureRule.ofOrder(max(2*p-2, 1)); + + lf.f = Constant(mesh, 1); + lf.qrf = QuadratureRule.ofOrder(2*p); + + %% set up solver and operator for nested iteration + % 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] = IterativeSolver.choose(fes, blf, 'multigrid', 'lowOrderVcycle'); + solver.tol = 1e-6; + solver.maxIter = 100; + + %% adaptive loop + ell = 0; + meshSufficientlyFine = false; + while ~meshSufficientlyFine + %% setup + tic; + ell = ell + 1; + A = assemble(blf, fes); + rhs = assemble(lf, fes); + + freeDofs = getFreeDofs(fes); + A = A(freeDofs,freeDofs); + solver.setupSystemMatrix(A); + solver.setupRhs(rhs(freeDofs), u.data(freeDofs)'); + + % exact solution as reference + tic; + x_ex = A \ rhs(freeDofs); + directSolveTime(p, ell) = toc; + + %% iterative solve + % solver.solve() implements the algebraic solver loop and + % terminates automatically once the wanted accuracy is reached + % Another option is to manually implement a loop and implement one + % solver step with solver.step() and stop by setting a stopping + % criterion + solverIsConverged = false; + while ~solverIsConverged + solver.step(); + u.setFreeData(solver.x) + eta2 = estimate(blf, lf, u); + estimator = sqrt(sum(eta2)); + solver.tol = lamalg*estimator; + solverIsConverged = solver.isConverged; + end + + + %% estimate error and store data + nDofs(p,ell) = getDofs(fes).nDofs; + errEst(p,ell) = estimator; + iterations(p, ell) = solver.iterationCount(); + nElem(p,ell) = mesh.nElements; + printLogMessage('number of dofs: %d, estimator: %.2e after %d iterations', ... + nDofs(p,ell), errEst(p,ell), solver.iterationCount); + + %% stoping criterion + meshSufficientlyFine = (nDofs(p,ell) > nDofsMax); + + %% refine mesh + if ~meshSufficientlyFine + marked = markDoerflerSorting(eta2, theta); + mesh.refineLocally(marked, 'NVB'); + u.setData(prolongate(P, u)); + end + time(p,ell) = toc; + end +end + +figure() +for p = 1:pmax + idx = find(nDofs(p,:) > 0); + complexity = [cumsum(nDofs(p, idx) .* iterations(p, idx))]; + loglog(complexity, errEst(p,idx), '-o', 'LineWidth', 2, 'DisplayName', ['p=',num2str(p)]); + hold on +end +for p = unique([1,pmax]) + x = complexity / complexity(1); + loglog(complexity(1)*x, errEst(p,1)*x.^(-p/2), '--', 'LineWidth', 2, 'Color', 'k', 'DisplayName', ['\alpha = ', num2str(p), '/2']) +end +legend +xlabel('computational cost') +ylabel('\eta') +title('error estimator over number of total computational cost') + + +%% local function for residual a posteriori error estimation +% \eta(T)^2 = h_T^2 * || \Delta u ||_{L^2(T)}^2 +% + h_T * || [[(Du - fvec) * n]] ||_{L^2(E) \cap \Omega}^2 +function indicators = estimate(blf, lf, u) +p = u.fes.finiteElement.order; +mesh = u.fes.mesh; +trafo = getAffineTransformation(mesh); + +% compute volume residual element-wise +% For p=1, the diffusion term vanishes in the residual. +if p == 1 + f = CompositeFunction(@(f) f.^2, lf.f); +else + f = CompositeFunction(@(v,f) (v(1,:,:) + v(4,:,:) + f).^2, Hessian(u), lf.f); +end +qr = QuadratureRule.ofOrder(2*p); +volumeResidual = integrateElement(f, qr); + +% compute edge residual edge-wise +qr = QuadratureRule.ofOrder(max(p-1, 1), '1D'); +edgeResidual = integrateNormalJump(Gradient(u), qr, ... + @(j) zeros(size(j)), {}, vertcat(mesh.boundaries{:}), ... % no jump on dirichlet edges + @(j) j.^2, {}, ':'); % normal jump on inner edges + +% combine the residuals suitably +hT = sqrt(trafo.area); +indicators = hT.^2 .* volumeResidual + ... + hT .* sum(edgeResidual(mesh.element2edges), Dim.Vector); +end diff --git a/examples/goafemIterativeSolver.m b/examples/goafemIterativeSolver.m new file mode 100644 index 0000000000000000000000000000000000000000..50a7df3835d03592ed0f894e33081a62b8935177 --- /dev/null +++ b/examples/goafemIterativeSolver.m @@ -0,0 +1,150 @@ +% ****************************************************************************** +% Example of a GOAFEM algorithm with iterative solver. +% ****************************************************************************** + +%% paramters +nDofsMax = 1e4; +theta = 0.5; +pmax = 4; +lamalg = 0.1; + +%% setup geometry & spaces +[nElem, nDofs, nIterPrimal, nIterDual, goalErrEst] = deal(zeros(1, 1000)); + +for p = 1:pmax + printLogMessage('*** GOAFEM with p = %d and iterative solver ***', p) + mesh = Mesh.loadFromGeometry('unitsquare'); + mesh.refineUniform(1, 'RGB'); + fes = FeSpace(mesh, HigherOrderH1Fe(p), 'dirichlet', ':'); + u = FeFunction(fes); + z = FeFunction(fes); + u.setData(0); + z.setData(0); + + %% set problem data + % primal: -\Delta u = "lorentzian peak 1" + % dual: -\Delta z = "lorentzian peak 2" + blf = BilinearForm(); + blf.a = Constant(mesh, 1); + blf.qra = QuadratureRule.ofOrder(max(2*p-2, 1)); + + lfF = LinearForm(); + lfF.f = MeshFunction(mesh, @(x) lorentzian(x, [0.7;0.7], 1e-1)); + lfF.qrf = QuadratureRule.ofOrder(2*p); + + lfG = LinearForm(); + lfG.f = MeshFunction(mesh, @(x) lorentzian(x, [0.2;0.3], 1e-2)); + lfG.qrf = QuadratureRule.ofOrder(2*p); + + %% set up solver and lifting operator for nested iteration + [solver, P] = IterativeSolver.choose(fes, blf, 'multigrid', 'lowOrderVcycle'); + solver.tol = 1e-6; + solver.maxIter = 1000; + + %% adaptive loop + ell = 0; + meshSufficientlyFine = false; + while ~meshSufficientlyFine + ell = ell + 1; + + %% assemble & solve FEM system iteratively + freeDofs = getFreeDofs(fes); + A = assemble(blf, fes); + A = A(freeDofs, freeDofs); + rhs = [assemble(lfF, fes), assemble(lfG, fes)]; + rhs = rhs(freeDofs,:); + uz0 = [u.data', z.data']; + solver.setupSystemMatrix(A); + solver.setupRhs(rhs, uz0(freeDofs,:)); + + solverIsConverged = false; + while ~solverIsConverged + solver.step(); + u.setFreeData(solver.x(:,1)); + z.setFreeData(solver.x(:,2)); + eta2 = estimate(blf, lfF, u); + zeta2 = estimate(blf, lfG, z); + primalEstimator = sqrt(sum(eta2)); + dualEstimator = sqrt(sum(zeta2)); + solver.tol = lamalg*[primalEstimator, dualEstimator]; + solverIsConverged = solver.isConverged; + end + u.setFreeData(solver.x(:,1)); + z.setFreeData(solver.x(:,2)); + + %% estimate error and store data + eta2 = estimate(blf, lfF, u); + zeta2 = estimate(blf, lfG, z); + nDofs(ell) = getDofs(fes).nDofs; + nElem(ell) = mesh.nElements; + nIterPrimal(ell) = solver.iterationCount(1); + nIterDual(ell) = solver.iterationCount(2); + goalErrEst(ell) = primalEstimator*dualEstimator; + printLogMessage('number of dofs: %d, iterations (primal/dual): %d / %d, estimator: %.2e', ... + nDofs(ell), nIterPrimal(ell), nIterDual(ell), goalErrEst(ell)); + + %% stoping criterion + meshSufficientlyFine = (nDofs(ell) > nDofsMax); + + %% refine mesh and transfer solutions to finer mesh for nested iteration + if ~meshSufficientlyFine + marked = markGoafemMS(eta2, zeta2, theta); + mesh.refineLocally(marked, 'NVB'); + u.setData(prolongate(P, u)); + z.setData(prolongate(P, z)); + end + end +end + +%% plot convergence rates +figure() +idx = 1:ell; +x = cumsum(nDofs(idx).*max(nIterPrimal(idx), nIterDual(idx))); +yyaxis left +loglog(x, goalErrEst(idx), '-x', 'LineWidth', 2, 'MarkerSize', 8); +hold on +loglog(x, goalErrEst(1)*(x/x(1)).^(-p), '--', 'LineWidth', 2, 'Color', 'k') +ylabel('goal error estimator of final iterates') +yyaxis right +stem(x, nIterPrimal(idx), 's', 'filled') +stem(x, nIterDual(idx), 'o', 'filled') +ylabel('number of iterations') +legend('\eta_l \zeta_l', ['\alpha=', num2str(p)], 'primal iterations', 'dual iteration') +xlabel('total work') +title(['GOAFEM p=', num2str(p)]) + +%% local function for residual a posteriori error estimation +% \eta(T)^2 = h_T^2 * || \Delta u ||_{L^2(T)}^2 +% + h_T * || [[(Du - fvec) * n]] ||_{L^2(E) \cap \Omega}^2 +function indicators = estimate(blf, lf, u) + p = u.fes.finiteElement.order; + mesh = u.fes.mesh; + trafo = getAffineTransformation(mesh); + + % compute volume residual element-wise + % For p=1, the diffusion term vanishes in the residual. + if p == 1 + f = lf.f; + else + f = CompositeFunction(@(v,f) (v(1,:,:) + v(4,:,:) + f).^2, Hessian(u), lf.f); + end + qr = QuadratureRule.ofOrder(2*p); + volumeResidual = integrateElement(f, qr); + + % compute edge residual edge-wise + qr = QuadratureRule.ofOrder(max(p-1, 1), '1D'); + edgeResidual = integrateNormalJump(Gradient(u), qr, ... + @(j) zeros(size(j)), {}, vertcat(mesh.boundaries{:}), ... % no jump on dirichlet edges + @(j) j.^2, {}, ':'); % normal jump on inner edges + + % combine the resdiuals suitably + hT = sqrt(trafo.area); + indicators = hT.^2 .* volumeResidual + ... + hT .* sum(edgeResidual(mesh.element2edges), Dim.Vector); +end + +% Lorentz-peak 1/(eps+||x-x0||^2) +function y = lorentzian(x, x0, eps) + dx = x - x0; + y = 1 ./ (eps + vectorProduct(dx,dx)); +end diff --git a/examples/goalOrientedAFEM.m b/examples/goalOrientedAFEM.m index 6207bb6c78a6f13afc91c3fddf1c577c6e73f45f..69297791ea0e34e073a1890ebafb6db2d40e3140 100644 --- a/examples/goalOrientedAFEM.m +++ b/examples/goalOrientedAFEM.m @@ -39,7 +39,7 @@ for p = 1:pmax lfG.qrfvec = QuadratureRule.ofOrder(max(p-1, 1)); %% set up lifting operators for rhs FEM-data - P = LoFeProlongation(ncFes); + P = LoMeshProlongation(ncFes); %% adaptive loop ell = 1; diff --git a/examples/iterativeLinearization.m b/examples/iterativeLinearization.m index d37f9228b165a009ee53c9584bc827148038bbad..e4bf7d3b7c2dfe85e1f7485ab22b24c5f5aa97c6 100644 --- a/examples/iterativeLinearization.m +++ b/examples/iterativeLinearization.m @@ -27,7 +27,7 @@ for k = 1:length(linearizations) u.setData(0); %% nested iteration - P = LoFeProlongation(fes); + P = LoMeshProlongation(fes); %% adaptive algorithm ell = 0; diff --git a/examples/storingLevelOrientedData.m b/examples/storingLevelOrientedData.m new file mode 100644 index 0000000000000000000000000000000000000000..74888820894fc9ebda98813c71343a9d20efdd8d --- /dev/null +++ b/examples/storingLevelOrientedData.m @@ -0,0 +1,154 @@ +% Example of an adaptive FEM algorithm with storing and displaying +% level-oriented data + +function leveldata = storingLevelOrientedData(doPlots, doStore) + arguments + doPlots (1,1) logical = true + doStore (1,1) logical = false + end + + %% paramters + nEmax = 1e4; + theta = 0.5; + + %% initialization + mesh = Mesh.loadFromGeometry('Lshape'); + fes = FeSpace(mesh, LowestOrderH1Fe()); + u = FeFunction(fes); + + %% initialize output data structure + pathToStorage = 'results'; + leveldata = LevelData(pathToStorage); + leveldata.metaData('problem') = 'Poisson'; + leveldata.metaData('domain') = 'Lshape'; + leveldata.metaData('method') = 'S1'; + leveldata.metaData('identifier') = 'example'; + + %% problem data + blf = BilinearForm(); + lf = LinearForm(); + blf.a = Constant(mesh, 1); + lf.f = Constant(mesh, 1); + + % reference value for energy norm of exact solution + normExactSquared = 0.2140750232; + + %% adaptive loop + meshSufficientlyFine = false; + while ~meshSufficientlyFine + %% compute data for rhs coefficient and assemble forms + A = assemble(blf, fes); + F = assemble(lf, fes); + + %% solve FEM system + freeDofs = getFreeDofs(fes); + tic; + u.setFreeData(A(freeDofs,freeDofs) \ F(freeDofs)); + runtimeSolve = toc; + + %% approximate error + err = sqrt(normExactSquared - u.data * A * u.data'); + + %% compute refinement indicators + tic; + eta2 = estimate(blf, lf, u); + eta = sqrt(sum(eta2)); + runtimeEstimate = toc; + + %% compute efficiency index + eff = err / eta; + + %% storing error quantities and general data + leveldata.append('ndof', uint32(length(freeDofs)), ... + 'eta', eta, ... + 'err', err, ... + 'coordinates', mesh.coordinates, ... + 'elements', mesh.elements); + + %% storing results of timing + leveldata.setTime(leveldata.nLevel, ... + 'runtimeSolve', runtimeSolve, ... + 'runtimeEstimate', runtimeEstimate); + + %% storing absolute values + leveldata.setAbsolute(leveldata.nLevel, 'eff', eff); + + %% print information on current level to command line + leveldata.printLevel(); + + %% save intermediate results to file + % (prevents data loss in case of crashes) + if doStore + leveldata.saveToFile(); + end + + %% stoping criterion + meshSufficientlyFine = (mesh.nElements > nEmax); + + %% refine mesh + if ~meshSufficientlyFine + marked = markDoerflerBinning(eta2, theta); + mesh.refineLocally(marked, 'NVB'); + end + end + + %% post-processing variables + leveldata.setTime(':', 'runtimeTotal', ... + sum(leveldata.get(':', 'runtimeSolve', 'runtimeEstimate'), 2)); + + %% plot results + if doPlots + % plot error quantities (in general, converging to zeros) + figure(); + leveldata.plot('ndof'); + + % plot runtimes (in general, diverging to infinity) + figure(); + leveldata.plotTime('ndof'); + + % plot absolute values in semi-logarithmic plot + figure(); + leveldata.plotAbsolute('ndof'); + end + + %% additional visualization/storage capabilities + if doStore + % export error plot to file + leveldata.plotToFile('ndof'); + + % save all values to comma-separated table + leveldata.saveToTable(); + + % plot of all triangulations one after another + figure(); + leveldata.plotTriangulation(); + + % export refinement video (generates video of about 2MB) + leveldata.plotTriangulationToFile(); + end +end + + + +%% local function for residual a posteriori error estimation +% \eta(T)^2 = h_T^2 * || \Delta u + f ||_{L^2(T)}^2 + h_T * || [Du] ||_{L^2(E)}^2 +function indicators = estimate(~, ~, u) + fes = u.fes; + mesh = fes.mesh; + + %% compute volume residual element-wise + f = CompositeFunction(@(Du) vectorProduct(Du, Du), Gradient(u)); + qrTri = QuadratureRule.ofOrder(1); + volumeRes = integrateElement(f, qrTri); + + %% compute edge residual edge-wise + qrEdge = QuadratureRule.ofOrder(1, '1D'); + edgeRes = integrateNormalJump(Gradient(u), qrEdge, @(j) j.^2, {}, ':'); + dirichlet = getCombinedBndEdges(mesh, fes.bnd.dirichlet); + edgeRes(dirichlet) = 0; + + %% combine the resdiuals suitably + hT = sqrt(getAffineTransformation(mesh).area); + indicators = hT.^2 .* volumeRes + ... + hT .* sum(edgeRes(mesh.element2edges), Dim.Vector); +end diff --git a/examples/timingWithLevelDataCollection.m b/examples/timingWithLevelDataCollection.m new file mode 100644 index 0000000000000000000000000000000000000000..720e65e52a421384ed721ef1889de8db12a39fe0 --- /dev/null +++ b/examples/timingWithLevelDataCollection.m @@ -0,0 +1,22 @@ +% ****************************************************************************** +% Example of timing with LevelDataCollection +% ****************************************************************************** + + +%% parameters +nRuns = 10; + +%% run time measurements +storeResults = false; +leveldatacollection = ... + TimeIt('debugTiming', nRuns, storeResults, 'storingLevelOrientedData', false, false); + +%% print statistical analysis of timing results +leveldatacollection.printStatistics(); + +%% plot results +figure(); +leveldatacollection.plotStatistics('ndof'); + +%% save results to comma-separated file +% leveldatacollection.saveToTable(); diff --git a/experiments/p_laplace.m b/experiments/p_laplace.m new file mode 100644 index 0000000000000000000000000000000000000000..9bee4bdbb032684aa188423747fe374f545fd6f1 --- /dev/null +++ b/experiments/p_laplace.m @@ -0,0 +1,172 @@ +%% paramters +nElemMax = 1e5; +theta = 0.5; +lambda = 0.1; +deltaZ = 0.1; +linearization = 'kacanov'; +geometry = 'Lshape'; +p = 3; + +%% initialisation +[nElem, eta, E] = deal(zeros(1,1000)); + +%% setup geometry & spaces +mesh = Mesh.loadFromGeometry(geometry); +mesh.refineUniform(2); +fes = FeSpace(mesh, LowestOrderH1Fe(), 'dirichlet', ':'); +u = FeFunction(fes); + +%% set problem data given in [Heid, Praetorius, Wihler; 2021] +Du = Gradient(u); +g = Constant(mesh, 1); +[blf, lf] = setProblemData(mesh, fes, Du, g, linearization, p); +eDensity = CompositeFunction(@(t) muIntegral(vectorProduct(t, t),p), Du); +u.setFreeData(rand(1,length(getFreeDofs(u.fes)))); + +%% nested iteration +P = LoMeshProlongation(fes); + +%% adaptive algorithm +i = 1; +Eold = 0; +qr = QuadratureRule.ofOrder(1); % tweak? +while 1 + % the volume residual for lowest order is independent of the approximate + % solution, so it can be computed once + vol = estimateVolume(g); + + %% inner iteration to solve the nonlinear problem + nIterations = 1; + while true + %% solve linearized problem + A = assemble(blf, fes); + F = assemble(lf, fes); + updateDataU(u, deltaZ, A, F, linearization); + + %% compute remainder of error estimator and energy + edge = estimateEdge(u, mesh, p); + eta2 = combineEstimators(vol, edge, mesh); + eta(i) = sqrt(sum(eta2)); + nElem(i) = mesh.nElements; + + if i > 1, Eold = E(i-1); end + E(i) = sum(integrateElement(eDensity, qr)) + u.data * F; + + %% stopping criterion of the iterative solver + if sqrt(E(i) - Eold) <= lambda * eta(i) % norm(e) <= fehler + break + end + nIterations = nIterations + 1; + i = i+1; + end + + %% stoping criterion for the AFEM algorithm + printLogMessage('number of elements: %d, iterations: %d, estimator: %.2e', ... + nElem(i), nIterations, eta(i)); + if nElem(i) > nElemMax + break + end + + %% refine mesh + marked = markDoerflerSorting(eta2, theta); + mesh.refineLocally(marked, 'NVB'); + u.setData(prolongate(P, u)); + i = i+1; +end + +%% plot convergence rates +plotData(nElem, 'number of elements', eta, 'error estimator', linearization,p) + +%% helper function for plotting +function plotData(xdata, xlab, ydata, ylab, name,p) + figure() + for k = 1:size(xdata,1) + idx = find(xdata(k,:) > 0); + loglog(xdata(k,idx), ydata(k,idx), '-o', 'LineWidth', 2, 'DisplayName', strcat(name,', p=', num2str(p))); + hold on + end + x = xdata(1,idx) / xdata(1,1); + loglog(xdata(1,1)*x, ydata(1,1)*x.^(-1/2), '--', 'LineWidth', 2, 'Color', 'k', 'DisplayName', '\alpha = 1/2') + legend + xlabel(xlab) + ylabel(ylab) + title([ylab, ' over ', xlab]) +end + +%% local function for problem data +function [blf, lf] = setProblemData(mesh, fes, Du, g, linearization, p) + blf = BilinearForm(); + lf = LinearForm(); + + switch linearization + case "zarantonello" + blf.a = Constant(mesh, 1); + lf.f = g; + lf.fvec = CompositeFunction(@(t) -mu(vectorProduct(t, t),p) .* t, Du); + case "kacanov" + blf.a = CompositeFunction(@(t) mu(vectorProduct(t, t),p), Du); + lf.f = g; + lf.fvec = []; + case "newton" + blf.a = CompositeFunction(@(t) mu(vectorProduct(t, t),p) .* [1;0;0;1] ... + + 2*muDerivative(vectorProduct(t, t),p).*vectorProduct(t, t, [2,1], [2,1]'), Du); + lf.f = g; + lf.fvec = CompositeFunction(@(t) -mu(vectorProduct(t, t),p) .* t, Du); + end + + [blf.qra, lf.qrf, lf.qrfvec] = deal(QuadratureRule.ofOrder(1)); +end + +function updateDataU(u, deltaZ, A, F, linearization) + freeDofs = getFreeDofs(u.fes); + v = FeFunction(u.fes); + switch linearization + case "zarantonello" + v.setFreeData(A(freeDofs,freeDofs) \ F(freeDofs)); + u.setData(u.data + deltaZ*v.data); + case "kacanov" + %u.setData(A \ F); + u.setFreeData(A(freeDofs,freeDofs) \ F(freeDofs)); + case "newton" + v.setFreeData(A(freeDofs,freeDofs) \ F(freeDofs)); + u.setData(u.data + v.data); + end +end + +%% local function for residual a posteriori error estimation +% \eta(T)^2 = h_T^2 * || g ||_{L^2(T)}^2 +% + h_T * || [[ mu(|Du|^2)*Du * n]] ||_{L^2(E) \cap \Omega}^2 +function vol = estimateVolume(g) + qr = QuadratureRule.ofOrder(1); + vol = integrateElement(g, qr); +end + +function edge = estimateEdge(u, mesh, p) + qr = QuadratureRule.ofOrder(1, '1D'); + dirichlet = vertcat(mesh.boundaries{:}); + + f = CompositeFunction(@(t) mu(vectorProduct(t, t),p).*t, Gradient(u)); + edge = integrateNormalJump(f, qr, ... + @(j) zeros(size(j)), {}, dirichlet, ... % no jump on dirichlet edges + @(j) j.^2, {}, ':'); % normal jump on inner edges + +end + +function indicators = combineEstimators(vol, edge, mesh) + trafo = getAffineTransformation(mesh); + hT = sqrt(trafo.area); + indicators = hT.^2.*vol + hT.*sum(edge(mesh.element2edges), Dim.Vector); +end + +%% nonlinearities +function y = mu(t,p) + y = t.^(p/2 - 1); +end + +function y = muIntegral(t,p) + y = p/4 * t.^(p/2); +end + +function y = muDerivative(t,p) + y = (p/2 - 1) * t.^(p/2 - 2); +end diff --git a/lib/assembly/@BilinearForm/BilinearForm.m b/lib/assembly/@BilinearForm/BilinearForm.m index fc76b96c640553918080f09d79386e6a1e0bc42e..1221dd7053b48b1cea9af6cce316a872aa0add83 100644 --- a/lib/assembly/@BilinearForm/BilinearForm.m +++ b/lib/assembly/@BilinearForm/BilinearForm.m @@ -18,6 +18,11 @@ classdef BilinearForm < handle %% methods methods (Access=public) mat = assemble(obj, fes); + mat = assemblePatchwise(obj, fes); + end + + methods (Access=protected) + data = computeVolumeData(obj, fes); end methods (Static, Access=protected) diff --git a/lib/assembly/@BilinearForm/assemble.m b/lib/assembly/@BilinearForm/assemble.m index 400fc151e01c04a9186a0a8d9a08f8f3dacbaf48..7a33f904e97f751d8983841258ef6356cad3cf64 100644 --- a/lib/assembly/@BilinearForm/assemble.m +++ b/lib/assembly/@BilinearForm/assemble.m @@ -10,35 +10,8 @@ arguments obj fes FeSpace end -%% setup -if all([isempty(obj.a), isempty(obj.b), isempty(obj.c), isempty(obj.robin)]) - error('BilinearForm:NoCoefficients', 'No coefficients are given.') -end - -%% integrate element data -mat = 0; -phi = TestFunction(fes); -Dphi = TestFunctionGradient(fes); - -% diffusion -if ~isempty(obj.a) - f = CompositeFunction(@BilinearForm.diffusionPart, obj.a, Dphi); - mat = mat + integrateElement(f, obj.qra); -end - -% convection -if ~isempty(obj.b) - f = CompositeFunction(@BilinearForm.convectionPart, obj.b, Dphi, phi); - mat = mat + integrateElement(f, obj.qrb); -end - -% reaction -if ~isempty(obj.c) - f = CompositeFunction(@BilinearForm.reactionPart, obj.c, phi); - mat = mat + integrateElement(f, obj.qrc); -end - %% construct sparse matrix +mat = computeVolumeData(obj, fes); dofs = getDofs(fes); if ~isequal(mat, 0) @@ -52,6 +25,7 @@ end %% integrate and add boundary data % Robin data if ~isempty(obj.robin) + phi = TestFunction(fes); f = CompositeFunction(@BilinearForm.robinPart, obj.robin, phi); idx = getCombinedBndEdges(fes.mesh, fes.bnd.robin); [I, J] = getLocalDofs(size(dofs.edge2Dofs, Dim.Vector)); diff --git a/lib/assembly/@BilinearForm/assemblePatchwise.m b/lib/assembly/@BilinearForm/assemblePatchwise.m new file mode 100644 index 0000000000000000000000000000000000000000..036f370b2328dc311010a86b990403266725dea2 --- /dev/null +++ b/lib/assembly/@BilinearForm/assemblePatchwise.m @@ -0,0 +1,23 @@ +% assemblePatchwise Assembles matrix of bilinear form patchwise. +% +% A = assemblePatchwise(obj, fes) assembles the data of the given +% BilinearForm for a FeSpace fes and returns a PatchwiseMatrix A. +% +% See also: BilinearForm + +function mat = assemblePatchwise(obj, fes, patches) +arguments + obj + fes FeSpace + patches {mustBeIndexVector} = ':' +end + +if ~isempty(obj.robin) + error('Patchwise assembly not implemented for Robin boundary!') +end + +% in the long run, this should work just with non-assembled data: +data = computeVolumeData(obj, fes); +mat = PatchwiseMatrix(fes, data, patches); + +end \ No newline at end of file diff --git a/lib/assembly/@BilinearForm/computeVolumeData.m b/lib/assembly/@BilinearForm/computeVolumeData.m new file mode 100644 index 0000000000000000000000000000000000000000..d13e55444301092bdfbaea550391ba11608ac4c5 --- /dev/null +++ b/lib/assembly/@BilinearForm/computeVolumeData.m @@ -0,0 +1,41 @@ +% computeVolumeData Computes raw data of bilinear form for volume terms. +% +% data = computeData(obj, fes) computes the elementwise data of the given +% BilinearForm for a FeSpace fes for every non-trivial dof-pairing. +% +% See also: BilinearForm + +function data = computeVolumeData(obj, fes) +arguments + obj + fes FeSpace +end + +if all([isempty(obj.a), isempty(obj.b), isempty(obj.c), isempty(obj.robin)]) + error('BilinearForm:NoCoefficients', 'No coefficients are given.') +end + +%% integrate element data +data = 0; +phi = TestFunction(fes); +Dphi = TestFunctionGradient(fes); + +% diffusion +if ~isempty(obj.a) + f = CompositeFunction(@BilinearForm.diffusionPart, obj.a, Dphi); + data = data + integrateElement(f, obj.qra); +end + +% convection +if ~isempty(obj.b) + f = CompositeFunction(@BilinearForm.convectionPart, obj.b, Dphi, phi); + data = data + integrateElement(f, obj.qrb); +end + +% reaction +if ~isempty(obj.c) + f = CompositeFunction(@BilinearForm.reactionPart, obj.c, phi); + data = data + integrateElement(f, obj.qrc); +end + +end diff --git a/lib/assembly/@PatchwiseMatrix/PatchwiseMatrix.m b/lib/assembly/@PatchwiseMatrix/PatchwiseMatrix.m new file mode 100644 index 0000000000000000000000000000000000000000..8f68725a98dc187dcd0b481f9fd404f03491f519 --- /dev/null +++ b/lib/assembly/@PatchwiseMatrix/PatchwiseMatrix.m @@ -0,0 +1,63 @@ +% PatchwiseMatrix (handle class) Patchwise matrix of bilinear form. + +classdef PatchwiseMatrix < handle + properties (GetAccess=public, SetAccess=protected) + fes + activePatches + end + + properties (Access=protected) + patchDofs + patchElements + patchesAsFreeDofs + patchwiseChol + end + + properties (Dependent) + nPatches + end + + methods (Access=public) + function obj = PatchwiseMatrix(fes, data, patches) + arguments + fes FeSpace + data + patches {mustBeIndexVector} + end + + obj.fes = fes; + [obj.patchDofs, obj.patchElements] = assemblePatchDofs(obj); + idx = 1:obj.nPatches; + obj.activePatches = reshape(idx(patches), 1, []); + obj.buildLocalMatrixFactorization(data); + end + + function dofs = getPatchDofs(obj, idx) + arguments + obj + idx {mustBeIndexVector} + end + dofs = vertcat(obj.patchDofs{idx}); + end + + function elements = getPatchElements(obj, idx) + arguments + obj + idx {mustBeIndexVector} + end + elements = vertcat(obj.patchElements{idx}); + end + end + + methods + function nPatches = get.nPatches(obj) + nPatches = numel(obj.patchDofs); + end + end + + methods (Access=protected) + [patchDofs, patchElements] = assemblePatchDofs(obj) + buildLocalMatrixFactorization(obj, data) + end +end + diff --git a/lib/assembly/@PatchwiseMatrix/assemblePatchDofs.m b/lib/assembly/@PatchwiseMatrix/assemblePatchDofs.m new file mode 100644 index 0000000000000000000000000000000000000000..c4cd1a8f9145d8bcafd95c70f8bee17da3b3a4b5 --- /dev/null +++ b/lib/assembly/@PatchwiseMatrix/assemblePatchDofs.m @@ -0,0 +1,83 @@ +% assemblePatchDofs Assembles lists of unique inner dofs for each patch in +% underlying mesh. +% +% assemblePatchDofs(obj) + +function [patchDofs, patchElements] = assemblePatchDofs(obj) +%% preallocate arrays +fes = obj.fes; +mesh = fes.mesh; + +%% compute patch membership of (free) vertices/edges/elements +dirichletEdges = getCombinedBndEdges(mesh, fes.bnd.dirichlet); +vertexNotOnDirichletBnd = ~ismember(1:mesh.nCoordinates, mesh.edges(:,dirichletEdges)); + +free = computeFreeEdges(mesh, dirichletEdges); +[patchEdges, nEdgesPerPatch] = computePatchMembership(mesh.edges(:,free)); +patchEdges = free(patchEdges); + +[patchElements, nElementsPerPatch] = computePatchMembership(mesh.elements); + +%% combine vertex/edge/element dofs for each patch +vertexDofs = asVector(createVertexDofs(fes, vertexNotOnDirichletBnd)); +edgeDofs = asVector(createInnerEdgeDofs(fes, patchEdges)); +elementDofs = asVector(createInnerElementDofs(fes, patchElements)); + +nLocalDofs = getDofConnectivity(fes.finiteElement); +[patchDofs, nDofsPerPatch] = interleaveGroupedArrays(... + vertexDofs, nLocalDofs(1)*double(vertexNotOnDirichletBnd), ... + edgeDofs, nLocalDofs(2)*nEdgesPerPatch', ... + elementDofs, nLocalDofs(3)*nElementsPerPatch'); + +patchDofs = mat2cell(patchDofs', nDofsPerPatch); +patchElements = mat2cell(patchElements, nElementsPerPatch); + +end + +%% auxiliary functions + +% compute free edges without setdiff -> linear complexity +function freeEdges = computeFreeEdges(mesh, dirichlet) + freeEdges = true(mesh.nEdges,1); + freeEdges(dirichlet) = false; + freeEdges = find(freeEdges)'; +end + +% requires one global sort of connectivity arrays -> log-linear complexity +function [patches, count] = computePatchMembership(objects) + n = size(objects, 1); + N = size(objects, 2); + + count = accumarray(objects(:), 1); + ptr = cumsum([1; count]); + patches = zeros(n*N, 1); + + for i = 1:N + for k = 1:n + idx = objects(k,i); + patches(ptr(idx)) = i; + ptr(idx) = ptr(idx) + 1; + end + end +end + +function [totalArray, totalGroupSize] = interleaveGroupedArrays(array, groupSize) +arguments (Repeating) + array + groupSize +end + +totalGroupPointer = cumsum([1; reshape(vertcat(groupSize{:}), [], 1)])'; +totalArray = zeros(1, totalGroupPointer(end)-1); +totalGroupSize = sum(vertcat(groupSize{:}), 1); + +N = numel(groupSize); +for n = 1:N + % add vector [0 1 2 0 1 0 1 2 3 ...] to group starting locations to + % make space for all group members + offset = totalGroupPointer(n:N:(end-1)) - cumsum([0, groupSize{n}(1:(end-1))]); + idx = (1:numel(array{n})) + repelem(offset, groupSize{n}) - 1; + totalArray(idx) = array{n}; +end + +end diff --git a/lib/assembly/@PatchwiseMatrix/buildLocalMatrixFactorization.m b/lib/assembly/@PatchwiseMatrix/buildLocalMatrixFactorization.m new file mode 100644 index 0000000000000000000000000000000000000000..e17b70cb22e4eec8b8eaf7f5edfd97ee29ecc8bd --- /dev/null +++ b/lib/assembly/@PatchwiseMatrix/buildLocalMatrixFactorization.m @@ -0,0 +1,38 @@ +% buildLocalMatrixFactorization Patchwise assembly of matrix and cholesky +% factorization. + +function buildLocalMatrixFactorization(obj, data) + +% since this operates only on free dofs, store global numbering +freeDofs = getFreeDofs(obj.fes); +dofs = getDofs(obj.fes); +global2freeDofs = zeros(dofs.nDofs, 1); +global2freeDofs(freeDofs) = 1:numel(freeDofs); +obj.patchesAsFreeDofs = cellfun(@(x) global2freeDofs(x), obj.patchDofs, 'UniformOutput', false); +nDofsPerElement = size(dofs.element2Dofs, 1); + +% store cholesky decomposition of local matrices associated to patches +obj.patchwiseChol = cell(obj.nPatches, 1); +for k = obj.activePatches + % get free dofs on patch and all dofs on patch + pDofs = obj.patchDofs{k}; + n = numel(pDofs); + pElements = obj.patchElements{k}; + allPatchDofs = dofs.element2Dofs(:,pElements); + + % find local numbering of free dofs and relation between all and free dofs + [isNeeded, localNumbering] = ismember(allPatchDofs, pDofs); + isNeeded = reshape(isNeeded, size(isNeeded,1), 1, size(isNeeded,2)); + idx = find(and(isNeeded, pagetranspose(isNeeded))); + + % assemble arrays for sparse matrix with all dofs on patch ... + I = repmat(localNumbering, nDofsPerElement, 1); + J = repelem(localNumbering, nDofsPerElement, 1); + localData = data(:,pElements); + + % ... and assemble/decompose only the parts that correspond to free dofs + obj.patchwiseChol{k} = ... + chol(accumarray([I(idx), J(idx)], localData(idx), [n, n])); +end + +end \ No newline at end of file diff --git a/lib/assembly/@PatchwiseMatrix/mldivide.m b/lib/assembly/@PatchwiseMatrix/mldivide.m new file mode 100644 index 0000000000000000000000000000000000000000..b8a157ec68a71e99a40422befff3d5bfdc444bc3 --- /dev/null +++ b/lib/assembly/@PatchwiseMatrix/mldivide.m @@ -0,0 +1,21 @@ +% mldivide Solve linear System A * x = y for patchwise matrix A. This means +% solving local problems on patches associated to vertices +% (depending on level) and add the patchwise solutions +% together. mldivide(A, y) can also be called by A \ y. + +function x = mldivide(obj, y) + +x = zeros(size(y)); +lower = struct('UT', true, 'TRANSA', true); +upper = struct('UT', true); + +for k = obj.activePatches + U = obj.patchwiseChol{k}; + idx = obj.patchesAsFreeDofs{k}; + + % solve local patch problems (update is additive) + update = linsolve(U, linsolve(U, y(idx,:), lower), upper); + x(idx,:) = x(idx,:) + update; +end + +end diff --git a/lib/functions/@Evaluable/plot.m b/lib/functions/@Evaluable/plot.m index 846bcdbad41b2d9775af7d91458d34b461b9acf6..995d7a9c25ab0d7ccea95673f4d6deb69fd52664 100644 --- a/lib/functions/@Evaluable/plot.m +++ b/lib/functions/@Evaluable/plot.m @@ -26,7 +26,10 @@ end data = eval(obj, Barycentric2D([1;0;0]), 1); nComponents = size(data, Dim.Vector); ax = cell(nComponents, 1); -for i = 1:nComponents +ax{1} = gca; +cla(ax{1}) +% TODO better handling of several components +for i = 2:nComponents ax{i} = axes(figure()); end diff --git a/lib/mesh/@Mesh/Mesh.m b/lib/mesh/@Mesh/Mesh.m index 48544b7906a04d13b775b2d01df769d33c1c660e..45fe85963d81fdba976e9c5ccac0a76d07898d22 100644 --- a/lib/mesh/@Mesh/Mesh.m +++ b/lib/mesh/@Mesh/Mesh.m @@ -100,6 +100,7 @@ classdef Mesh < handle %% static methods methods (Static) obj = loadFromGeometry(geometryIdentifier) + obj = unitTriangle() [edges, element2edges, flipEdges, boundary2edges] = computeEdgeInformation(elements, boundaries) end end diff --git a/lib/mesh/@Mesh/unitTriangle.m b/lib/mesh/@Mesh/unitTriangle.m new file mode 100644 index 0000000000000000000000000000000000000000..fbc0c23a0651786320034cf63e55d50699a2523e --- /dev/null +++ b/lib/mesh/@Mesh/unitTriangle.m @@ -0,0 +1,19 @@ +% unitTriangle Construct Mesh object on unit triangle. +% +% mesh = Mesh.unitTriangle() constructs a mesh with one element and +% homogeneous dirichlet boundary conditions on the unit triangle. In +% particular, coordinates, elements, and edges are the same as in the +% mesh constructed by mesh = Mesh.loadGeometry('unittriangle'). This +% method, however, is faster. +% +% See also: Mesh + +function obj = unitTriangle() + +coordinates = [0, 1, 0; 0, 0, 1]; +elements = [2;3;1]; +boundaries = {[1, 2, 3; 2, 3, 1]}; + +obj = Mesh(coordinates, elements, boundaries); + +end \ No newline at end of file diff --git a/lib/solvers/@IterativeSolver/IterativeSolver.m b/lib/solvers/@IterativeSolver/IterativeSolver.m new file mode 100644 index 0000000000000000000000000000000000000000..be5b90b45bbca63d72b0cb500295acc8cb234787 --- /dev/null +++ b/lib/solvers/@IterativeSolver/IterativeSolver.m @@ -0,0 +1,133 @@ +% IterativeSolver (abstract handle class) Interface for iterative solvers. +% +% solver.setupSystemMatrix(A) sets up system Matrix for the linear system +% A*x=b. The matrix A must be quadratic. +% +% solver.setupRhs(b, [x0]) sets up right-hand side for the linear system A*x=b +% and initial guess x0 (0 per default). The right-hand side b can have +% multiple columns, but b and x0 must have the same size. +% +% solver.solve() performs an automatic loop of solver.step() until +% convergence in each column of the right-hand side is reached, checking +% the stopping criterion after every step. +% +% solver.step() performs one step of the iterative solver on active +% components of the right-hand side and automatoically keeps track of +% iteration numbers. +% +% tf = solver.applyStoppingCriterion() checks stopping criterion and set +% converged components to inactive. +% +% Abstract methods to implement: +% +% solver.computeUpdate() computes the update of active components of the +% solution. +% +% 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 + %% properties + properties (Access=public) + maxIter (1,:) double {mustBePositive} + tol (1,:) double {mustBePositive} + end + + properties (SetAccess=protected, GetAccess=public) + A + b + x + iterationCount + activeComponents + end + + methods (Abstract, Access=public) + isConverged(obj) + end + + methods (Abstract, Access=protected) + computeUpdate(obj) + end + + %% generic routines for iterative solvers + % overwrite to include additional steps + methods (Access=public) + function obj = IterativeSolver() + obj.maxIter = 100; + obj.tol = 1e-8; + end + + function setupSystemMatrix(obj, A) + arguments + obj + A (:,:) double {IterativeSolver.mustBeQuadratic} + end + obj.A = A; + end + + function setupRhs(obj, b, x0) + arguments + obj + b (:,:) double + x0 (:,:) double {IterativeSolver.mustBeCompatible(b, x0)} = zeros(size(b)); + end + assert(isequal(size(obj.A,2), size(b,1)), ... + 'Right-hand size and matrix must be of compatible size.') + obj.b = b; + obj.x = x0; + obj.iterationCount = zeros(1, size(b,2)); + obj.activeComponents = true(1, size(b,2)); + end + + function x = solve(obj) + while ~all(applyStoppingCriterion(obj)) + obj.step() + end + + if any(obj.iterationCount >= obj.maxIter) + warning('Maximal number of iterations exhausted! maxIter = %d', obj.maxIter) + end + + x = obj.x; + end + + function step(obj) + obj.computeUpdate(); + obj.iterationCount(obj.activeComponents) = obj.iterationCount(obj.activeComponents) + 1; + end + + function tf = applyStoppingCriterion(obj) + tf = isConverged(obj); + 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) + function mustBeQuadratic(A) + if ~(size(A,1) == size(A,2)) + eidType = 'Solver:matrixNotQuadratic'; + msgType = 'Matrix must be quadratic.'; + throwAsCaller(MException(eidType,msgType)) + end + end + + function mustBeCompatible(b, x0) + if ~isempty(x0) && ~isequal(size(b), size(x0)) + eidType = 'Solver:dataNotCompatible'; + msgType = 'Size of RHS and initial guess must be compatible.'; + throwAsCaller(MException(eidType,msgType)) + end + end + end +end \ No newline at end of file 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/CgSolver.m b/lib/solvers/CgSolver.m new file mode 100644 index 0000000000000000000000000000000000000000..acfb831c4c5c86c9828e73605b5149eb0b51af9d --- /dev/null +++ b/lib/solvers/CgSolver.m @@ -0,0 +1,12 @@ +% CgSolver (subclass of PcgSolver) Solves linear equations iteratively using +% the CG method without preconditioner. + +classdef CgSolver < PcgSolver + %% methods + methods (Access=public) + % preconditioner = identity + function Cx = preconditionAction(obj, x) + Cx = x; + end + end +end \ No newline at end of file diff --git a/lib/solvers/DirectSolver.m b/lib/solvers/DirectSolver.m new file mode 100644 index 0000000000000000000000000000000000000000..91ca5446ade055b0a071bc3843a1fddee82e9e28 --- /dev/null +++ b/lib/solvers/DirectSolver.m @@ -0,0 +1,21 @@ +% DirectSolver (subclass of IterativeSolver) Solves linear equations +% directly. This is mainly intended for testing purposes! +% +% See also: IterativeSolver + +classdef DirectSolver < IterativeSolver + %% implement abstract superclass methods + methods (Access=public) + function tf = isConverged(obj) + % converges in the first step + tf = (obj.iterationCount > 0); + end + end + + methods (Access=protected) + function computeUpdate(obj) + % direct solve + obj.x = obj.A \ obj.b; + end + end +end \ No newline at end of file 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 new file mode 100644 index 0000000000000000000000000000000000000000..1a278c82dda3a969218f6e1d6f730baa11c4bdcc --- /dev/null +++ b/lib/solvers/PcgSolver.m @@ -0,0 +1,86 @@ +% PcgSolver (abstract subclass of IterativeSolver) Solves linear equations +% iteratively by a preconditioned conjugate gradient (PCG) method. +% +% See also: IterativeSolver + +classdef PcgSolver < IterativeSolver + %% properties + properties (SetAccess=protected, GetAccess=public) + residualCNorm + 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.C.apply(obj.residual); + obj.searchDirection = obj.Cresidual; + obj.residualCNorm = sqrt(dot(obj.residual, obj.Cresidual, 1)); + obj.normb = vecnorm(b, 2, 1); + end + end + + %% implement abstract superclass methods + methods (Access=public) + function tf = isConverged(obj) + tf = ((obj.iterationCount >= obj.maxIter) ... + | (obj.residualCNorm < obj.tol) ... + | (obj.residualCNorm ./ obj.normb < obj.tol)); + end + end + + methods (Access=protected) + function computeUpdate(obj) + % only iterate on active components + idx = find(obj.activeComponents); + + % update solution + AsearchDirection = obj.A * obj.searchDirection(:,idx); + dAd = dot(obj.searchDirection(:,idx), AsearchDirection, 1); + if dAd < eps + alpha = 1; + else + alpha = obj.residualCNorm(:,idx).^2 ./ dAd; + end + obj.x(:,idx) = obj.x(:,idx) + alpha .* obj.searchDirection(:,idx); + + % update residual + obj.residual(:,idx) = obj.residual(:,idx) - alpha .* AsearchDirection; + obj.Cresidual(:,idx) = obj.C.apply(obj.residual(:,idx)); + residualCNormOld = obj.residualCNorm(:,idx); + obj.residualCNorm(:,idx) = sqrt(dot(obj.residual(:,idx), obj.Cresidual(:,idx), 1)); + + % update search direction + beta = (obj.residualCNorm(:,idx) ./ residualCNormOld).^2; + obj.searchDirection(:,idx) = obj.Cresidual(:,idx) + beta .* obj.searchDirection(:,idx); + end + end +end \ No newline at end of file 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/lib/spaces/@FeSpace/FeSpace.m b/lib/spaces/@FeSpace/FeSpace.m index 357fb167d8f0ab37b57a72b631843a5fcd93e7ff..39413fd4187254266dcd80e86aa98cad588a2692 100644 --- a/lib/spaces/@FeSpace/FeSpace.m +++ b/lib/spaces/@FeSpace/FeSpace.m @@ -84,6 +84,10 @@ classdef FeSpace < handle end freeDofs = obj.freeDofs; end + + dofs = createVertexDofs(obj, idx) + dofs = createInnerEdgeDofs(obj, idx) + dofs = createInnerElementDofs(obj, idx) end methods (Access=protected) diff --git a/lib/spaces/@FeSpace/assembleDofs.m b/lib/spaces/@FeSpace/assembleDofs.m index 37a867db465801e8a0fc2e76234800122960a461..ca5ad9ecc4c5d39f0fe6e3e41dbc63340888c717 100644 --- a/lib/spaces/@FeSpace/assembleDofs.m +++ b/lib/spaces/@FeSpace/assembleDofs.m @@ -10,41 +10,34 @@ nDofs = dofCoupling * [3;3;1]; nEdgeDofs = dofCoupling * [2;1;0]; dofs.element2Dofs = zeros(nDofs, mesh.nElements); dofs.edge2Dofs = zeros(nEdgeDofs, mesh.nEdges); -n = 0; % current number of local ... -k = 0; % ... local edge ... -N = 0; % ... and global Dofs + +dofs.nDofs = dofCoupling * [mesh.nCoordinates; mesh.nEdges; mesh.nElements]; %% vertex Dofs if dofCoupling(1) ~= 0 - dofs.element2Dofs(1:3,:) = mesh.elements; - dofs.edge2Dofs(1:2,:) = mesh.edges; - k = k + 2; - n = n + 3; - N = N + mesh.nCoordinates; + dofNumbers = createVertexDofs(obj); + dofs.element2Dofs(1:3,:) = dofNumbers(mesh.elements); + dofs.edge2Dofs(1:2,:) = dofNumbers(mesh.edges); end %% edge dofs if dofCoupling(2) ~= 0 nInnerEdgeDofs = dofCoupling(2); - innerEdgeDofs = N + reshape(1:(nInnerEdgeDofs*mesh.nEdges), nInnerEdgeDofs, mesh.nEdges); - for i = 1:3 - idx = (n+1):(n+nInnerEdgeDofs); - dofs.element2Dofs(idx,:) = innerEdgeDofs(:,mesh.element2edges(i,:)); - dofs.element2Dofs(idx,mesh.flipEdges(i,:)) = flipud(dofs.element2Dofs(idx,mesh.flipEdges(i,:))); - n = n + nInnerEdgeDofs; + dofNumbers = createInnerEdgeDofs(obj); + idx = dofCoupling(1) * 3; + for k = 1:3 + idx = idx(end) + (1:nInnerEdgeDofs); + dofs.element2Dofs(idx,:) = dofNumbers(:,mesh.element2edges(k,:)); + dofs.element2Dofs(idx,mesh.flipEdges(k,:)) = flipud(dofs.element2Dofs(idx,mesh.flipEdges(k,:))); end - dofs.edge2Dofs((k+1):(k+nInnerEdgeDofs),:) = innerEdgeDofs; - N = N + nInnerEdgeDofs*mesh.nEdges; + idx = dofCoupling(1) * 2 + (1:nInnerEdgeDofs); + dofs.edge2Dofs(idx,:) = dofNumbers; end %% inner dofs if dofCoupling(3) ~= 0 - nInnerDofsPerElement = dofCoupling(3); - dofs.element2Dofs((n+1):(n+nInnerDofsPerElement),:) ... - = N + reshape(1:(nInnerDofsPerElement*mesh.nElements), nInnerDofsPerElement, mesh.nElements); - N = N + nInnerDofsPerElement*mesh.nElements; + idx = dofCoupling(1:2) * [3;3] + (1:dofCoupling(3)); + dofs.element2Dofs(idx,:) = createInnerElementDofs(obj); end -dofs.nDofs = N; - end diff --git a/lib/spaces/@FeSpace/createInnerEdgeDofs.m b/lib/spaces/@FeSpace/createInnerEdgeDofs.m new file mode 100644 index 0000000000000000000000000000000000000000..35a8ddca1f09d9d35153ab6cad1bc552b055bedf --- /dev/null +++ b/lib/spaces/@FeSpace/createInnerEdgeDofs.m @@ -0,0 +1,38 @@ +% createInnerEdgeDofs Generate global DOF-numbers associated with inner edge +% dofs. +% +% dofs = createInnerEdgeDofs(obj) generates dofs for all edges. +% +% dofs = createInnerEdgeDofs(obj, idx) generates dofs only for edges given by +% index vector idx. + +function dofs = createInnerEdgeDofs(obj, idx) + +arguments + obj + idx (1,:) {mustBeIndexVector} = ':' +end + +N = obj.mesh.nEdges; +nLocalDofs = getDofConnectivity(obj.finiteElement); + +if nLocalDofs(2) == 0 + dofs = []; + return +end + +% dof numbers are simply given by vertex numbers +if ischar(idx) && isequal(idx, ':') + dofs = 1:N; +elseif islogical(idx) + assert(isequal(size(idx), [1, N])) + dofs = find(idx); +else + assert(all(0 < idx) && all(idx <= N)) + dofs = idx; +end + +offset = obj.mesh.nCoordinates * nLocalDofs(1); +dofs = offset + (nLocalDofs(2)*(dofs-1) + (1:nLocalDofs(2))'); + +end diff --git a/lib/spaces/@FeSpace/createInnerElementDofs.m b/lib/spaces/@FeSpace/createInnerElementDofs.m new file mode 100644 index 0000000000000000000000000000000000000000..93a41d41af9838079c37dde598ba327933a2a0be --- /dev/null +++ b/lib/spaces/@FeSpace/createInnerElementDofs.m @@ -0,0 +1,38 @@ +% createInnerElementDofs Generate global DOF-numbers associated with inner +% element dofs. +% +% dofs = createInnerElementDofs(obj) generates dofs for all elements. +% +% dofs = createInnerElementDofs(obj, idx) generates dofs only for elements +% given by index vector idx. + +function dofs = createInnerElementDofs(obj, idx) + +arguments + obj + idx (1,:) {mustBeIndexVector} = ':' +end + +N = obj.mesh.nElements; +nLocalDofs = getDofConnectivity(obj.finiteElement); + +if nLocalDofs(3) == 0 + dofs = []; + return +end + +% dof numbers are simply given by vertex numbers +if ischar(idx) && isequal(idx, ':') + dofs = 1:N; +elseif islogical(idx) + assert(isequal(size(idx), [1, N])) + dofs = find(idx); +else + assert(all(0 < idx) && all(idx <= N)) + dofs = idx; +end + +offset = nLocalDofs(1:2) * [obj.mesh.nCoordinates; obj.mesh.nEdges]; +dofs = offset + (nLocalDofs(3)*(dofs-1) + (1:nLocalDofs(3))'); + +end diff --git a/lib/spaces/@FeSpace/createVertexDofs.m b/lib/spaces/@FeSpace/createVertexDofs.m new file mode 100644 index 0000000000000000000000000000000000000000..d8985325cdf623718cbcc192cf546a19f202168a --- /dev/null +++ b/lib/spaces/@FeSpace/createVertexDofs.m @@ -0,0 +1,34 @@ +% createVertexDofs Generate global DOF-numbers associated with vertex dofs. +% +% dofs = createVertexDofs(obj) generates dofs for all vertices. +% +% dofs = createVertexDofs(obj, idx) generates dofs only for vertices given by +% index vector idx. + +function dofs = createVertexDofs(obj, idx) + +arguments + obj + idx (1,:) {mustBeIndexVector} = ':' +end + +N = obj.mesh.nCoordinates; +nLocalDofs = getDofConnectivity(obj.finiteElement); + +if nLocalDofs(1) == 0 + dofs = []; + return +end + +% dof numbers are simply given by vertex numbers +if ischar(idx) && isequal(idx, ':') + dofs = 1:N; +elseif islogical(idx) + assert(isequal(size(idx), [1, N])) + dofs = find(idx); +else + assert(all(0 < idx) && all(idx <= N)) + dofs = idx; +end + +end diff --git a/lib/spaces/interpolation/LinearInterpolation.m b/lib/spaces/interpolation/LinearInterpolation.m index d5d1e7db815f156d168e6d17cb4d3fe2a0844ab2..02409a6f344140117475d2c6ff1365985232da3f 100644 --- a/lib/spaces/interpolation/LinearInterpolation.m +++ b/lib/spaces/interpolation/LinearInterpolation.m @@ -1,7 +1,5 @@ % LinearInterpolation (subclass of Evaluable) Provide piecewise linear -% interpolant of function. This is used mainly internally for prolongation. -% -% See also: LinearFeProlongation +% interpolant of function. This can be used for approximate prolongation. classdef LinearInterpolation < Evaluable %% properties diff --git a/lib/spaces/interpolation/LoFeProlongation.m b/lib/spaces/prolongation/LoMeshProlongation.m similarity index 84% rename from lib/spaces/interpolation/LoFeProlongation.m rename to lib/spaces/prolongation/LoMeshProlongation.m index 2c2603e8cf42a7371dabb010b93874d389047f30..b6ebbda297ba295c74b35676da1b6077d78484b9 100644 --- a/lib/spaces/interpolation/LoFeProlongation.m +++ b/lib/spaces/prolongation/LoMeshProlongation.m @@ -1,29 +1,30 @@ -% LoFeProlongation (subclass of Prolongation) Prolongate lowest order L^2/H^1 +% LoMeshProlongation (subclass of MeshProlongation) Prolongate lowest order L^2/H^1 % conforming finite element function to refined mesh. % -% P = LoFeProlongation(fes) returns a handle to the prolongation object +% P = LoMeshProlongation(fes) returns a handle to the prolongation object % associated to the finite element space fes. The prolongation matrix % P.matrix is set automatically at mesh refinement. % -% prolongate(P, u) returns the prolongated data of FeFunction u. +% See also Prolongation -classdef LoFeProlongation < Prolongation +classdef LoMeshProlongation < MeshProlongation %% properties properties (Access=protected) feType end + %% methods methods (Access=public) - function obj = LoFeProlongation(fes) - obj = obj@Prolongation(fes); + function obj = LoMeshProlongation(fes) + obj = obj@MeshProlongation(fes); switch class(fes.finiteElement) case 'LowestOrderL2Fe' obj.feType = 'L2'; case 'LowestOrderH1Fe' obj.feType = 'H1'; otherwise - eid = 'LoFeProlongation:wrongFeType'; - msg = 'LoFeProlongation needs a lowest order L2 or H1 finite element space.'; + eid = 'LoMeshProlongation:wrongFeType'; + msg = 'LoMeshProlongation needs a lowest order L2 or H1 finite element space.'; throwAsCaller(MException(eid, msg)); end end @@ -87,5 +88,9 @@ classdef LoFeProlongation < Prolongation sum(data.nInnerNodes.*data.nRefinedElements); obj.matrix = sparse(I, J, V, nNewDofs, dofs.nDofs); end + + % no-op override to avoid wrong call to superclass method + function connectDofs(~, ~, ~) + end end end diff --git a/lib/spaces/interpolation/FeProlongation.m b/lib/spaces/prolongation/MeshProlongation.m similarity index 84% rename from lib/spaces/interpolation/FeProlongation.m rename to lib/spaces/prolongation/MeshProlongation.m index 4dc5d6b97ac4e223403a4794c378ad118f6e3a8a..efc1c76e4366696b9540258fe17c1d06a48cd296 100644 --- a/lib/spaces/interpolation/FeProlongation.m +++ b/lib/spaces/prolongation/MeshProlongation.m @@ -1,22 +1,31 @@ -% FeProlongation (subclass of Prolongation) Provides prolongation operator for +% MeshProlongation (subclass of Prolongation) Provides prolongation operator for % general FeFunction to a refined mesh. % -% P = FeProlongation(fes) returns a handle to the prolongation object +% P = MeshProlongation(fes) returns a handle to the prolongation object % associated to the finite element space fes. The prolongation matrix % P.matrix is set automatically at mesh refinement. % -% prolongate(P, u) returns the prolongated data of FeFunction u. +% See also: Prolongation -classdef FeProlongation < Prolongation +classdef MeshProlongation < Prolongation %% properties properties (Access=private) postRefineListener end + + properties (Access=protected) + fes FeSpace + listenerHandle + end %% methods methods (Access=public) - function obj = FeProlongation(fes) - obj = obj@Prolongation(fes); + function obj = MeshProlongation(fes) + obj = obj@Prolongation(); + obj.fes = fes; + + mesh = fes.mesh; + obj.listenerHandle = mesh.listener('IsAboutToRefine', @obj.setupMatrix); obj.postRefineListener = fes.mesh.listener('RefineCompleted', @obj.connectDofs); assert(isa(obj.fes.finiteElement, 'NodalFiniteElement'), ... @@ -26,10 +35,8 @@ classdef FeProlongation < Prolongation methods (Access=protected) function setupMatrix(obj, ~, data) - % general idea: compute *all* dofs for each new element and store - % them consecutively - % those are connected to the actual new dofs when their numbering - % per element is available (-> obj.connectDofs) + % general idea: compute *all* dofs for each new element and store them consecutively + % those are connected to the actual new dofs when their numbering per element is available (-> obj.connectDofs) % de-allocate for memory-efficiency obj.matrix = []; @@ -61,8 +68,7 @@ classdef FeProlongation < Prolongation for k = find(data.nRefinedElements ~= 0)' unitTriangle = getBisectedUnitTriangle(class(data.bisection{k})); newDofLocations = getNewLocationsElementwise(unitTriangle, dofLocations); - newDofWeights = divideElementwise(... - reshape(evalShapeFunctions(fe, newDofLocations), nLocalDofs, nLocalDofs, []), localMatrix); + newDofWeights = divideElementwise(evalShapeFunctions(fe, newDofLocations), localMatrix); nNewLocs = newDofLocations.nNodes; elems = getRefinedElementIdx(data, k); @@ -91,7 +97,7 @@ end %% local functions function mesh = getBisectedUnitTriangle(bisecMethod) - mesh = Mesh.loadFromGeometry('unittriangle'); + mesh = Mesh.unitTriangle(); % bisect unit triangle according to given bisection rule % TODO: this should be handled by bisection method itself? @@ -123,10 +129,8 @@ function elementwiseLocations = getNewLocationsElementwise(mesh, locations) end function C = divideElementwise(A, B) - % TODO: from 2022a on, this can be replaced by pagemrdivide(A, B) n = size(A,1); - C = B' \ reshape(permute(A, [2,1,3]), n, []); - C = reshape(permute(reshape(C, n, n, []), [2,1,3]), n, []); + C = reshape(pagemrdivide(reshape(A, n, n, []), B), n, []); end function idx = getConsecutiveIndices(parents, nIndices) diff --git a/lib/spaces/interpolation/Prolongation.m b/lib/spaces/prolongation/Prolongation.m similarity index 55% rename from lib/spaces/interpolation/Prolongation.m rename to lib/spaces/prolongation/Prolongation.m index 97adb20868910b445d6a1884181623ff17abeab0..f6ca211341438e536a09b9776e0c4121647c2912 100644 --- a/lib/spaces/interpolation/Prolongation.m +++ b/lib/spaces/prolongation/Prolongation.m @@ -1,5 +1,9 @@ % Prolongation (abstract handle class) Interface for prolongation of FeFunctions % from coarse to fine mesh. +% +% prolongate(P, u) returns the prolongated data of FeFunction u. +% +% restrict(P, u) returns the restricted data of FeFunction u. classdef Prolongation < handle %% properties @@ -7,18 +11,11 @@ classdef Prolongation < handle matrix {mustBeSparse} end - properties (Access=protected) - fes FeSpace - listenerHandle - end %% methods methods (Access=public) function obj = Prolongation(fes) - obj.fes = fes; - mesh = fes.mesh; obj.matrix = []; - obj.listenerHandle = mesh.listener('IsAboutToRefine', @obj.setupMatrix); end function newFeData = prolongate(obj, u) @@ -32,10 +29,29 @@ classdef Prolongation < handle end newFeData = obj.matrix * u.data'; end + + function newFeData = restrict(obj, u) + arguments + obj + u FeFunction + end + + if isempty(obj.matrix) + error('Prolongation matrix not set. Are you sure there was mesh refinement?') + end + newFeData = (u.data * obj.matrix)'; + end end - methods (Abstract, Access=protected) - setupMatrix(obj, src, event) + methods (Static, Access=public) + function P = chooseFor(fes) + if isa(fes.finiteElement, 'LowestOrderH1Fe') ... + || isa(fes.finiteElement, 'LowestOrderL2Fe') + P = LoMeshProlongation(fes); + else + P = MeshProlongation(fes); + end + end end end diff --git a/lib/spaces/prolongation/SpaceProlongation.m b/lib/spaces/prolongation/SpaceProlongation.m new file mode 100644 index 0000000000000000000000000000000000000000..83d690006d817d2a48c865941be819e44386eaa4 --- /dev/null +++ b/lib/spaces/prolongation/SpaceProlongation.m @@ -0,0 +1,57 @@ +% SpaceProlongation (subclass of Prolongation) Provides prolongation operator +% from S^p to S^q with q > p +% +% P = SpaceProlongation(sourceFes, targetFes) returns a handle to the +% prolongation object for the prolongation sourceFes -> targetFes. The +% prolongation matrix P.matrix is set up at construction. +% +% See also: Prolongation + +classdef SpaceProlongation < Prolongation + %% methods + methods (Access=public) + function obj = SpaceProlongation(sourceFes, targetFes) + unitTriangleInclusionMatrix = SpaceProlongation.getUnitTriangleInclusionMatrix(sourceFes.finiteElement, targetFes.finiteElement); + + % Copy the matrix from the unit triangle for each element + mat = repmat(unitTriangleInclusionMatrix(:), [1, sourceFes.mesh.nElements]); + + % Create index sets for the accumarray + fromDofs = getDofs(sourceFes); + toDofs = getDofs(targetFes); + I = repmat(fromDofs.element2Dofs, size(toDofs.element2Dofs, 1), 1); + J = repelem(toDofs.element2Dofs, size(fromDofs.element2Dofs, 1), 1); + + % assemble matrix and account for multiply computed dofs + obj.matrix = sparse(I(:), J(:), mat(:), fromDofs.nDofs, toDofs.nDofs); + dofCounts = accumarray(toDofs.element2Dofs(:), 1, [toDofs.nDofs, 1])'; + obj.matrix = obj.matrix ./ dofCounts; + end + end + + methods (Static, Access=protected) + function matrix = getUnitTriangleInclusionMatrix(sourceFE, targetFE) + unittriangle = Mesh.unitTriangle(); + fromFes = FeSpace(unittriangle, sourceFE); + toFes = FeSpace(unittriangle, targetFE); + Vertices = 1:getDofs(fromFes).nDofs; + matrix = SpaceProlongation.interpolateData(eye(length(Vertices)), fromFes, toFes)'; + + % Get local to global map in unit triangle + matrix = matrix(getDofs(fromFes).element2Dofs, getDofs(toFes).element2Dofs); + end + + function interpolatedData = interpolateData(data, fromFes, toFes) + Dofs = 1:getDofs(toFes).nDofs; + nComponents = size(data, 2); + interpolatedData = zeros(numel(Dofs), nComponents); + feFunctionWrapper = FeFunction(fromFes); + for k = 1:nComponents + feFunctionWrapper.setData(data(:,k)); + wholeData = nodalInterpolation(feFunctionWrapper, toFes); + interpolatedData(:,k) = wholeData(Dofs); + end + end + end +end + diff --git a/lib/storage/@LevelData/LevelData.m b/lib/storage/@LevelData/LevelData.m new file mode 100644 index 0000000000000000000000000000000000000000..5b4955d15d2da5300edffc333fa6568f8d35ca78 --- /dev/null +++ b/lib/storage/@LevelData/LevelData.m @@ -0,0 +1,299 @@ +classdef LevelData < handle & matlab.mixin.CustomDisplay +%%LEVELDATA class representing results from level-oriented computations + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + properties + % map for general metadata + metaData (1,1) dictionary + % Root folder for file storage + root + % Structure array storing string representations of variables + dictionary + % Array of level data + level (1,:) struct + % Dictionary of data types + type (1,1) dictionary + end + + properties (Access=private) + % Dictionary of categories for cataloguing and plotting + category (1,1) dictionary + end + + properties (Dependent) + % Path to folder for file storage + foldername (1,:) char + % Name of file for storage + filename (1,:) char + % Cell array of variable names + label (1,:) cell + % Number of levels + nLevel (1,1) int32 + % Number of variables + nVariable (1,1) int32 + % Number of scalar variables + nScalarVariable (1,1) int32 + % Cell array of names of scalar variables + scalarVariable (1,:) cell + % Number of absolute value variables + nAbsoluteVariable (1,1) int32 + % Number of time variables + nTimeVariable (1,1) int32 + % Boolean indicating whether data has been stored for a single level + isInitialRun (1,1) logical + % Cell array of names of absolute value variables + absoluteVariable (1,:) cell + % Cell array of names of timing variables + timeVariable (1,:) cell + end + + properties (Dependent, Access=private) + % Boolean vector determining whether variable is a scalar value + isScalar (1,:) logical + end + + methods + %% CONSTRUCTOR + function obj = LevelData(rootpath) + %%LEVELDATA creates an object for storing level-oriented data, + %file storage is initialised in the specified optional path + % leveldata = LEVELDATA(rootpath) + + arguments + rootpath {mustBeTextScalar} = 'results' + end + obj.root = rootpath; + + % TODO: set output of hostname to string and remove cast + obj.metaData = dictionary(... + "problem", "problem", ... + "domain", "domain", ... + "method", "method", ... + "identifier", "main", ... + "hostname", string(getHostname()), ... + "timestamp", string(datetime('now', 'Format', 'yyyy-MM-dd_HH:mm:ss'))); + + obj.category = dictionary(); + + % Initialise dictionary with some default values + obj.dictionary = getDefaultDictionary(); + end + + %% SET GLOBAL VARIABLES + function set.root(obj, path) + arguments + obj + path {mustBeTextScalar} + end + obj.root = path; + end + + %% GET GLOBAL VARIABLES + function label = get.label(obj) + % List of names of all variables + if obj.nLevel > 0 + label = fieldnames(obj.level(1)); + else + label = {}; + end + end + + function nLevel = get.nLevel(obj) + nLevel = length(obj.level); + end + + function nVariable = get.nVariable(obj) + nVariable = length(obj.label); + end + + function path = get.foldername(obj) + % Name of folder for file storage + rootpath = obj.root; + if numel(rootpath) > 0 && ~strcmp(rootpath(end), '/') + rootpath = [rootpath, '/']; + end + path = rootpath + strjoin([obj.metaData("problem"), obj.metaData("domain"), obj.metaData("method")], '_'); + end + + function file = get.filename(obj) + % Name of file for file storage + file = [obj.metaData("identifier")]; + end + + function bool = get.isScalar(obj) + % Creates boolean vector determining whether a variable + % contains a scalar value per level only + bool = [obj.type(obj.label).isScalar]'; + end + + function variables = get.scalarVariable(obj) + % Returns list of names of all scalar variables + variables = obj.label(obj.isScalar); + end + + function nVariable = get.nScalarVariable(obj) + nVariable = nnz(obj.isScalar); + end + + function variables = get.timeVariable(obj) + % Returns list of names of all time variables + idx = (obj.category(obj.label) == DataCategory.TIME); + variables = obj.label(idx); + end + + function nVariable = get.nTimeVariable(obj) + nVariable = nnz(obj.category.values() == DataCategory.TIME); + end + + function variables = get.absoluteVariable(obj) + % Returns list of names of all absolute variables + idx = (obj.category(obj.label) == DataCategory.ABSOLUTE); + variables = obj.label(idx); + end + + function nVariable = get.nAbsoluteVariable(obj) + nVariable = nnz(obj.category.values() == DataCategory.ABSOLUTE); + end + + function bool = get.isInitialRun(obj) + % True if only one level is stored + bool = (obj.nLevel <= 1); + end + + + %% READ LEVEL DATA + data = get(obj, jLevel, variableName) + + %% MODIFY LEVEL DATA + set(obj, jLevel, variableName, value) + setTime(obj, jLevel, variableName, value) + setAbsolute(obj, jLevel, variableName, value) + + function append(obj, varargin) + %%APPEND simplified addition of specified data for the + %subsequent level, the arguments must be specified as + %pairs of variable names and data (numeric or string / + %characters) + % APPEND(obj, variableName, value, ...) + + arguments + obj + end + + arguments (Repeating) + varargin + end + + assert(mod(length(varargin), 2) == 0, ... + 'Invalid number of arguments'); + nlvl = obj.nLevel + 1; + obj.set(nlvl, varargin{:}); + end + + function remove(obj, variableName) + %%REMOVE removes the specified list of variables + % REMOVE(obj, variableName, ...) + + arguments + obj + end + + arguments (Repeating) + variableName {mustBeTextScalar} + end + + obj.level = rmfield(obj.level, variableName); + obj.type(variableName) = []; + end + + function removeLevel(obj, jLevel) + %%REMOVELEVEL removes all data for the specified list jLevel + %of level numbers + % REMOVELEVEL(obj, jLevel) + for k = 1:length(jLevel) + obj.level(jLevel(k)) = []; + end + end + + function removeNonscalar(obj) + %%REMOVENONSCALAR removes all non-scalar data completely; + %e.g., to reduce storage size + % REMOVENONSCALAR(obj) + variableNonScalar = setdiff(obj.label, obj.scalarVariable); + obj.remove(variableNonScalar{:}); + end + + %% OUTPUT LEVEL DATA + printHeader(obj) + printLevel(obj, jLevel) + ax = plot(obj, xVariable, yVariable) + ax = plotTime(obj, xVariable, yVariable) + ax = plotAbsolute(obj, xVariable, yVariable) + ax = plotTriangulation(obj, jLevel) + + %% EXPORT LEVEL DATA + saveToFile(obj, folder, file) + saveToTable(obj, separator) + plotToFile(obj, xVariable, yVariable) + plotTriangulationToFile(obj, jLevel) + end + + methods (Access=protected) + %% CUSTOM DISPLAY + propgrp = getPropertyGroups(obj) + end + + %% AUXILIARY FUNCTIONS + methods (Access = private) + function idx = getIndex(obj, variableName) + [~, idx] = ismember(variableName, obj.label); + end + + function width = getWidth(~, type) + minimalWidth = 8; + width = num2str(max(type.printWidth, minimalWidth)); + end + + function spec = getHeaderSpecifier(obj, separator) + % Creates formatting string for the header of the output to command line + spec = cell(1, obj.nScalarVariable); + for j = 1:obj.nScalarVariable + t = obj.type(obj.scalarVariable{j}); + spec{j} = assembleSpecifier(obj.getWidth(t), 's'); + end + spec = strjoin(spec, separator) + "\n"; + end + + function spec = getFormatSpecifier(obj, separator) + % Creates formatting string for printing to command line + spec = cell(1, obj.nScalarVariable); + for j = 1:obj.nScalarVariable + t = obj.type(obj.scalarVariable{j}); + spec{j} = assembleSpecifier(obj.getWidth(t), t.formatSpec); + end + spec = strjoin(spec, separator) + "\n"; + end + + ax = plotLevel(obj, plotFunction, xVariable, variableName) + plotLevelTriangulation(obj, ax, jLevel) + end +end + +function spec = assembleSpecifier(width, format) + spec = ['%', num2str(width), format]; +end diff --git a/lib/storage/@LevelData/get.m b/lib/storage/@LevelData/get.m new file mode 100644 index 0000000000000000000000000000000000000000..c130089623ad775a2c9d93b97a96c12135fc96ff --- /dev/null +++ b/lib/storage/@LevelData/get.m @@ -0,0 +1,74 @@ +function data = get(obj, jLevel, variableName) +%%GET extracts data from this LevelData object on a given list jLevel of +%level numbers. One or more variables to be returned can be specified by their +%names. +% data = GET(obj, jLevel, variableName, ...) +% data = GET(obj, ':', variableName, ...) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + jLevel {mustBeIndexVector} = ':' + end + + arguments (Repeating) + variableName {mustBeTextScalar} + end + + if strcmp(jLevel, ':') + jLevel = 1:obj.nLevel; + end + + % Initialise return variable + data = nan(length(jLevel), length(variableName)); + + % filter non-existing variables + existingVariables = ismember(variableName, obj.label); + if any(~existingVariables) + warning(['Variable(s) ', strjoin(variableName(~existingVariables), ', '), ' not found']); + end + variableName = variableName(existingVariables); + + % Iterate over all variables + for jVariable = 1:length(variableName) + name = variableName{jVariable}; + + % Determine index of current variable + idx = obj.getIndex(name); + if obj.isScalar(idx) + % Extract scalar variables for each level + value = [obj.level.(name)]; + else + % Extract non-scalar variables only in case of a single level + if length(jLevel) > 1 + warning('Export of level-oriented scalar variables only'); + value = []; + else + data(1,jVariable) = obj.level(jLevel).(name); + return + end + end + + % Save extracted data to return variable + data(:,jVariable) = value; + % Post-process character arrays + if obj.type(name).rawType == RawType.TEXT + data = mat2cell(char(data), ones(length(data), 1)); + end + end +end diff --git a/lib/storage/@LevelData/getPropertyGroups.m b/lib/storage/@LevelData/getPropertyGroups.m new file mode 100644 index 0000000000000000000000000000000000000000..3063a08ad7e4e79ddc2c4a74372b35879995a836 --- /dev/null +++ b/lib/storage/@LevelData/getPropertyGroups.m @@ -0,0 +1,24 @@ +function propgrp = getPropertyGroups(obj) + if ~isscalar(obj) + propgrp = getPropertyGroups@matlab.mixin.CustomDisplay(obj); + else + % some rudimentary statistics about variable counts + statList = ["nLevel", "nVariable", "nScalarVariable", "nAbsoluteVariable", "nTimeVariable"]; + statTitle = "Variable Counts"; + statGrp = matlab.mixin.util.PropertyGroup(statList, statTitle); + + % metadata as a struct + metaDataList = struct(); + for k = obj.metaData.keys' + metaDataList.(k{1}) = obj.metaData(k{1}); + end + metaDataTitle = "Metadata"; + metaDataGrp = matlab.mixin.util.PropertyGroup(metaDataList, metaDataTitle); + + % storage related properties + storeList = ["root", "foldername", "filename"]; + storeTitle = "Storage details"; + storeGrp = matlab.mixin.util.PropertyGroup(storeList, storeTitle); + propgrp = [statGrp, metaDataGrp, storeGrp]; + end +end \ No newline at end of file diff --git a/lib/storage/@LevelData/plot.m b/lib/storage/@LevelData/plot.m new file mode 100644 index 0000000000000000000000000000000000000000..1151963f6c7313b49169568fbd5fa489971ca05b --- /dev/null +++ b/lib/storage/@LevelData/plot.m @@ -0,0 +1,38 @@ +function ax = plot(obj, xVariable, yVariable) +%%PLOT plots various scalar variables (specified in yVariable) with respect +%to the variable xVariable, returns handle to axis object +% ax = PLOT(obj, xVariable, yVariable, ...) +% +% See also LevelData/plotTime, LevelData/plotAbsolute + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + xVariable {mustBeTextScalar} + end + + arguments (Repeating) + yVariable {mustBeTextScalar} + end + + if isempty(yVariable) + yVariable = setdiff(obj.label, xVariable); + end + + ax = obj.plotLevel(DataCategory.ERROR, xVariable, yVariable); +end \ No newline at end of file diff --git a/lib/storage/@LevelData/plotAbsolute.m b/lib/storage/@LevelData/plotAbsolute.m new file mode 100644 index 0000000000000000000000000000000000000000..931353fe3b137c3be13c32a0fe90d7ceeac0eaa0 --- /dev/null +++ b/lib/storage/@LevelData/plotAbsolute.m @@ -0,0 +1,39 @@ +function ax = plotAbsolute(obj, xVariable, yVariable) +%%PLOTABSOLUTE plots various scalar absolute variables (specified in +%yVariable) with respect to the variable xVariable, returns handle to axis +%object +% ax = PLOTABSOLUTE(obj, xVariable, yVariable, ...) +% +% See also LevelData/plot, LevelData/plotTime + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + xVariable {mustBeTextScalar} + end + + arguments (Repeating) + yVariable {mustBeTextScalar} + end + + if isempty(yVariable) + yVariable = setdiff(obj.label, xVariable); + end + + ax = obj.plotLevel(DataCategory.ABSOLUTE, xVariable, yVariable); +end \ No newline at end of file diff --git a/lib/storage/@LevelData/plotLevel.m b/lib/storage/@LevelData/plotLevel.m new file mode 100644 index 0000000000000000000000000000000000000000..455d8290b990e4e7597c84b78ffebf60232c673a --- /dev/null +++ b/lib/storage/@LevelData/plotLevel.m @@ -0,0 +1,74 @@ +function ax = plotLevel(obj, category, xVariable, yVariable) +%%PLOTLEVEL auxiliary private function for creation of plots for usage in +%LevelData/plot, LevelData/plotTime, and LevelData/plotAbsolute, uses the +%specified DataCategory for the plot of variables in yVariable with +%respect to xVariable, returns handle to axis object +% ax = PLOTLEVEL(obj, category, xVariable, yVariable, ...) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + category DataCategory + xVariable {mustBeTextScalar} + yVariable (1,:) cell + end + + % Extract variables with correct category, type, and shape for y-axis + idx = (obj.category(yVariable) == category) ... + & ([obj.type(yVariable).rawType] == RawType.FLOAT) ... + & [obj.type(yVariable).isScalar]; + yVariable = yVariable(idx); + + % Create handle to currently active axis object + ax = gca; + + % Extract variable for x-axis + xValue = obj.get(1:obj.nLevel, xVariable); + + % Create unified list for formatting plot lines + [COLOURORDER, MARKER] = getPlotStyle(); + + % Iterate over given variables + for j = 1:length(yVariable) + % Extract value for y-axis + yValue = obj.get(1:obj.nLevel, yVariable{j}); + + % Extract label for legend from dictionary + if obj.dictionary.isKey(yVariable{j}) + variableLabel = obj.dictionary(yVariable{j}); + else + variableLabel = yVariable{j}; + end + + % Create plot + category.plotFunction( ... + ax, xValue, yValue, '-', ... + 'Marker', MARKER{mod(j, length(MARKER))}, ... + 'Color', COLOURORDER(j, :), ... + 'LineWidth', 1.5, ... + 'DisplayName', variableLabel); + % Add new line into the current figure when calling plotConvergence again + hold(ax, 'on'); + end + + % Set plot appearance + title(ax, category.title); + xlabel(ax, xVariable); + ylabel(ax, category.yLabel); + configureLegend(ax, category.legendLocation); +end diff --git a/lib/storage/@LevelData/plotTime.m b/lib/storage/@LevelData/plotTime.m new file mode 100644 index 0000000000000000000000000000000000000000..3b7693f5f684259fe1465bff7b0ccdff03de12fc --- /dev/null +++ b/lib/storage/@LevelData/plotTime.m @@ -0,0 +1,38 @@ +function ax = plotTime(obj, xVariable, yVariable) +%%PLOTTIME plots various scalar time variables (specified in yVariable) with +%respect to the variable xVariable, returns handle to axis object +% ax = PLOTTIME(obj, xVariable, yVariable, ...) +% +% See also LevelData/plot, LevelData/plotAbsolute + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + xVariable {mustBeTextScalar} + end + + arguments (Repeating) + yVariable {mustBeTextScalar} + end + + if isempty(yVariable) + yVariable = setdiff(obj.label, xVariable); + end + + ax = obj.plotLevel(DataCategory.TIME, xVariable, yVariable); +end \ No newline at end of file diff --git a/lib/storage/@LevelData/plotToFile.m b/lib/storage/@LevelData/plotToFile.m new file mode 100644 index 0000000000000000000000000000000000000000..a5fc72f656e0c723daa6968841873aeeae339e64 --- /dev/null +++ b/lib/storage/@LevelData/plotToFile.m @@ -0,0 +1,45 @@ +function plotToFile(obj, xVariable, yVariable) +%%PLOTTOFILE creates plots of various scalar variables (specified in +%yVariable) with respect %to the variable xVariable and stores it to a file, +%filename is generated automatically from information in LevelData object +% PLOTTOFILE(obj, xVariable, yVariable) +% +% See also LevelData/plot + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + xVariable {mustBeTextScalar} + end + + arguments (Repeating) + yVariable {mustBeTextScalar} + end + + if isempty(yVariable) + yVariable = setdiff(obj.label, xVariable); + end + + ensureFolderExists(obj.foldername); + h = createStandardFigure(); + obj.plot(xVariable, yVariable{:}); + + % Export plot + print(h, '-dpng', '-r600', obj.foldername + '/' + obj.filename + '.png'); + close(h); +end diff --git a/lib/storage/@LevelData/plotTriangulation.m b/lib/storage/@LevelData/plotTriangulation.m new file mode 100644 index 0000000000000000000000000000000000000000..caae9243368c7b71ea42969b4b75d276acc8a8c0 --- /dev/null +++ b/lib/storage/@LevelData/plotTriangulation.m @@ -0,0 +1,48 @@ +function ax = plotTriangulation(obj, jLevel, opt) +%%PLOTTRIANGULATION creates (a series of) plots for triangulations (must be +%stored as pair c4n (double: nNodes x 2) and n4e (int: nElem x 3) or as +%pair coordinates (double: 2 x nNodes) and elements (int: 3 x nElem) +% ax = PLOTTRIANGULATION(obj) plots all levels +% ax = PLOTTRIANGULATION(obj, jLevel) plots levels specified in jLevel +% ax = PLOTTRIANGULATION(obj, jLevel, opt) additionally specifies options: +% - timePerLevel (default 1s) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + jLevel (1,:) {mustBeIndexVector} = ':' + opt.timePerLevel {mustBeNumeric} = 1 + end + + if strcmp(jLevel, ':') + jLevel = 1:obj.nLevel; + end + + % Create axes object + ax = gca; + + % Plot every frame + % TODO: store whole mesh such that also boundary can be plotted? + for k = jLevel + coordinates = obj.level(k).coordinates; + elements = obj.level(k).elements; + mesh = Mesh(coordinates, elements, {}); + plot(mesh, 'FaceAlpha', 0.0); + pause(opt.timePerLevel); + end +end \ No newline at end of file diff --git a/lib/storage/@LevelData/plotTriangulationToFile.m b/lib/storage/@LevelData/plotTriangulationToFile.m new file mode 100644 index 0000000000000000000000000000000000000000..65db0d8d202941a4609e4ada808382c5bfe55e64 --- /dev/null +++ b/lib/storage/@LevelData/plotTriangulationToFile.m @@ -0,0 +1,79 @@ +function plotTriangulationToFile(obj, jLevel, opt) +%%PLOTTRIANGULATIONTOFILE creates (a series of) plots for triangulations +%and stores it to a file, filename is generated automatically from +%information in LevelData object +% ax = PLOTTRIANGULATIONTOFILE(obj) plots all levels +% ax = PLOTTRIANGULATIONTOFILE(obj, jLevel) plots levels specified in jLevel +% ax = PLOTTRIANGULATIONTOFILE(obj, jLevel, opt) additionally specifies options: +% - timePerLevel (default 1s) +% +% See also LevelData/plotTriangulation + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + jLevel (1,:) {mustBeIndexVector} = ':' + opt.timePerLevel {mustBeNumeric} = 1 + end + + if strcmp(jLevel, ':') + jLevel = 1:obj.nLevel; + end + + ensureFolderExists(obj.foldername); + h = createStandardFigure(); + + if length(jLevel) == 1 + % Create single image + coordinates = obj.level(jLevel).coordinates; + elements = obj.level(jLevel).elements; + mesh = Mesh(coordinates, elements, {}); + plot(mesh); + print(h, '-dpng', '-r600', obj.foldername + '/' + obj.filename + '.png'); + else + % Create video as series of images + createTriangulationAVI(obj, h, jLevel, opt.timePerLevel); + end + + close(h); +end + + +function createTriangulationAVI(obj, h, jLevel, secondsPerFrame) + % video quality parameter + FPS = 1 / secondsPerFrame; + + % Create video writer + videowriter = VideoWriter(obj.foldername + '/' + obj.filename + '.avi'); + set(videowriter, 'FrameRate', FPS); + open(videowriter); + + ax = gca(); + set(ax, 'NextPlot', 'replaceChildren'); + + % Plot every frame + for k = jLevel + coordinates = obj.level(k).coordinates; + elements = obj.level(k).elements; + mesh = Mesh(coordinates, elements, {}); + plot(mesh, 'FaceAlpha', 0.0); + writeVideo(videowriter, getframe(h)); + end + + close(videowriter); +end diff --git a/lib/storage/@LevelData/printHeader.m b/lib/storage/@LevelData/printHeader.m new file mode 100644 index 0000000000000000000000000000000000000000..3888afcbde7a1d02ca10127356b141b45f0e5303 --- /dev/null +++ b/lib/storage/@LevelData/printHeader.m @@ -0,0 +1,32 @@ +function printHeader(obj) +%%PRINTHEADER prints header for output to command line +% PRINTHEADER(obj) +% +% See also LevelData/printLevel + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + % Create header with variable names + specifier = obj.getHeaderSpecifier(' '); + header = sprintf(specifier, obj.scalarVariable{:}); + + % Print header to command line + fprintf(header); + + % Print separating horizontal line + fprintf([repmat('-', 1, length(header)-1), '\n']); +end \ No newline at end of file diff --git a/lib/storage/@LevelData/printLevel.m b/lib/storage/@LevelData/printLevel.m new file mode 100644 index 0000000000000000000000000000000000000000..110e49f1c597600ba3cedd711f4b866ad4497d72 --- /dev/null +++ b/lib/storage/@LevelData/printLevel.m @@ -0,0 +1,49 @@ +function printLevel(obj, jLevel) +%%PRINTLEVEL prints information on the given levels to command line +% PRINTLEVEL(obj) defaults to final level +% PRINTLEVEL(obj, jLevel) +% +% See also LevelData/printHeader + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + jLevel (1,:) double = obj.nLevel + end + + % Print header in case of plotting the first level + if obj.isInitialRun() + obj.printHeader(); + end + + % Iterate over given list of levels + specifier = obj.getFormatSpecifier(' '); + for k = 1:length(jLevel) + % Extract data of all variables + data = cell(obj.nVariable, 1); + ind = 0; + for jVariable = 1:obj.nVariable + if obj.isScalar(jVariable) + ind = ind + 1; + data{ind} = obj.level(jLevel(k)).(obj.label{jVariable}); + end + end + % Print information on current level to command line + fprintf(specifier, data{1:ind}); + end +end diff --git a/lib/storage/@LevelData/saveToFile.m b/lib/storage/@LevelData/saveToFile.m new file mode 100644 index 0000000000000000000000000000000000000000..d18503ed503331bff2e8355fb61604c082f80370 --- /dev/null +++ b/lib/storage/@LevelData/saveToFile.m @@ -0,0 +1,35 @@ +function saveToFile(obj, folder, file) +%%SAVETOFILE saves this object to a file with automatically +%generated folder and file names if no optional arguments are +%specified +% obj.SAVETOFILE() +% obj.SAVETOFILE(folder, file) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + folder {mustBeTextScalar} = obj.foldername + file {mustBeTextScalar} = obj.filename + end + + % Create problem- and method-specific folder + ensureFolderExists(folder); + % Save this object to file + pathToMat = folder + '/' + file + '.mat'; + save(pathToMat, 'obj', '-v7.3'); +end diff --git a/lib/storage/@LevelData/saveToTable.m b/lib/storage/@LevelData/saveToTable.m new file mode 100644 index 0000000000000000000000000000000000000000..7527c78ce0d47270bd01b9d2c35d1efedcb504ba --- /dev/null +++ b/lib/storage/@LevelData/saveToTable.m @@ -0,0 +1,48 @@ +function saveToTable(obj, separator) +%%SAVETOTABLE stores all level information in a text file, defaults to +%csv-format (separator = ','), filename is generated automatically from +%information in LevelData object +% SAVETOTABLE(obj) +% SAVETOTABLE(obj, separator) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + separator {mustBeTextScalar} = ',' + end + + % Open file + ensureFolderExists(obj.foldername); + fid = fopen(obj.foldername + '/' + obj.filename + '.csv', 'w'); + + % Save header to file + specifier = obj.getHeaderSpecifier(separator); + fprintf(fid, specifier, obj.scalarVariable{:}); + + % Save information on each level to file + specifier = obj.getFormatSpecifier(separator); + for k = 1:obj.nLevel + data = cell(obj.nScalarVariable, 1); + for j = 1:obj.nScalarVariable + data{j} = obj.level(k).(obj.scalarVariable{j}); + end + fprintf(fid, specifier, data{:}); + end + + fclose(fid); +end diff --git a/lib/storage/@LevelData/set.m b/lib/storage/@LevelData/set.m new file mode 100644 index 0000000000000000000000000000000000000000..7a3ee72195dcda906172a451be472995e9f035ab --- /dev/null +++ b/lib/storage/@LevelData/set.m @@ -0,0 +1,65 @@ +function set(obj, jLevel, variableName, value) +%%SET stores specified data to this LevelData object for the specified list +%jLevel of levels. The variableName/value pairs can be repeating, their first +%dimension must correspond to number of levels +% SET(obj, jLevel, variableName, value, ...) +% SET(obj, ':', variableName, value, ...) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + jLevel {mustBeIndexVector} = ':' + end + + arguments (Repeating) + variableName {mustBeTextScalar} + value + end + + if strcmp(jLevel, ':') + jLevel = 1:obj.nLevel; + end + + % Determine number of arguments + nArgument = length(variableName); + + % Store data to LevelData object + for j = 1:nArgument + name = variableName{j}; + val = value{j}; + + if isa(val, 'ValueDetails') + % Copy type argument + currentType = val; + valueList = repmat({nan}, length(jLevel), 1); + else + [valueList, currentType] = splitIntoLevelWiseData(length(jLevel), val); + end + + % Store type and category + if ~ismember(name, obj.label) + obj.type(name) = currentType.adaptPrintWidthTo(name); + obj.category(name) = DataCategory.ERROR; + end + + % Store level-oriented data + for k = 1:length(jLevel) + obj.level(jLevel(k)).(name) = valueList{k}; + end + end +end \ No newline at end of file diff --git a/lib/storage/@LevelData/setAbsolute.m b/lib/storage/@LevelData/setAbsolute.m new file mode 100644 index 0000000000000000000000000000000000000000..d5fbf92a22bc227a71c503f2f1e56d0cd875aa23 --- /dev/null +++ b/lib/storage/@LevelData/setAbsolute.m @@ -0,0 +1,68 @@ +function setAbsolute(obj, jLevel, variableName, value) +%%SETABSOLUTE stores specified data of absolute-niveau type (constants / +%eigenvalues / efficiency indices / ...) to this LevelData object for the +%specified list jLevel of levels, repeated pairs of variable names and values, +%first dimension of data must correspond to number of levels +% SETABSOLUTE(obj, jLevel, variableName, value, ...) +% SETABSOLUTE(obj, ':', variableName, value, ...) +% +% See also LevelData/set + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + jLevel {mustBeIndexVector} = ':' + end + + arguments (Repeating) + variableName {mustBeTextScalar} + value + end + + if strcmp(jLevel, ':') + jLevel = 1:obj.nLevel; + end + + % Determine number of arguments + nArgument = length(variableName); + + % Store data to LevelData object + for j = 1:nArgument + name = variableName{j}; + val = value{j}; + + if isa(val, 'ValueDetails') + % Copy type argument + currentType = val; + valueList = repmat({nan}, length(jLevel), 1); + else + [valueList, currentType] = splitIntoLevelWiseData(length(jLevel), val); + end + + % Store type and category + if ~ismember(name, obj.absoluteVariable) + obj.type(name) = currentType.adaptPrintWidthTo(name); + obj.category(name) = DataCategory.ABSOLUTE; + end + + % Store level-oriented data + for k = 1:length(jLevel) + obj.level(jLevel(k)).(name) = valueList{k}; + end + end +end \ No newline at end of file diff --git a/lib/storage/@LevelData/setTime.m b/lib/storage/@LevelData/setTime.m new file mode 100644 index 0000000000000000000000000000000000000000..c492f28de989cb54686235506c863c0ebceeb1ed --- /dev/null +++ b/lib/storage/@LevelData/setTime.m @@ -0,0 +1,68 @@ +function setTime(obj, jLevel, variableName, value) +%%SETTIME stores specified data of increasing type (time / ndof / ...) to +%this LevelData object for the specified list jLevel of levels, repeated pairs +%of variable names and values, first dimension of data must correspond to number +%of levels +% SETTIME(obj, jLevel, variableName, value, ...) +% SETTIME(obj, ':', variableName, value, ...) +% +% See also LevelData/set + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + jLevel {mustBeIndexVector} = ':' + end + + arguments (Repeating) + variableName {mustBeTextScalar} + value + end + + if strcmp(jLevel, ':') + jLevel = 1:obj.nLevel; + end + + % Determine number of arguments + nArgument = length(variableName); + + % Store data to LevelData object + for j = 1:nArgument + name = variableName{j}; + val = value{j}; + + if isa(val, 'ValueDetails') + % Copy type argument + currentType = val; + valueList = repmat({nan}, length(jLevel), 1); + else + [valueList, currentType] = splitIntoLevelWiseData(length(jLevel), val); + end + + % Store type and category + if ~ismember(name, obj.label) + obj.type(name) = currentType.adaptPrintWidthTo(name); + obj.category(name) = DataCategory.TIME; + end + + % Store level-oriented data + for k = 1:length(jLevel) + obj.level(jLevel(k)).(name) = valueList{k}; + end + end +end \ No newline at end of file diff --git a/lib/storage/@LevelDataCollection/LevelDataCollection.m b/lib/storage/@LevelDataCollection/LevelDataCollection.m new file mode 100644 index 0000000000000000000000000000000000000000..bf886751ea8dee3113f5e910143440ba1514010c --- /dev/null +++ b/lib/storage/@LevelDataCollection/LevelDataCollection.m @@ -0,0 +1,182 @@ +classdef LevelDataCollection < handle +%%LEVELDATACOLLECTION class representing a collection of LevelData objects + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + + properties + metaData + % Root folder for file storage + root = 'results' + % Cell array of LevelData objects + item = cell(0) + end + + properties (Dependent) + % Path to folder for file storage + foldername (1,:) char + % Name of file for storage + filename (1,:) char + % Number of items + nItem (1,1) integer + % Cell array of names of timing variables + timeVariable (1,:) cell + % Number of time variables + nTimeVariable (1,1) int32 + % Boolean indicating whether data has been stored for a single level + isInitialRun (1,1) boolean + end + + properties (Access=protected) + % Separator for printing to command line + separator + % Minimal width of variables for printing in command line + minimalWidth = 8; + end + + methods + %% CONSTRUCTOR + function obj = LevelDataCollection(rootpath, identifier) + %%LEVELDATACOLLECTION + + % Set root path for file storage + if nargin >= 1 + obj.root = rootpath; + end + + obj.metaData = dictionary(... + "problem", "problem", ... + "domain", "domain", ... + "method", "method", ... + "identifier", "main", ... + "hostname", string(getHostname()), ... + "timestamp", string(datetime('now', 'Format', 'yyyy-MM-dd_HH:mm:ss'))); + + % Set identifier + if nargin >= 2 + obj.metaData("identifier") = identifier; + end + end + + %% GET FUNCTIONS FOR DEPENDENT VARIABLES + function nItem = get.nItem(obj) + nItem = length(obj.item); + end + + function timeVariable = get.timeVariable(obj) + if isempty(obj.item) + timeVariable = {}; + else + timeVariable = obj.item{1}.timeVariable; + end + end + + function nVariable = get.nTimeVariable(obj) + nVariable = length(obj.timeVariable); + end + + function bool = get.isInitialRun(obj) + bool = (obj.nItem <= 1); + end + + function path = get.foldername(obj) + rootpath = obj.root; + if numel(rootpath) > 0 && ~strcmp(rootpath(end), '/') + rootpath = [rootpath, '/']; + end + path = rootpath + strjoin([obj.metaData("problem"), obj.metaData("domain"), obj.metaData("method")], '_'); + end + + function file = get.filename(obj) + file = obj.metaData("identifier") + '_collection'; + end + + function spec = getHeaderSpecifier(obj, separator) + % Creates formatting string for the header of the output to command line + spec = cell(1, obj.nTimeVariable+1); + spec{1} = ' run'; + for j = 1:obj.nTimeVariable + t = obj.item{1}.type(obj.timeVariable{j}); + spec{j+1} = assembleSpecifier(obj.getWidth(t), 's'); + end + spec = strjoin(spec, separator) + "\n"; + end + + function spec = getFormatSpecifier(obj, separator) + % Creates formatting string for printing to command line + spec = cell(1, obj.nTimeVariable+1); + spec{1} = '%4d'; + for j = 1:obj.nTimeVariable + t = obj.item{1}.type(obj.timeVariable{j}); + spec{j+1} = assembleSpecifier(obj.getWidth(t), t.formatSpec); + end + spec = strjoin(spec, separator) + "\n"; + end + + %% READ ITEM DATA + output = get(obj, jItem, varargin) + + %% MODIFY ITEMS + set(obj, jItem, varargin) + + function append(obj, data) + %%APPEND simplified addition specified of data to this list, one or + %more LevelData objects + % APPEND(obj, data) + + arguments + obj + end + + arguments (Repeating) + data LevelData + end + + indices = obj.nItem + (1:length(data)); + obj.set(indices, data{:}); + end + + function remove(obj, indices) + %%REMOVE removes the specified list of objects + % REMOVE(obj, indices) + indices = intersect(indices, 1:obj.nItem); + obj.item{indices} = []; + end + + %% OUTPUT ITEM DATA + printHeader(obj) + printItem(obj, jItem) + printStatistics(obj, variableName) + plotStatistics(obj, xVariable, variableName) + + %% EXPORT COMPLETE DATA + saveToFile(obj, folder, file) + saveToTable(obj, separator) + end + + methods (Access = private) + function width = getWidth(obj, type) + width = num2str(max(type.printWidth, obj.minimalWidth)); + end + + printTable(obj, fid, variableName, data, separator) + end +end + +function spec = assembleSpecifier(width, format) + spec = ['%', num2str(width), format]; +end \ No newline at end of file diff --git a/lib/storage/@LevelDataCollection/get.m b/lib/storage/@LevelDataCollection/get.m new file mode 100644 index 0000000000000000000000000000000000000000..025502e872f02d5df631df4b34724f74325186e6 --- /dev/null +++ b/lib/storage/@LevelDataCollection/get.m @@ -0,0 +1,60 @@ +function output = get(obj, jItem, variableName) +%%GET extracts data from this LevelDataCollection object for a given list +%jItem of item numbers. One or more variables to be returned can be specified by +%their names. +% output = GET(obj, jItem, variableName, ...) +% output = GET(obj, ':', variableName, ...) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + jItem {mustBeIndexVector} = ':' + end + + arguments (Repeating) + variableName {mustBeTextScalar} + end + + if strcmp(jItem, ':') + jItem = 1:obj.nItem; + end + + % Initialise output variable + output = cell(1, length(variableName)); + + % Determine number of levels (uses first item!) + nLevel = obj.item{1}.nLevel; + + % Iterate over variables + for jVariable = 1:length(variableName) + name = variableName{jVariable}; + % Check for presence of variable + if ~ismember(name, obj.timeVariable) + warning(['Variable ', name, ' not found']); + data = []; + else + data = zeros(nLevel, length(jItem)); + % Iterate over items + for k = 1:length(jItem) + data(:,k) = obj.item{jItem(k)}.get(':', name); + end + end + % Store to return variable + output{jVariable} = data; + end +end \ No newline at end of file diff --git a/lib/storage/@LevelDataCollection/plotStatistics.m b/lib/storage/@LevelDataCollection/plotStatistics.m new file mode 100644 index 0000000000000000000000000000000000000000..3c81e352890e4e5efae7738d45e13f209b75d13c --- /dev/null +++ b/lib/storage/@LevelDataCollection/plotStatistics.m @@ -0,0 +1,90 @@ +function ax = plotStatistics(obj, xVariable, yVariable) +%%PLOTSTATISTICS plots with statistical information (mean value with min +%and max value) of scalar time variables (specified in yVariable) with +%respect to the variable xVariable, returns handle to axis object +% PLOTSTATISTICS(obj, xVariable, yVariable, ...) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + xVariable {mustBeTextScalar} + end + + arguments (Repeating) + yVariable + end + + % Choose time variables for plotting + if isempty(yVariable) + yVariable = obj.timeVariable; + else + yVariable = intersect(yVariable, obj.timeVariable); + end + + % Extract data for x-axis + xData = obj.item{1}.get(':', xVariable); + + % Extract data from each item and on each level + data = obj.get(':', yVariable{:}); + + % Create plot + ax = plotData(xData, yVariable, data); + + % Add title + title(ax, 'Runtime plot'); + + % Add axes labels + xlabel(ax, xVariable); + ylabel(ax, 'runtime (in sec.)'); + + % Update legend + configureLegend(ax, 'northwest'); +end + + +function ax = plotData(xData, variableName, data) +%%PLOTDATA auxiliary private function for creation of plots, returns handle +%to axis object +% ax = PLOTDATA(xData, variableName, data) + + % Create handle to currently active axis object + ax = gca; + + % Create unified list for formatting plot lines + [COLOURORDER, MARKER] = getPlotStyle(); + + % Iterate over given variables + for j = 1:length(variableName) + % Compute statistics + yMean = mean(data{j}, 2); + yMin = min(data{j}, [], 2); + yMax = max(data{j}, [], 2); + % Extract label for legend from dictionary + variableLabel = variableName{j}; + % Create plot + errorbar(ax, xData, yMean, ... + yMean - yMin, yMax - yMean, ... + 'LineStyle', '-', 'Marker', '.', ... + 'Color', COLOURORDER(j,:), ... + 'DisplayName', variableLabel); + % Use double-logarithmic scales + set(ax, 'XScale', 'log', 'YScale', 'log'); + % Add new line into the current figure when calling plotConvergence again + hold(ax, 'on'); + end +end diff --git a/lib/storage/@LevelDataCollection/printHeader.m b/lib/storage/@LevelDataCollection/printHeader.m new file mode 100644 index 0000000000000000000000000000000000000000..b3225a5b71aa7b68a27a5c8de60bdfc82d0324d9 --- /dev/null +++ b/lib/storage/@LevelDataCollection/printHeader.m @@ -0,0 +1,32 @@ +function printHeader(obj) +%%PRINTHEADER prints header for output to command line +% PRINTHEADER(obj) +% +% See also LevelDataCollection/printItem + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + + % Create header with variable names + header = sprintf(obj.getHeaderSpecifier(' '), obj.timeVariable{:}); + + % Print header to command line + fprintf(header); + + % Print separating horizontal line + fprintf([repmat('-', 1, length(header)-1), '\n']); +end diff --git a/lib/storage/@LevelDataCollection/printItem.m b/lib/storage/@LevelDataCollection/printItem.m new file mode 100644 index 0000000000000000000000000000000000000000..b83bfd210266fae755f8f4acf813b735dafee24e --- /dev/null +++ b/lib/storage/@LevelDataCollection/printItem.m @@ -0,0 +1,48 @@ +function printItem(obj, jItem) +%%PRINTITEM prints information on the given list of item indices to command +%line +% PRINTITEM(obj) defaults to final level +% PRINTITEM(obj, jItem) +% +% See also LevelDataCollection/printHeader + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + jItem = obj.nItem + end + + % Print header in case of plotting the first level + if obj.isInitialRun() + obj.printHeader(); + end + + % Iterate over given list of item indices + for k = 1:length(jItem) + % Extract data of all time variables + data = cell(obj.nTimeVariable+1, 1); + data{1} = jItem(k); + ind = 1; + for l = 1:obj.nTimeVariable + ind = ind + 1; + data{ind} = obj.item{jItem(k)}.level.(obj.timeVariable{l}); + end + % Print information on current item to command line + fprintf(obj.getFormatSpecifier(' '), data{1:ind}); + end +end \ No newline at end of file diff --git a/lib/storage/@LevelDataCollection/printStatistics.m b/lib/storage/@LevelDataCollection/printStatistics.m new file mode 100644 index 0000000000000000000000000000000000000000..1c9766a445b5d2e4ddf419950995a9145ce54850 --- /dev/null +++ b/lib/storage/@LevelDataCollection/printStatistics.m @@ -0,0 +1,50 @@ +function printStatistics(obj, variable) +%%PRINTSTATISTICS prints statististical information of scalar time +%variables (specified in variable) +% PRINTSTATISTICS(obj, variable, ...) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + end + + arguments (Repeating) + variable {mustBeTextScalar} + end + + % Choose time variables for plotting + if isempty(variable) + variable = obj.timeVariable; + else + variable = intersect(variable, obj.timeVariable); + end + + % Extract data from each item and on each level + data = obj.get(':', variable{:}); + + % Print separator between tables + fprintf('\n'); + + % Print table for each given variable + for j = 1:length(variable) + obj.printTable(1, variable{j}, data{j}, ' '); + fprintf('\n\n'); + end +end + + diff --git a/lib/storage/@LevelDataCollection/printTable.m b/lib/storage/@LevelDataCollection/printTable.m new file mode 100644 index 0000000000000000000000000000000000000000..7756663e2784ed9bde0f747d0d9a34c3d595f3f4 --- /dev/null +++ b/lib/storage/@LevelDataCollection/printTable.m @@ -0,0 +1,48 @@ +function printTable(obj, fid, variableName, data, separator) +%%PRINTTABLE auxiliary private function for printing statististical +%information for data of scalar time variables variableName to command +%line (fid=1) or file (specified by file identifier fid) +% PRINTTABLE(obj, fid, variableName, data) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + + % Define formatting strings for title and headline + TITLE = ['# TIME STATISTICS - ', variableName]; + HEADLINE = sprintf(['%5s', repmat([separator, '%11s'], 1, 5)],... + 'level', 'mean', 'median', 'std', 'min', 'max'); + + if fid == 1 + % Print to command line (including title) + fprintf(fid, [TITLE, '\n\n', HEADLINE, '\n', ... + repmat('-', 1, length(HEADLINE)), '\n']); + else + % Print to file + fprintf(fid, [HEADLINE, '\n']); + end + + % Compute statistical information + statisticalData = [mean(data, 2),... + median(data, 2),... + std(data, 0, 2),... + min(data, [], 2),... + max(data, [], 2)]; + + % Print information + fprintf(fid, ['%5d', repmat([separator, '%8.5e'], 1, 5), '\n'],... + [1:size(data, 1); permute(statisticalData, [2 1])]); +end \ No newline at end of file diff --git a/lib/storage/@LevelDataCollection/saveToFile.m b/lib/storage/@LevelDataCollection/saveToFile.m new file mode 100644 index 0000000000000000000000000000000000000000..d1aca0c2a977d9513ad522b6b3d40cb21047c62b --- /dev/null +++ b/lib/storage/@LevelDataCollection/saveToFile.m @@ -0,0 +1,35 @@ +function saveToFile(obj, folder, file) +%%SAVETOFILE saves this object to a file with automatically +%generated folder and file names if no optional arguments are +%specified +% obj.SAVETOFILE() +% obj.SAVETOFILE(folder, file) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + folder = obj.foldername + file = obj.filename + end + + % Create problem- and method-specific folder + ensureFolderExists(folder); + + % Save this object to file + save(folder + '/' + file + '.mat', 'obj', '-v7.3'); +end diff --git a/lib/storage/@LevelDataCollection/saveToTable.m b/lib/storage/@LevelDataCollection/saveToTable.m new file mode 100644 index 0000000000000000000000000000000000000000..1ec56e3af4dd4af377b38c19933271e5e15dd937 --- /dev/null +++ b/lib/storage/@LevelDataCollection/saveToTable.m @@ -0,0 +1,40 @@ +function saveToTable(obj, separator) +%%SAVETOTABLE stores statistical information for all time variables to a +%text file, defaults to csv-format (separator = ','), filename is generated +%automatically from information in LevelData object +% SAVETOTABLE(obj) +% SAVETOTABLE(obj, separator) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + separator {mustBeTextScalar} = ',' + end + + % Create problem- and method-specific folder + ensureFolderExists(obj.foldername); + + % Save data for each variable to a separate file + data = obj.get(':', obj.timeVariable{:}); + for j = 1:obj.nTimeVariable + fid = fopen(obj.foldername + '/' + obj.filename + '_' + ... + obj.timeVariable{j} + '.csv', 'w'); + obj.printTable(fid, obj.timeVariable{j}, data{j}, separator); + fclose(fid); + end +end diff --git a/lib/storage/@LevelDataCollection/set.m b/lib/storage/@LevelDataCollection/set.m new file mode 100644 index 0000000000000000000000000000000000000000..6ca051f4beade0d6e069ee543d982243f2498f45 --- /dev/null +++ b/lib/storage/@LevelDataCollection/set.m @@ -0,0 +1,39 @@ +function set(obj, jItem, data) +%%SET stores specified list of LevelData objects to the indices determined +%in the jItem array +% SET(obj, jItem, data) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + obj + jItem + end + + arguments (Repeating) + data LevelData + end + + % Number of items and item indices must coincide + assert(length(jItem) == length(data), ... + 'Number of indices must equal number of given arguments'); + + % Store items + for k = 1:length(jItem) + obj.item{jItem(k)} = data{k}; + end +end \ No newline at end of file diff --git a/lib/storage/DataCategory.m b/lib/storage/DataCategory.m new file mode 100644 index 0000000000000000000000000000000000000000..f4ea739eee9d02b7de5f64161fdc34135701490f --- /dev/null +++ b/lib/storage/DataCategory.m @@ -0,0 +1,23 @@ +classdef DataCategory + enumeration + ERROR(@loglog, 'Convergence history plot', 'error', 'northeast') + ABSOLUTE(@semilogx, 'Value plot', 'value', 'northeast') + TIME(@loglog, 'Time plot', 'runtime', 'northwest') + end + + properties (SetAccess=immutable) + plotFunction function_handle + title string + yLabel string + legendLocation string + end + + methods + function obj = DataCategory(plotFunction, title, yLabel, legendLocation) + obj.plotFunction = plotFunction; + obj.title = title; + obj.yLabel = yLabel; + obj.legendLocation = legendLocation; + end + end +end \ No newline at end of file diff --git a/lib/storage/TimeIt.m b/lib/storage/TimeIt.m new file mode 100644 index 0000000000000000000000000000000000000000..dfc7b06e12aff8d96cb7c38535242a70f3378a3d --- /dev/null +++ b/lib/storage/TimeIt.m @@ -0,0 +1,77 @@ +function leveldatacollection = TimeIt(identifier, nRun, storeResults, functionName, arguments) +%%TIMEIT function wrapper for multiple runs of functions storing timing +%data in LevelData objects +% leveldatacollection = TIMEIT(identifier, nRun, functionName, arguments, ...) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + arguments + identifier {mustBeTextScalar} + nRun (1,1) double + storeResults (1,1) logical + functionName {mustBeTextScalar} + end + + arguments (Repeating) + arguments + end + + % Welcome statement + fprintf('\n#\n# MooAFEM - TIME MEASUREMENT\n'); + fprintf('# Current Time: %s\n#\n\n', datestr(now)); + fprintf('This is TimeIt wrapper for function "%s"\n\n', functionName); + + % Initialisation of collection for LevelData objects + leveldatacollection = LevelDataCollection(); + leveldatacollection.metaData("identifier") = identifier; + + % Run experiments + for j = 1:nRun + % Run current experiment + temporaryIdentifier = sprintf('timingRun%04d', j); + outputList = fevalc(functionName, arguments); + leveldata = outputList{1}; + leveldata.metaData("identifier") = temporaryIdentifier; + % Remove unused information + leveldata.removeNonscalar(); + % Store information + leveldatacollection.append(leveldata); + if storeResults + % Save intermediate collection to file + leveldatacollection.saveToFile(); + end + % Print information on current run + leveldatacollection.printItem(); + end + + % Closing + fprintf('\n'); +end + + +function output = fevalc(functionName, arguments) %#ok<INUSD> +%%FEVALC suppresses output to commandline + + % Create function call + functioncall = 'feval(functionName, arguments{:})'; + % Call function + [~, output] = evalc(functioncall); + % Store output in cell variable + if ~iscell(output) + output = {output}; + end +end diff --git a/lib/storage/Type/RawType.m b/lib/storage/Type/RawType.m new file mode 100644 index 0000000000000000000000000000000000000000..94bdffa19c127733384cf6dbebfe85e9a00fd95f --- /dev/null +++ b/lib/storage/Type/RawType.m @@ -0,0 +1,18 @@ +classdef RawType + enumeration + INT('d') + FLOAT('.5e') + TEXT('s') + end + + properties (SetAccess=immutable, GetAccess=public) + formatSpec + + end + + methods + function obj = RawType(formatSpec) + obj.formatSpec = formatSpec; + end + end +end \ No newline at end of file diff --git a/lib/storage/Type/ValueDetails.m b/lib/storage/Type/ValueDetails.m new file mode 100644 index 0000000000000000000000000000000000000000..e82680e5e5bc514d1530283422e44ee2fadaa34d --- /dev/null +++ b/lib/storage/Type/ValueDetails.m @@ -0,0 +1,107 @@ +classdef ValueDetails +%%ValueDetails class representing the type and printing parameters of a level +%oriented data + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + + properties (GetAccess=public, SetAccess=protected) + % Raw type + rawType + % Shape of data (scalar, vector, or array) + shape + % Number of characters required for printing + printWidth + end + + properties (Dependent) + % Format specification for printing + formatSpec + % Boolean determining if type is scalar + isScalar (1,1) boolean + end + + methods + %% CONSTRUCTOR + function obj = ValueDetails(rawType, shape, printWidth) + %%VALUEDETAILS creates an instance of ValueDetails given an example + %value of the corresponding data + % obj = VALUEDETAILS(rawType, shape, printWidth) + + obj.rawType = rawType; + obj.shape = shape; + obj.printWidth = printWidth; + end + + function obj = adaptPrintWidthTo(obj, name) + %ADAPTPRINTWIDTHTO updates the print width considering the length of + %variable name + obj.printWidth = max(obj.printWidth, length(name)); + end + + function formatSpec = get.formatSpec(obj) + formatSpec = obj.rawType.formatSpec; + end + + function bool = get.isScalar(obj) + bool = strcmp(obj.shape, 's'); + end + end + + % AUXILIARY + methods (Static) + function details = fromExample(value) + %%FROMEXAMPLE creates an instance of ValueDetails given an example + %value of the corresponding data + % obj = FROMEXAMPLE(value) + + % Determine data type and width + if isnan(value) + % NaN is treated as integer to avoid problems with plotting + rawType = RawType.INT; + printWidth = 3; + elseif isinteger(value) + rawType = RawType.INT; + S = cellstr(num2str(value)); + printWidth = max(cellfun(@(x) length(x), S(:))); + elseif isfloat(value) + rawType = RawType.FLOAT; + floatPrecision = str2double(RawType.FLOAT.formatSpec(2)); + printWidth = 6 + floatPrecision; + elseif ischar(value) + rawType = RawType.TEXT; + printWidth = length(value); + elseif isstring(value) + rawType = RawType.TEXT; + printWidth = max(cellfun(@(x) length(x), value(:))); + else + error('Invalid data type'); + end + + % Determine shape + if isscalar(value) + shape = 's'; + elseif isvector(value) + shape = 'v'; + else + shape = 'a'; + end + + details = ValueDetails(rawType, shape, printWidth); + end + end +end diff --git a/lib/storage/Type/splitIntoLevelWiseData.m b/lib/storage/Type/splitIntoLevelWiseData.m new file mode 100644 index 0000000000000000000000000000000000000000..d399959292ad181a627cda0b7569d19a40c02ef8 --- /dev/null +++ b/lib/storage/Type/splitIntoLevelWiseData.m @@ -0,0 +1,64 @@ + +function [valueList, type] = splitIntoLevelWiseData(nLevel, value) +%%DETERMINETYPEVALUE auxiliary function for determining type from an array +%of values +% [type, valueList] = DETERMINETYPEVALUE(nLevel, variableName, value) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + if nLevel == 1 + % Array value represents data on a single level + type = ValueDetails.fromExample(value); + valueList = {value}; + else + if isvector(value) + % Array value represents a vector of scalar values for each % level + assert(length(value) == nLevel, 'Length of argument invalid'); + type = ValueDetails.fromExample(value(1)); + valueList = mat2cell(value(:), ones(nLevel, 1)); + else + % Array value represents an higher dimensional objects for each % level + dims = size(value); + [levelDim, idx] = extractSingleLevelDimension(dims, nLevel); + % Extract values + valueList = cell(nLevel, 1); + for k = 1:nLevel + idx(levelDim) = k; + valueList{k} = subsref(value, idx); + end + type = ValueDetails.fromExample(valueList{end}); + end + end +end + +function [levelDim, idx] = extractSingleLevelDimension(dims, nLevel) + levelDim = find(dims == nLevel); + if isempty(levelDim) + error('Unable to extract data from argument'); + end + + if length(levelDim) > 1 + % Several dimensions could represent the level information + levelDim = levelDim(1); + warning('Unclear dimensions of argument. Assume level-oriented data in dimension %d.', levelDim); + end + + % index that is ':' for all dimensions, and where the index in the level + % dimension can be changed + idx.subs = repmat({':'}, 1, ndims(value)); + idx.type = '()'; +end \ No newline at end of file diff --git a/lib/storage/configureLegend.m b/lib/storage/configureLegend.m new file mode 100644 index 0000000000000000000000000000000000000000..fe1a7c95992effeec2425a89aab902fb2315572b --- /dev/null +++ b/lib/storage/configureLegend.m @@ -0,0 +1,31 @@ +function configureLegend(ax, location) +%%CONFIGURELEGEND formats the legend of the current axis and displays it in +%the specified location +% CONFIGURELEGEND(ax, location) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + + % Create handle to legend in current axis + leg = legend(ax); + + % Specify location + set(leg, 'Location', location); + + % Set interpreter to latex + set(leg, 'Interpreter', 'latex'); +end diff --git a/lib/storage/createStandardFigure.m b/lib/storage/createStandardFigure.m new file mode 100644 index 0000000000000000000000000000000000000000..2f1d4cfb83fe436d9b8c68b09ffe1ae40374d046 --- /dev/null +++ b/lib/storage/createStandardFigure.m @@ -0,0 +1,26 @@ +function h = createStandardFigure() +%%CREATESTANDARDFIGURE creates a new figure object with default choice of +%size, the modification of this file allows a unified formatting of all +%figures for the output in the LevelData class +% h = CREATESTANDARDFIGURE() + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + % Create figure + h = figure('Units', 'centimeters', 'Position', 3 + [0 0 32 24]); + +end \ No newline at end of file diff --git a/lib/storage/ensureFolderExists.m b/lib/storage/ensureFolderExists.m new file mode 100644 index 0000000000000000000000000000000000000000..d29bd1942e064b5e3107b429554e64c9b533e411 --- /dev/null +++ b/lib/storage/ensureFolderExists.m @@ -0,0 +1,30 @@ +function ensureFolderExists(path) +%%ENSUREFOLDEREXISTS creates a folder if it does not exist yet +% ENSUREFOLDEREXISTS(PATH) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + if isfolder(path) + folderExists = true; + else + folderExists = mkdir(path); + end + + if ~folderExists + error('Could not create folder %s', path); + end +end \ No newline at end of file diff --git a/lib/storage/getDefaultDictionary.m b/lib/storage/getDefaultDictionary.m new file mode 100644 index 0000000000000000000000000000000000000000..61e7218ea3474c8c7fdefc169f9d03edb7cc3745 --- /dev/null +++ b/lib/storage/getDefaultDictionary.m @@ -0,0 +1,52 @@ +function dict = getDefaultDictionary() +%%GETDEFAULTDICTIONARY allows to specify default structure arrays for +%dictionaries for legend labels in plots +% dict = GETDEFAULTDICTIONARY() + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + + % Dictionary for the latex description of data in the + % convergence history plot + dict =... + dictionary('eta' , '$\eta_\ell$',... + 'etaVol' , '$\eta_{\textrm{vol}}$',... + 'etaJump' , '$\eta_{\textrm{jump}}$',... + 'oscDb' , '$\textrm{osc}(\textrm{D} u_{\textrm{D}}, \mathcal{F}_\ell(\Gamma_{\textrm{D}})$',... + 'oscNb' , '$\textrm{osc}(t_{\textrm{N}}, \mathcal{F}_\ell(\Gamma_{\textrm{N}})$',... + 'oscF' , '$\textrm{osc}(f, \mathcal{T}_\ell)$',... + 'mu' , '$|| f - \Pi_0 f ||_{L^2(\Omega)}$',... + 'est' , 'built-in',... + 'res' , '$LS(f; \sigma_\ell, u_\ell)$',... + 'resRed' , '$LS(\Pi_0 f; \sigma_\ell, u_\ell)$',... + 'resDiv' , '$|| f + \textrm{div} \sigma_\ell ||_{L^2(\Omega)}$',... + 'resL2' , '$|| \sigma_\ell - \nabla u_\ell ||_{L^2(\Omega)}$',... + 'resDev' , '$|| \textrm{dev} \sigma_\ell - \textrm{D} u_\ell ||_{L^2(\Omega)}$',... + 'resMat' , '$|| C^{-1} \sigma_\ell - \varepsilon u_\ell ||_{L^2(\Omega)}$',... + 'errU' , '$|| u - u_\ell ||_{H^1(\Omega)}$',... + 'errL2U' , '$|| u - u_\ell ||_{L^2(\Omega)}$',... + 'errGradU' , '$||| u - u_\ell |||$',... + 'errSigma' , '$|| \sigma - \sigma_\ell ||_{H(\textrm{div},\Omega)}$',... + 'errL2Sigma' , '$|| \sigma - \sigma_\ell ||_{L^2(\Omega)}$',... + 'errDivSigma' , '$|| \textrm{div} (\sigma - \sigma_\ell) ||_{L^2(\Omega)}$',... + 'runtime' , 'runtime',... + 'setupTime' , 'setuptime',... + 'solutionTime' , 'solutiontime',... + 'estimatorTime' , 'estimatortime',... + 'refinementTime', 'refinementtime',... + 'condition' , 'condition'); +end diff --git a/lib/storage/getHostname.m b/lib/storage/getHostname.m new file mode 100644 index 0000000000000000000000000000000000000000..dd77b972d89fc99b6ab719448422de76d5854631 --- /dev/null +++ b/lib/storage/getHostname.m @@ -0,0 +1,38 @@ +function hostname = getHostname() +%%GETHOSTNAME returns the hostname on UNIX systems +% hostname = GETHOSTNAME() + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + + %% READ HOSTNAME + if ispc() + hostname = 'win'; + elseif ismac() + hostname = 'mac'; + elseif isunix() + [~, hostname] = system('cat /etc/hostname'); + hostname = hostname(1:end-1); + else + hostname = 'unknown'; + end + + %% CHECK RESULT + if isempty(hostname) + hostname = 'no_hostname'; + end +end diff --git a/lib/storage/getPlotStyle.m b/lib/storage/getPlotStyle.m new file mode 100644 index 0000000000000000000000000000000000000000..46e0482e8e2e05ac0cee5f42d9cab397e829b3be --- /dev/null +++ b/lib/storage/getPlotStyle.m @@ -0,0 +1,52 @@ +function [colorOrder, marker] = getPlotStyle() +%%GETPLOTSTYLE creates lists of default colours and markers for plotting +% [colorOrder, marker] = GETPLOTSTYLE() + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + + % Generate a sequence of colours for the graphs + % Matlab default + colorOrder = [0 0.4470 0.7410; + 0.8500 0.3250 0.0980; + 0.9290 0.6940 0.1250; + 0.4940 0.1840 0.5560; + 0.4660 0.6740 0.1880; + 0.3010 0.7450 0.9330; + 0.6350 0.0780 0.1840]; + + % ALTERNATIVE: TU corporate design colours + % colorOrder = [0 0.4 0.6; + % 0.3922 0.3882 0.3882; + % 0 0.4941 0.4431; + % 0.7294 0.2745 0.5098; + % 0.8824 0.5373 0.1333]; + + % ALTERNATIVE: variable length colour sequence + % nColor = max(ceil(nVariable^(1/3)), 2); + nColor = 3; + [red, green, blue] = meshgrid(linspace(0, 1, nColor)); + % colorOrder = [red(:), green(:), blue(:)]; + % Append + colorOrder = [colorOrder; red(:), green(:), blue(:)]; + % Remove white as line colour + colorOrder = colorOrder(1:end-1,:); + + % Define sequence of markers + marker = {'o', '+', 'x', 'square', 'diamond', '*', '^', 'v', ... + 'pentagram', 'hexagram', '<', '>', '.', '_', '|'}; +end diff --git a/runAdditiveSchwarzPreconditioner.m b/runAdditiveSchwarzPreconditioner.m new file mode 100644 index 0000000000000000000000000000000000000000..971ffd8d1f8979c73c28c1216fafa76b00187343 --- /dev/null +++ b/runAdditiveSchwarzPreconditioner.m @@ -0,0 +1,124 @@ +function leveldata = runAdditiveSchwarzPreconditioner(pMax) + + % Proceed input + if nargin < 1 + pMax = 1e1; + end + + % Number of refinement levels + nLevels = 2; + + % Initialize LevelData + leveldata = LevelData('results'); + leveldata.problem = 'Poisson'; + leveldata.domain = 'UnitSquare'; + leveldata.method = 'Sp_PCG_AdditiveSchwarz'; + + % Run experiment for various p + for p = 1:pMax + % Load mesh + mesh = Mesh.loadFromGeometry('unitsquare'); + %mesh.refineUniform(); + + % Create FE space + fes = FeSpace(mesh, HigherOrderH1Fe(p), 'dirichlet', ':'); + % u = FeFunction(fes); + + % Setup variational problem + blf = BilinearForm(); + lf = LinearForm(); + blf.a = Constant(mesh, 1); + blf.qra = QuadratureRule.ofOrder(max(2*(p-1), 1)); + lf.f = Constant(mesh, 1); + + % Choose iterative solver + 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 + A = assemble(blf, fes); + F = assemble(lf, fes); + freeDofs = getFreeDofs(fes); + solver.setupSystemMatrix(A(freeDofs,freeDofs)); + solver.setupRhs(F(freeDofs)); % initializes x0 as well + % Mesh refinement + mesh.refineUniform(); + end + + % Assemble matrices on fine mesh + A = assemble(blf, fes); + if ~issymmetric(A) + %warning(['symmetrizing, norm(asym(A)) = ', num2str(norm(A - A', 1))]) + A = (A + A') / 2; + end + F = assemble(lf, fes); + freeDofs = getFreeDofs(fes); + solver.setupSystemMatrix(A(freeDofs,freeDofs)); + solver.setupRhs(F(freeDofs)); % initializes x0 as well + + % A = A(freeDofs,freeDofs); + + % figure(2); + % spy(A); + % title('Matrix A'); + + % Compute C + % C = zeros(length(freeDofs)); + % for j = 1:length(freeDofs) + % ej = zeros(length(freeDofs), 1); + % ej(j) = 1; + % C(:,j) = solver.preconditionAction(ej); + % end + % if ~issymmetric(C) + % % warning(['symmetrizing, norm(asym(C)) = ', num2str(norm(C - C', 1))]) + % C = (C + C') / 2; + % end + + % figure(3); + % CC = C; + % CC(abs(C) < 1e-10) = 0; + % spy(CC); + % title('Matrix C'); + % + % asymC = (C - C'); + + % figure(4); + % largeAsymC = asymC; + % largeAsymC(abs(largeAsymC) < max(abs(largeAsymC(:))) / 10) = 0; + % spy(largeAsymC); + % title('Large entries of asym(C)'); + + % figure(5); + % asymC(abs(asymC) < 1e-10) = 0; + % spy(asymC); + % title('Nonzero entries of asym(C)'); + + % Compute conditional number + % CA = C*A; + % lambda = eig(full(CA)); + % condition = max(abs(lambda)) / min(abs(lambda)); + % lambdaMin = eigs(@(x)applyCA(x, A, solver), length(freeDofs), 1, 'smallestreal'); + % lambdaMax = eigs(@(x)applyCA(x, A, solver), length(freeDofs), 1, 'largestreal'); + % lambdaMin = eigs(CA, 1, 'smallestabs'); + % lambdaMax = eigs(CA, 1, 'largestabs'); + % condition = lambdaMax / lambdaMin; + + % Print result to commandline + % leveldata.append('p', uint32(p), 'condition', condition); + % leveldata.printLevel(); + end + + % Plot condition + figure(); + leveldata.plot('p'); +end + + +function CAx = applyCA(x, A, solver) + % CAx = solver.preconditionAction(A * x); + % CAx = (A * x); + CAx = solver.preconditionAction(x); +end diff --git a/runIterativeSolver.m b/runIterativeSolver.m new file mode 100644 index 0000000000000000000000000000000000000000..ce2bcf8b6d8cd180066937d7c5843e07e05907f6 --- /dev/null +++ b/runIterativeSolver.m @@ -0,0 +1,145 @@ +function leveldata = runIterativeSolver(maxNiter) + + % Proceed input + if nargin < 1 + maxNiter = 1e2; + end + + % Polynomial degree + p = 4; + + % Number of refinement levels + nLevels = 6; + + % Load mesh + mesh = Mesh.loadFromGeometry('unitsquare'); + mesh.refineUniform(); + + % Create FE space + fes = FeSpace(mesh, HigherOrderH1Fe(p), 'dirichlet', ':'); + u = FeFunction(fes); + ustar = FeFunction(fes); + + % Setup variational problem + blf = BilinearForm(); + lf = LinearForm(); + blf.a = Constant(mesh, 1); + blf.qra = QuadratureRule.ofOrder(max(2*(p-1), 1)); + lf.f = Constant(mesh, 1); + + % Choose iterative solver + 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'); + leveldata.problem = 'Poisson'; + leveldata.domain = 'UnitSquare'; + leveldata.method = 'Sp_PCG_AdditiveSchwarz'; + + for k = 1:nLevels-1 + % Output + fprintf('Assemble data for level = %d\n', k); + % Assemble matrices on intermediate meshes + A = assemble(blf, fes); + F = assemble(lf, fes); + freeDofs = getFreeDofs(fes); + solver.setupSystemMatrix(A(freeDofs,freeDofs)); + solver.setupRhs(F(freeDofs)); % initializes x0 as well + % Mesh refinement + mesh.refineUniform(); + end + + fprintf('Assemble data for level = %d\n', nLevels); + + % Assemble matrices on fine mesh + A = assemble(blf, fes); + F = assemble(lf, fes); + freeDofs = getFreeDofs(fes); + solver.setupSystemMatrix(A(freeDofs,freeDofs)); + solver.setupRhs(F(freeDofs)); % initializes x0 as well + + % Compute exact solution + fprintf('Do exact solve\n'); + xstar = A(freeDofs,freeDofs) \ F(freeDofs); + + % Refinement loop + nIter = 1; + while true + ndof = getDofs(fes).nDofs; + + % Compute energy norm + errorU = sqrt((xstar - solver.x)' * A(freeDofs,freeDofs) * (xstar - solver.x)); + + % Compute contraction factor for energy norm + if nIter == 1 + contraction = Inf; + else + contraction = errorU / errorUPrevious; + end + + % Store variables + leveldata.append('ndof', uint32(ndof), ... + 'errorU', errorU); + leveldata.setAbsolute(nIter, 'nIter', uint32(nIter), ... + 'contraction', contraction); + + % Print level information + leveldata.printLevel(); + + % Plot solution + % if p == 2 + % x = zeros(mesh.nCoordinates + mesh.nEdges, 1); + % x(freeDofs) = solver.x; + % figure(1); + % plotS2(mesh.clone(), x); + % + % figure(2); + % x(freeDofs) = xstar; + % plotS2(mesh.clone(), x); + % title("Exact algebraic solution") + % else + % figure(1); + % u.setFreeData(solver.x); + % plot(u); + % + % figure(2); + % ustar.setFreeData(xstar); + % plot(ustar); + % end + % pause(2) + + % Break condition + % if ndof > maxNdof + % break; + % end + + % Mesh refinement + % mesh.refineUniform(); + + if nIter > maxNiter + break; + end + + % One iterative solver step + solver.step(); + + % Update iteration counter + nIter = nIter + 1; + errorUPrevious = errorU; + end + + % Plot solution + u.setFreeData(solver.x); + plot(u); + + % Plot error + figure(); + leveldata.plot('nIter'); + + % Plot contraction + figure(); + leveldata.plotAbsolute('nIter'); +end diff --git a/test.m b/test.m new file mode 100644 index 0000000000000000000000000000000000000000..505e85f64341a4cb0f5b3508fa0e7e2d6eb4d257 --- /dev/null +++ b/test.m @@ -0,0 +1,61 @@ +mesh = Mesh.loadFromGeometry('unitsquare'); +%mesh.refineUniform(); + +fes1 = FeSpace(mesh, HigherOrderH1Fe(1), 'dirichlet', ':'); +fes2 = FeSpace(mesh, HigherOrderH1Fe(15), 'dirichlet', ':'); + +testMatrix = ones(getDofs(fes2).nDofs, 1); +inclusionMatrix = MeshProlongation(fes1, fes2); +inclusionMatrix = full(inclusionMatrix); +Vertices = 1:getDofs(fes1).nDofs; +inclusionMatrix2 = interpolateData(eye(length(Vertices)), fes1, fes2)'; +diff = inclusionMatrix - inclusionMatrix2; + +function inclusionMatrix = MeshProlongation(fromFes, toFes) + %Create the prolongation matrix on the unit triangle + unittriangle = Mesh.loadFromGeometry('unittriangle'); + fromFesUnitTriangle = FeSpace(unittriangle, ... + HigherOrderH1Fe(fromFes.finiteElement.order)); + toFesUnitTriangle = FeSpace(unittriangle, ... + HigherOrderH1Fe(toFes.finiteElement.order)); + Vertices = 1:getDofs(fromFesUnitTriangle).nDofs; + unitTriangleInclusionMatrix = ... + permute(interpolateData(eye(length(Vertices)),... + fromFesUnitTriangle, toFesUnitTriangle), [2 1]); + % Get local to global map in unit triangle + + unitTriangleInclusionMatrix = unitTriangleInclusionMatrix( ... + getDofs(fromFesUnitTriangle).element2Dofs, ... + getDofs(toFesUnitTriangle).element2Dofs); + % Get dofs of the finite element spaces on the meshes + fromFesDofs = getDofs(fromFes); + toFesDofs = getDofs(toFes); + + % Copy the matrix from the unit triangle for each element + mat = repmat(unitTriangleInclusionMatrix, ... + [1, 1, fromFes.mesh.nElements]); + + % Create index sets for the accumarray + temp = fromFesDofs.element2Dofs(:, :); + temp2 = toFesDofs.element2Dofs(:, :); + I = repmat(permute(temp, [1 3 2]), [1, size(temp2, 1), 1]); + J = repmat(permute(temp2, [3 1 2]), [size(temp, 1), 1, 1]); + ind = [I(:), J(:)]; + + inclusionMatrix = ... + accumarray(ind, mat(:), [], @mean, [], true); +end + +function interpolatedData = interpolateData(data, fromFes, toFes) + Dofs = 1:getDofs(toFes).nDofs; + nComponents = size(data, 2); + interpolatedData = zeros(numel(Dofs), nComponents); + feFunctionWrapper = FeFunction(fromFes); + for k = 1:nComponents + feFunctionWrapper.setData(data(:,k)); + wholeData = nodalInterpolation(feFunctionWrapper, toFes); + interpolatedData(:,k) = wholeData(Dofs); + end +end + + diff --git a/tests/TestCoordinateTransformations.m b/tests/TestCoordinateTransformations.m index 7ea0d17488000c475c1ecddb1d19743c836b0c8e..ff492ae1a221871385f4611d06174e88bf276c27 100644 --- a/tests/TestCoordinateTransformations.m +++ b/tests/TestCoordinateTransformations.m @@ -6,7 +6,7 @@ end methods (TestMethodSetup) function createUnitTriangleCoordinates(testCase) - mesh = Mesh.loadFromGeometry('unittriangle'); + mesh = Mesh.unitTriangle(); testCase.coordinates = mesh.coordinates; testCase.fatalAssertNumElements(testCase.coordinates, 6); end diff --git a/tests/TestFeSpace.m b/tests/TestFeSpace.m index 579d6eba5d07c1d287a3501aba60b97299219fd3..fa8f58e1c70e52025fb4f913b77c64600ab4f40a 100644 --- a/tests/TestFeSpace.m +++ b/tests/TestFeSpace.m @@ -56,7 +56,7 @@ methods (Test, ParameterCombination='sequential') end function dofDataIsComputedCorrectly(testCase, fe, nDof, nFreeDof) - mesh = Mesh.loadFromGeometry('unittriangle'); + mesh = Mesh.unitTriangle(); fes = FeSpace(mesh, fe); testCase.verifyEqual(getDofs(fes).nDofs, nDof) testCase.verifyEqual(nnz(getFreeDofs(fes)), nFreeDof) diff --git a/tests/TestFiniteElementFunctions.m b/tests/TestFiniteElementFunctions.m index f208227e5759ddeef015997f8a24a3eea1b81e43..246c94d328c2fe3320052dc57bbf339f4b92cdcf 100644 --- a/tests/TestFiniteElementFunctions.m +++ b/tests/TestFiniteElementFunctions.m @@ -81,7 +81,7 @@ methods (Test) fes = FeSpace(mesh, testCase.u.fes.finiteElement); qr = QuadratureRule.ofOrder(max(fes.finiteElement.order,1)); v = FeFunction(fes); - P = FeProlongation(fes); + P = MeshProlongation(fes); for k = 1:min(5, size(getDofs(fes).element2Dofs, 1)) testCase.setToElementwiseBasisFunction(v, k); before = sum(integrateElement(v, qr)); @@ -96,12 +96,7 @@ end methods (Access=private) function refineAndInterpolate(~, mesh, fes, v, val) v.setData(val); - if isa(fes.finiteElement, 'LowestOrderH1Fe') ... - || isa(fes.finiteElement, 'LowestOrderL2Fe') - P = LoFeProlongation(fes); - else - P = FeProlongation(fes); - end + P = Prolongation.chooseFor(fes); mesh.refineUniform(); v.setData(prolongate(P, v)); end diff --git a/tests/TestIterativeSolver.m b/tests/TestIterativeSolver.m new file mode 100644 index 0000000000000000000000000000000000000000..6a32e6af2f72e24d59a80be2ceb5d94690187682 --- /dev/null +++ b/tests/TestIterativeSolver.m @@ -0,0 +1,94 @@ +classdef TestIterativeSolver < matlab.unittest.TestCase + +properties (TestParameter) + p = struct('low', 1, 'medium', 2, 'high', 5); + variant = struct('CG', ["cg", ""],... + 'direct', ["direct", ""], ... + 'jacobiPCG', ["pcg", "jacobi"], ... + 'iChol', ["pcg", "iChol"], ... + 'loAdditiveSchwarzPCG', ["pcg", "additiveSchwarzLowOrder"], ... + 'hoAdditiveSchwarzPCG', ["pcg", "additiveSchwarzHighOrder"], ... + 'lowMG', ["multigrid", "lowOrderVcycle"], ... + 'highMG', ["multigrid", "highOrderVcycle"]); +end + +methods (Test) + 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 = IterativeSolver.choose(fes, blf, s, v); + + for k = 1:3 + [xstar, A, F] = assembleData(testCase, blf, lf, fes); + solver.setupSystemMatrix(A); + if k < 3, mesh.refineUniform(); end + end + + 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 solverIsLinear(testCase, p, variant) + [mesh, fes, blf, lf] = setupProblem(testCase, p); + s = variant(1); v = variant(2); + solver = IterativeSolver.choose(fes, blf, s, v); + + for k = 1:3 + [xstar, A, F] = assembleData(testCase, blf, lf, fes); + solver.setupSystemMatrix(A); + if k < 3, mesh.refineLocally(k); end + end + + solver.setupRhs([F, -pi*F], 0*[F, F]); + solver.solve(); + + testCase.verifyEqual(-pi*solver.x(:,1), solver.x(:,2), 'RelTol', 2e-5); + testCase.verifyEqual(solver.x(:,1), xstar, 'RelTol', 2e-5); + end +end + +methods (Access=private) + function [mesh, fes, blf, lf] = setupProblem(~, p) + mesh = Mesh.loadFromGeometry('unitsquare'); + fes = FeSpace(mesh, HigherOrderH1Fe(p), 'dirichlet', ':'); + blf = BilinearForm(); + lf = LinearForm(); + blf.a = Constant(mesh, 1); + blf.qra = QuadratureRule.ofOrder(max(2*(p-1), 1)); + blf.c = Constant(mesh, 1); + blf.qrc = QuadratureRule.ofOrder(2*p); + lf.f = Constant(mesh, 1); + end + + function [xstar, A, F] = assembleData(~, blf, lf, fes) + A = assemble(blf, fes); + F = assemble(lf, fes); + freeDofs = getFreeDofs(fes); + A = A(freeDofs,freeDofs); + F = F(freeDofs); + xstar = A \ F; + end +end + + +end diff --git a/tests/TestPatchwiseMatrix.m b/tests/TestPatchwiseMatrix.m new file mode 100644 index 0000000000000000000000000000000000000000..d44b7b64dd3a32b36406483f4e93764cc8bdd63b --- /dev/null +++ b/tests/TestPatchwiseMatrix.m @@ -0,0 +1,69 @@ +classdef TestPatchwiseMatrix < matlab.unittest.TestCase + +properties (TestParameter) + p = {1, 2, 3, 4, 5}; +end + +methods (Test) + function lowestOrderIsDiagonalInverse(testCase) + mesh = Mesh.loadFromGeometry('Lshape'); + fes = FeSpace(mesh, LowestOrderH1Fe(), 'dirichlet', [], 'neumann', ':'); + blf = BilinearForm(); + blf.a = Constant(mesh, 1); + blf.c = Constant(mesh, 1); + for ell = 1:5 + mesh.refineUniform(); + A = assemble(blf, fes); + B = assemblePatchwise(blf, fes); + actual = B \ full(diag(A)); + testCase.verifyEqual(actual, ones(getDofs(fes).nDofs, 1), 'RelTol', 10*eps) + end + end + + function dofsAreInCorrectlyManyPatches(testCase, p) + mesh = Mesh.loadFromGeometry('Lshape'); + for ell = 1:5, mesh.refineLocally(1); end + fes = FeSpace(mesh, HigherOrderH1Fe(p), 'dirichlet', 1, 'neumann', 2); + blf = BilinearForm(); + blf.a = Constant(mesh, 1); + blf.qra = QuadratureRule.ofOrder(max(2*(p-1),1)); + A = assemblePatchwise(blf, fes); + countOccurrence = accumarray(getPatchDofs(A, ':'), 1, [getDofs(fes).nDofs, 1]); + + isFree = false(getDofs(fes).nDofs, 1); + isFree(getFreeDofs(fes)) = true; + % vertex dofs should occur exactly once (if free) + vertexDofs = createVertexDofs(fes, ':'); + testCase.verifyEqual(countOccurrence(vertexDofs), 1*isFree(vertexDofs)); + % edge dofs should occur exactly twice (if free) + edgeDofs = createInnerEdgeDofs(fes, ':'); + testCase.verifyEqual(countOccurrence(edgeDofs), 2*isFree(edgeDofs)); + % inner dofs should occur exactly three times + elementDofs = createInnerElementDofs(fes, ':'); + testCase.verifyEqual(countOccurrence(elementDofs), 3*isFree(elementDofs)); + end + + function patchwiseRelatesToFullMatrix(testCase, p) + mesh = Mesh.loadFromGeometry('unitsquare'); + for ell = 1:5, mesh.refineLocally(1); end + fes = FeSpace(mesh, HigherOrderH1Fe(p), 'dirichlet', [], 'neumann', ':'); + blf = BilinearForm(); + blf.a = Constant(mesh, 1); + blf.qra = QuadratureRule.ofOrder(max(2*(p-1),1)); + blf.c = MeshFunction(mesh, @(x) 1+x(1,:,:)); + blf.qrc = QuadratureRule.ofOrder(2*p+1); + A = assemble(blf, fes); + B = assemblePatchwise(blf, fes); + for k = 1:10:51 + vertexDof = createVertexDofs(fes, k); + idx = getPatchDofs(B, k); + expected = zeros(getDofs(fes).nDofs, 1); + expected(vertexDof) = 1; + actual = (B \ expected); + actual(idx) = A(idx,idx) * actual(idx); + testCase.verifyEqual(actual, expected, 'AbsTol', sqrt(eps)) + end + end +end + +end diff --git a/tmp/plotS2.m b/tmp/plotS2.m new file mode 100644 index 0000000000000000000000000000000000000000..92ee44046520096223c78bb8688872588c419297 --- /dev/null +++ b/tmp/plotS2.m @@ -0,0 +1,49 @@ +function ax = plotS2(mesh, x) +%%PLOTS2 plots a piecewise P2 and globally continuous function (Lagrange +%finite element function) given by the coefficient vector x on the +%triangulation meshdata +% ax = PLOTS2(meshdata, x) + +% Copyright 2023 Philipp Bringmann +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with this program. If not, see <http://www.gnu.org/licenses/>. +% + + + % Create axes object + ax = gca; + + % Uniform red-refinement of triangulation + mesh.refineUniform(); + + % S1 plot of nodal interpolation on red-refinement + if size(x, 2) == 1 + % Plot of scalar function + pat = trisurf(mesh.elements', mesh.coordinates(1,:)',... + mesh.coordinates(2,:)', x); + if(mesh.nElements > 2000) + set(pat, 'EdgeColor', 'none'); + end + elseif size(x, 2) == 2 + % Plot of vector field + set(ax, 'XLim', 2.1*[min(mesh.coordinates(1,:)), max(mesh.coordinates(1,:))],... + 'YLim', 1.1*[min(mesh.coordinates(2,:)), max(mesh.coordinates(2,:))]); + quiver(mesh.coordinates(1,:), mesh.coordinates(2,:), x(:,1), x(:,2)); + else + error('Invalid size of coefficient vector'); + end + + % Draw title + title(ax, {'S2 function plot'; [num2str(mesh.nEdges + mesh.nCoordinates), ' dofs']}); +end