From 44904a461bb8b41c5951fb99f115003bb687778d Mon Sep 17 00:00:00 2001
From: Paul Dechamps <paul.dechamps@uliege.be>
Date: Sun, 29 Sep 2024 10:36:26 +0200
Subject: [PATCH] (feat) Added mesh update capability to the solver interface

New surface mesh is imposed in the viscous solver in 2D and stagnation point is recomputed at the first coupling iteration
---
 blast/coupler.py                       |  47 +++---
 blast/interfaces/DSolversInterface.py  | 192 +++++++++++++++++++++++--
 blast/interfaces/dart/DartInterface.py |  26 +++-
 blast/utils.py                         |   4 +-
 4 files changed, 227 insertions(+), 42 deletions(-)

diff --git a/blast/coupler.py b/blast/coupler.py
index 9c3d903..2878d67 100644
--- a/blast/coupler.py
+++ b/blast/coupler.py
@@ -85,6 +85,7 @@ class Coupler:
             # Write inviscid Cp distribution.
             if couplIter == 0:
                 self.isol.initializeViscousSolver()
+                self.isol.updateStagnation()
                 if write:
                     self.isol.writeCp(sfx='_inviscid'+self.filesfx)
 
@@ -142,11 +143,7 @@ class Coupler:
         # Inviscid solver
 
         self.isol.reset()
-        # Viscous solver
-        for s in self.isol.sec:
-            for reg in s:
-                reg.reset()
-        
+
         # Blowing velocity
         for b in self.isol.iBnd:
             for i in range(len(b.blowingVel)):
@@ -156,22 +153,28 @@ class Coupler:
                 for i in range(len(side.blowingVel)):
                     side.blowingVel[i] = 0.
         self.isol.setBlowingVelocity()
+
+        # Viscous solver
+        if self.isol.vinit:
+            for s in self.isol.sec:
+                for reg in s:
+                    reg.reset()
         
-        for iSec in range(self.isol.cfg['nSections']):
-            for i, reg in enumerate(self.isol.sec[iSec]):
-                if reg.name == 'wake':
-                    iReg = 1
-                elif reg.name == 'lower' or reg.name == 'upper':
-                    iReg = 0
-                else:
-                    raise RuntimeError('Invalid region', reg.name)
-                loc = np.zeros(reg.getnNodes())
-                for iPoint in range(reg.getnNodes()):
-                    loc[iPoint] = reg.loc[iPoint]
-                self.isol.xxExt[iSec][i] = loc
-                self.isol.deltaStarExt[iSec][i] = np.zeros(reg.getnNodes())
-                self.isol.vtExt[iSec][i] = np.zeros(reg.getnNodes())
-                reg.setxxExt(self.isol.xxExt[iSec][i])
-                reg.setVtExt(self.isol.vtExt[iSec][i])
-                reg.setDeltaStarExt(self.isol.deltaStarExt[iSec][i])
+            for iSec in range(self.isol.cfg['nSections']):
+                for i, reg in enumerate(self.isol.sec[iSec]):
+                    if reg.name == 'wake':
+                        iReg = 1
+                    elif reg.name == 'lower' or reg.name == 'upper':
+                        iReg = 0
+                    else:
+                        raise RuntimeError('Invalid region', reg.name)
+                    loc = np.zeros(reg.getnNodes())
+                    for iPoint in range(reg.getnNodes()):
+                        loc[iPoint] = reg.loc[iPoint]
+                    self.isol.xxExt[iSec][i] = loc
+                    self.isol.deltaStarExt[iSec][i] = np.zeros(reg.getnNodes())
+                    self.isol.vtExt[iSec][i] = np.zeros(reg.getnNodes())
+                    reg.setxxExt(self.isol.xxExt[iSec][i])
+                    reg.setVtExt(self.isol.vtExt[iSec][i])
+                    reg.setDeltaStarExt(self.isol.deltaStarExt[iSec][i])
 
diff --git a/blast/interfaces/DSolversInterface.py b/blast/interfaces/DSolversInterface.py
index 50eae4b..b7542bc 100644
--- a/blast/interfaces/DSolversInterface.py
+++ b/blast/interfaces/DSolversInterface.py
@@ -2,7 +2,6 @@ import numpy as np
 import blast
 import tbox
 from blast.interfaces.DDataStructure import Group
-import time
 
 class SolversInterface:
     def __init__(self, _vSolver, _cfg):
@@ -17,25 +16,22 @@ class SolversInterface:
         self.vBnd = [[Group('vAirfoil'+str(k) if iReg == 0 else 'vWake'+str(k), self.getnDim()) for iReg in range(2)] for k in range(self.cfg['nSections'])]
         
         # Initialize interpolator and meshes
-        initialTime = time.time()
-        self.setMesh()
+        self.getWing()
         if self.cfg['interpolator'] == 'Matching':
             from blast.interfaces.interpolators.DMatchingInterpolator import MatchingInterpolator as interp
             # Initialize viscous structures for matching computations
             for iSec in range(len(self.vBnd)):
                 for iReg, reg in enumerate(self.vBnd[iSec]):
                     reg.initStructures(self.iBnd[iReg].nPoints)
-                    self.vBnd[iSec][iReg].nodesCoord = self.iBnd[iReg].nodesCoord
-                    self.vBnd[iSec][iReg].elemsCoord = self.iBnd[iReg].elemsCoord
-                    self.vBnd[iSec][iReg].connectListNodes = self.iBnd[iReg].connectListNodes
-                    self.vBnd[iSec][iReg].connectListElems = self.iBnd[iReg].connectListElems
+                    self.vBnd[iSec][iReg].nodesCoord = self.iBnd[iReg].nodesCoord.copy()
+                    self.vBnd[iSec][iReg].elemsCoord = self.iBnd[iReg].elemsCoord.copy()
+                    self.vBnd[iSec][iReg].connectListNodes = self.iBnd[iReg].connectListNodes.copy()
+                    self.vBnd[iSec][iReg].connectListElems = self.iBnd[iReg].connectListElems.copy()
 
         elif self.cfg['interpolator'] == 'Rbf':
             from blast.interfaces.interpolators.DRbfInterpolator import RbfInterpolator as interp
-            print('RBF interpolator initialized.')
-            print('Loading viscous mesh... ', end='')
+            print('Initializing RBF interpolator.')
             self.getVWing()
-            print('done in {:.2f}s.'.format(time.time()-initialTime))
         else:
             raise RuntimeError('Incorrect interpolator specified in config.')
 
@@ -99,6 +95,102 @@ class SolversInterface:
         else:
             raise RuntimeError('Invalid direction', dir)
     
+    def updateMesh(self):
+        """Collect the mesh on blowing boundaries and update the mesh in the viscous solver.
+        This function does not update connectivity lists
+        """
+        if self.getnDim() != 2:
+            raise RuntimeError('SolverInterface::updateMesh - Only 2D meshes are supported.')
+        if self.cfg['interpolator'] != 'Matching':
+            raise RuntimeError('SolverInterface::updateMesh - Only Matching interpolator is supported.')
+
+        meshList = self.getMesh()
+
+        # Update the mesh in data structures
+        for ireg, reg in enumerate(self.iBnd):
+            for i, row in enumerate(reg.connectListNodes):
+                for idim in range(self.getnDim()):
+                    reg.nodesCoord[i, idim] = meshList[ireg][row, idim]
+
+        # Recompute elements data
+        for ireg, reg in enumerate(self.iBnd):
+            for i in range(len(reg.nodesCoord[:,0])-1):
+                for idim in range(self.getnDim()):
+                    reg.elemsCoord[i, idim] = (reg.nodesCoord[i, idim] + reg.nodesCoord[i+1, idim])/2
+        
+        # Update the mesh in the data structures
+        for iSec in range(len(self.vBnd)):
+            for iReg, reg in enumerate(self.vBnd[iSec]):
+                self.vBnd[iSec][iReg].nodesCoord = self.iBnd[iReg].nodesCoord.copy()
+                self.vBnd[iSec][iReg].elemsCoord = self.iBnd[iReg].elemsCoord.copy()
+
+        # Set mesh in the viscous solver
+        for iSec in range(self.cfg['nSections']):
+            # Compute section chord
+            xMin = np.min(self.vBnd[iSec][0].nodesCoord[:,0])
+            xMax = np.max(self.vBnd[iSec][0].nodesCoord[:,0])
+            chord = xMax - xMin
+            for reg in self.sec[iSec]:
+                if reg.name == 'wake':
+                    iReg = 1
+                elif reg.name == 'lower' or reg.name == 'upper':
+                    iReg = 0
+                else:
+                    raise RuntimeError('Invalid region', reg.name)
+                
+                xIdx = 0
+                if self.getnDim() == 2:
+                    yIdx = 1
+                    zIdx = 2
+                elif self.getnDim() == 3:
+                    yIdx = 2
+                    zIdx = 1
+                else:
+                    raise RuntimeError('Incorrect number of dimensions', self.getnDim())
+
+                nodeRows = tbox.std_vector_int()
+                if self.cfg['interpolator'] == 'Matching':
+                    for val in self.vBnd[iSec][iReg].getNodesRow(reg.name):
+                        nodeRows.push_back(int(val))
+                else:
+                    for i in range(len(self.vBnd[iSec][iReg].getNodesCoord(reg.name, xIdx))):
+                        nodeRows.push_back(int(0))
+
+                connectElems = tbox.std_vector_int()
+                if self.cfg['interpolator'] == 'Matching':
+                    for val in self.vBnd[iSec][iReg].getConnectList(reg.name):
+                        connectElems.push_back(int(val))
+                else:
+                    for i in range(len(self.vBnd[iSec][iReg].getNodesCoord(reg.name, xIdx))-1):
+                        connectElems.push_back(int(0))
+                
+                # x, y, z
+                xv = tbox.std_vector_double()
+                for val in self.vBnd[iSec][iReg].getNodesCoord(reg.name, xIdx):
+                    xv.push_back(val)
+                yv = tbox.std_vector_double()
+                for val in self.vBnd[iSec][iReg].getNodesCoord(reg.name, yIdx):
+                    yv.push_back(val)
+                zv = tbox.std_vector_double()
+                for val in self.vBnd[iSec][iReg].getNodesCoord(reg.name, zIdx):
+                    zv.push_back(val)
+
+                # Set mesh
+                reg.setMesh(xv,
+                            yv,
+                            zv,
+                            chord,
+                            xMin,
+                            nodeRows,
+                            connectElems)
+    
+    def updateStagnation(self):
+        """Update the stagnation points in the viscous solver.
+        """
+        for iSec in range(self.cfg['nSections']):
+            for reg in self.vBnd[iSec]:
+                reg.computeStagPoint()
+    
     def initializeViscousSolver(self):
         """Initialize the viscous sections in the viscous solver.
         """
@@ -289,4 +381,82 @@ class SolversInterface:
 
                 self.vBnd[iy][iReg].initStructures(nodesSec.shape[0])
                 self.vBnd[iy][iReg].nodesCoord = nodesSec
-                self.vBnd[iy][iReg].elemsCoord = cgSec
\ No newline at end of file
+                self.vBnd[iy][iReg].elemsCoord = cgSec
+
+    # Inviscid solver methods
+    def run()->int:
+        """Run inviscid solver
+        """
+        raise NotImplementedError('SolverInterface - run not implemented')
+    
+    def reset()->None:
+        """Reset solution
+        """
+        raise NotImplementedError('SolverInterface - reset not implemented')
+    
+    def save(self)->None:
+        """Save solution
+        """
+        raise NotImplementedError('SolverInterface - save not implemented')
+    
+    def writeCp(self)->None:
+        """Write pressure coefficient
+        """
+        raise NotImplementedError('SolverInterface - writeCp not implemented')
+    
+    # Inviscid solver properties
+    def getVerbose(self)->int:
+        """Get verbose
+        """
+        raise NotImplementedError('SolverInterface - getVerbose not implemented')
+
+    # Flow properties and aerodynamic coefficients
+    def getAoA(self)->float:
+        """Get angle of attack
+        """
+        raise NotImplementedError('SolverInterface - getAoA not implemented')
+
+    def getCl(self)->float:
+        """Get lift coefficient
+        """
+        raise NotImplementedError('SolverInterface - getCl not implemented')
+    
+    def getCd(self)->float:
+        """Get inviscid drag coefficient
+        """
+        raise NotImplementedError('SolverInterface - getCd not implemented')
+    
+    def getCm(self)->float:
+        """Get moment coefficient
+        """
+        raise NotImplementedError('SolverInterface - getCm not implemented')
+    
+    def getnDim(self)->int:
+        """Get number of dimensions
+        """
+        raise NotImplementedError('SolverInterface - getnDim not implemented')
+    
+    def getWing(self)->None:
+        """Get wing mesh
+        """
+        raise NotImplementedError('SolverInterface - getWing not implemented')
+    
+    def getMesh(self)->list:
+        """Get mesh
+        """
+        raise NotImplementedError('SolverInterface - getMesh not implemented')
+    
+    def getInviscidBC(self)->None:
+        """Get inviscid boundary condition
+        """
+        raise NotImplementedError('SolverInterface - getInviscidBC not implemented')
+    
+    def setBlowingVelocity(self)->None:
+        """Set blowing velocity
+        """
+        raise NotImplementedError('SolverInterface - setBlowingVelocity not implemented')
+    
+    def getnNodesDomain(self)->int:
+        """Get number of nodes in domain
+        """
+        raise NotImplementedError('SolverInterface - getnNodesDomain not implemented')
diff --git a/blast/interfaces/dart/DartInterface.py b/blast/interfaces/dart/DartInterface.py
index 0334541..7913ab5 100644
--- a/blast/interfaces/dart/DartInterface.py
+++ b/blast/interfaces/dart/DartInterface.py
@@ -76,7 +76,8 @@ class DartInterface(SolversInterface):
                     for j in range(size):
                         data[i,3+j] = vData[vElems[i%vElems.size()].nodes[0].row][j]
                 i += 1
-            print('writing data file ' + 'Cp' +sfx + '.dat')
+            if self.getVerbose() > 1:
+                print('writing data file ' + 'Cp' +sfx + '.dat')
             np.savetxt('Cp'+sfx+'.dat', data, fmt='%1.6e', delimiter=', ', header='x, y, z, Cp', comments='')
         
         elif self.getnDim() == 3:
@@ -90,7 +91,8 @@ class DartInterface(SolversInterface):
                 np.savetxt(self.iobj['msh'].name+'_Cp_surface'+sfx+'.dat', data, fmt='%1.6e', delimiter=', ', header='x, y, z, Cp', comments='')
                 import modules.dartflo.dart.utils as invUtils
                 if self.cfg['Format'] == 'vtk':
-                    print('Saving Cp files in vtk format. Msh {:s}, Tag {:.0f}'.format(self.iobj['msh'].name, self.cfg['saveTag']))
+                    if self.getVerbose() > 1:
+                        print('Saving Cp files in vtk format. Msh {:s}, Tag {:.0f}'.format(self.iobj['msh'].name, self.cfg['saveTag']))
                     invUtils.write_slices(self.iobj['msh'].name, self.cfg['writeSections'], self.cfg['saveTag'], sfx=sfx)
     
     def save(self, sfx='inviscid'):
@@ -154,17 +156,27 @@ class DartInterface(SolversInterface):
             for i, blw in enumerate(self.iBnd[n].blowingVel):
                 self.blw[n].setU(i, blw)
 
-    def setMesh(self):
+    def getWing(self):
         if self.getnDim() == 2:
-            self.getAirfoil()
+            self.__getWing2D()
         elif self.getnDim() == 3:
-            self.getWing()
+            self.__getWing3D()
         else:
             raise RuntimeError('dartInterface::Could not resolve how to set\
             the mesh. Either ''nDim'' is incorrect or ''msh'' was not set for\
             3D computations')
+    
+    def getMesh(self):
+        """Return the mesh on blowing boundaries
+        """
+        meshList = [np.zeros((b.nodes.size(), self.getnDim())) for b in self.blw]
+        for ib, b in enumerate(self.blw):
+            for inod, n in enumerate(b.nodes):
+                for idim in range(self.getnDim()):
+                    meshList[ib][inod, idim] = n.pos[idim]
+        return meshList
 
-    def getAirfoil(self):
+    def __getWing2D(self):
         """Create data structure for information transfer.
         """
         self.iBnd[0].initStructures(self.blw[0].nodes.size())
@@ -332,7 +344,7 @@ class DartInterface(SolversInterface):
             elemCoordW[i,3] = i
         self.iBnd[1].elemsCoord = elemCoordW
 
-    def getWing(self):
+    def __getWing3D(self):
         """Obtain the nodes' location and row and cg of all elements.
         """
 
diff --git a/blast/utils.py b/blast/utils.py
index 4da3ce6..db3d523 100644
--- a/blast/utils.py
+++ b/blast/utils.py
@@ -44,7 +44,7 @@ def initBL(Re, Minf, CFL0, nSections, xtrF = [None, None], span=0, verb=None):
         if xtrF[i] is None:
             xtrF[i] = -1
         if xtrF[i] != -1 and not(0<= xtrF[i] <= 1):
-            raise RuntimeError('Incorrect forced transition location') 
+            raise RuntimeError('Incorrect forced transition location')
 
     solver = blast.Driver(Re, Minf, CFL0, nSections, xtrF_top=xtrF[0], xtrF_bot=xtrF[1], _span=span, _verbose=verbose)
     return solver
@@ -70,7 +70,7 @@ def initBlast(iconfig, vconfig, iSolver='DART'):
     if 'sfx' not in vconfig:
         vconfig['sfx'] = ''
     # Viscous solver
-    vSolver = initBL(vconfig['Re'], vconfig['Minf'], vconfig['CFL0'], vconfig['nSections'], xtrF=vconfig['xtrF'])
+    vSolver = initBL(vconfig['Re'], vconfig['Minf'], vconfig['CFL0'], vconfig['nSections'], xtrF=vconfig['xtrF'], verb=vconfig['Verb'])
 
     # Inviscid solver
     if iSolver == 'DART':
-- 
GitLab