From 2dfc93ccfd3a48db0ee0457bd208920c9ee45978 Mon Sep 17 00:00:00 2001 From: Paul Dechamps <paul.dechamps@uliege.be> Date: Wed, 26 Feb 2025 11:41:20 +0100 Subject: [PATCH] (refactor) Renamed su2 interface --- blast/blUtils.py | 2 +- blast/interfaces/su2/SU2Interface.py | 361 --------------------- blast/interfaces/su2/blSU2Interface.py | 426 +++++++++++++++++-------- 3 files changed, 290 insertions(+), 499 deletions(-) delete mode 100644 blast/interfaces/su2/SU2Interface.py diff --git a/blast/blUtils.py b/blast/blUtils.py index 4abbd09..b7edca0 100644 --- a/blast/blUtils.py +++ b/blast/blUtils.py @@ -90,7 +90,7 @@ def initBlast(iconfig, vconfig, iSolver='DART', task='analysis'): if iSolver == 'DART': from blast.interfaces.dart.blDartInterface import DartInterface as interface elif iSolver == 'SU2': - from blast.interfaces.su2.SU2Interface import SU2Interface as interface + from blast.interfaces.su2.blSU2Interface import SU2Interface as interface else: raise RuntimeError(f"Solver {iSolver} currently not implemented") diff --git a/blast/interfaces/su2/SU2Interface.py b/blast/interfaces/su2/SU2Interface.py deleted file mode 100644 index 816ae48..0000000 --- a/blast/interfaces/su2/SU2Interface.py +++ /dev/null @@ -1,361 +0,0 @@ -#!/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. - -# SU2 interface -# Paul Dechamps -import numpy as np -import os -import sys - -from blast.interfaces.blSolversInterface import SolversInterface -import pysu2 - -class SU2Interface(SolversInterface): - def __init__(self, su2config, vconfig, task='analysis'): - self.filename = su2config.get('filename') - self._modify_meshpath(self.filename, su2config.get('meshfile')) - self.verbose = su2config.get('Verb', 0) - self.have_mpi, self.comm, self.status, self.myid = self.__setup_mpi() - self.solver = pysu2.CSinglezoneDriver(self.filename, 1, self.comm) - - # Prepare solver - self.comm.barrier() - self.solver.Preprocess(0) - - self.wingMarkersToID = {key: i for i, key in enumerate(self.solver.GetMarkerTags())} - self.wingTags = su2config.get('wingTags') - print('SU2Interface::SU2 solver initialized for', task) - - SolversInterface.__init__(self, vconfig) - - def __setup_mpi(self): - import sys - from mpi4py import MPI - if 'mpi4py.MPI' in sys.modules: - have_mpi = True - comm = MPI.COMM_WORLD - status = MPI.Status() - myid = comm.Get_rank() - else: - have_mpi = False - comm = None - status = None - myid = 0 - return have_mpi, comm, status, myid - - def run(self): - #Remove output in the terminal - if self.getVerbose() < 2: - sys.stdout = open(os.devnull, 'w') - - self.solver.Run() - self.solver.Postprocess() - # write outputs - self.solver.Monitor(0) - self.solver.Output(0) - self.comm.barrier() - #restore stdout - if self.getVerbose() < 2: - sys.stdout = sys.__stdout__ - if self.getVerbose() > 0: - print(f'SU2Interface::SU2 solver finished in {self.solver.GetOutputValue("INNER_ITER"):.0f} iterations. RMS_DENSITY: {self.solver.GetOutputValue("RMS_DENSITY"):.2f}') - return 0 - - def writeCp(self, sfx=''): - """ Write Cp distribution around the airfoil on file. - """ - - #GEt farfield pressure, velocity and density - vel_x_idx = self.solver.GetPrimitiveIndices()["VELOCITY_X"] - vel_y_idx = self.solver.GetPrimitiveIndices()["VELOCITY_Y"] - pressure_idx = self.solver.GetPrimitiveIndices()["PRESSURE"] - density_idx = self.solver.GetPrimitiveIndices()["DENSITY"] - - nNodes_farfield = self.solver.GetNumberMarkerNodes(self.wingMarkersToID['farfield']) - velocity_farfield = np.zeros(nNodes_farfield) - pressure_farfield = np.zeros(nNodes_farfield) - density_farfield = np.zeros(nNodes_farfield) - primitives = self.solver.Primitives() - for inode in range(nNodes_farfield): - nodeIndex = self.solver.GetMarkerNode(self.wingMarkersToID['farfield'], inode) - # Velocity - vel_x = primitives.Get(nodeIndex, vel_x_idx) - vel_y = primitives.Get(nodeIndex, vel_y_idx) - velocity_farfield[inode] = np.sqrt(vel_x**2 + vel_y**2) - # Pressure - pressure_farfield[inode] = primitives.Get(nodeIndex, pressure_idx) - # Density - density_farfield[inode] = primitives.Get(nodeIndex, density_idx) - ref_pressure = np.mean(pressure_farfield) - ref_velocity = np.mean(velocity_farfield) - ref_density = np.mean(density_farfield) - - cp = np.empty(0, dtype=float) - for body in self.iBnd: - for reg in body: - for inode, nodeIndexFloat in enumerate(reg.nodesCoord[:,-1]): - nodeIndex = int(nodeIndexFloat) - pressure = self.solver.Primitives().Get(nodeIndex, pressure_idx) - cp = np.append(cp, (pressure - ref_pressure)/(1/2*ref_density*ref_velocity**2)) - np.savetxt(f'Cp{sfx}.dat', np.column_stack((self.iBnd[0][0].nodesCoord[:, 0], cp))) - - def save(self, writer): - pass - - def getAoA(self): - return self.solver.GetOutputValue('AOA') * np.pi/180 - - def getnDim(self): - return self.solver.GetNumberDimensions() - - def getnBodies(self): - return 1 - - def getSpan(self, bodyname): - return 1.0 - - def getBlowingTypes(self, bodyname): - return['wing'] - - def getMinf(self): - vel_x_idx = self.solver.GetPrimitiveIndices()["VELOCITY_X"] - vel_y_idx = self.solver.GetPrimitiveIndices()["VELOCITY_Y"] - sound_speed_idx = self.solver.GetPrimitiveIndices()["SOUND_SPEED"] - - nNodes_farfield = self.solver.GetNumberMarkerNodes(self.wingMarkersToID['farfield']) - mach_farfield = np.zeros(nNodes_farfield) - primitives = self.solver.Primitives() - for inode in range(nNodes_farfield): - nodeIndex = self.solver.GetMarkerNode(self.wingMarkersToID['farfield'], inode) - vel_x = primitives.Get(nodeIndex, vel_x_idx) - vel_y = primitives.Get(nodeIndex, vel_y_idx) - sound_speed = primitives.Get(nodeIndex, sound_speed_idx) - mach_farfield[inode] = np.sqrt(vel_x**2 + vel_y**2) / sound_speed - return np.mean(mach_farfield) - - def getCl(self): - return self.solver.GetOutputValue('LIFT') - - def getCd(self): - return self.solver.GetOutputValue('DRAG') - - def getCm(self): - return self.solver.Get_Mx() - - def getVerbose(self): - return self.verbose - - def reset(self): - if self.getVerbose() > 0: - print('SU2Interface::Warning: Cannot reset solver.') - pass - - def getInviscidBC(self): - """ Extract the inviscid paramaters at the edge of the boundary layer - and use it as a boundary condition in the viscous solver. - """ - if self.getnDim() == 2: - velComponents = ['VELOCITY_X', 'VELOCITY_Y'] - elif self.getnDim() == 3: - velComponents = ['VELOCITY_X', 'VELOCITY_Y', 'VELOCITY_Z'] - else: - raise RuntimeError('su2Interface::Incorrect number of dimensions.') - - velIdx = np.zeros(self.getnDim(), dtype=int) - for i, velComp in enumerate(velComponents): - velIdx[i] = self.solver.GetPrimitiveIndices()[velComp] - soundSpeedIdx = self.solver.GetPrimitiveIndices()["SOUND_SPEED"] - densityIdx = self.solver.GetPrimitiveIndices()["DENSITY"] - - for body in self.iBnd: - for reg in body: - for inode, nodeIndexFloat in enumerate(reg.nodesCoord[:,-1]): - nodeIndex = int(nodeIndexFloat) - # Velocity - for idim in range(self.getnDim()): - reg.V[inode,idim] = self.solver.Primitives().Get(int(nodeIndex), int(velIdx[idim])) - # Density - reg.Rho[inode] = self.solver.Primitives().Get(nodeIndex, densityIdx) - # Mach number - vel_norm = 0.0 - for idim in range(self.getnDim()): - vel_norm += reg.V[inode,idim]**2 - reg.M[inode] = np.sqrt(vel_norm) / self.solver.Primitives().Get(nodeIndex, soundSpeedIdx) - - def setBlowingVelocity(self): - """ - Extracts the blowing velocity from the viscous calculation and applies it - as a boundary condition in the inviscid solver. - - Notes - ----- - - In SU2, the "Grid velocity" is a vector defined at the nodes in the global - frame of reference. - - In BLASTER, the blowing velocity is computed as a **scalar normal component** - at the element level. - - To impose the correct boundary condition, the normal blowing velocity must - be projected into the global frame and interpolated to the nodes. - """ - # Compute blowing velocity in the global frame of reference on the elements - blw_elems = np.zeros((self.iBnd[0][0].elemsCoord.shape[0], self.getnDim()), dtype=float) - normals = [] - for ielm in range(len(self.iBnd[0][0].elemsCoord)): - nod1 = ielm - nod2 = ielm + 1 - - x1 = self.iBnd[0][0].nodesCoord[nod1,:self.getnDim()] - x2 = self.iBnd[0][0].nodesCoord[nod2,:self.getnDim()] - - # Components of the tangent vector - t = np.empty(self.getnDim()) - for idim in range(self.getnDim()): - t[idim] = x2[idim] - x1[idim] - normt = np.linalg.norm(t) - - # Components of the normal vector - n = np.empty(self.getnDim()) - if self.getnDim() == 2: - # 90° rotation - n[0] = t[1] / normt - n[1] = -t[0] / normt - elif self.getnDim() == 3: - # Compute using cross product with another edge - raise RuntimeError('3D normal not implemented yet (use SU2 function if possible).') - normals.append(n) - - for idim in range(self.getnDim()): - blw_elems[ielm, idim] = self.iBnd[0][0].blowingVel[ielm] * n[idim] - - # Compute blowing velocity at nodes - blw_nodes = np.zeros((self.iBnd[0][0].nodesCoord.shape[0], 3), dtype=float) - for inode in range(self.iBnd[0][0].nodesCoord.shape[0]): - # Look for the elements sharing node inode - # At TE, elm1 is the last element of upper side and elm2 is the last element of lower side. - if inode == 0 or inode == self.iBnd[0][0].nodesCoord.shape[0]-1: - elm1 = 0 - elm2 = -1 - else: - elm1 = inode - 1 - elm2 = inode - - # Because the blowing velocity on the lower side points downwards - sign = 1 - if inode > self.iBnd[0][0].nodesCoord.shape[0]//2: - sign = -1 - - # The blowing velocity on one node is the average - # of the blowing velocity on the elements sharing the node - for idim in range(self.getnDim()): - blw_nodes[inode, idim] = sign * 0.5*(blw_elems[elm1, idim] + blw_elems[elm2, idim]) - - blw_nodes[blw_nodes < -0.01] = -0.01 - blw_nodes[blw_nodes > 0.01] = 0.01 - - # Set blowing velocity in SU2 solver - for arfTag in self.wingTags: - for ivertex in range(self.solver.GetNumberMarkerNodes(self.wingMarkersToID[arfTag])): - nodeIndex = self.solver.GetMarkerNode(self.wingMarkersToID[arfTag], ivertex) - blasterIndex = np.where(self.iBnd[0][0].nodesCoord[:, -1] == nodeIndex)[0][0] - self.solver.SetBlowingVelocity(nodeIndex, blw_nodes[blasterIndex, 0], blw_nodes[blasterIndex, 1], blw_nodes[blasterIndex, 2]) - - def getWing(self): - if self.getnDim() == 2: - self._getAirfoil(0) - elif self.getnDim() == 3: - self._getWing3D(0) - else: - raise RuntimeError('su2Interface::Incorrect number of dimensions.') - - def _getAirfoil(self, ibody): - """ Retreives the mesh used for SU2 Euler calculation. - Airfoil is already in seilig format (Upper TE -> Lower TE). - """ - nodesCoord = np.zeros((0, 4)) - for arfTag in self.wingTags: - nodesOnSide = np.zeros((self.solver.GetNumberMarkerNodes(self.wingMarkersToID[arfTag]), 4)) - for i in range(self.solver.GetNumberMarkerNodes(self.wingMarkersToID[arfTag])): - nodeIndex = self.solver.GetMarkerNode(self.wingMarkersToID[arfTag], i) - nodesOnSide[i, :self.getnDim()] = self.solver.Coordinates().Get(nodeIndex) - nodesOnSide[i, -1] = nodeIndex - # Sort in ascending order of first column - nodesOnSide = nodesOnSide[nodesOnSide[:,0].argsort()] - if len(self.wingTags) > 1: - if arfTag == 'airfoil': - nodesOnSide = np.flip(nodesOnSide, axis=0) - elif arfTag == 'airfoil_': - nodesOnSide = np.delete(nodesOnSide, 0, axis=0) - else: - # If we cannot identify upper and lower sides, we sort using - # the angle between the node and the TE. - # This method can fail for highly cambered airfoils. - - # Find the trailing edge - te = nodesOnSide[np.argmax(nodesOnSide[:, 0])] - - # Compute angles relative to TE - angles = np.arctan2(nodesOnSide[:, 1] - te[1], nodesOnSide[:, 0] - te[0]) - - # Sort by angle in descending order - nodesOnSide = nodesOnSide[np.argsort(-angles)] - - # Selig format - te_index = np.argmax(nodesOnSide[:, 0]) # Leading edge has min x - nodesOnSide[:te_index] = np.flip(nodesOnSide[:te_index], axis=0) - nodesOnSide[te_index:] = np.flip(nodesOnSide[te_index:], axis=0) - - # Duplicate TE point - nodesOnSide = np.row_stack((nodesOnSide[-1], nodesOnSide)) - nodesCoord = np.row_stack((nodesCoord, nodesOnSide)) - - elemsCoord = np.zeros((nodesCoord.shape[0]-1, 4)) - for i in range(nodesCoord.shape[0]-1): - elemsCoord[i, :self.getnDim()] = (nodesCoord[i, :self.getnDim()] + nodesCoord[i+1, :self.getnDim()]) / 2 - elemsCoord[i, -1] = i - - # Check that the airfoil has closed trailing edge - if np.any(nodesCoord[0, [0,self.getnDim()-1]] != nodesCoord[-1, [0,self.getnDim()-1]]): - raise RuntimeError('su2Interface::Airfoil has an open trailing edge.') - - # Check that the airfoil has one point at the leading edge - if len(np.where(nodesCoord[:,0] == np.min(nodesCoord[:,0]))[0]) > 1: - print(len(np.where(nodesCoord[:,0] == np.min(nodesCoord[:,0]))[0])) - raise RuntimeError('su2Interface::Airfoil has multiple points at the leading edge.') - - # Check for duplicated elements - if len(np.unique(elemsCoord[:,-1])) != elemsCoord.shape[0]: - raise RuntimeError('su2Interface::Duplicated elements in airfoil mesh.') - - # Fill the data structures - self.iBnd[ibody][0].initStructures(nodesCoord.shape[0], elemsCoord.shape[0]) - self.iBnd[ibody][0].nodesCoord = nodesCoord - self.iBnd[ibody][0].elemsCoord = elemsCoord - self.iBnd[ibody][0].setConnectList(np.array(nodesCoord[:,-1],dtype=int), - np.array(elemsCoord[:,-1],dtype=int)) - - def _modify_meshpath(self, config, mesh): - # Open config file and look for line MESH_FILENAME - # replace this line with MESH_FILENAME = mesh - with open(config, 'r') as f: - lines = f.readlines() - for i, line in enumerate(lines): - if line.startswith('MESH_FILENAME'): - lines[i] = f'MESH_FILENAME = {mesh}\n' - with open(config, 'w') as f: - f.writelines(lines) - break - else: - raise RuntimeError('su2Interface::MESH_FILENAME not found in config file.') diff --git a/blast/interfaces/su2/blSU2Interface.py b/blast/interfaces/su2/blSU2Interface.py index fa07bf4..816ae48 100644 --- a/blast/interfaces/su2/blSU2Interface.py +++ b/blast/interfaces/su2/blSU2Interface.py @@ -15,24 +15,32 @@ # See the License for the specific language governing permissions and # limitations under the License. - # SU2 interface # Paul Dechamps import numpy as np +import os +import sys from blast.interfaces.blSolversInterface import SolversInterface +import pysu2 class SU2Interface(SolversInterface): - def __init__(self, fileName, vSolver, cfg): + def __init__(self, su2config, vconfig, task='analysis'): + self.filename = su2config.get('filename') + self._modify_meshpath(self.filename, su2config.get('meshfile')) + self.verbose = su2config.get('Verb', 0) self.have_mpi, self.comm, self.status, self.myid = self.__setup_mpi() - try: - import pysu2 - self.solver = pysu2.CSinglezoneDriver(fileName, 1, self.comm) - print('SU2 successfully loaded.') - except: - print(ccolors.ANSI_RED, 'Could not load SU2. Make sure it is installed.', ccolors.ANSI_RESET) - raise RuntimeError('Import error') - SolversInterface.__init__(self, vSolver, cfg) + self.solver = pysu2.CSinglezoneDriver(self.filename, 1, self.comm) + + # Prepare solver + self.comm.barrier() + self.solver.Preprocess(0) + + self.wingMarkersToID = {key: i for i, key in enumerate(self.solver.GetMarkerTags())} + self.wingTags = su2config.get('wingTags') + print('SU2Interface::SU2 solver initialized for', task) + + SolversInterface.__init__(self, vconfig) def __setup_mpi(self): import sys @@ -48,162 +56,306 @@ class SU2Interface(SolversInterface): status = None myid = 0 return have_mpi, comm, status, myid - + def run(self): - self.comm.barrier() - # run solver + #Remove output in the terminal + if self.getVerbose() < 2: + sys.stdout = open(os.devnull, 'w') + self.solver.Run() self.solver.Postprocess() # write outputs - self.solver.Monitor(0) + self.solver.Monitor(0) self.solver.Output(0) self.comm.barrier() - return 1 + #restore stdout + if self.getVerbose() < 2: + sys.stdout = sys.__stdout__ + if self.getVerbose() > 0: + print(f'SU2Interface::SU2 solver finished in {self.solver.GetOutputValue("INNER_ITER"):.0f} iterations. RMS_DENSITY: {self.solver.GetOutputValue("RMS_DENSITY"):.2f}') + return 0 def writeCp(self, sfx=''): - cp = np.zeros((self.nPoints[0], 4)) - for i in range(len(self.iBnd[0].nodesCoord[:,0])): - cp[i, :3] = self.solver.GetVertexCoord(int(self.bndMarkers[0]), int(self.iBnd[0].connectListNodes[i])) - cp[i, 3] = self.solver.GetVertexCp(int(self.bndMarkers[0]), int(self.iBnd[0].connectListNodes[i])) - np.savetxt('Cp'+sfx+'.dat', cp, fmt='%1.8e', delimiter=', ', - header='{0:>14s}, {1:>14s}, {2:>14s}, {3:>14s}'.format('x','y', 'z', 'Cp'), comments='') - + """ Write Cp distribution around the airfoil on file. + """ + + #GEt farfield pressure, velocity and density + vel_x_idx = self.solver.GetPrimitiveIndices()["VELOCITY_X"] + vel_y_idx = self.solver.GetPrimitiveIndices()["VELOCITY_Y"] + pressure_idx = self.solver.GetPrimitiveIndices()["PRESSURE"] + density_idx = self.solver.GetPrimitiveIndices()["DENSITY"] + + nNodes_farfield = self.solver.GetNumberMarkerNodes(self.wingMarkersToID['farfield']) + velocity_farfield = np.zeros(nNodes_farfield) + pressure_farfield = np.zeros(nNodes_farfield) + density_farfield = np.zeros(nNodes_farfield) + primitives = self.solver.Primitives() + for inode in range(nNodes_farfield): + nodeIndex = self.solver.GetMarkerNode(self.wingMarkersToID['farfield'], inode) + # Velocity + vel_x = primitives.Get(nodeIndex, vel_x_idx) + vel_y = primitives.Get(nodeIndex, vel_y_idx) + velocity_farfield[inode] = np.sqrt(vel_x**2 + vel_y**2) + # Pressure + pressure_farfield[inode] = primitives.Get(nodeIndex, pressure_idx) + # Density + density_farfield[inode] = primitives.Get(nodeIndex, density_idx) + ref_pressure = np.mean(pressure_farfield) + ref_velocity = np.mean(velocity_farfield) + ref_density = np.mean(density_farfield) + + cp = np.empty(0, dtype=float) + for body in self.iBnd: + for reg in body: + for inode, nodeIndexFloat in enumerate(reg.nodesCoord[:,-1]): + nodeIndex = int(nodeIndexFloat) + pressure = self.solver.Primitives().Get(nodeIndex, pressure_idx) + cp = np.append(cp, (pressure - ref_pressure)/(1/2*ref_density*ref_velocity**2)) + np.savetxt(f'Cp{sfx}.dat', np.column_stack((self.iBnd[0][0].nodesCoord[:, 0], cp))) + def save(self, writer): pass def getAoA(self): + return self.solver.GetOutputValue('AOA') * np.pi/180 + + def getnDim(self): + return self.solver.GetNumberDimensions() + + def getnBodies(self): return 1 - + + def getSpan(self, bodyname): + return 1.0 + + def getBlowingTypes(self, bodyname): + return['wing'] + def getMinf(self): - return 1 - + vel_x_idx = self.solver.GetPrimitiveIndices()["VELOCITY_X"] + vel_y_idx = self.solver.GetPrimitiveIndices()["VELOCITY_Y"] + sound_speed_idx = self.solver.GetPrimitiveIndices()["SOUND_SPEED"] + + nNodes_farfield = self.solver.GetNumberMarkerNodes(self.wingMarkersToID['farfield']) + mach_farfield = np.zeros(nNodes_farfield) + primitives = self.solver.Primitives() + for inode in range(nNodes_farfield): + nodeIndex = self.solver.GetMarkerNode(self.wingMarkersToID['farfield'], inode) + vel_x = primitives.Get(nodeIndex, vel_x_idx) + vel_y = primitives.Get(nodeIndex, vel_y_idx) + sound_speed = primitives.Get(nodeIndex, sound_speed_idx) + mach_farfield[inode] = np.sqrt(vel_x**2 + vel_y**2) / sound_speed + return np.mean(mach_farfield) + def getCl(self): - return self.solver.Get_LiftCoeff() - + return self.solver.GetOutputValue('LIFT') + def getCd(self): - return self.solver.Get_DragCoeff() + return self.solver.GetOutputValue('DRAG') def getCm(self): return self.solver.Get_Mx() - + def getVerbose(self): - return 1 - + return self.verbose + def reset(self): - self.solver.Preprocess(0) - - def imposeInvBC(self, couplIter): + if self.getVerbose() > 0: + print('SU2Interface::Warning: Cannot reset solver.') + pass + + def getInviscidBC(self): """ Extract the inviscid paramaters at the edge of the boundary layer and use it as a boundary condition in the viscous solver. + """ + if self.getnDim() == 2: + velComponents = ['VELOCITY_X', 'VELOCITY_Y'] + elif self.getnDim() == 3: + velComponents = ['VELOCITY_X', 'VELOCITY_Y', 'VELOCITY_Z'] + else: + raise RuntimeError('su2Interface::Incorrect number of dimensions.') - Params - ------ - - couplIter (int): Coupling iteration. + velIdx = np.zeros(self.getnDim(), dtype=int) + for i, velComp in enumerate(velComponents): + velIdx[i] = self.solver.GetPrimitiveIndices()[velComp] + soundSpeedIdx = self.solver.GetPrimitiveIndices()["SOUND_SPEED"] + densityIdx = self.solver.GetPrimitiveIndices()["DENSITY"] + + for body in self.iBnd: + for reg in body: + for inode, nodeIndexFloat in enumerate(reg.nodesCoord[:,-1]): + nodeIndex = int(nodeIndexFloat) + # Velocity + for idim in range(self.getnDim()): + reg.V[inode,idim] = self.solver.Primitives().Get(int(nodeIndex), int(velIdx[idim])) + # Density + reg.Rho[inode] = self.solver.Primitives().Get(nodeIndex, densityIdx) + # Mach number + vel_norm = 0.0 + for idim in range(self.getnDim()): + vel_norm += reg.V[inode,idim]**2 + reg.M[inode] = np.sqrt(vel_norm) / self.solver.Primitives().Get(nodeIndex, soundSpeedIdx) + + def setBlowingVelocity(self): """ - # Retreive parameters. - for n in range(len(self.iBnd)): - for iRow, row in enumerate(self.iBnd[n].connectListNodes): - row=int(row) - self.iBnd[n].V[iRow,:] = self.solver.GetVertexVelocity(self.bndMarkers[n], row) - self.iBnd[n].M[iRow] = self.solver.GetVertexMach(self.bndMarkers[n], row) - self.iBnd[n].Rho[iRow] = self.solver.GetVertexDensity(self.bndMarkers[n], row) - if self.iBnd[n].Rho[iRow] == 0: - self.iBnd[n].Rho[iRow] = 1 - self.setViscousSolver(couplIter) - - def imposeBlwVel(self): - """ Extract the solution of the viscous calculation (blowing velocity) - and use it as a boundary condition in the inviscid solver. + Extracts the blowing velocity from the viscous calculation and applies it + as a boundary condition in the inviscid solver. + + Notes + ----- + - In SU2, the "Grid velocity" is a vector defined at the nodes in the global + frame of reference. + - In BLASTER, the blowing velocity is computed as a **scalar normal component** + at the element level. + - To impose the correct boundary condition, the normal blowing velocity must + be projected into the global frame and interpolated to the nodes. """ - if self.iBnd[0].connectListNodes[0] != self.iBnd[0].connectListNodes[-1]: - raise RuntimeError("List of connectivity is incorrect.") - - self.getBlowingBoundary() - # Impose blowing velocities. - for n in range(len(self.bndMarkers)): - if n == 0: - self.iBnd[n].blowingVel[0] = .5 * (self.iBnd[n].blowingVel[0] + self.iBnd[n].blowingVel[-1]) - self.iBnd[n].blowingVel = self.iBnd[n].blowingVel[self.iBnd[n].connectListElems[:-1].argsort()] - elif n == 1: - self.iBnd[1].blowingVel = np.insert(self.iBnd[1].blowingVel, 0, self.iBnd[0].blowingVel[0]) - self.iBnd[n].blowingVel = self.iBnd[n].blowingVel[self.iBnd[n].connectListElems.argsort()] - for iVertex in range(len(self.iBnd[n].blowingVel)-1): - self.solver.SetBlowing(self.bndMarkers[n], iVertex, self.iBnd[n].blowingVel[iVertex]) - - def setMesh(self): - - if self.cfg['nDim'] == 2: - self.getAirfoil() - elif self.cfg['nDim'] == 3: - self.getWing() + # Compute blowing velocity in the global frame of reference on the elements + blw_elems = np.zeros((self.iBnd[0][0].elemsCoord.shape[0], self.getnDim()), dtype=float) + normals = [] + for ielm in range(len(self.iBnd[0][0].elemsCoord)): + nod1 = ielm + nod2 = ielm + 1 + + x1 = self.iBnd[0][0].nodesCoord[nod1,:self.getnDim()] + x2 = self.iBnd[0][0].nodesCoord[nod2,:self.getnDim()] + + # Components of the tangent vector + t = np.empty(self.getnDim()) + for idim in range(self.getnDim()): + t[idim] = x2[idim] - x1[idim] + normt = np.linalg.norm(t) + + # Components of the normal vector + n = np.empty(self.getnDim()) + if self.getnDim() == 2: + # 90° rotation + n[0] = t[1] / normt + n[1] = -t[0] / normt + elif self.getnDim() == 3: + # Compute using cross product with another edge + raise RuntimeError('3D normal not implemented yet (use SU2 function if possible).') + normals.append(n) + + for idim in range(self.getnDim()): + blw_elems[ielm, idim] = self.iBnd[0][0].blowingVel[ielm] * n[idim] + + # Compute blowing velocity at nodes + blw_nodes = np.zeros((self.iBnd[0][0].nodesCoord.shape[0], 3), dtype=float) + for inode in range(self.iBnd[0][0].nodesCoord.shape[0]): + # Look for the elements sharing node inode + # At TE, elm1 is the last element of upper side and elm2 is the last element of lower side. + if inode == 0 or inode == self.iBnd[0][0].nodesCoord.shape[0]-1: + elm1 = 0 + elm2 = -1 + else: + elm1 = inode - 1 + elm2 = inode + + # Because the blowing velocity on the lower side points downwards + sign = 1 + if inode > self.iBnd[0][0].nodesCoord.shape[0]//2: + sign = -1 + + # The blowing velocity on one node is the average + # of the blowing velocity on the elements sharing the node + for idim in range(self.getnDim()): + blw_nodes[inode, idim] = sign * 0.5*(blw_elems[elm1, idim] + blw_elems[elm2, idim]) + + blw_nodes[blw_nodes < -0.01] = -0.01 + blw_nodes[blw_nodes > 0.01] = 0.01 + + # Set blowing velocity in SU2 solver + for arfTag in self.wingTags: + for ivertex in range(self.solver.GetNumberMarkerNodes(self.wingMarkersToID[arfTag])): + nodeIndex = self.solver.GetMarkerNode(self.wingMarkersToID[arfTag], ivertex) + blasterIndex = np.where(self.iBnd[0][0].nodesCoord[:, -1] == nodeIndex)[0][0] + self.solver.SetBlowingVelocity(nodeIndex, blw_nodes[blasterIndex, 0], blw_nodes[blasterIndex, 1], blw_nodes[blasterIndex, 2]) + + def getWing(self): + if self.getnDim() == 2: + self._getAirfoil(0) + elif self.getnDim() == 3: + self._getWing3D(0) else: - raise RuntimeError('su2Interface::Could not resolve how to set\ - the mesh. Either ''nDim'' is incorrect or ''msh'' was not set for\ - 3D computations') + raise RuntimeError('su2Interface::Incorrect number of dimensions.') - def getAirfoil(self): + def _getAirfoil(self, ibody): """ Retreives the mesh used for SU2 Euler calculation. Airfoil is already in seilig format (Upper TE -> Lower TE). """ - alltags = self.solver.GetAllBoundaryMarkers() - if 'wake' not in alltags: - #raise RuntimeError('Can not find tag wake.') - self.bndMarkers = [alltags['airfoil']] - del self.iBnd[1] - del self.vBnd[0][1] + nodesCoord = np.zeros((0, 4)) + for arfTag in self.wingTags: + nodesOnSide = np.zeros((self.solver.GetNumberMarkerNodes(self.wingMarkersToID[arfTag]), 4)) + for i in range(self.solver.GetNumberMarkerNodes(self.wingMarkersToID[arfTag])): + nodeIndex = self.solver.GetMarkerNode(self.wingMarkersToID[arfTag], i) + nodesOnSide[i, :self.getnDim()] = self.solver.Coordinates().Get(nodeIndex) + nodesOnSide[i, -1] = nodeIndex + # Sort in ascending order of first column + nodesOnSide = nodesOnSide[nodesOnSide[:,0].argsort()] + if len(self.wingTags) > 1: + if arfTag == 'airfoil': + nodesOnSide = np.flip(nodesOnSide, axis=0) + elif arfTag == 'airfoil_': + nodesOnSide = np.delete(nodesOnSide, 0, axis=0) + else: + # If we cannot identify upper and lower sides, we sort using + # the angle between the node and the TE. + # This method can fail for highly cambered airfoils. + + # Find the trailing edge + te = nodesOnSide[np.argmax(nodesOnSide[:, 0])] + + # Compute angles relative to TE + angles = np.arctan2(nodesOnSide[:, 1] - te[1], nodesOnSide[:, 0] - te[0]) + + # Sort by angle in descending order + nodesOnSide = nodesOnSide[np.argsort(-angles)] + + # Selig format + te_index = np.argmax(nodesOnSide[:, 0]) # Leading edge has min x + nodesOnSide[:te_index] = np.flip(nodesOnSide[:te_index], axis=0) + nodesOnSide[te_index:] = np.flip(nodesOnSide[te_index:], axis=0) + + # Duplicate TE point + nodesOnSide = np.row_stack((nodesOnSide[-1], nodesOnSide)) + nodesCoord = np.row_stack((nodesCoord, nodesOnSide)) + + elemsCoord = np.zeros((nodesCoord.shape[0]-1, 4)) + for i in range(nodesCoord.shape[0]-1): + elemsCoord[i, :self.getnDim()] = (nodesCoord[i, :self.getnDim()] + nodesCoord[i+1, :self.getnDim()]) / 2 + elemsCoord[i, -1] = i + + # Check that the airfoil has closed trailing edge + if np.any(nodesCoord[0, [0,self.getnDim()-1]] != nodesCoord[-1, [0,self.getnDim()-1]]): + raise RuntimeError('su2Interface::Airfoil has an open trailing edge.') + + # Check that the airfoil has one point at the leading edge + if len(np.where(nodesCoord[:,0] == np.min(nodesCoord[:,0]))[0]) > 1: + print(len(np.where(nodesCoord[:,0] == np.min(nodesCoord[:,0]))[0])) + raise RuntimeError('su2Interface::Airfoil has multiple points at the leading edge.') + + # Check for duplicated elements + if len(np.unique(elemsCoord[:,-1])) != elemsCoord.shape[0]: + raise RuntimeError('su2Interface::Duplicated elements in airfoil mesh.') + + # Fill the data structures + self.iBnd[ibody][0].initStructures(nodesCoord.shape[0], elemsCoord.shape[0]) + self.iBnd[ibody][0].nodesCoord = nodesCoord + self.iBnd[ibody][0].elemsCoord = elemsCoord + self.iBnd[ibody][0].setConnectList(np.array(nodesCoord[:,-1],dtype=int), + np.array(elemsCoord[:,-1],dtype=int)) + + def _modify_meshpath(self, config, mesh): + # Open config file and look for line MESH_FILENAME + # replace this line with MESH_FILENAME = mesh + with open(config, 'r') as f: + lines = f.readlines() + for i, line in enumerate(lines): + if line.startswith('MESH_FILENAME'): + lines[i] = f'MESH_FILENAME = {mesh}\n' + with open(config, 'w') as f: + f.writelines(lines) + break else: - self.bndMarkers = [alltags['airfoil'], alltags['wake']] - - self.nPoints = [self.solver.GetNumberVertices(tag) for tag in self.bndMarkers] - data = [np.zeros((nPts, 4)) for nPts in self.nPoints] - for n in range(len(self.nPoints)): - for i in range(self.nPoints[n]): - data[n][i,:3] = self.solver.GetVertexCoord(self.bndMarkers[n], i) - data[n][i,3] = i - - # Find TE and reorder coordinates - teNode = data[0][:,0].argmax() - """if teNode != 0: - save = data[0][0,:].copy() - data[0][0,:] = data[0][teNode,:].copy() - data[0][teNode,:] = save - teNode = data[0][:,0].argmax() - print('Airfoil coordinates were reordered automatically.')""" - - for i in range(len(data[0][0,:])): - data[0][:teNode+1,i] = np.flip(data[0][:teNode+1,i]) - data[0][teNode+1:,i] = np.flip(data[0][teNode+1:,i]) - - # Duplicate TE point - data[0] = np.row_stack((data[0], data[0][0,:])) - # Find LE point - self.leNode = data[0][:,0].argmin()+1 - # Recompute number of points - self.nPoints = [len(data[n]) for n in range(len(data))] - - dataElems = [np.zeros((nPts-1, 3)) for nPts in self.nPoints] - for n in range(len(data)): - for iPoint in range(len(data[n][:,0]) - 1): - dataElems[n][iPoint, :] = .5*(data[n][iPoint+1,:3] + data[n][iPoint, :3]) - # Nodes coordinates in the transformed space - self.phantomNodesCoord = [d.copy() for d in data] - self.phantomNodesCoord[0][:self.leNode, 0] *= -1 - - # Compute elements cg coordinates. - self.elemCgs = [np.zeros((nPts-1, 4)) for nPts in self.nPoints] - for n in range(len(self.elemCgs)): - for i in range(len(self.elemCgs[n][:,0])): - self.elemCgs[n][i,:] = (data[n][i,:] + data[n][i+1,:]) / 2 - - # Elements cg coordinates in the transformed space - self.phantomElemsCoord = [ecg.copy() for ecg in self.elemCgs] - self.phantomElemsCoord[0][:self.leNode, 0] *= -1 - - for n, bnd in enumerate(self.iBnd): - bnd.elemsCoord = dataElems[n] - bnd.nodesCoord = data[n] - bnd.setConnectList(data[n][:,3], data[n][:,3]) - bnd.V = np.zeros((self.nPoints[n], 3)) - bnd.M = np.zeros(self.nPoints[n]) - bnd.Rho = np.zeros(self.nPoints[n]) - self.solver.InitBlowing() + raise RuntimeError('su2Interface::MESH_FILENAME not found in config file.') -- GitLab