From de1200e87205dff8d4e06c85f5aa6a631227c7aa Mon Sep 17 00:00:00 2001
From: acrovato <a.crovato@uliege.be>
Date: Fri, 6 Dec 2024 11:15:32 +0100
Subject: [PATCH] Add adaptive mesh refinement and 2D test case

---
 dart/src/wSolver.cpp |  75 +++++++++++++++++++++++++++
 dart/src/wSolver.h   |   1 +
 dart/tests/amr.py    | 117 +++++++++++++++++++++++++++++++++++++++++++
 ext/amfe             |   2 +-
 4 files changed, 194 insertions(+), 1 deletion(-)
 create mode 100644 dart/tests/amr.py

diff --git a/dart/src/wSolver.cpp b/dart/src/wSolver.cpp
index b9a7392..4e9f57f 100644
--- a/dart/src/wSolver.cpp
+++ b/dart/src/wSolver.cpp
@@ -206,6 +206,81 @@ void Solver::save(MshExport *mshWriter, std::string const &suffix)
     std::cout << "done." << std::endl;
 }
 
+/**
+ * @brief Compute refinment sensor and write new mesh sizes
+ * @todo Remesh internally
+ * @todo Investigate combination of gradient sensor with Laplacian sensor
+ * @todo Investigate effect of user-defined parameters (thresholds, refinement/coarsening factors, termination, etc.)
+ * @todo Add capability to define region of interest (LE, TE, exclude tip, etc.)
+ */
+bool Solver::refine(double minSize, tbox::MshExport *mshWriter, std::string const &suffix)
+{
+    // Compute deltaRho at cell in the streamwise direction
+    std::vector<double> dRhoE(pbl->msh->elems.size());
+    std::vector<double> dRho(pbl->msh->nodes.size()), mSize(pbl->msh->nodes.size());
+    tbb::parallel_for_each(pbl->fluid->adjMap.begin(), pbl->fluid->adjMap.end(), [&](std::pair<Element *, std::vector<Element *>> &p) {
+        dRhoE[p.first->no - 1] = std::abs(pbl->fluid->rho->eval(*(p.first), phi, 0) - pbl->fluid->rho->eval(*(p.second[0]), phi, 0));
+    });
+    // Average deltaRho and compute mesh size at nodes
+    tbb::parallel_for_each(pbl->fluid->neMap.begin(), pbl->fluid->neMap.end(), [&](std::pair<Node *, std::vector<Element *>> p) {
+        dRho[p.first->row] = 0.;
+        mSize[p.first->row] = 0.;
+        for (auto e : p.second)
+        {
+            dRho[p.first->row] += dRhoE[e->no - 1];
+            if (pbl->nDim == 2)
+                mSize[p.first->row] += std::sqrt(2 / std::sqrt(3) * e->getDetJ(0));
+            else
+                mSize[p.first->row] += std::cbrt(2 / std::sqrt(2) * e->getDetJ(0));
+        }
+        dRho[p.first->row] /= p.second.size();
+        mSize[p.first->row] /= p.second.size();
+    });
+
+    // Sort the mesh size of the element according the their deltaRho
+    std::vector<std::pair<int, double>> sorter(pbl->msh->nodes.size());
+    tbb::parallel_for_each(pbl->msh->nodes.begin(), pbl->msh->nodes.end(), [&](Node *n) {
+        sorter[n->row] = std::pair<int, double>(n->row, dRho[n->row]);
+    });
+    std::sort(sorter.begin(), sorter.end(), [&](const std::pair<int, double> &a, const std::pair<int, double> &b) {
+        return a.second > b.second;
+    });
+    // Coarsen the 30% elements which have lowest dRho, and refine the 30% that have highest dRho
+    size_t refBnd = static_cast<size_t>(std::floor(0.3 * sorter.size()));
+    size_t crsBnd = static_cast<size_t>(std::floor(0.7 * sorter.size()));
+    tbb::parallel_for(tbb::blocked_range<size_t>(0, sorter.size()), [&](tbb::blocked_range<size_t> rng) {
+        for (size_t i = rng.begin(); i < rng.end(); ++i)
+        {
+            // Manually prevent refine in certain regions
+            // Node *n = pbl->msh->nodes[sorter[i].first];
+            // if (n->pos(1) > 0.98 * 14 && n->pos(1) < 1.02 * 14)
+            //    if (mSize[sorter[i].first] < 0.045)
+            //        continue;
+            if (i < refBnd)
+                mSize[sorter[i].first] *= 0.5;
+            else if (i > crsBnd)
+                mSize[sorter[i].first] *= 2.0;
+            else
+                mSize[sorter[i].first] *= 1.0;
+        }
+    });
+
+    // Write sensor and new mesh sizes
+    Results rSens;
+    rSens.scalars_at_nodes["sensor"] = &dRho;
+    mshWriter->saveList(pbl->msh->name + "_sensor" + suffix, rSens);
+    Results rSize;
+    rSize.scalars_at_nodes["size"] = &mSize;
+    mshWriter->saveList(pbl->msh->name + "_size" + suffix, rSize);
+
+    // Stop the refinement loop if the mesh size is lower that the specified target
+    std::cout << std::fixed << std::setprecision(5) << "min. size: " << *std::min_element(mSize.begin(), mSize.end()) << std::endl;
+    if (*std::min_element(mSize.begin(), mSize.end()) < minSize)
+        return true;
+    else
+        return false;
+}
+
 /**
  * @brief Compute total aerodynamic load coefficients on boundaries
  */
diff --git a/dart/src/wSolver.h b/dart/src/wSolver.h
index a988bca..ca26877 100644
--- a/dart/src/wSolver.h
+++ b/dart/src/wSolver.h
@@ -79,6 +79,7 @@ public:
     void reset();
     virtual Status run();
     void save(tbox::MshExport *mshWriter, std::string const &suffix = "");
+    bool refine(double minSize, tbox::MshExport *mshWriter, std::string const &suffix = "");
 
     int getRow(size_t i) const { return rows[i]; };
 
diff --git a/dart/tests/amr.py b/dart/tests/amr.py
new file mode 100644
index 0000000..baf7989
--- /dev/null
+++ b/dart/tests/amr.py
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Copyright 2020 University of Liège
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+## Compute transonic potential flow around a RAE 2822
+# Adrien Crovato
+#
+# Test the adaptive mesh refinement capability
+#
+# CAUTION
+# This test is not stable. The adaptive mesh refinement procedure must be improved.
+
+import dart.utils as floU
+import dart.default as floD
+import tbox.utils as tboxU
+import fwk
+from fwk.testing import *
+import math
+
+def main():
+    # timer
+    tms = fwk.Timers()
+    tms['total'].start()
+
+    # define flow variables
+    alpha = 2*math.pi/180 # must be zero for this testfile
+    M_inf = 0.72
+    c_ref = 1
+    dim = 2
+
+    # define initial mesh parameters
+    mName = 'rae2822'
+    pars = {'xLgt' : 5, 'yLgt' : 5, 'msF' : 2.0, 'msTe' : 0.1, 'msLe' : 0.1}
+
+    # AMR loop
+    counter = 0
+    Cls = []
+    Cds = []
+    Cms = []
+    while True:
+        # mesh the geometry
+        tms['msh'].start()
+        msh, gmshWriter = floD.mesh(dim, f'models/{mName}.geo', pars, ['field', 'airfoil', 'downstream'])
+        tms['msh'].stop()
+
+        # set the problem and add fluid, initial/boundary conditions
+        tms['pre'].start()
+        pbl, _, _, bnd, _ = floD.problem(msh, dim, alpha, 0., M_inf, c_ref, c_ref, 0.25, 0., 0., 'airfoil')
+        tms['pre'].stop()
+
+        # solve problem
+        tms['solver'].start()
+        solver = floD.newton(pbl)
+        solver.run()
+        solver.save(gmshWriter)
+        stop = solver.refine(0.005, gmshWriter)
+        tms['solver'].stop()
+
+        # extract data
+        Cls.append(solver.Cl)
+        Cds.append(solver.Cd)
+        Cms.append(solver.Cm)
+        Cp = floU.extract(bnd.groups[0].tag.elems, solver.Cp)
+        tboxU.write(Cp, f'Cp_airfoil_{counter}.dat', '%1.4e', ',', 'x, y, cp', '')
+
+        # stop criterion
+        if counter > 10 or stop:
+            break
+        else:
+            counter += 1
+            pars['-bgm'] = f'{mName}_size.pos'
+            pars['counter'] = counter # pass the counter to force remesh
+
+    # display results
+    print('{0:>8s} {1:>8s} {2:>8s} {3:>8s} {4:>8s} {5:>8s}'.format('run', 'Mach', 'AoA', 'Cl', 'Cd', 'Cm'))
+    i = 0
+    while i < len(Cls):
+        print('{0:8d} {1:8.3f} {2:8.1f} {3:8.4f} {4:8.4f} {5:8.4f}'.format(i, M_inf, alpha*180/math.pi, Cls[i], Cds[i], Cms[i]))
+        i = i+1
+    print('\n')
+
+    # display timers
+    tms['total'].stop()
+    print('CPU statistics')
+    print(tms)
+
+    # visualize solution and plot results
+    floD.initViewer(pbl)
+    floD.plot(Cp[:,0], Cp[:,-1], {'xlabel': 'x', 'ylabel': 'Cp', 'title': 'Cl = {0:.{3}f}, Cd = {1:.{3}f}, Cm = {2:.{3}f}'.format(solver.Cl, solver.Cd, solver.Cm, 4), 'invert': True})
+
+    # check results
+    tests = CTests()
+    tests.add(CTest('iteration count', solver.nIt, 15, 3, forceabs=True))
+    tests.add(CTest('refinement count', counter, 4, 1, forceabs=True))
+    tests.add(CTest('Cl', solver.Cl, 0.85, 1e-2))
+    tests.add(CTest('Cd', solver.Cd, 0.0025, 1e-2))
+    tests.run()
+
+    # eof
+    print('')
+
+if __name__ == "__main__":
+    main()
diff --git a/ext/amfe b/ext/amfe
index c11ff6e..fb9deb4 160000
--- a/ext/amfe
+++ b/ext/amfe
@@ -1 +1 @@
-Subproject commit c11ff6ee646765a19f9d43496d354e939a84b408
+Subproject commit fb9deb4bef7e323b709568d214205a15dd78359d
-- 
GitLab