diff --git a/Common/include/CConfig.hpp b/Common/include/CConfig.hpp index 8d5e4f37789..d5c2b4139af 100644 --- a/Common/include/CConfig.hpp +++ b/Common/include/CConfig.hpp @@ -1295,6 +1295,20 @@ class CConfig { /*--- Additional flamelet solver options ---*/ FluidFlamelet_ParsedOptions flamelet_ParsedOptions; /*!< \brief Additional flamelet solver options */ + /*--- Mesh adaptation options ---*/ + bool Compute_Metric; /*!< \brief Determines if error estimation is taking place */ + bool Normalize_Metric; /*!< \brief Determines if metric tensor normalization is taking place */ + unsigned short Kind_Hessian_Method; /*!< \brief Numerical method for computation of Hessians. */ + unsigned short nMetric_Sensor; /*!< \brief Number of sensors to use for adaptation. */ + string* Metric_Sensor; /*!< \brief Sensors to use for adaptation (first entry is normalized, rest are Hessian-only). */ + + unsigned short Metric_Norm; /*!< \brief Lp-norm for mesh adaptation */ + unsigned long Metric_Complexity; /*!< \brief Constraint mesh complexity */ + unsigned short nAdapt_Time_Subinterval; /*!< \brief Number of unsteady time sub-intervals for adaptation. */ + su2double Metric_Hmax, /*!< \brief Maximum cell size */ + Metric_Hmin, /*!< \brief Minimum cell size */ + Metric_ARmax; /*!< \brief Maximum cell aspect ratio */ + /*! * \brief Set the default values of config options not set in the config file using another config object. * \param config - Config object to use the default values from. @@ -10244,4 +10258,91 @@ class CConfig { */ const FluidFlamelet_ParsedOptions& GetFlameletParsedOptions() const { return flamelet_ParsedOptions; } + /*! + * \brief Check if error estimation is being carried out + * \return TRUE<\code> if error estimation is taking place + */ + bool GetCompute_Metric(void) const { return Compute_Metric; } + + /*! + * \brief Check if metric tensor normalization is being carried out + * \return TRUE<\code> if metric normalization is taking place + */ + bool GetNormalize_Metric(void) const { return Normalize_Metric; } + + /*! + * \brief Get the kind of method for computation of Hessians used for anisotropy. + * \return Numerical method for computation of Hessians used for anisotropy. + */ + unsigned short GetKind_Hessian_Method(void) const { return Kind_Hessian_Method; } + + /*! + * \brief Get complete array of metric sensor names + * \return Array of sensor names + */ + string* GetMetric_Sensor() const { + return Metric_Sensor; + } + + /*! + * \brief Get metric sensor name by index + * \param[in] iSens - Index of the sensor + * \return Sensor name string + */ + string GetMetric_Sensor(unsigned short iSens) const { + if (iSens >= nMetric_Sensor) + SU2_MPI::Error("Sensor index out of range.", CURRENT_FUNCTION); + return Metric_Sensor[iSens]; + } + + /*! + * \brief Get the complete list of metric sensor names + * \return Vector of sensor name strings + */ + vector GetMetric_SensorList() const { + return vector(Metric_Sensor, Metric_Sensor + nMetric_Sensor); + } + + /*! + * \brief Get number of adaptation sensors + * \return Number of sensors + */ + unsigned short GetnMetric_Sensor() const { return nMetric_Sensor; } + + /*! + * \brief Get adaptation norm value (Lp) + */ + unsigned short GetMetric_Norm(void) const { return Metric_Norm; } + + /*! + * \brief Get maximum cell size + * \return Maximum cell size + */ + su2double GetMetric_Hmax(void) const { return Metric_Hmax; } + + /*! + * \brief Get minimum cell size + * \return Minimum cell size + */ + su2double GetMetric_Hmin(void) const { return Metric_Hmin; } + + /*! + * \brief Get maximum cell aspect ratio + * \return Maximum cell aspect ratio + */ + su2double GetMetric_ARmax(void) const { return Metric_ARmax; } + + /*! + * \brief Get constraint complexity + * \return Mesh complexity + */ + unsigned long GetMetric_Complexity(void) const { return Metric_Complexity; } + + /*! + * \brief Get number of unsteady adaptation sub-intervals + * \note Currently only one sub-interval supported + * \return Number of unsteady adaptation sub-intervals + */ + unsigned long GetnAdapt_Time_Subinterval(void) const { return nAdapt_Time_Subinterval; } + }; diff --git a/Common/include/option_structure.hpp b/Common/include/option_structure.hpp index b3d2478cf5f..766c2f42c90 100644 --- a/Common/include/option_structure.hpp +++ b/Common/include/option_structure.hpp @@ -2686,6 +2686,8 @@ enum PERIODIC_QUANTITIES { PERIODIC_LIM_PRIM_1 , /*!< \brief Primitive limiter communication phase 1 of 2 (periodic only). */ PERIODIC_LIM_PRIM_2 , /*!< \brief Primitive limiter communication phase 2 of 2 (periodic only). */ PERIODIC_IMPLICIT , /*!< \brief Implicit update communication to ensure consistency across periodic boundaries. */ + PERIODIC_GRAD_ADAPT , /*!< \brief Gradient vectors for anisotropic sizing metric (periodic only). */ + PERIODIC_HESSIAN , /*!< \brief Hessian tensors for anisotropic sizing metric (periodic only). */ }; /*! @@ -2717,6 +2719,9 @@ enum class MPI_QUANTITIES { MESH_DISPLACEMENTS , /*!< \brief Mesh displacements at the interface. */ SOLUTION_TIME_N , /*!< \brief Solution at time n. */ SOLUTION_TIME_N1 , /*!< \brief Solution at time n-1. */ + SENSOR_ADAPT , /*!< \brief Sensors for anisotropic sizing metric tensor. */ + GRADIENT_ADAPT , /*!< \brief Gradient vectors for anisotropic sizing metric tensor. */ + HESSIAN , /*!< \brief Hessian tensors for anisotropic sizing metric tensor. */ }; /*! @@ -2874,6 +2879,15 @@ static const MapType Deform_Kind_Map = { MakePair("RBF", DEFORM_KIND::RBF) }; +/*! + * \brief Type of sensor for anisotropic metrics. + */ +enum class SensorType : unsigned char { + PRIMITIVE, /*!< \brief Value read directly from the primitive variable array. */ + DERIVED, /*!< \brief Officially-supported computed quantity (e.g. Mach number). */ + CUSTOM, /*!< \brief User-defined sensor populated externally via the Python wrapper. */ +}; + #undef MakePair /* END_CONFIG_ENUMS */ diff --git a/Common/src/CConfig.cpp b/Common/src/CConfig.cpp index 65a84ed437d..89bd38f0bbc 100644 --- a/Common/src/CConfig.cpp +++ b/Common/src/CConfig.cpp @@ -3083,6 +3083,60 @@ void CConfig::SetConfig_Options() { /*!\brief ROM_SAVE_FREQ \n DESCRIPTION: How often to save snapshots for unsteady problems.*/ addUnsignedShortOption("ROM_SAVE_FREQ", rom_save_freq, 1); + /*--- options that are used for mesh adaptation ---*/ + /*!\par CONFIG_CATEGORY:Adaptation Options \ingroup Config*/ + + /*!\brief COMPUTE_METRIC \n DESCRIPTION: Compute an error estimate */ + addBoolOption("COMPUTE_METRIC", Compute_Metric, false); + /*!\brief NORMALIZE_METRIC \n DESCRIPTION: Normalize the metric tensor */ + addBoolOption("NORMALIZE_METRIC", Normalize_Metric, false); + /*!\brief NUM_METHOD_HESS + * \n DESCRIPTION: Numerical method for Hessian computation \n OPTIONS: See \link Gradient_Map \endlink. \n DEFAULT: GREEN_GAUSS. \ingroup Config*/ + addEnumOption("NUM_METHOD_HESS", Kind_Hessian_Method, Gradient_Map, GREEN_GAUSS); + + /*!\brief METRIC_SENSOR \n DESCRIPTION: Sensors for mesh adaptation metric field */ + addStringListOption("METRIC_SENSOR", nMetric_Sensor, Metric_Sensor); + /*!\brief METRIC_NORM \n DESCRIPTION: Lp-norm for mesh adaptation */ + addUnsignedShortOption("METRIC_NORM", Metric_Norm, 2); + /*!\brief METRIC_COMPLEXITY \n DESCRIPTION: Constraint mesh complexity */ + addUnsignedLongOption("METRIC_COMPLEXITY", Metric_Complexity, 10000); + /*!\brief ADAP_TIME_SUBINTERVAL \n DESCRIPTION: Number of time subintervals in unsteady mesh adaptation */ + addUnsignedShortOption("ADAP_TIME_SUBINTERVAL", nAdapt_Time_Subinterval, 1); + + /*!\brief METRIC_HMAX \n DESCRIPTION: Constraint maximum cell size */ + addDoubleOption("METRIC_HMAX", Metric_Hmax, 10.0); + /*!\brief METRIC_HMIN \n DESCRIPTION: Constraint minimum cell size */ + addDoubleOption("METRIC_HMIN", Metric_Hmin, 1.0E-8); + /*!\brief METRIC_ARMAX \n DESCRIPTION: Constraint maximum cell aspect ratio */ + addDoubleOption("METRIC_ARMAX", Metric_ARmax, 1.0E6); + /*!\brief METRIC_HGRAD \n DESCRIPTION: Size gradation smoothing parameter */ + addPythonOption("METRIC_HGRAD"); + + /*!\brief ADAP_ITER \n DESCRIPTION: Mesh adaptation inner iterations per complexity */ + addPythonOption("ADAP_ITER"); + /*!\brief ADAP_COMPLEXITIES \n DESCRIPTION: List of constraint (target) mesh complexities for mesh convergence study */ + addPythonOption("ADAP_COMPLEXITIES"); + /*!\brief ADAP_FLOW_ITERS \n DESCRIPTION: Primal solver iterations at each target complexity */ + addPythonOption("ADAP_FLOW_ITERS"); + /*!\brief ADAP_ADJ_ITERS \n DESCRIPTION: Adjoint solver iterations at each target complexity */ + addPythonOption("ADAP_ADJ_ITERS"); + /*!\brief ADAP_FLOW_CFLS \n DESCRIPTION: Primal solver CFL number at each target complexity */ + addPythonOption("ADAP_FLOW_CFLS"); + /*!\brief ADAP_ADJ_CFLS \n DESCRIPTION: Adjoint solver CFL number at each target complexity */ + addPythonOption("ADAP_ADJ_CFLS"); + /*!\brief ADAP_RESIDUAL_REDUCTIONS \n DESCRIPTION: Residual reduction at each target complexity */ + addPythonOption("ADAP_RESIDUAL_REDUCTIONS"); + /*!\brief ADAP_HMAXS \n DESCRIPTION: Maximum cell size at each target complexity */ + addPythonOption("ADAP_HMAXS"); + /*!\brief ADAP_HMINS \n DESCRIPTION: Minimum cell size at each target complexity */ + addPythonOption("ADAP_HMINS"); + /*!\brief ADAP_HGRAD \n DESCRIPTION: Size gradation smoothing parameter */ + addPythonOption("ADAP_HGRAD"); + /*!\brief ADAP_HAUSD \n DESCRIPTION: Hausdorff distance parameter for surface remeshing */ + addPythonOption("ADAP_HAUSD"); + /*!\brief ADAP_ANGLE \n DESCRIPTION: Sharp angle detection parameter for surface remeshing */ + addPythonOption("ADAP_ANGLE"); + /* END_CONFIG_OPTIONS */ } @@ -5713,6 +5767,30 @@ void CConfig::SetPostprocessing(SU2_COMPONENT val_software, unsigned short val_i SU2_MPI::Error("BOUNDED_SCALAR discretization can only be used for incompressible problems.", CURRENT_FUNCTION); } + /*--- Checks for mesh adaptation ---*/ + if (Compute_Metric) { + /*--- Check that config is valid for requested sensor ---*/ + for (unsigned short iSensor = 0; iSensor < nMetric_Sensor; iSensor++) { + const string& sensor_name = Metric_Sensor[iSensor]; + /*--- If using GOAL, it must be the only sensor and the discrete adjoint must be used ---*/ + /*--- TODO: goal-oriented adaptation ---*/ + if (sensor_name == "GOAL") { + SU2_MPI::Error("Adaptation sensor GOAL not yet supported.", CURRENT_FUNCTION); + } + } + + /*--- Only GG Hessians for now ---*/ + if (Kind_Hessian_Method != GREEN_GAUSS) { + SU2_MPI::Error("NUM_METHOD_HESS must be GREEN_GAUSS.", CURRENT_FUNCTION); + } + + /*--- Make sure only using single adaptation sub-interval for steady problems ---*/ + if(TimeMarching == TIME_MARCHING::STEADY) + nAdapt_Time_Subinterval = 1; + if (nAdapt_Time_Subinterval != 1) + SU2_MPI::Error("Adaptation sub-intervals not yet supported. Set ADAP_TIME_SUBINTERVAL = 1 or remove from config.", CURRENT_FUNCTION); + } + } void CConfig::SetMarkers(SU2_COMPONENT val_software) { @@ -7914,6 +7992,34 @@ void CConfig::SetOutput(SU2_COMPONENT val_software, unsigned short val_izone) { cout << "Actuator disk BEM method propeller data read from file: " << GetBEM_prop_filename() << endl; } } + + if (val_software == SU2_COMPONENT::SU2_CFD || val_software == SU2_COMPONENT::SU2_SOL) { + if (Compute_Metric) { + cout << endl <<"---------------- Mesh Adaptation Information ( Zone " << iZone << " ) -----------------" << endl; + cout << "Adaptation sensor(s): "; + for (unsigned short iSensor = 0; iSensor < nMetric_Sensor; iSensor++) { + cout << Metric_Sensor[iSensor]; + if (iSensor < nMetric_Sensor - 1 ) cout << ", "; + } + cout << endl; + switch (Kind_Hessian_Method) { + case GREEN_GAUSS: cout << "Hessian for adaptive metric: Green-Gauss." << endl; break; + } + if (Normalize_Metric) { + cout << "Target complexity: " << Metric_Complexity << endl; + if (TimeMarching != TIME_MARCHING::STEADY) { + cout << " Unsteady adaptation sub-intervals: " << nAdapt_Time_Subinterval << endl; + cout << " Target space-time complexity: " << Metric_Complexity * nAdapt_Time_Subinterval << endl; + } + cout << "Lp norm: " << Metric_Norm << endl; + cout << "Min. edge length: " << Metric_Hmin << endl; + cout << "Max. edge length: " << Metric_Hmax << endl; + } + else { + cout << "Output unnormalized metric field." << endl; + } + } + } } bool CConfig::TokenizeString(string & str, string & option_name, diff --git a/SU2_CFD/include/drivers/CDriverBase.hpp b/SU2_CFD/include/drivers/CDriverBase.hpp index 1a4101bee95..043ede2e46c 100644 --- a/SU2_CFD/include/drivers/CDriverBase.hpp +++ b/SU2_CFD/include/drivers/CDriverBase.hpp @@ -515,6 +515,23 @@ class CDriverBase { */ map GetPrimitiveIndices() const; + /*! + * \brief Get the local index of a named metric sensor in the flow solver. + * Used by Python custom sensor registries to cache indices before the run loop. + * \param[in] sensor_name - Name as listed in METRIC_SENSOR config. + * \return Sensor index, or -1 if not found. + */ + short GetMetricSensorIndex(const std::string& sensor_name) const; + + /*! + * \brief Get a read/write view of adaptation sensor values on all mesh nodes of the flow solver. + * \warning Adaptation sensors are only available for flow solvers with metric sensors configured. + */ + inline CPyWrapperMatrixView AdaptSensors() { + auto* solver = GetSolverAndCheckMarker(FLOW_SOL); + return CPyWrapperMatrixView(const_cast(solver->GetNodes()->GetSensor_Adapt()), "AdaptSensors", false); + } + /*! * \brief Get a read/write view of the current primitive variables on all mesh nodes of the flow solver. * \warning Primitive variables are only available for flow solvers. diff --git a/SU2_CFD/include/drivers/CSinglezoneDriver.hpp b/SU2_CFD/include/drivers/CSinglezoneDriver.hpp index e4c6b752ce8..bcda037e0cf 100644 --- a/SU2_CFD/include/drivers/CSinglezoneDriver.hpp +++ b/SU2_CFD/include/drivers/CSinglezoneDriver.hpp @@ -110,4 +110,10 @@ class CSinglezoneDriver : public CDriver { */ bool Monitor(unsigned long TimeIter) override; + /*! + * \brief Perform all steps to compute the metric tensor. + * \param[in] restartMetric - Whether this is the initial sub-interval metric computation for an unsteady restart. + */ + virtual void ComputeMetricField(bool restartMetric = false); + }; diff --git a/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp b/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp index dc5a74819a9..d69af1d7edc 100644 --- a/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp +++ b/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp @@ -179,6 +179,140 @@ void computeGradientsGreenGauss(CSolver* solver, MPI_QUANTITIES kindMpiComm, PER solver->InitiateComms(&geometry, &config, kindMpiComm); solver->CompleteComms(&geometry, &config, kindMpiComm); } + +template +void computeHessiansGreenGauss(CSolver* solver, MPI_QUANTITIES kindMpiComm, PERIODIC_QUANTITIES kindPeriodicComm, + CGeometry& geometry, const CConfig& config, const GradientType& gradient, + const size_t varBegin, const size_t varEnd, const int idxVel, GradientType& hessian) { + const size_t nPointDomain = geometry.GetnPointDomain(); + +#ifdef HAVE_OMP + constexpr size_t OMP_MAX_CHUNK = 512; + + const auto chunkSize = computeStaticChunkSize(nPointDomain, omp_get_max_threads(), OMP_MAX_CHUNK); +#endif + + const size_t nSymMat = 3 * (nDim - 1); + + /*--- For each (non-halo) volume integrate over its faces (edges). ---*/ + + SU2_OMP_FOR_DYN(chunkSize) + for (size_t iPoint = 0; iPoint < nPointDomain; ++iPoint) { + auto nodes = geometry.nodes; + + /*--- Clear the Hessian. --*/ + + for (size_t iVar = varBegin; iVar < varEnd; ++iVar) + for (size_t iMat = 0; iMat < nSymMat; ++iMat) hessian(iPoint, iVar, iMat) = 0.0; + + /*--- Handle averaging and division by volume in one constant. ---*/ + + su2double halfOnVol = 0.5 / (nodes->GetVolume(iPoint) + nodes->GetPeriodicVolume(iPoint)); + + /*--- Add a contribution due to each neighbor. ---*/ + + for (size_t iNeigh = 0; iNeigh < nodes->GetnPoint(iPoint); ++iNeigh) { + size_t iEdge = nodes->GetEdge(iPoint,iNeigh); + size_t jPoint = nodes->GetPoint(iPoint,iNeigh); + + /*--- Determine if edge points inwards or outwards of iPoint. + * If inwards we need to flip the area vector. ---*/ + + su2double dir = (iPoint < jPoint)? 1.0 : -1.0; + su2double weight = dir * halfOnVol; + + const auto area = geometry.edges->GetNormal(iEdge); + + for (size_t iVar = varBegin; iVar < varEnd; ++iVar) { + /*--- Precompute per-dimension fluxes for this neighbor. ---*/ + su2double flux[nDim]; + for (size_t i = 0; i < nDim; ++i) + flux[i] = weight * (gradient(iPoint, iVar, i) + gradient(jPoint, iVar, i)); + + /*--- Diagonal entries. ---*/ + for (size_t i = 0; i < nDim; ++i) { + const size_t ind = i * (i + 1) / 2 + i; + hessian(iPoint, iVar, ind) += flux[i] * area[i]; + } + + /*--- Off-diagonal entries (lower triangle only, symmetric by construction). ---*/ + for (size_t i = 1; i < nDim; ++i) { + for (size_t j = 0; j < i; ++j) { + const size_t ind = i * (i + 1) / 2 + j; + hessian(iPoint, iVar, ind) += 0.5 * (flux[i] * area[j] + flux[j] * area[i]); + } + } + } // variables + } // neighbors + } // points + END_SU2_OMP_FOR + + /*--- Add edges of markers that contribute to the Hessians ---*/ + for (size_t iMarker = 0; iMarker < geometry.GetnMarker(); ++iMarker) { + if ((config.GetMarker_All_KindBC(iMarker) != INTERNAL_BOUNDARY) && + (config.GetMarker_All_KindBC(iMarker) != NEARFIELD_BOUNDARY) && + (config.GetMarker_All_KindBC(iMarker) != PERIODIC_BOUNDARY)) { + /*--- Work is shared in inner loop as two markers + * may try to update the same point. ---*/ + + SU2_OMP_FOR_STAT(32) + for (size_t iVertex = 0; iVertex < geometry.GetnVertex(iMarker); ++iVertex) { + size_t iPoint = geometry.vertex[iMarker][iVertex]->GetNode(); + auto nodes = geometry.nodes; + + /*--- Halo points do not need to be considered. ---*/ + + if (!nodes->GetDomain(iPoint)) continue; + + su2double volume = nodes->GetVolume(iPoint) + nodes->GetPeriodicVolume(iPoint); + const auto area = geometry.vertex[iMarker][iVertex]->GetNormal(); + + for (size_t iVar = varBegin; iVar < varEnd; ++iVar) { + /*--- Precompute per-dimension fluxes for this boundary vertex. ---*/ + su2double flux[nDim]; + for (size_t i = 0; i < nDim; ++i) + flux[i] = gradient(iPoint, iVar, i) / volume; + + /*--- Diagonal entries. ---*/ + for (size_t i = 0; i < nDim; ++i) { + const size_t ind = i * (i + 1) / 2 + i; + hessian(iPoint, iVar, ind) -= flux[i] * area[i]; + } + + /*--- Off-diagonal entries (lower triangle only, symmetric by construction). ---*/ + for (size_t i = 1; i < nDim; ++i) { + for (size_t j = 0; j < i; ++j) { + const size_t ind = i * (i + 1) / 2 + j; + hessian(iPoint, iVar, ind) -= 0.5 * (flux[i] * area[j] + flux[j] * area[i]); + } + } + } // variables + } // vertices + END_SU2_OMP_FOR + } //found right marker + } // iMarkers + + /*--- Compute the corrections for symmetry planes and Euler walls. ---*/ + /*--- TODO? correctHessiansSymmetry() ---*/ + + /*--- If no solver was provided we do not communicate ---*/ + + if (solver == nullptr) return; + + /*--- Account for periodic contributions. ---*/ + + for (size_t iPeriodic = 1; iPeriodic <= config.GetnMarker_Periodic()/2; ++iPeriodic) + { + solver->InitiatePeriodicComms(&geometry, &config, iPeriodic, kindPeriodicComm); + solver->CompletePeriodicComms(&geometry, &config, iPeriodic, kindPeriodicComm); + } + + /*--- Obtain the gradients at halo points from the MPI ranks that own them. ---*/ + + solver->InitiateComms(&geometry, &config, kindMpiComm); + solver->CompleteComms(&geometry, &config, kindMpiComm); + +} } // namespace detail @@ -205,3 +339,23 @@ void computeGradientsGreenGauss(CSolver* solver, MPI_QUANTITIES kindMpiComm, PER break; } } + +template +void computeHessiansGreenGauss(CSolver* solver, MPI_QUANTITIES kindMpiComm, PERIODIC_QUANTITIES kindPeriodicComm, + CGeometry& geometry, const CConfig& config, const GradientType& gradient, + const size_t varBegin, const size_t varEnd, const int idxVel, GradientType& hessian) { + switch (geometry.GetnDim()) { + case 2: + detail::computeHessiansGreenGauss<2>(solver, kindMpiComm, kindPeriodicComm, geometry, config, gradient, + varBegin, varEnd, idxVel, hessian); + break; + case 3: + detail::computeHessiansGreenGauss<3>(solver, kindMpiComm, kindPeriodicComm, geometry, config, gradient, + varBegin, varEnd, idxVel, hessian); + break; + default: + SU2_MPI::Error("Too many dimensions to compute Hessians.", CURRENT_FUNCTION); + break; + } +} + diff --git a/SU2_CFD/include/metrics/computeMetrics.hpp b/SU2_CFD/include/metrics/computeMetrics.hpp new file mode 100644 index 00000000000..9816bff3575 --- /dev/null +++ b/SU2_CFD/include/metrics/computeMetrics.hpp @@ -0,0 +1,374 @@ +/*! + * \file computeMetrics.hpp + * \brief Generic implementation of the metric tensor computation. + * \note This allows the same implementation to be used for goal-oriented + * or feature-based mesh adaptation. + * \author B. Munguía + * \version 8.4.0 "Harrier" + * + * SU2 Project Website: https://su2code.github.io + * + * The SU2 Project is maintained by the SU2 Foundation + * (http://su2foundation.org) + * + * Copyright 2012-2025, SU2 Contributors (cf. AUTHORS.md) + * + * SU2 is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * SU2 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with SU2. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include "../../../Common/include/parallelization/omp_structure.hpp" +#include "../../../Common/include/linear_algebra/blas_structure.hpp" +#include "../../../Common/include/toolboxes/geometry_toolbox.hpp" + +namespace tensor { +struct metric { + template + static void get(MetricType& metric_field, const unsigned long iPoint, + const unsigned short iSensor, MatrixType& mat, unsigned short nDim) { + switch(nDim) { + case 2: { + mat[0][0] = metric_field(iPoint, 0); mat[0][1] = metric_field(iPoint, 1); + mat[1][0] = metric_field(iPoint, 1); mat[1][1] = metric_field(iPoint, 2); + break; + } + case 3: { + mat[0][0] = metric_field(iPoint, 0); mat[0][1] = metric_field(iPoint, 1); mat[0][2] = metric_field(iPoint, 3); + mat[1][0] = metric_field(iPoint, 1); mat[1][1] = metric_field(iPoint, 2); mat[1][2] = metric_field(iPoint, 4); + mat[2][0] = metric_field(iPoint, 3); mat[2][1] = metric_field(iPoint, 4); mat[2][2] = metric_field(iPoint, 5); + break; + } + } + } + + template + static void set(MetricType& metric_field, const unsigned long iPoint, + const unsigned short iSensor, MatrixType& mat, ScalarType scale, + unsigned short nDim) { + switch(nDim) { + case 2: { + metric_field(iPoint, 0) = mat[0][0] * scale; + metric_field(iPoint, 1) = mat[0][1] * scale; + metric_field(iPoint, 2) = mat[1][1] * scale; + break; + } + case 3: { + metric_field(iPoint, 0) = mat[0][0] * scale; + metric_field(iPoint, 1) = mat[0][1] * scale; + metric_field(iPoint, 2) = mat[1][1] * scale; + metric_field(iPoint, 3) = mat[0][2] * scale; + metric_field(iPoint, 4) = mat[1][2] * scale; + metric_field(iPoint, 5) = mat[2][2] * scale; + break; + } + } + } +}; + +struct hessian { + template + static void get(MetricType& metric_field, const unsigned long iPoint, + const unsigned short iSensor, MatrixType& mat, unsigned short nDim) { + switch(nDim) { + case 2: { + mat[0][0] = metric_field(iPoint, iSensor, 0); mat[0][1] = metric_field(iPoint, iSensor, 1); + mat[1][0] = metric_field(iPoint, iSensor, 1); mat[1][1] = metric_field(iPoint, iSensor, 2); + break; + } + case 3: { + mat[0][0] = metric_field(iPoint, iSensor, 0); mat[0][1] = metric_field(iPoint, iSensor, 1); mat[0][2] = metric_field(iPoint, iSensor, 3); + mat[1][0] = metric_field(iPoint, iSensor, 1); mat[1][1] = metric_field(iPoint, iSensor, 2); mat[1][2] = metric_field(iPoint, iSensor, 4); + mat[2][0] = metric_field(iPoint, iSensor, 3); mat[2][1] = metric_field(iPoint, iSensor, 4); mat[2][2] = metric_field(iPoint, iSensor, 5); + break; + } + } + } + + template + static void set(MetricType& metric_field, const unsigned long iPoint, + const unsigned short iSensor, MatrixType& mat, ScalarType scale, + unsigned short nDim) { + switch(nDim) { + case 2: { + metric_field(iPoint, iSensor, 0) = mat[0][0] * scale; + metric_field(iPoint, iSensor, 1) = mat[0][1] * scale; + metric_field(iPoint, iSensor, 2) = mat[1][1] * scale; + break; + } + case 3: { + metric_field(iPoint, iSensor, 0) = mat[0][0] * scale; + metric_field(iPoint, iSensor, 1) = mat[0][1] * scale; + metric_field(iPoint, iSensor, 2) = mat[1][1] * scale; + metric_field(iPoint, iSensor, 3) = mat[0][2] * scale; + metric_field(iPoint, iSensor, 4) = mat[1][2] * scale; + metric_field(iPoint, iSensor, 5) = mat[2][2] * scale; + break; + } + } + } +}; +} // namespace tensor + +namespace detail { + +/*! + * \brief Compute determinant of eigenvalues for different dimensions. + * \param[in] EigVal - Array of eigenvalues. + * \return Determinant value. + */ +template +ScalarType computeDeterminant(const ScalarType* EigVal) { + if constexpr (nDim == 2) { + return EigVal[0] * EigVal[1]; + } else if constexpr (nDim == 3) { + return EigVal[0] * EigVal[1] * EigVal[2]; + } else { + static_assert(nDim == 2 || nDim == 3, "Only 2D and 3D supported"); + return ScalarType(0.0); + } +} + +/*! + * \brief Make the eigenvalues of the metrics positive. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + * \param[in] iSensor - Index of the sensor to work on. + * \param[in,out] metric - Metric container. + */ +template +void setPositiveDefiniteMetrics(CGeometry& geometry, const CConfig& config, + unsigned short iSensor, MetricType& metric) { + + const unsigned long nPointDomain = geometry.GetnPointDomain(); + + ScalarType A[nDim][nDim], EigVec[nDim][nDim], EigVal[nDim], work[nDim]; + + /*--- Minimum eigenvalue threshold ---*/ + const ScalarType eps = 1e-20; + + for (auto iPoint = 0ul; iPoint < nPointDomain; ++iPoint) { + /*--- Get full metric tensor ---*/ + Tensor::get(metric, iPoint, iSensor, A, nDim); + + /*--- Compute eigenvalues and eigenvectors ---*/ + CBlasStructure::EigenDecomposition(A, EigVec, EigVal, nDim, work); + + /*--- Make positive definite by taking absolute value of eigenvalues ---*/ + /*--- Handle NaN and very small values that could cause numerical issues ---*/ + for (auto iDim = 0; iDim < nDim; iDim++) { + if (std::isnan(EigVal[iDim])) { + /*--- NaN detected, set to small positive value ---*/ + EigVal[iDim] = eps; + } else { + /*--- Take absolute value and ensure minimum threshold ---*/ + EigVal[iDim] = max(fabs(EigVal[iDim]), eps); + } + } + + CBlasStructure::EigenRecomposition(A, EigVec, EigVal, nDim); + + /*--- Store upper half of metric tensor ---*/ + Tensor::set(metric, iPoint, iSensor, A, 1.0, nDim); + } +} + +/*! + * \brief Integrate the Hessian field for the Lp-norm normalization of the metric. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + * \param[in] iSensor - Index of the sensor to work on. + * \param[in] metric - Metric container. + * \return Integral of the metric tensor determinant. +*/ +template +ScalarType integrateMetrics(CGeometry& geometry, const CConfig& config, + unsigned short iSensor, MetricType& metric) { + + const unsigned long nPointDomain = geometry.GetnPointDomain(); + + /*--- Constants defining normalization ---*/ + const ScalarType p = config.GetMetric_Norm(); + const ScalarType normExp = p / (2.0 * p + nDim); + + ScalarType localIntegral = 0.0; + ScalarType globalIntegral = 0.0; + for (auto iPoint = 0ul; iPoint < nPointDomain; ++iPoint) { + auto nodes = geometry.nodes; + + /*--- Calculate determinant ---*/ + ScalarType det; + if constexpr (nDim == 2) { + const ScalarType m00 = metric(iPoint, 0); + const ScalarType m01 = metric(iPoint, 1); + const ScalarType m11 = metric(iPoint, 2); + det = m00 * m11 - m01 * m01; + } else if constexpr (nDim == 3) { + const ScalarType m00 = metric(iPoint, 0); + const ScalarType m01 = metric(iPoint, 1); + const ScalarType m11 = metric(iPoint, 2); + const ScalarType m02 = metric(iPoint, 3); + const ScalarType m12 = metric(iPoint, 4); + const ScalarType m22 = metric(iPoint, 5); + det = m00 * (m11 * m22 - m12 * m12) - m01 * (m01 * m22 - m02 * m12) + m02 * (m01 * m12 - m02 * m11); + } + + /*--- Integrate determinant ---*/ + const ScalarType Vol = SU2_TYPE::GetValue(nodes->GetVolume(iPoint)); + localIntegral += pow(abs(det), normExp) * Vol; + } + + CBaseMPIWrapper::Allreduce(&localIntegral, &globalIntegral, 1, MPI_DOUBLE, MPI_SUM, SU2_MPI::GetComm()); + + return globalIntegral; +} + +/*! + * \brief Perform an Lp-norm normalization of the metric. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + * \param[in] iSensor - Index of the sensor to work on. + * \param[in] integral - Integral of the metric tensor determinant. + * \param[in,out] metric - Metric container. + */ +template +void normalizeMetrics(CGeometry& geometry, const CConfig& config, + unsigned short iSensor, ScalarType integral, + MetricType& metric) { + + const unsigned long nPointDomain = geometry.GetnPointDomain(); + + /*--- Constants defining normalization ---*/ + const ScalarType p = config.GetMetric_Norm(); + const ScalarType N = SU2_TYPE::GetValue(config.GetMetric_Complexity() * config.GetnAdapt_Time_Subinterval()); + const ScalarType globalFactor = pow(N / integral, 2.0 / nDim); + const ScalarType normExp = -1.0 / (2.0 * p + nDim); + + /*--- Size constraints ---*/ + const ScalarType hmin = SU2_TYPE::GetValue(config.GetMetric_Hmin()); + const ScalarType hmax = SU2_TYPE::GetValue(config.GetMetric_Hmax()); + const ScalarType eigmax = 1.0 / pow(hmin, 2.0); + const ScalarType eigmin = 1.0 / pow(hmax, 2.0); + const ScalarType armax2 = pow(SU2_TYPE::GetValue(config.GetMetric_ARmax()), 2.0); + + ScalarType A[nDim][nDim], EigVec[nDim][nDim], EigVal[nDim], work[nDim]; + + for (auto iPoint = 0ul; iPoint < nPointDomain; ++iPoint) { + /*--- Decompose metric ---*/ + Tensor::get(metric, iPoint, iSensor, A, nDim); + CBlasStructure::EigenDecomposition(A, EigVec, EigVal, nDim, work); + + /*--- Normalize eigenvalues ---*/ + const ScalarType det = computeDeterminant(EigVal); + const ScalarType factor = globalFactor * pow(abs(det), normExp); + for (auto iDim = 0u; iDim < nDim; ++iDim) + EigVal[iDim] = factor * EigVal[iDim]; + + /*--- Clip by user-specified size constraints ---*/ + for (auto iDim = 0u; iDim < nDim; ++iDim) + EigVal[iDim] = min(max(abs(EigVal[iDim]), eigmin), eigmax); + + /*--- Clip by user-specified aspect ratio ---*/ + unsigned short iMax = 0; + for (auto iDim = 1; iDim < nDim; ++iDim) + iMax = (EigVal[iDim] > EigVal[iMax])? iDim : iMax; + + for (auto iDim = 0u; iDim < nDim; ++iDim) + EigVal[iDim] = max(EigVal[iDim], EigVal[iMax]/armax2); + + /*--- Recompose and store metric ---*/ + CBlasStructure::EigenRecomposition(A, EigVec, EigVal, nDim); + Tensor::set(metric, iPoint, iSensor, A, 1.0, nDim); + } +} +} // end namespace detail + +/*! + * \brief Make the eigenvalues of the metrics positive. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + * \param[in] iSensor - Index of the sensor to work on. + * \param[in,out] metric - Metric container. + */ +template +void setPositiveDefiniteMetrics(CGeometry& geometry, const CConfig& config, + unsigned short iSensor, MetricType& metric) { + switch (geometry.GetnDim()) { + case 2: + detail::setPositiveDefiniteMetrics<2, ScalarType, Tensor>(geometry, config, iSensor, metric); + break; + case 3: + detail::setPositiveDefiniteMetrics<3, ScalarType, Tensor>(geometry, config, iSensor, metric); + break; + default: + SU2_MPI::Error("Too many dimensions for metric computation.", CURRENT_FUNCTION); + break; + } +} + +/*! + * \brief Integrate the Hessian field for the Lp-norm normalization of the metric. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + * \param[in] iSensor - Index of the sensor to work on. + * \param[in] metric - Metric container. + * \return Integral of the metric tensor determinant. +*/ +template +ScalarType integrateMetrics(CGeometry& geometry, const CConfig& config, + unsigned short iSensor, MetricType& metric) { + su2double integral; + switch (geometry.GetnDim()) { + case 2: + integral = detail::integrateMetrics<2, ScalarType>(geometry, config, iSensor, metric); + break; + case 3: + integral = detail::integrateMetrics<3, ScalarType>(geometry, config, iSensor, metric); + break; + default: + SU2_MPI::Error("Too many dimensions for metric integration.", CURRENT_FUNCTION); + break; + } + + return integral; +} + +/*! + * \brief Perform an Lp-norm normalization of the metric. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + * \param[in] iSensor - Index of the sensor to work on. + * \param[in] integral - Integral of the metric tensor determinant. + * \param[in,out] metric - Metric container. + */ +template +void normalizeMetrics(CGeometry& geometry, const CConfig& config, + unsigned short iSensor, ScalarType integral, + MetricType& metric) { + switch (geometry.GetnDim()) { + case 2: + detail::normalizeMetrics<2, ScalarType, Tensor>(geometry, config, iSensor, integral, metric); + break; + case 3: + detail::normalizeMetrics<3, ScalarType, Tensor>(geometry, config, iSensor, integral, metric); + break; + default: + SU2_MPI::Error("Too many dimensions for metric normalization.", CURRENT_FUNCTION); + break; + } +} diff --git a/SU2_CFD/include/metrics/metricUtils.hpp b/SU2_CFD/include/metrics/metricUtils.hpp new file mode 100644 index 00000000000..6768399f882 --- /dev/null +++ b/SU2_CFD/include/metrics/metricUtils.hpp @@ -0,0 +1,260 @@ +/*! + * \file metricUtils.hpp + * \brief Utility functions for mesh adaptation metric computation. + * \author B. Munguía + * \version 8.4.0 "Harrier" + * + * SU2 Project Website: https://su2code.github.io + * + * The SU2 Project is maintained by the SU2 Foundation + * (http://su2foundation.org) + * + * Copyright 2012-2025, SU2 Contributors (cf. AUTHORS.md) + * + * SU2 is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * SU2 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with SU2. If not, see . + */ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "../../../Common/include/CConfig.hpp" +#include "../solvers/CSolver.hpp" +#include "../variables/CPrimitiveIndices.hpp" +#include "../../../Common/include/parallelization/mpi_structure.hpp" + +namespace MetricUtils { + +/*! + * \brief Build a map of supported derived sensor names to their point-wise evaluator lambdas. + * + * Derived sensors are officially-supported computed quantities that are not stored directly + * as primitive variables. Supported sensors: + * - VELOCITY velocity magnitude (all flow solvers) + * - MACH local Mach number (compressible flow solvers only) + * + * Each lambda has the signature \c su2double(const su2double* prim) where \p prim is a pointer + * to the primitive variable array for a single point. + * + * \param[in] primitive_map - Map of primitive variable names to their array indices. + * \param[in] nDim - Number of spatial dimensions. + * \param[in] incompressible - True for incompressible solvers (MACH is excluded). + * \return Map of derived sensor names to evaluator lambdas. + */ +inline std::map> +DerivedNameToFunctionMap(const std::map& primitive_map, + unsigned long nDim, bool incompressible) { + using SensorFn = std::function; + std::map derived_map; + + const unsigned short vel_x = primitive_map.at("VELOCITY_X"); + const unsigned short vel_y = primitive_map.at("VELOCITY_Y"); + const unsigned short vel_z = (nDim == 3) ? primitive_map.at("VELOCITY_Z") + : std::numeric_limits::max(); + + /*--- Velocity magnitude: supported by all flow solvers ---*/ + derived_map["VELOCITY"] = [vel_x, vel_y, vel_z, nDim](const su2double* prim) -> su2double { + su2double vel2 = prim[vel_x] * prim[vel_x] + prim[vel_y] * prim[vel_y]; + if (nDim == 3) vel2 += prim[vel_z] * prim[vel_z]; + return std::sqrt(vel2); + }; + + /*--- Mach number: compressible solvers only ---*/ + if (!incompressible) { + const unsigned short a_idx = primitive_map.at("SOUND_SPEED"); + derived_map["MACH"] = [vel_x, vel_y, vel_z, a_idx, nDim](const su2double* prim) -> su2double { + su2double vel2 = prim[vel_x] * prim[vel_x] + prim[vel_y] * prim[vel_y]; + if (nDim == 3) vel2 += prim[vel_z] * prim[vel_z]; + return std::sqrt(vel2) / std::max(std::abs(prim[a_idx]), su2double(1e-20)); + }; + } + + return derived_map; +} + +/*! + * \brief Resolve sensor names to solver and variable indices, storing them directly in each solver. + * + * Maps sensor names from config (e.g., "DENSITY", "TEMPERATURE") to their corresponding + * solver index and variable index within that solver. Works for both flow solvers + * (using primitive variables) and non-flow solvers (using solution fields). + * Directly stores the resolved sensors in each solver via SetMetricSensors(). + * + * \param[in] config - Configuration containing sensor names + * \param[in] geometry - Geometry for dimension info + * \param[in,out] solver_container - Array of solvers [iSol] - indices will be set + * \return True if all sensors were successfully resolved + */ +inline bool ResolveSensorIndices( + const CConfig* config, + const CGeometry* geometry, + CSolver** solver_container) { + + const int rank = SU2_MPI::GetRank(); + + /*--- Get sensor names from config ---*/ + std::vector sensor_names = config->GetMetric_SensorList(); + + /*--- Group sensors by solver (in config order) ---*/ + std::map> sensors_by_solver; + + /*--- Build maps of available variables across all solvers. + * var_map: name -> (iSol, prim_idx) for PRIMITIVE sensors + * derived_map: name -> evaluator lambda for DERIVED sensors ---*/ + std::map> var_map; + std::map> derived_map; + + for (unsigned short iSol = 0; iSol < MAX_SOLS; iSol++) { + if (solver_container[iSol] == nullptr) continue; + + std::string solver_name = solver_container[iSol]->GetSolverName(); + + /*--- For flow solvers, get primitive variable names ---*/ + if (solver_name.find("FLOW") != std::string::npos || + solver_name.find("EULER") != std::string::npos || + solver_name.find("NAVIER_STOKES") != std::string::npos || + solver_name.find("RANS") != std::string::npos || + solver_name.find("INC") != std::string::npos || + solver_name.find("NEMO") != std::string::npos) { + + const auto nDim = geometry->GetnDim(); + const auto nSpecies = config->GetnSpecies(); + const bool incompressible = config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE; + const bool nemo = config->GetKind_FluidModel() == ENUM_FLUIDMODEL::MUTATIONPP || + config->GetKind_FluidModel() == ENUM_FLUIDMODEL::SU2_NONEQ; + + CPrimitiveIndices indices(incompressible, nemo, nDim, nSpecies); + std::map primitive_map = PrimitiveNameToIndexMap(indices); + + for (const auto& [varname, varidx] : primitive_map) { + var_map[varname] = std::make_pair(iSol, varidx); + } + + /*--- Build derived sensor map. VELOCITY is supported by all flow solvers; + * MACH is excluded for incompressible (see DerivedNameToFunctionMap). ---*/ + derived_map = DerivedNameToFunctionMap(primitive_map, nDim, incompressible); + } else { + /*--- For non-flow solvers, use solution fields ---*/ + std::vector solution_fields = solver_container[iSol]->GetSolutionFields(); + + /*--- Remove PointID if present (first entry) ---*/ + if (!solution_fields.empty() && solution_fields[0].find("PointID") != std::string::npos) { + solution_fields.erase(solution_fields.begin()); + } + + /*--- Remove quotation marks and build map ---*/ + unsigned short idx = 0; + for (auto& field_name : solution_fields) { + if (field_name.size() >= 2 && field_name.front() == '\"' && field_name.back() == '\"') { + field_name = field_name.substr(1, field_name.size() - 2); + } + var_map[field_name] = std::make_pair(iSol, idx++); + } + } + } + + /*--- Resolve each sensor in config order, grouping by solver. + * Three categories are distinguished: + * PRIMITIVE — index into primitive variable array (prim_idx is valid). + * DERIVED — supported computed quantity (e.g. Mach); evaluator lambda stored in fn. + * CUSTOM — filled externally via Python wrapper (CDriverBase::AdaptSensors). + * Config order is preserved so iSensor=0 is always the first listed sensor + * (whose Hessian drives the metric and is normalised). ---*/ + bool all_resolved = true; + for (const auto& sensor_name : sensor_names) { + auto it = var_map.find(sensor_name); + if (it != var_map.end()) { + /*--- PRIMITIVE sensor ---*/ + const unsigned short iSol = it->second.first; + const unsigned short var_index = it->second.second; + sensors_by_solver[iSol].push_back( + {var_index, sensor_name, SensorType::PRIMITIVE, {}}); + if (rank == MASTER_NODE) { + std::cout << " Primitive sensor '" << sensor_name << "' resolved to solver " + << iSol << ", variable index " << var_index << std::endl; + } + } else { + auto dit = derived_map.find(sensor_name); + if (dit != derived_map.end()) { + /*--- DERIVED sensor ---*/ + sensors_by_solver[FLOW_SOL].push_back( + {std::numeric_limits::max(), sensor_name, + SensorType::DERIVED, dit->second}); + if (rank == MASTER_NODE) { + std::cout << " Derived sensor '" << sensor_name << "' registered." << std::endl; + } + } else { + /*--- CUSTOM sensor — must be populated via Python wrapper ---*/ + if (rank == MASTER_NODE) { + std::cout << " Custom sensor '" << sensor_name << "' detected." << std::endl; + } + all_resolved = false; + sensors_by_solver[FLOW_SOL].push_back( + {std::numeric_limits::max(), sensor_name, + SensorType::CUSTOM, {}}); + } + } + } + + /*--- Set sensor list directly in each solver ---*/ + for (const auto& [iSol, sensors] : sensors_by_solver) { + solver_container[iSol]->SetMetricSensors(sensors); + } + + /*--- Return true if at least one sensor was resolved or reserved ---*/ + return !sensors_by_solver.empty(); +} + +/*! + * \brief Allocate metric sensor arrays in all solvers. + * + * Allocates the necessary arrays for metric computation in each solver that has + * metric sensors. Should be called after ResolveSensorIndices() has set the indices. + * + * \param[in,out] solver_container - Array of solvers [iSol] + */ +inline void InitializeMetrics(CSolver** solver_container) { + + /*--- Allocate arrays in each solver that has metric sensors ---*/ + for (unsigned short iSol = 0; iSol < MAX_SOLS; iSol++) { + if (solver_container[iSol] == nullptr) continue; + + const auto nSensors = solver_container[iSol]->GetnMetricSensor(); + if (nSensors > 0) { + solver_container[iSol]->AllocateMetricSensorArrays(nSensors); + } + } +} + +/*! + * \brief Get total number of sensors in all solvers. + * \param[in] solver_container - Array of solvers [iSol] + * \return Number of sensors in all solvers. + */ +inline unsigned short TotalNumSensors(CSolver** solver_container) { + unsigned short num_sensor = 0; + for (unsigned short iSol = 0; iSol < MAX_SOLS; iSol++) { + if (solver_container[iSol] != nullptr) { + num_sensor += solver_container[iSol]->GetnMetricSensor(); + } + } + return num_sensor; +} + +} // namespace MetricUtils diff --git a/SU2_CFD/include/output/CFlowOutput.hpp b/SU2_CFD/include/output/CFlowOutput.hpp index 6a9984a6f0a..b19bdcbfa29 100644 --- a/SU2_CFD/include/output/CFlowOutput.hpp +++ b/SU2_CFD/include/output/CFlowOutput.hpp @@ -352,4 +352,20 @@ class CFlowOutput : public CFVMOutput{ * \param[in] config - Definition of the particular problem per zone. */ void SetFixedCLScreenOutput(const CConfig *config); + + /*! + * \brief Add mesh adaptation outputs. + * \param[in] config - Definition of the particular problem. + */ + void AddMeshAdaptationOutputs(const CConfig* config); + + /*! + * \brief Load mesh adaptation outputs. + * \param[in] config - Definition of the particular problem. + * \param[in] solver - The container holding all solution data. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] iPoint - Index of the point. + */ + void LoadMeshAdaptationOutputs(const CConfig* config, const CSolver* const* solver, const CGeometry* geometry, + unsigned long iPoint); }; diff --git a/SU2_CFD/include/solvers/CSolver.hpp b/SU2_CFD/include/solvers/CSolver.hpp index eba2e5cc07d..366df11e03a 100644 --- a/SU2_CFD/include/solvers/CSolver.hpp +++ b/SU2_CFD/include/solvers/CSolver.hpp @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -85,7 +86,8 @@ class CSolver { nSecondaryVar, /*!< \brief Number of primitive variables of the problem. */ nSecondaryVarGrad, /*!< \brief Number of primitive variables of the problem in the gradient computation. */ nVarGrad, /*!< \brief Number of variables for deallocating the LS Cvector. */ - nDim; /*!< \brief Number of dimensions of the problem. */ + nDim, /*!< \brief Number of dimensions of the problem. */ + nSymMat; /*!< \brief Number of symmetric matrix componenents for Hessian and metric tensor. */ unsigned long nPoint; /*!< \brief Number of points of the computational grid. */ unsigned long nPointDomain; /*!< \brief Number of points of the computational grid. */ su2double Max_Delta_Time, /*!< \brief Maximum value of the delta time for all the control volumes. */ @@ -207,6 +209,16 @@ class CSolver { vector fields; + /*--- Metric sensors for mesh adaptation ---*/ + struct MetricSensorInfo { + unsigned short prim_idx; /*!< \brief Primitive variable index (PRIMITIVE type only). */ + std::string name; /*!< \brief Sensor name as specified in config. */ + SensorType type = SensorType::PRIMITIVE; /*!< \brief Category of sensor. */ + std::function fn; /*!< \brief Point-wise evaluator for DERIVED sensors. */ + }; + vector MetricSensors; /*!< \brief Sensor list in config order, one entry per sensor. */ + + #ifdef HAVE_LIBROM std::unique_ptr u_basis_generator; #endif @@ -580,6 +592,35 @@ class CSolver { */ inline virtual void SetPrimitive_Limiter(CGeometry *geometry, const CConfig *config) { } + /*! + * \brief Copy PRIMITIVE sensor values from primitive variable array into Sensor_Adapt. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + */ + virtual void SetPrimitive_SensorAdapt(CGeometry *geometry, const CConfig *config); + + /*! + * \brief Evaluate DERIVED sensors (e.g. Mach number) and store results in Sensor_Adapt. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + */ + virtual void SetDerived_SensorAdapt(CGeometry *geometry, const CConfig *config); + + /*! + * \brief Allocate Gradient_Adapt and Hessian arrays for the sensors assigned to this solver. + * \param[in] nSensors - Number of sensors to allocate arrays for. + */ + virtual void AllocateMetricSensorArrays(unsigned short nSensors); + + /*! + * \brief Compute the Green-Gauss Hessian of the solution. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + * \param[in] idxVel - Index to velocity, -1 if no velocity is present in the solver. + * \param[in] reconstruction - indicator that the gradient being computed is for upwind reconstruction. + */ + void SetHessian_GG(CGeometry *geometry, const CConfig *config, short idxVel, const unsigned short Kind_Solver); + /*! * \brief Compute the projection of a variable for MUSCL reconstruction. * \note The result should be halved when added to i (or subtracted from j). @@ -4257,6 +4298,26 @@ class CSolver { */ inline vector GetSolutionFields() const{return fields;} + /*! + * \brief Get the number of metric sensors assigned to this solver. + * \return Number of metric sensors in this solver. + */ + inline unsigned short GetnMetricSensor() const { return static_cast(MetricSensors.size()); } + + /*! + * \brief Get the metric sensor list for this solver. + * \return Vector of MetricSensorInfo entries in config order. + */ + inline const vector& GetMetricSensors() const { return MetricSensors; } + + /*! + * \brief Set the metric sensor list for this solver. + * \param[in] sensors - Sensor entries in config order. + */ + inline void SetMetricSensors(const vector& sensors) { + MetricSensors = sensors; + } + /*! * \brief A virtual member. * \param[in] geometry - Geometrical definition. @@ -4371,24 +4432,44 @@ class CSolver { END_SU2_OMP_FOR } -inline void CustomSourceResidual(CGeometry *geometry, CSolver **solver_container, - CNumerics **numerics_container, CConfig *config, unsigned short iMesh) { + inline void CustomSourceResidual(CGeometry *geometry, CSolver **solver_container, + CNumerics **numerics_container, CConfig *config, unsigned short iMesh) { - AD::StartNoSharedReading(); + AD::StartNoSharedReading(); - SU2_OMP_FOR_STAT(roundUpDiv(nPointDomain,2*omp_get_max_threads())) - for (auto iPoint = 0ul; iPoint < nPointDomain; iPoint++) { - /*--- Get control volume size. ---*/ - su2double Volume = geometry->nodes->GetVolume(iPoint); - /*--- Compute the residual for this control volume and subtract. ---*/ - for (auto iVar = 0ul; iVar < nVar; iVar++) { - LinSysRes(iPoint,iVar) -= base_nodes->GetUserDefinedSource()(iPoint, iVar) * Volume; + SU2_OMP_FOR_STAT(roundUpDiv(nPointDomain,2*omp_get_max_threads())) + for (auto iPoint = 0ul; iPoint < nPointDomain; iPoint++) { + /*--- Get control volume size. ---*/ + su2double Volume = geometry->nodes->GetVolume(iPoint); + /*--- Compute the residual for this control volume and subtract. ---*/ + for (auto iVar = 0ul; iVar < nVar; iVar++) { + LinSysRes(iPoint,iVar) -= base_nodes->GetUserDefinedSource()(iPoint, iVar) * Volume; + } } + END_SU2_OMP_FOR + + AD::EndNoSharedReading(); } - END_SU2_OMP_FOR - AD::EndNoSharedReading(); -} + /*! + * \brief Compute the goal-oriented metric. + * \param[in] solver - Physical definition of the problem. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + * \param[in] restartMetric - Whether this is the initial sub-interval metric computation for an unsteady restart. + */ + void ComputeMetric(CSolver **solver, CGeometry *geometry, const CConfig *config, bool restartMetric); + + /*! + * \brief Sum up the weighted Hessians to obtain the goal-oriented metric. + * \param[in] solver - Physical definition of the problem. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + * \param[in] iSensor - Index of the sensor to work on. + * \param[in] restartMetric - Whether this is the initial sub-interval metric computation for an unsteady restart. + */ + void AddMetrics(CSolver **solver, const CGeometry *geometry, const CConfig *config, + const unsigned short iSensor, bool restartMetric); protected: /*! diff --git a/SU2_CFD/include/variables/CVariable.hpp b/SU2_CFD/include/variables/CVariable.hpp index bc4bc55e57c..707de335e5d 100644 --- a/SU2_CFD/include/variables/CVariable.hpp +++ b/SU2_CFD/include/variables/CVariable.hpp @@ -103,6 +103,11 @@ class CVariable { VectorType SolutionExtra_BGS_k; /*!< \brief Intermediate storage, enables cross term extraction as that is also pushed to Solution. */ + MatrixType Sensor_Adapt; /*!< \brief Variables for which we need gradients for anisotropy in mesh adaptation. */ + CVectorOfMatrix Gradient_Adapt; /*!< \brief Gradient of sensor used for anisotropy in mesh adaptation. */ + CVectorOfMatrix Hessian; /*!< \brief Hessian of sensor used for anisotropy in mesh adaptation. */ + su2matrix Metric; /*!< \brief Metric tensor used for anisotropy in mesh adaptation. */ + protected: unsigned long nPoint = 0; /*!< \brief Number of points in the domain. */ unsigned long nDim = 0; /*!< \brief Number of dimension of the problem. */ @@ -112,6 +117,8 @@ class CVariable { unsigned long nSecondaryVar = 0; /*!< \brief Number of secondary variables. */ unsigned long nSecondaryVarGrad = 0; /*!< \brief Number of secondaries for which a gradient is computed. */ unsigned long nAuxVar = 0; /*!< \brief Number of auxiliary variables. */ + unsigned long nSymMat = 0; /*!< \brief Number of symmetric matrix componenents for Hessian and metric tensor. */ + /*--- Only allow default construction by derived classes. ---*/ CVariable() = default; @@ -2371,4 +2378,139 @@ class CVariable { inline virtual const su2double *GetScalarSources(unsigned long iPoint) const { return nullptr; } inline virtual const su2double *GetScalarLookups(unsigned long iPoint) const { return nullptr; } + + /*! + * \brief Set the gradient of the solution. + * \param[in] iPoint - Point index. + * \param[in] gradient - Gradient of the solution. + */ + inline void SetSensor_Adapt(unsigned long iPoint, unsigned long iVar, su2double primitive) { Sensor_Adapt(iPoint,iVar) = primitive; } + + /*! + * \brief Get the gradient of the entire solution. + * \return Reference to gradient. + */ + inline const MatrixType& GetSensor_Adapt(void) const { return Sensor_Adapt; } + + /*! + * \brief Get the value of the solution gradient. + * \param[in] iPoint - Point index. + * \return Value of the gradient solution. + */ + inline su2double *GetSensor_Adapt(unsigned long iPoint) { return Sensor_Adapt[iPoint]; } + + /*! + * \brief Get the value of the solution gradient. + * \param[in] iPoint - Point index. + * \param[in] iVar - Variable index. + * \return Value of the solution gradient. + */ + inline su2double GetSensor_Adapt(unsigned long iPoint, unsigned long iVar) const { return Sensor_Adapt(iPoint,iVar); } + + /*! + * \brief Set the gradient of the solution. + * \param[in] iPoint - Point index. + * \param[in] gradient - Gradient of the solution. + */ + inline void SetGradient_Adapt(unsigned long iPoint, su2double** gradient) { + for (unsigned long iVar = 0; iVar < nVar; iVar++) + for (unsigned long iDim = 0; iDim < nDim; iDim++) + Gradient_Adapt(iPoint,iVar,iDim) = gradient[iVar][iDim]; + } + + /*! + * \brief Get the gradient of the entire solution. + * \return Reference to gradient. + */ + inline CVectorOfMatrix& GetGradient_Adapt(void) { return Gradient_Adapt; } + + /*! + * \brief Get the value of the solution gradient. + * \param[in] iPoint - Point index. + * \return Value of the gradient solution. + */ + inline CMatrixView GetGradient_Adapt(unsigned long iPoint) { return Gradient_Adapt[iPoint]; } + + /*! + * \brief Get the value of the solution gradient. + * \param[in] iPoint - Point index. + * \param[in] iVar - Variable index. + * \param[in] iDim - Dimension index. + * \return Value of the solution gradient. + */ + inline su2double GetGradient_Adapt(unsigned long iPoint, unsigned long iVar, unsigned long iDim) const { return Gradient_Adapt(iPoint,iVar,iDim); } + + /*! + * \brief Set the Hessian of the solution. + * \param[in] iPoint - Point index. + * \param[in] iVar - Variable index. + * \param[in] iMat - Hessian tensor index. + * \param[in] hess - Hessian of the solution. + */ + inline void SetHessian(unsigned long iPoint, unsigned long iVar, unsigned long iMat, su2double hess) { Hessian(iPoint,iVar,iMat) = hess; } + + /*! + * \brief Get the Hessian tensor field. + * \return Reference to the Hessian field. + */ + inline CVectorOfMatrix& GetHessian(void) { return Hessian; } + + /*! + * \brief Get the value of the Hessian. + * \param[in] iPoint - Point index. + * \param[in] iVar - Variable index. + * \param[in] iMat - Hessian tensor index. + * \return Value of the Hessian. + */ + inline su2double GetHessian(unsigned long iPoint, unsigned long iVar, unsigned long iMat) const { return Hessian(iPoint,iVar,iMat); } + + /*! + * \brief Set the value of the metric. + * \param[in] iMat - Metric tensor index. + * \param[in] metric - Metric value. + */ + inline void SetMetric(unsigned long iPoint, unsigned short iMat, double metric) { Metric(iPoint,iMat) = metric; } + + /*! + * \brief Get the metric of the entire solution. + * \return Reference to the metric tensor field. + */ + inline su2matrix& GetMetric(void) { return Metric; } + + /*! + * \brief Add the value of the metric. + * \param[in] iMat - Metric tensor index. + * \param[in] metric - Metric value. + */ + inline void AddMetric(unsigned long iPoint, unsigned short iMat, double metric) { Metric(iPoint,iMat) += metric; } + + /*! + * \brief Get the value of the metric. + * \param[in] iMat - Metric tensor index. + */ + inline double GetMetric(unsigned long iPoint, unsigned short iMat) const { return Metric(iPoint,iMat); } + + /*! + * \brief Allocate Gradient_Adapt and Hessian arrays for specified sensor indices. + * \param[in] nSensors - Number of metric sensors + */ + inline void AllocateMetricSensorArrays(unsigned short nSensors) { + if (nSensors == 0) return; + if (nDim == 0 || nPoint == 0) + SU2_MPI::Error("nDim and nPoint must be set before allocating metric arrays.", CURRENT_FUNCTION); + + /*--- Allocate if not already allocated or resize if needed ---*/ + if (Sensor_Adapt.size() == 0 || Sensor_Adapt.cols() != nSensors) { + Sensor_Adapt.resize(nPoint, nSensors) = 0.0; + } + if (Gradient_Adapt.size() == 0 || Gradient_Adapt.cols() != nSensors) { + Gradient_Adapt.resize(nPoint, nSensors, nDim, 0.0); + } + if (Hessian.size() == 0 || Hessian.cols() != nSensors) { + Hessian.resize(nPoint, nSensors, nSymMat, 0.0); + } + if (Metric.size() == 0 || Metric.cols() != nSymMat) { + Metric.resize(nPoint, nSymMat) = 0.0; + } + } }; diff --git a/SU2_CFD/src/drivers/CDriverBase.cpp b/SU2_CFD/src/drivers/CDriverBase.cpp index 00f60466904..bf4d1b8b0f1 100644 --- a/SU2_CFD/src/drivers/CDriverBase.cpp +++ b/SU2_CFD/src/drivers/CDriverBase.cpp @@ -436,3 +436,13 @@ map CDriverBase::GetPrimitiveIndices() const { main_config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE, main_config->GetNEMOProblem(), nDim, main_config->GetnSpecies())); } + +short CDriverBase::GetMetricSensorIndex(const string& sensor_name) const { + auto* flow_solver = solver_container[selected_zone][INST_0][MESH_0][FLOW_SOL]; + if (flow_solver == nullptr) return -1; + const auto& sensors = flow_solver->GetMetricSensors(); + for (short i = 0; i < static_cast(sensors.size()); ++i) { + if (sensors[i].name == sensor_name) return i; + } + return -1; +} diff --git a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp index 23d6c790db6..3741727c05c 100644 --- a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp +++ b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp @@ -29,6 +29,7 @@ #include "../../include/definition_structure.hpp" #include "../../include/output/COutput.hpp" #include "../../include/iteration/CIteration.hpp" +#include "../../include/metrics/metricUtils.hpp" CSinglezoneDriver::CSinglezoneDriver(char* confFile, unsigned short val_nZone, @@ -39,6 +40,29 @@ CSinglezoneDriver::CSinglezoneDriver(char* confFile, /*--- Initialize the counter for TimeIter ---*/ TimeIter = 0; + + /*--- Resolve and allocate metric sensor arrays if metric computation is enabled. + * Done here so the arrays are ready regardless of whether the C++ StartSolver() + * main loop or the Python wrapper (Preprocess/Run/Postprocess) is used. ---*/ + if (config_container[ZONE_0]->GetCompute_Metric()) { + if (rank == MASTER_NODE) + cout << "Resolving metric sensor indices." << endl; + + bool resolved = MetricUtils::ResolveSensorIndices( + config_container[ZONE_0], + geometry_container[ZONE_0][INST_0][MESH_0], + solver_container[ZONE_0][INST_0][MESH_0] + ); + + if (resolved) { + MetricUtils::InitializeMetrics(solver_container[ZONE_0][INST_0][MESH_0]); + unsigned long total_num_sensor = MetricUtils::TotalNumSensors(solver_container[ZONE_0][INST_0][MESH_0]); + if (rank == MASTER_NODE && total_num_sensor > 0) + cout << "Successfully resolved " << total_num_sensor << " metric sensors." << endl; + } else if (rank == MASTER_NODE) { + cout << "Warning: COMPUTE_METRIC is enabled but no valid sensors found." << endl; + } + } } CSinglezoneDriver::~CSinglezoneDriver() = default; @@ -140,6 +164,17 @@ void CSinglezoneDriver::Preprocess(unsigned long TimeIter) { SU2_MPI::Barrier(SU2_MPI::GetComm()); + /*--- Compute the initial metric tensor if performing an unsteady restart. ---*/ + /*--- The metric at RestartIter-1 is the endpoint of sub-interval i-1, and the + initial metric of sub-interval i, so we calculate it after setting the + initial condition. ---*/ + if (config_container[ZONE_0]->GetTime_Domain() && + config_container[ZONE_0]->GetCompute_Metric() && + config_container[ZONE_0]->GetRestart() && + TimeIter == config_container[ZONE_0]->GetRestart_Iter()) { + ComputeMetricField(true); + } + /*--- Run a predictor step ---*/ if (config_container[ZONE_0]->GetPredictor()) iteration_container[ZONE_0][INST_0]->Predictor(output_container[ZONE_0], integration_container, geometry_container, solver_container, @@ -175,6 +210,11 @@ void CSinglezoneDriver::Postprocess() { iteration_container[ZONE_0][INST_0]->Relaxation(output_container[ZONE_0], integration_container, geometry_container, solver_container, numerics_container, config_container, surface_movement, grid_movement, FFDBox, ZONE_0, INST_0); + /*--- Compute metric for anisotropic mesh adaptation ---*/ + + if (config_container[ZONE_0]->GetCompute_Metric()) + ComputeMetricField(); + } void CSinglezoneDriver::Update() { @@ -312,3 +352,36 @@ bool CSinglezoneDriver::Monitor(unsigned long TimeIter){ bool CSinglezoneDriver::GetTimeConvergence() const{ return output_container[ZONE_0]->GetCauchyCorrectedTimeConvergence(config_container[ZONE_0]); } + +void CSinglezoneDriver::ComputeMetricField(bool restartMetric) { + + auto solver = solver_container[ZONE_0][INST_0][MESH_0]; + auto solver_flow = solver_container[ZONE_0][INST_0][MESH_0][FLOW_SOL]; + auto geometry = geometry_container[ZONE_0][INST_0][MESH_0]; + auto config = config_container[ZONE_0]; + int idxVel = -1; + + if (rank == MASTER_NODE){ + cout << endl <<"----------------------------- Compute Metric ----------------------------" << endl; + cout << "Storing primitive variables needed for gradients in metric." << endl; + } + + /*--- Populate primitive and derived sensor slots in Sensor_Adapt. + * Custom sensors (CUSTOM type) must have been set already via the Python wrapper + * (CustomSensorRegistry.populate) before ComputeMetricField is called. ---*/ + solver_flow->SetPrimitive_SensorAdapt(geometry, config); + solver_flow->SetDerived_SensorAdapt(geometry, config); + solver_flow->InitiateComms(geometry, config, MPI_QUANTITIES::SENSOR_ADAPT); + solver_flow->CompleteComms(geometry, config, MPI_QUANTITIES::SENSOR_ADAPT); + + if (config->GetKind_Hessian_Method() == GREEN_GAUSS) { + if(rank == MASTER_NODE) cout << "Computing Hessians using Green-Gauss." << endl; + solver_flow->SetHessian_GG(geometry, config, idxVel, RUNTIME_FLOW_SYS); + } + else { + SU2_MPI::Error("Unsupported Hessian method.", CURRENT_FUNCTION); + } + + if(rank == MASTER_NODE) cout << "Computing feature-based metric tensor." << endl; + solver_flow->ComputeMetric(solver, geometry, config, restartMetric); +} diff --git a/SU2_CFD/src/output/CFlowCompOutput.cpp b/SU2_CFD/src/output/CFlowCompOutput.cpp index 54a30d1b3a2..83a17f8960f 100644 --- a/SU2_CFD/src/output/CFlowCompOutput.cpp +++ b/SU2_CFD/src/output/CFlowCompOutput.cpp @@ -307,6 +307,9 @@ void CFlowCompOutput::SetVolumeOutputFields(CConfig *config){ AddCommonFVMOutputs(config); + // Sensor value/gradient/Hessian, metric tensor, and dual-cell volume + AddMeshAdaptationOutputs(config); + if (config->GetTime_Domain()) { SetTimeAveragedFields(); } @@ -394,6 +397,8 @@ void CFlowCompOutput::LoadVolumeData(CConfig *config, CGeometry *geometry, CSolv LoadCommonFVMOutputs(config, geometry, iPoint); + LoadMeshAdaptationOutputs(config, solver, geometry, iPoint); + if (config->GetTime_Domain()) { LoadTimeAveragedData(iPoint, Node_Flow); } diff --git a/SU2_CFD/src/output/CFlowIncOutput.cpp b/SU2_CFD/src/output/CFlowIncOutput.cpp index b13edb7732f..bed54cef475 100644 --- a/SU2_CFD/src/output/CFlowIncOutput.cpp +++ b/SU2_CFD/src/output/CFlowIncOutput.cpp @@ -391,6 +391,9 @@ void CFlowIncOutput::SetVolumeOutputFields(CConfig *config){ AddCommonFVMOutputs(config); + // Sensor value/gradient/Hessian, metric tensor, and dual-cell volume + AddMeshAdaptationOutputs(config); + if (config->GetTime_Domain()) { SetTimeAveragedFields(); } @@ -480,6 +483,8 @@ void CFlowIncOutput::LoadVolumeData(CConfig *config, CGeometry *geometry, CSolve LoadCommonFVMOutputs(config, geometry, iPoint); + LoadMeshAdaptationOutputs(config, solver, geometry, iPoint); + if (config->GetTime_Domain()) { LoadTimeAveragedData(iPoint, Node_Flow); } diff --git a/SU2_CFD/src/output/CFlowOutput.cpp b/SU2_CFD/src/output/CFlowOutput.cpp index 54bdc18d783..f7c2c9b1285 100644 --- a/SU2_CFD/src/output/CFlowOutput.cpp +++ b/SU2_CFD/src/output/CFlowOutput.cpp @@ -4176,3 +4176,129 @@ void CFlowOutput::AddTurboOutput(unsigned short nZone){ AddHistoryOutput("KineticEnergyLoss_Stage", "KELC_all", ScreenOutputFormat::SCIENTIFIC, "TURBO_PERF", "Machine Kinetic Energy Loss Coefficient", HistoryFieldType::DEFAULT); AddHistoryOutput("TotPressureLoss_Stage", "TPLC_all", ScreenOutputFormat::SCIENTIFIC, "TURBO_PERF", "Machine Pressure Loss Coefficient", HistoryFieldType::DEFAULT); } + +void CFlowOutput::AddMeshAdaptationOutputs(const CConfig* config) { + + auto ToTitleCase = [](const std::string& str) -> std::string { + if (str.empty()) return str; + std::string result = str; + for (auto& c : result) c = static_cast(tolower(static_cast(c))); + result[0] = static_cast(toupper(static_cast(result[0]))); + return result; + }; + + // Anisotropic metric tensor, sensor gradients/Hessians, and dual-cell volume + if(config->GetCompute_Metric()) { + // Sensor values (primitive and custom) + for (auto iSensor = 0u; iSensor < config->GetnMetric_Sensor(); iSensor++){ + string sens_str = config->GetMetric_Sensor(iSensor); + string sens_title = ToTitleCase(sens_str); + AddVolumeOutput("SENSOR_" + sens_str, "Sensor_" + sens_title, "SENSOR_VALUE", "Value of sensor " + sens_title); + } + + // Gradients + for (auto iSensor = 0u; iSensor < config->GetnMetric_Sensor(); iSensor++){ + string sens_str = config->GetMetric_Sensor(iSensor); + string sens_title = ToTitleCase(sens_str); + // Common gradient vector components for both 2D and 3D + AddVolumeOutput("GRADIENT_" + sens_str + "_X", "Gradient_" + sens_title + "_x", "SENSOR_GRADIENT", "x-component of the " + sens_title + " gradient"); + AddVolumeOutput("GRADIENT_" + sens_str + "_Y", "Gradient_" + sens_title + "_y", "SENSOR_GRADIENT", "y-component of the " + sens_title + " gradient"); + // Additional component for 3D + if (nDim == 3) { + AddVolumeOutput("GRADIENT_" + sens_str + "_Z", "Gradient_" + sens_title + "_z", "SENSOR_GRADIENT", "z-component of the " + sens_title + " gradient"); + } + } + + // Hessians + for (auto iSensor = 0u; iSensor < config->GetnMetric_Sensor(); iSensor++){ + string sens_str = config->GetMetric_Sensor(iSensor); + string sens_title = ToTitleCase(sens_str); + // Common Hessian tensor components for both 2D and 3D + AddVolumeOutput("HESSIAN_" + sens_str + "_XX", "Hessian_" + sens_title + "_xx", "SENSOR_HESSIAN", "x-x-component of the " + sens_title + " Hessian"); + AddVolumeOutput("HESSIAN_" + sens_str + "_XY", "Hessian_" + sens_title + "_xy", "SENSOR_HESSIAN", "x-y-component of the " + sens_title + " Hessian"); + AddVolumeOutput("HESSIAN_" + sens_str + "_YY", "Hessian_" + sens_title + "_yy", "SENSOR_HESSIAN", "y-y-component of the " + sens_title + " Hessian"); + // Additional components for 3D + if (nDim == 3) { + AddVolumeOutput("HESSIAN_" + sens_str + "_XZ", "Hessian_" + sens_title + "_xz", "SENSOR_HESSIAN", "x-z-component of the " + sens_title + " Hessian"); + AddVolumeOutput("HESSIAN_" + sens_str + "_YZ", "Hessian_" + sens_title + "_yz", "SENSOR_HESSIAN", "y-z-component of the " + sens_title + " Hessian"); + AddVolumeOutput("HESSIAN_" + sens_str + "_ZZ", "Hessian_" + sens_title + "_zz", "SENSOR_HESSIAN", "z-z-component of the " + sens_title + " Hessian"); + } + } + + // Metric tensor + // Common metric tensor components for both 2D and 3D + AddVolumeOutput("METRIC_XX", "Metric_xx", "METRIC", "x-x-component of the metric"); + AddVolumeOutput("METRIC_XY", "Metric_xy", "METRIC", "x-y-component of the metric"); + AddVolumeOutput("METRIC_YY", "Metric_yy", "METRIC", "y-y-component of the metric"); + // Additional components for 3D + if (nDim == 3) { + AddVolumeOutput("METRIC_XZ", "Metric_xz", "METRIC", "x-z-component of the metric"); + AddVolumeOutput("METRIC_YZ", "Metric_yz", "METRIC", "y-z-component of the metric"); + AddVolumeOutput("METRIC_ZZ", "Metric_zz", "METRIC", "z-z-component of the metric"); + } + + // Dual-cell volume + AddVolumeOutput("VOLUME", "Volume", "VOLUME", "Dual-cell volume"); + AddVolumeOutput("TOTAL_VOLUME", "Total_Volume", "PERIODIC_VOLUME", "Dual-cell volume, including periodic volume"); + } +} + +void CFlowOutput::LoadMeshAdaptationOutputs(const CConfig* config, const CSolver* const* solver, const CGeometry* geometry, + unsigned long iPoint) { + + const auto* Node_Flow = solver[FLOW_SOL]->GetNodes(); + const auto* Node_Geo = geometry->nodes; + if(config->GetCompute_Metric()) { + const auto& sensors = solver[FLOW_SOL]->GetMetricSensors(); + + // Sensor values (primitive and custom) + for (auto iSensor = 0u; iSensor < sensors.size(); iSensor++) { + const string& sens_str = sensors[iSensor].name; + SetVolumeOutputValue("SENSOR_" + sens_str, iPoint, Node_Flow->GetSensor_Adapt(iPoint, iSensor)); + } + + // Gradients + for (auto iSensor = 0u; iSensor < sensors.size(); iSensor++){ + const string& sens_str = sensors[iSensor].name; + // Common gradient vector components for both 2D and 3D + SetVolumeOutputValue("GRADIENT_" + sens_str + "_X", iPoint, Node_Flow->GetGradient_Adapt(iPoint, iSensor, 0)); + SetVolumeOutputValue("GRADIENT_" + sens_str + "_Y", iPoint, Node_Flow->GetGradient_Adapt(iPoint, iSensor, 1)); + // Additional component for 3D + if (nDim == 3) { + SetVolumeOutputValue("GRADIENT_" + sens_str + "_Z", iPoint, Node_Flow->GetGradient_Adapt(iPoint, iSensor, 2)); + } + } + + // Hessians + for (auto iSensor = 0u; iSensor < sensors.size(); iSensor++){ + const string& sens_str = sensors[iSensor].name; + // Common Hessian tensor components for both 2D and 3D + SetVolumeOutputValue("HESSIAN_" + sens_str + "_XX", iPoint, Node_Flow->GetHessian(iPoint, iSensor, 0)); + SetVolumeOutputValue("HESSIAN_" + sens_str + "_XY", iPoint, Node_Flow->GetHessian(iPoint, iSensor, 1)); + SetVolumeOutputValue("HESSIAN_" + sens_str + "_YY", iPoint, Node_Flow->GetHessian(iPoint, iSensor, 2)); + // Additional components for 3D + if (nDim == 3) { + SetVolumeOutputValue("HESSIAN_" + sens_str + "_XZ", iPoint, Node_Flow->GetHessian(iPoint, iSensor, 3)); + SetVolumeOutputValue("HESSIAN_" + sens_str + "_YZ", iPoint, Node_Flow->GetHessian(iPoint, iSensor, 4)); + SetVolumeOutputValue("HESSIAN_" + sens_str + "_ZZ", iPoint, Node_Flow->GetHessian(iPoint, iSensor, 5)); + } + } + + // Metric tensor + // Common metric tensor components for both 2D and 3D + SetVolumeOutputValue("METRIC_XX", iPoint, Node_Flow->GetMetric(iPoint, 0)); + SetVolumeOutputValue("METRIC_XY", iPoint, Node_Flow->GetMetric(iPoint, 1)); + SetVolumeOutputValue("METRIC_YY", iPoint, Node_Flow->GetMetric(iPoint, 2)); + + // Additional components for 3D + if (nDim == 3) { + SetVolumeOutputValue("METRIC_XZ", iPoint, Node_Flow->GetMetric(iPoint, 3)); + SetVolumeOutputValue("METRIC_YZ", iPoint, Node_Flow->GetMetric(iPoint, 4)); + SetVolumeOutputValue("METRIC_ZZ", iPoint, Node_Flow->GetMetric(iPoint, 5)); + } + + // Dual-cell volume + SetVolumeOutputValue("VOLUME", iPoint, Node_Geo->GetVolume(iPoint)); + SetVolumeOutputValue("TOTAL_VOLUME", iPoint, Node_Geo->GetVolume(iPoint) + Node_Geo->GetPeriodicVolume(iPoint)); + } +} diff --git a/SU2_CFD/src/output/CNEMOCompOutput.cpp b/SU2_CFD/src/output/CNEMOCompOutput.cpp index fb6b8f90662..fb16527ad81 100644 --- a/SU2_CFD/src/output/CNEMOCompOutput.cpp +++ b/SU2_CFD/src/output/CNEMOCompOutput.cpp @@ -298,6 +298,9 @@ void CNEMOCompOutput::SetVolumeOutputFields(CConfig *config){ AddCommonFVMOutputs(config); + // Sensor value/gradient/Hessian, metric tensor, and dual-cell volume + AddMeshAdaptationOutputs(config); + if (config->GetTime_Domain()) { SetTimeAveragedFields(); } @@ -384,6 +387,8 @@ void CNEMOCompOutput::LoadVolumeData(CConfig *config, CGeometry *geometry, CSolv LoadCommonFVMOutputs(config, geometry, iPoint); + LoadMeshAdaptationOutputs(config, solver, geometry, iPoint); + if (config->GetTime_Domain()) { LoadTimeAveragedData(iPoint, Node_Flow); } diff --git a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp index c74d0d40b05..e966f9fd34c 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp @@ -53,8 +53,10 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ } /*--- We always have 3 coords, independent of the actual value of nDim ---*/ + /*--- We also always have 6 tensor components ---*/ const int NCOORDS = 3; + const int NTENSOR = 6; const unsigned short nDim = dataSorter->GetnDim(); unsigned short iDim = 0; @@ -139,30 +141,85 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ fieldname.erase(remove(fieldname.begin(), fieldname.end(), '"'), fieldname.end()); - /*--- Check whether this field is a vector or scalar. ---*/ + /*--- Check whether this field is a vector, tensor, or scalar. ---*/ + /*--- Check tensor components first (longer patterns) to avoid false matches ---*/ - bool output_variable = true, isVector = false; - size_t found = fieldNames[iField].find("_x"); + bool output_variable = true, isVector = false, isTensor = false; + + // Check for tensor components first (_xx, _xy, _yy, _xz, _yz, _zz) + size_t found = fieldNames[iField].find("_xx"); if (found!=string::npos) { output_variable = true; - isVector = true; + isTensor = true; + } + found = fieldNames[iField].find("_xy"); + if (found!=string::npos) { + /*--- We have found a tensor, so skip the XY component. ---*/ + output_variable = false; + isTensor = true; + VarCounter++; + } + found = fieldNames[iField].find("_yy"); + if (found!=string::npos) { + /*--- We have found a tensor, so skip the YY component. ---*/ + output_variable = false; + isTensor = true; + VarCounter++; + } + found = fieldNames[iField].find("_xz"); + if (found!=string::npos) { + /*--- We have found a tensor, so skip the XZ component. ---*/ + output_variable = false; + isTensor = true; + VarCounter++; } - found = fieldNames[iField].find("_y"); + found = fieldNames[iField].find("_yz"); if (found!=string::npos) { - /*--- We have found a vector, so skip the Y component. ---*/ + /*--- We have found a tensor, so skip the YZ component. ---*/ output_variable = false; + isTensor = true; VarCounter++; } - found = fieldNames[iField].find("_z"); + found = fieldNames[iField].find("_zz"); if (found!=string::npos) { - /*--- We have found a vector, so skip the Z component. ---*/ + /*--- We have found a tensor, so skip the ZZ component. ---*/ output_variable = false; + isTensor = true; VarCounter++; } - /*--- Write the point data as an vector or a scalar. ---*/ + // Check for vector components only if not a tensor (_x, _y, _z) + if (!isTensor) { + found = fieldNames[iField].find("_x"); + if (found!=string::npos) { + output_variable = true; + isVector = true; + } + found = fieldNames[iField].find("_y"); + if (found!=string::npos) { + /*--- We have found a vector, so skip the Y component. ---*/ + output_variable = false; + VarCounter++; + } + found = fieldNames[iField].find("_z"); + if (found!=string::npos) { + /*--- We have found a vector, so skip the Z component. ---*/ + output_variable = false; + VarCounter++; + } + } + + /*--- Write the point data as an vector, tensor, or a scalar. ---*/ + + if (output_variable && isTensor) { - if (output_variable && isVector) { + /*--- Adjust the string name to remove the tensor component suffix ---*/ + + fieldname.erase(fieldname.end()-3,fieldname.end()); + + AddDataArray(VTKDatatype::FLOAT32, fieldname, NTENSOR, myPoint*NTENSOR, GlobalPoint*NTENSOR); + + } else if (output_variable && isVector) { /*--- Adjust the string name to remove the leading "X-" ---*/ @@ -252,33 +309,111 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ VarCounter = varStart; for (iField = varStart; iField < fieldNames.size(); iField++) { - /*--- Check whether this field is a vector or scalar. ---*/ + /*--- Check whether this field is a vector, tensor, or scalar. ---*/ + /*--- Check tensor components first (longer patterns) to avoid double counting ---*/ - bool output_variable = true, isVector = false; - size_t found = fieldNames[iField].find("_x"); + bool output_variable = true, isVector = false, isTensor = false; + + // Check for tensor components first (_xx, _xy, _yy, _xz, _yz, _zz) + size_t found = fieldNames[iField].find("_xx"); if (found!=string::npos) { output_variable = true; - isVector = true; + isTensor = true; + } + found = fieldNames[iField].find("_xy"); + if (found!=string::npos) { + /*--- We have found a tensor, so skip the XY component. ---*/ + output_variable = false; + isTensor = true; + VarCounter++; } - found = fieldNames[iField].find("_y"); + found = fieldNames[iField].find("_yy"); if (found!=string::npos) { - /*--- We have found a vector, so skip the Y component. ---*/ + /*--- We have found a tensor, so skip the YY component. ---*/ output_variable = false; + isTensor = true; VarCounter++; } - found = fieldNames[iField].find("_z"); + found = fieldNames[iField].find("_xz"); if (found!=string::npos) { - /*--- We have found a vector, so skip the Z component. ---*/ + /*--- We have found a tensor, so skip the XZ component. ---*/ output_variable = false; + isTensor = true; + VarCounter++; + } + found = fieldNames[iField].find("_yz"); + if (found!=string::npos) { + /*--- We have found a tensor, so skip the YZ component. ---*/ + output_variable = false; + isTensor = true; + VarCounter++; + } + found = fieldNames[iField].find("_zz"); + if (found!=string::npos) { + /*--- We have found a tensor, so skip the ZZ component. ---*/ + output_variable = false; + isTensor = true; VarCounter++; } - /*--- Write the point data as an vector or a scalar. ---*/ + // Check for vector components only if not a tensor (_x, _y, _z) + if (!isTensor) { + found = fieldNames[iField].find("_x"); + if (found!=string::npos) { + output_variable = true; + isVector = true; + } + found = fieldNames[iField].find("_y"); + if (found!=string::npos) { + /*--- We have found a vector, so skip the Y component. ---*/ + output_variable = false; + VarCounter++; + } + found = fieldNames[iField].find("_z"); + if (found!=string::npos) { + /*--- We have found a vector, so skip the Z component. ---*/ + output_variable = false; + VarCounter++; + } + } + + /*--- Write the point data as a tensor, vector, or scalar. ---*/ + + if (output_variable && isTensor) { + + /*--- Resize buffer to accommodate tensor data ---*/ + dataBufferFloat.resize(myPoint*NTENSOR); + + for (iPoint = 0; iPoint < myPoint; iPoint++) { + dataBufferFloat[iPoint*NTENSOR + 0] = (float)dataSorter->GetData(VarCounter+0,iPoint); // XX + dataBufferFloat[iPoint*NTENSOR + 1] = (float)dataSorter->GetData(VarCounter+2,iPoint); // YY + dataBufferFloat[iPoint*NTENSOR + 3] = (float)dataSorter->GetData(VarCounter+1,iPoint); // XY + if (nDim == 2) { + /*--- For 2D tensors, set the off-diagonal z-components to 0, and zz-component to 1 ---*/ + + dataBufferFloat[iPoint*NTENSOR + 2] = 1.0; // ZZ = 1 + dataBufferFloat[iPoint*NTENSOR + 4] = 0.0; // YZ = 0 + dataBufferFloat[iPoint*NTENSOR + 5] = 0.0; // XZ = 0 + } else { + /*--- For 3D tensors, get the z-components ---*/ + dataBufferFloat[iPoint*NTENSOR + 2] = (float)dataSorter->GetData(VarCounter+5,iPoint); // ZZ + dataBufferFloat[iPoint*NTENSOR + 4] = (float)dataSorter->GetData(VarCounter+4,iPoint); // YZ + dataBufferFloat[iPoint*NTENSOR + 5] = (float)dataSorter->GetData(VarCounter+3,iPoint); // XZ + } + } + + WriteDataArray(dataBufferFloat.data(), VTKDatatype::FLOAT32, myPoint*NTENSOR, GlobalPoint*NTENSOR, + dataSorter->GetnPointCumulative(rank)*NTENSOR); - if (output_variable && isVector) { + VarCounter++; + + } else if (output_variable && isVector) { /*--- Load up the buffer for writing this rank's vector data. ---*/ + /*--- Resize buffer back to coordinate size ---*/ + dataBufferFloat.resize(myPoint*NCOORDS); + float val = 0.0; for (iPoint = 0; iPoint < myPoint; iPoint++) { for (iDim = 0; iDim < NCOORDS; iDim++) { @@ -298,6 +433,8 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ } else if (output_variable) { + /*--- For scalar data, ensure buffer is properly sized ---*/ + dataBufferFloat.resize(myPoint); /*--- For now, create a temp 1D buffer to load up the data for writing. This will be replaced with a derived data type most likely. ---*/ diff --git a/SU2_CFD/src/solvers/CBaselineSolver.cpp b/SU2_CFD/src/solvers/CBaselineSolver.cpp index 642b77c599f..bc24ae07112 100644 --- a/SU2_CFD/src/solvers/CBaselineSolver.cpp +++ b/SU2_CFD/src/solvers/CBaselineSolver.cpp @@ -40,6 +40,7 @@ CBaselineSolver::CBaselineSolver(CGeometry *geometry, CConfig *config) { /*--- Define geometry constants in the solver structure ---*/ nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); /*--- Routines to access the number of variables and string names. ---*/ diff --git a/SU2_CFD/src/solvers/CDiscAdjSolver.cpp b/SU2_CFD/src/solvers/CDiscAdjSolver.cpp index 5f6d8beecac..69d51d73230 100644 --- a/SU2_CFD/src/solvers/CDiscAdjSolver.cpp +++ b/SU2_CFD/src/solvers/CDiscAdjSolver.cpp @@ -41,6 +41,7 @@ CDiscAdjSolver::CDiscAdjSolver(CGeometry *geometry, CConfig *config, CSolver *di nVar = direct_solver->GetnVar(); nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); nMarker = config->GetnMarker_All(); nPoint = geometry->GetnPoint(); diff --git a/SU2_CFD/src/solvers/CEulerSolver.cpp b/SU2_CFD/src/solvers/CEulerSolver.cpp index 3f8d642e9c6..8101970afac 100644 --- a/SU2_CFD/src/solvers/CEulerSolver.cpp +++ b/SU2_CFD/src/solvers/CEulerSolver.cpp @@ -114,6 +114,7 @@ CEulerSolver::CEulerSolver(CGeometry *geometry, CConfig *config, ---*/ nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); nVar = nDim + 2; nPrimVar = nDim + 9; diff --git a/SU2_CFD/src/solvers/CIncEulerSolver.cpp b/SU2_CFD/src/solvers/CIncEulerSolver.cpp index 5ba375e7489..48fc4c57c74 100644 --- a/SU2_CFD/src/solvers/CIncEulerSolver.cpp +++ b/SU2_CFD/src/solvers/CIncEulerSolver.cpp @@ -105,6 +105,7 @@ CIncEulerSolver::CIncEulerSolver(CGeometry *geometry, CConfig *config, unsigned * Incompressible flow, primitive variables (P, vx, vy, vz, T, rho, beta, lamMu, EddyMu, Kt_eff, Cp, Cv) ---*/ nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); /*--- Make sure to align the sizes with the constructor of CIncEulerVariable. ---*/ nVar = nDim + 2; diff --git a/SU2_CFD/src/solvers/CNEMOEulerSolver.cpp b/SU2_CFD/src/solvers/CNEMOEulerSolver.cpp index 0d76dadf59e..f99bfef10e9 100644 --- a/SU2_CFD/src/solvers/CNEMOEulerSolver.cpp +++ b/SU2_CFD/src/solvers/CNEMOEulerSolver.cpp @@ -92,6 +92,7 @@ CNEMOEulerSolver::CNEMOEulerSolver(CGeometry *geometry, CConfig *config, nSpecies = config->GetnSpecies(); nMarker = config->GetnMarker_All(); nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); nPoint = geometry->GetnPoint(); nPointDomain = geometry->GetnPointDomain(); diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index 0ff0f125c1a..fcae5e5fe52 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -30,6 +30,7 @@ #include "../../include/gradients/computeGradientsGreenGauss.hpp" #include "../../include/gradients/computeGradientsLeastSquares.hpp" #include "../../include/limiters/computeLimiters.hpp" +#include "../../include/metrics/computeMetrics.hpp" #include "../../../Common/include/toolboxes/MMS/CIncTGVSolution.hpp" #include "../../../Common/include/toolboxes/MMS/CInviscidVortexSolution.hpp" #include "../../../Common/include/toolboxes/MMS/CMMSIncEulerSolution.hpp" @@ -1385,6 +1386,18 @@ void CSolver::GetCommCountAndType(const CConfig* config, COUNT_PER_POINT = nVar; MPI_TYPE = COMM_TYPE_DOUBLE; break; + case MPI_QUANTITIES::SENSOR_ADAPT: + COUNT_PER_POINT = GetnMetricSensor(); + MPI_TYPE = COMM_TYPE_DOUBLE; + break; + case MPI_QUANTITIES::GRADIENT_ADAPT: + COUNT_PER_POINT = GetnMetricSensor()*nDim; + MPI_TYPE = COMM_TYPE_DOUBLE; + break; + case MPI_QUANTITIES::HESSIAN: + COUNT_PER_POINT = GetnMetricSensor()*nSymMat; + MPI_TYPE = COMM_TYPE_DOUBLE; + break; default: SU2_MPI::Error("Unrecognized quantity for point-to-point MPI comms.", CURRENT_FUNCTION); @@ -1399,6 +1412,8 @@ namespace CommHelpers { case MPI_QUANTITIES::PRIMITIVE_GRADIENT: return nodes->GetGradient_Primitive(); case MPI_QUANTITIES::PRIMITIVE_GRAD_REC: return nodes->GetGradient_Reconstruction(); case MPI_QUANTITIES::AUXVAR_GRADIENT: return nodes->GetAuxVarGradient(); + case MPI_QUANTITIES::GRADIENT_ADAPT: return nodes->GetGradient_Adapt(); + case MPI_QUANTITIES::HESSIAN: return nodes->GetHessian(); default: return nodes->GetGradient(); } } @@ -1415,7 +1430,7 @@ void CSolver::InitiateComms(CGeometry *geometry, /*--- Local variables ---*/ - unsigned short iVar, iDim; + unsigned short iVar, iDim, iMat; unsigned short COUNT_PER_POINT = 0; unsigned short MPI_TYPE = 0; @@ -1441,6 +1456,7 @@ void CSolver::InitiateComms(CGeometry *geometry, /*--- Handle the different types of gradient and limiter. ---*/ const auto nVarGrad = COUNT_PER_POINT / nDim; + const auto nVarHess = COUNT_PER_POINT / nSymMat; auto& gradient = CommHelpers::selectGradient(base_nodes, commType); auto& limiter = CommHelpers::selectLimiter(base_nodes, commType); @@ -1504,15 +1520,25 @@ void CSolver::InitiateComms(CGeometry *geometry, case MPI_QUANTITIES::SENSOR: bufDSend[buf_offset] = base_nodes->GetSensor(iPoint); break; + case MPI_QUANTITIES::SENSOR_ADAPT: + for (iVar = 0; iVar < GetnMetricSensor(); iVar++) + bufDSend[buf_offset+iVar] = base_nodes->GetSensor_Adapt(iPoint, iVar); + break; case MPI_QUANTITIES::SOLUTION_GRADIENT: case MPI_QUANTITIES::PRIMITIVE_GRADIENT: case MPI_QUANTITIES::SOLUTION_GRAD_REC: case MPI_QUANTITIES::PRIMITIVE_GRAD_REC: case MPI_QUANTITIES::AUXVAR_GRADIENT: + case MPI_QUANTITIES::GRADIENT_ADAPT: for (iVar = 0; iVar < nVarGrad; iVar++) for (iDim = 0; iDim < nDim; iDim++) bufDSend[buf_offset+iVar*nDim+iDim] = gradient(iPoint, iVar, iDim); break; + case MPI_QUANTITIES::HESSIAN: + for (iVar = 0; iVar < nVarHess; iVar++) + for (iMat = 0; iMat < nSymMat; iMat++) + bufDSend[buf_offset+iVar*nSymMat+iMat] = gradient(iPoint, iVar, iMat); + break; case MPI_QUANTITIES::SOLUTION_FEA: for (iVar = 0; iVar < nVar; iVar++) { bufDSend[buf_offset+iVar] = base_nodes->GetSolution(iPoint, iVar); @@ -1557,7 +1583,7 @@ void CSolver::CompleteComms(CGeometry *geometry, /*--- Local variables ---*/ - unsigned short iDim, iVar; + unsigned short iDim, iVar, iMat; unsigned long iPoint, iRecv, nRecv, msg_offset, buf_offset; unsigned short COUNT_PER_POINT = 0; unsigned short MPI_TYPE = 0; @@ -1578,6 +1604,7 @@ void CSolver::CompleteComms(CGeometry *geometry, /*--- Handle the different types of gradient and limiter. ---*/ const auto nVarGrad = COUNT_PER_POINT / nDim; + const auto nVarHess = COUNT_PER_POINT / nSymMat; auto& gradient = CommHelpers::selectGradient(base_nodes, commType); auto& limiter = CommHelpers::selectLimiter(base_nodes, commType); @@ -1652,15 +1679,25 @@ void CSolver::CompleteComms(CGeometry *geometry, case MPI_QUANTITIES::SENSOR: base_nodes->SetSensor(iPoint,bufDRecv[buf_offset]); break; + case MPI_QUANTITIES::SENSOR_ADAPT: + for (iVar = 0; iVar < GetnMetricSensor(); iVar++) + base_nodes->SetSensor_Adapt(iPoint, iVar, bufDRecv[buf_offset+iVar]); + break; case MPI_QUANTITIES::SOLUTION_GRADIENT: case MPI_QUANTITIES::PRIMITIVE_GRADIENT: case MPI_QUANTITIES::SOLUTION_GRAD_REC: case MPI_QUANTITIES::PRIMITIVE_GRAD_REC: case MPI_QUANTITIES::AUXVAR_GRADIENT: + case MPI_QUANTITIES::GRADIENT_ADAPT: for (iVar = 0; iVar < nVarGrad; iVar++) for (iDim = 0; iDim < nDim; iDim++) gradient(iPoint,iVar,iDim) = bufDRecv[buf_offset+iVar*nDim+iDim]; break; + case MPI_QUANTITIES::HESSIAN: + for (iVar = 0; iVar < nVarHess; iVar++) + for (iMat = 0; iMat < nSymMat; iMat++) + gradient(iPoint, iVar, iMat) = bufDRecv[buf_offset+iVar*nSymMat+iMat]; + break; case MPI_QUANTITIES::SOLUTION_FEA: for (iVar = 0; iVar < nVar; iVar++) { base_nodes->SetSolution(iPoint, iVar, bufDRecv[buf_offset+iVar]); @@ -2167,6 +2204,55 @@ void CSolver::SetSolution_Gradient_LS(CGeometry *geometry, const CConfig *config computeGradientsLeastSquares(this, comm, commPer, *geometry, *config, weighted, solution, 0, nVar, idxVel, gradient, rmatrix); } +void CSolver::AllocateMetricSensorArrays(unsigned short nSensors) { + if (base_nodes == nullptr || nSensors == 0) return; + base_nodes->AllocateMetricSensorArrays(nSensors); +} + +void CSolver::SetPrimitive_SensorAdapt(CGeometry *geometry, const CConfig *config) { + /*--- Copy PRIMITIVE sensor values from primitive variable array into Sensor_Adapt. + * DERIVED and CUSTOM slots are skipped here. ---*/ + for (size_t iSensor = 0; iSensor < MetricSensors.size(); ++iSensor) { + if (MetricSensors[iSensor].type != SensorType::PRIMITIVE) continue; + + const auto prim_idx = MetricSensors[iSensor].prim_idx; + SU2_OMP_FOR_STAT(omp_chunk_size) + for (unsigned long iPoint = 0; iPoint < nPoint; iPoint++) { + base_nodes->SetSensor_Adapt(iPoint, iSensor, base_nodes->GetPrimitive(iPoint, prim_idx)); + } + END_SU2_OMP_FOR + } +} + +void CSolver::SetDerived_SensorAdapt(CGeometry *geometry, const CConfig *config) { + /*--- Evaluate DERIVED sensors via their stored lambdas (e.g. Mach number). + * Each lambda receives a pointer to the full primitive row for the point. ---*/ + for (size_t iSensor = 0; iSensor < MetricSensors.size(); ++iSensor) { + if (MetricSensors[iSensor].type != SensorType::DERIVED) continue; + + const auto& fn = MetricSensors[iSensor].fn; + SU2_OMP_FOR_STAT(omp_chunk_size) + for (unsigned long iPoint = 0; iPoint < nPoint; iPoint++) { + base_nodes->SetSensor_Adapt(iPoint, iSensor, fn(base_nodes->GetPrimitive(iPoint))); + } + END_SU2_OMP_FOR + } +} + +void CSolver::SetHessian_GG(CGeometry *geometry, const CConfig *config, short idxVel, const unsigned short Kind_Solver) { + const auto& solution = base_nodes->GetSensor_Adapt(); + auto& gradient = base_nodes->GetGradient_Adapt(); + auto nHess = GetnMetricSensor(); + + computeGradientsGreenGauss(this, MPI_QUANTITIES::GRADIENT_ADAPT, PERIODIC_GRAD_ADAPT, + *geometry, *config, solution, 0, nHess, idxVel, gradient); + + auto& hessian = base_nodes->GetHessian(); + + computeHessiansGreenGauss(this, MPI_QUANTITIES::HESSIAN, PERIODIC_HESSIAN, + *geometry, *config, gradient, 0, nHess, idxVel, hessian); +} + void CSolver::SetUndivided_Laplacian(CGeometry *geometry, const CConfig *config) { /*--- Loop domain points. ---*/ @@ -4348,3 +4434,82 @@ void CSolver::SavelibROM(CGeometry *geometry, CConfig *config, bool converged) { #endif } + + +void CSolver::ComputeMetric(CSolver **solver, CGeometry *geometry, const CConfig *config, bool restartMetric) { + /*--- TODO: - goal-oriented metric ---*/ + /*--- - metric intersection ---*/ + const unsigned long nPointDomain = geometry->GetnPointDomain(); + const unsigned short nSensor = GetnMetricSensor(); + + const unsigned long time_iter = config->GetTimeIter(); + const bool steady = (config->GetTime_Marching() == TIME_MARCHING::STEADY); + const bool time_stepping = (config->GetTime_Marching() == TIME_MARCHING::DT_STEPPING_1ST) || + (config->GetTime_Marching() == TIME_MARCHING::DT_STEPPING_2ND) || + (config->GetTime_Marching() == TIME_MARCHING::TIME_STEPPING); + const bool is_last_iter = (time_iter == config->GetnTime_Iter() - 1) || (steady); + const bool normalize = (config->GetNormalize_Metric()); + + /*--- Integrate and normalize the metric tensor field ---*/ + vector integrals; + for (auto iSensor = 0u; iSensor < nSensor; ++iSensor) { + SU2_OMP_MASTER + /*--- Make the Hessian eigenvalues positive definite ---*/ + auto& hessians = base_nodes->GetHessian(); + setPositiveDefiniteMetrics(*geometry, *config, iSensor, hessians); + + if (iSensor > 0) continue; + + /*--- Add Hessian of sensor at position 0 to metric tensor */ + AddMetrics(solver, geometry, config, iSensor, restartMetric); + + /*--- Integrate metric field on the last iteration (the end of the simulation if steady) ---*/ + auto& metrics = base_nodes->GetMetric(); + double integral = 0.0; + if (is_last_iter) + integral = integrateMetrics(*geometry, *config, iSensor, metrics); + + /*--- Normalize the metric field for steady simulations, or if requested for unsteady ---*/ + if (steady || (normalize && is_last_iter)) + normalizeMetrics(*geometry, *config, iSensor, integral, metrics); + + /*--- Store the integral to be written ---*/ + if (is_last_iter) { + integrals.push_back(integral); + if (rank == MASTER_NODE) { + const string& sensor_name = (iSensor < MetricSensors.size()) ? MetricSensors[iSensor].name : "unknown"; + cout << "Global metric normalization integral for sensor "; + cout << sensor_name << ": " << integral << endl; + } + } + END_SU2_OMP_MASTER + SU2_OMP_BARRIER + } +} + +void CSolver::AddMetrics(CSolver **solver, const CGeometry*geometry, const CConfig *config, + const unsigned short iSensor, bool restartMetric) { + /*--- TODO: - goal-oriented metric ---*/ + /*--- - metric intersection ---*/ + auto varFlo = solver[FLOW_SOL]->GetNodes(); + + const unsigned long nPointDomain = geometry->GetnPointDomain(); + const unsigned short nSymMat = 3*(nDim-1); + + const unsigned long time_iter = config->GetTimeIter(); + const bool time_stepping = (config->GetTime_Marching() == TIME_MARCHING::DT_STEPPING_1ST) || + (config->GetTime_Marching() == TIME_MARCHING::DT_STEPPING_2ND) || + (config->GetTime_Marching() == TIME_MARCHING::TIME_STEPPING); + const bool is_first_iter = (time_iter == 0) || (restartMetric); + const bool is_last_iter = (time_iter == config->GetnTime_Iter() - 1); + + double coeff = (time_stepping && (is_first_iter || is_last_iter))? 0.5 : 1.0; + if (time_stepping) coeff *= SU2_TYPE::GetValue(config->GetTime_Step()); + + for(auto iPoint = 0ul; iPoint < nPointDomain; ++iPoint) { + for (auto iMat = 0; iMat < nSymMat; ++iMat) { + double hess = SU2_TYPE::GetValue(varFlo->GetHessian(iPoint, iSensor, iMat)); + varFlo->AddMetric(iPoint, iMat, coeff * hess); + } + } +} diff --git a/SU2_CFD/src/solvers/CTurbSASolver.cpp b/SU2_CFD/src/solvers/CTurbSASolver.cpp index 23237370762..d5d3e106dca 100644 --- a/SU2_CFD/src/solvers/CTurbSASolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSASolver.cpp @@ -53,6 +53,7 @@ CTurbSASolver::CTurbSASolver(CGeometry *geometry, CConfig *config, unsigned shor /*--- Define geometry constants in the solver structure ---*/ nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); /*--- Single grid simulation ---*/ diff --git a/SU2_CFD/src/variables/CFlowVariable.cpp b/SU2_CFD/src/variables/CFlowVariable.cpp index 06f90656450..ac548611dc1 100644 --- a/SU2_CFD/src/variables/CFlowVariable.cpp +++ b/SU2_CFD/src/variables/CFlowVariable.cpp @@ -97,6 +97,13 @@ CFlowVariable::CFlowVariable(unsigned long npoint, unsigned long ndim, unsigned if (config->GetTime_Marching() == TIME_MARCHING::HARMONIC_BALANCE) { HB_Source.resize(nPoint, nVar) = su2double(0.0); } + + if (config->GetCompute_Metric()) { + unsigned short nHess = config->GetnMetric_Sensor(); + unsigned short nSymMat = 3 * (nDim - 1); + Sensor_Adapt.resize(nPoint, nHess) = su2double(0.0); + Metric.resize(nPoint, nSymMat) = 0.0; + } } void CFlowVariable::SetSolution_New() { diff --git a/SU2_CFD/src/variables/CVariable.cpp b/SU2_CFD/src/variables/CVariable.cpp index 7de950de622..81640adc4a9 100644 --- a/SU2_CFD/src/variables/CVariable.cpp +++ b/SU2_CFD/src/variables/CVariable.cpp @@ -51,6 +51,7 @@ CVariable::CVariable(unsigned long npoint, unsigned long ndim, unsigned long nva nPoint = npoint; nDim = ndim; nVar = nvar; + nSymMat = 3 * (nDim - 1); /*--- Allocate fields common to all problems. Do not allocate fields that are specific to one solver, i.e. not common, in this class. ---*/ @@ -79,6 +80,13 @@ CVariable::CVariable(unsigned long npoint, unsigned long ndim, unsigned long nva if (config->GetMultizone_Problem()) Solution_BGS_k.resize(nPoint,nVar) = su2double(0.0); + + /*--- Gradient and Hessian for anisotropic metric ---*/ + if (config->GetCompute_Metric()) { + unsigned short nHess = config->GetnMetric_Sensor(); + Gradient_Adapt.resize(nPoint,nHess,nDim,0.0); + Hessian.resize(nPoint,nHess,nSymMat,0.0); + } } void CVariable::Set_OldSolution() { diff --git a/SU2_PY/SU2/__init__.py b/SU2_PY/SU2/__init__.py index a548f451104..0d4732843e7 100644 --- a/SU2_PY/SU2/__init__.py +++ b/SU2_PY/SU2/__init__.py @@ -17,6 +17,7 @@ class DivergenceFailure(EvaluationFailure): from SU2 import run from SU2 import io from SU2 import eval +from SU2 import metric from SU2 import opt from SU2 import util diff --git a/SU2_PY/SU2/metric/__init__.py b/SU2_PY/SU2/metric/__init__.py new file mode 100644 index 00000000000..5a82cb2c411 --- /dev/null +++ b/SU2_PY/SU2/metric/__init__.py @@ -0,0 +1 @@ +from .resolver import CustomSensorRegistry diff --git a/SU2_PY/SU2/metric/resolver.py b/SU2_PY/SU2/metric/resolver.py new file mode 100644 index 00000000000..7a8dd419438 --- /dev/null +++ b/SU2_PY/SU2/metric/resolver.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +## \file resolver.py +# \brief Utility for custom metric sensors in mesh adaptation. +# \author B. Munguía +# \version 8.4.0 "Harrier" +# +# SU2 Project Website: https://su2code.github.io +# +# The SU2 Project is maintained by the SU2 Foundation +# (http://su2foundation.org) +# +# Copyright 2012-2026, SU2 Contributors (cf. AUTHORS.md) +# +# SU2 is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# SU2 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with SU2. If not, see . + +from __future__ import annotations + +from collections.abc import Callable +from typing import Any + +SU2Driver = Any +SensorFn = Callable[[SU2Driver], list[float]] + + +class CustomSensorRegistry: + """Registry mapping sensor names to callables for mesh adaptation.""" + + def __init__(self, sensors: dict[str, SensorFn] | None = None) -> None: + """ + Args: + sensors: Optional mapping of sensor name to callable. Each callable + must have the signature ``fn(driver) -> list[float]`` and names + must match entries in ``METRIC_SENSOR`` in the config exactly. + """ + self._sensors: dict[str, SensorFn] = ( + dict(sensors) if sensors is not None else {} + ) + self._indices: dict[str, int] = {} + + def __setitem__(self, name: str, fn: SensorFn) -> None: + """Register or replace the sensor callable for ``name``.""" + self._sensors[name] = fn + + def __getitem__(self, name: str) -> SensorFn: + """Return the sensor callable registered under ``name``.""" + return self._sensors[name] + + def initialize(self, driver: SU2Driver) -> None: + """Resolve and cache sensor indices. Call once after driver construction. + + Args: + driver: Active SU2 driver instance (e.g. ``CSinglezoneDriver``). + + Raises: + ValueError: If a registered sensor name is not listed in + ``METRIC_SENSOR`` in the config. + """ + for name in self._sensors: + idx = driver.GetMetricSensorIndex(name) + if idx < 0: + raise ValueError( + "Custom sensor '{}' not found in the driver. " + "Ensure it is listed in METRIC_SENSOR in the config.".format(name) + ) + self._indices[name] = idx + + def populate(self, driver: SU2Driver) -> None: + """Evaluate all sensors and push values to the driver in bulk. + + Call after ``Run()`` and before ``Postprocess()`` on each iteration. + + Args: + driver: Active SU2 driver instance (e.g. ``CSinglezoneDriver``). + """ + sensors = driver.AdaptSensors() + + for name, fn in self._sensors.items(): + iSensor = self._indices[name] + values = fn(driver) + + for iNode, value in enumerate(values): + sensors.Set(iNode, iSensor, value) diff --git a/SU2_PY/meson.build b/SU2_PY/meson.build index ced81290346..5c6d772b5e4 100644 --- a/SU2_PY/meson.build +++ b/SU2_PY/meson.build @@ -33,6 +33,10 @@ install_data(['SU2/io/config.py', 'SU2/io/__init__.py'], install_dir: join_paths(get_option('bindir'), 'SU2/io')) +install_data(['SU2/metric/resolver.py', + 'SU2/metric/__init__.py'], + install_dir: join_paths(get_option('bindir'), 'SU2/metric')) + install_data(['SU2/opt/project.py', 'SU2/opt/scipy_tools.py', 'SU2/opt/__init__.py'], diff --git a/TestCases/mesh_adaptation/metric/built_in/inv_naca0012.cfg b/TestCases/mesh_adaptation/metric/built_in/inv_naca0012.cfg new file mode 100644 index 00000000000..d05f47ff94d --- /dev/null +++ b/TestCases/mesh_adaptation/metric/built_in/inv_naca0012.cfg @@ -0,0 +1,108 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% SU2 configuration file % +% Case description: Inviscid NACA0012 pressure metric computation % +% Author: Brian Munguía % +% Institution: Stanford University % +% Date: 2026.08.04 % +% File Version 8.4.0 "Harrier" % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% SOLVER +% +SOLVER= EULER +MATH_PROBLEM= DIRECT +RESTART_SOL= NO + +% MESH ADAPTATION +% +COMPUTE_METRIC= YES +NORMALIZE_METRIC= YES +METRIC_SENSOR= PRESSURE, MACH +METRIC_NORM= 2 +METRIC_HMAX= 10.0 +METRIC_HMIN= 1.0e-6 +METRIC_HGRAD= 1.5 +% METRIC_ARMAX= 1.0e3 +METRIC_COMPLEXITY= 10000 +NUM_METHOD_HESS= GREEN_GAUSS +ADAP_ITER= 5 +ADAP_TIME_SUBINTERVAL= 1 +ADAP_COMPLEXITIES= ( 10000, 20000, 40000 ) + +% COMPRESSIBLE FREE-STREAM +% +MACH_NUMBER= 0.8 +AOA= 1.25 +FREESTREAM_TEMPERATURE= 300.0 +FREESTREAM_PRESSURE= 101325.0 + +% REFERENCE VALUES +% +REF_ORIGIN_MOMENT_X= 0.25 +REF_ORIGIN_MOMENT_Y= 0.00 +REF_ORIGIN_MOMENT_Z= 0.00 +REF_LENGTH= 1.0 +REF_AREA= 1.0 +REF_DIMENSIONALIZATION= DIMENSIONAL + +% BOUNDARY CONDITIONS +% +MARKER_EULER= ( airfoil ) +MARKER_FAR= ( farfield ) +MARKER_PLOTTING= ( airfoil ) +MARKER_MONITORING= ( airfoil ) + +% DISCRETIZATION +% +NUM_METHOD_GRAD= GREEN_GAUSS +CONV_NUM_METHOD_FLOW= JST +JST_SENSOR_COEFF= ( 0.5, 0.005 ) +CONV_NUM_METHOD_TURB= SCALAR_UPWIND +MUSCL_TURB= NO + +% SOLUTION METHODS +% +TIME_DISCRE_FLOW= EULER_IMPLICIT +TIME_DISCRE_TURB= EULER_IMPLICIT +CFL_NUMBER= 100.0 +CFL_REDUCTION_TURB= 0.5 +CFL_ADAPT= NO +LINEAR_SOLVER= FGMRES +LINEAR_SOLVER_ERROR= 0.1 +LINEAR_SOLVER_ITER= 10 + +% CONVERGENCE +% +ITER= 1000 +CONV_FIELD= REL_RMS_DENSITY +CONV_RESIDUAL_MINVAL= -3 +CONV_STARTITER= 0 + +% INPUT/OUTPUT +% +% Mesh input file +MESH_FILENAME= inv_naca0012.su2 +MESH_FORMAT= SU2 +% +% Restart input files +SOLUTION_FILENAME= restart_flow +SOLUTION_ADJ_FILENAME= restart_adj +% +% Output restart files +RESTART_FILENAME= restart_flow +RESTART_ADJ_FILENAME= restart_adj +% +% Output file names +VOLUME_FILENAME= flow +SURFACE_FILENAME= surface_flow +TABULAR_FORMAT= CSV +CONV_FILENAME= history +% +SCREEN_OUTPUT= ( INNER_ITER, RMS_DENSITY, REL_RMS_DENSITY, DRAG, LIFT, CAUCHY_TAVG_DRAG, CAUCHY_TAVG_LIFT ) +HISTORY_OUTPUT= ( INNER_ITER, REL_RMS_RES, RMS_RES, AERO_COEFF, TAVG_AERO_COEFF, CAUCHY ) +VOLUME_OUTPUT= ( COORDINATES, SOLUTION, PRIMITIVE, SENSOR_VALUE, SENSOR_GRADIENT, SENSOR_HESSIAN, METRIC, VOLUME, TOTAL_VOLUME ) +% +OUTPUT_FILES= ( RESTART, PARAVIEW ) +WRT_RESTART_COMPACT= NO diff --git a/TestCases/mesh_adaptation/metric/custom/metric.py b/TestCases/mesh_adaptation/metric/custom/metric.py new file mode 100644 index 00000000000..42c13c22a50 --- /dev/null +++ b/TestCases/mesh_adaptation/metric/custom/metric.py @@ -0,0 +1,63 @@ +from mpi4py import MPI + +import pysu2 +from SU2.metric import CustomSensorRegistry + +comm = MPI.COMM_WORLD + + +def total_pressure(driver) -> list[float]: + """Compute local total pressure at all nodes for compressible flow.""" + prim_idx = driver.GetPrimitiveIndices() + nDim = driver.GetNumberDimensions() + primVars = driver.Primitives() + nNodes = driver.GetNumberNodes() - driver.GetNumberHaloNodes() + + gamma = 1.4 + coeff = 0.5 * (gamma - 1.0) + exp_fac = gamma / (gamma - 1.0) + press_col = prim_idx["PRESSURE"] + vel_cols = [prim_idx["VELOCITY_X"], prim_idx["VELOCITY_Y"]] + a_col = prim_idx["SOUND_SPEED"] + if nDim == 3: + vel_cols.append(prim_idx["VELOCITY_Z"]) + + p_tot = [0.0] * nNodes + for iNode in range(nNodes): + row = primVars(iNode) + p = row[press_col] + a = row[a_col] + vel2 = sum(row[k] ** 2 for k in vel_cols) + mach2 = vel2 / (a * a) + + p_tot[iNode] = p * pow(1.0 + coeff * mach2, exp_fac) + return p_tot + + +def main(): + # Intialize driver + driver = pysu2.CSinglezoneDriver("rans_naca0012.cfg", 1, comm) + + # Initialize custom metric sensors + custom_sensors = CustomSensorRegistry({"TOTAL_PRESSURE": total_pressure}) + + # Initialize map of Sensor: idx + custom_sensors.initialize(driver) + + driver.Preprocess(0) + driver.Run() + + # Store custom sensor + custom_sensors.populate(driver) + + # Postprocess solution/metric and output + driver.Postprocess() + driver.Update() + driver.Monitor(0) + driver.Output(0) + + driver.Finalize() + + +if __name__ == "__main__": + main() diff --git a/TestCases/mesh_adaptation/metric/custom/rans_naca0012.cfg b/TestCases/mesh_adaptation/metric/custom/rans_naca0012.cfg new file mode 100644 index 00000000000..9c99ab1e70c --- /dev/null +++ b/TestCases/mesh_adaptation/metric/custom/rans_naca0012.cfg @@ -0,0 +1,111 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% SU2 configuration file % +% Case description: RANS NACA0012 total pressure metric computation % +% Author: Brian Munguía % +% Institution: Stanford University % +% Date: 2026.08.04 % +% File Version 8.4.0 "Harrier" % +% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +% SOLVER +% +SOLVER= RANS +KIND_TURB_MODEL= SA +SA_OPTIONS= NEGATIVE, EXPERIMENTAL +MATH_PROBLEM= DIRECT +RESTART_SOL= NO + +% MESH ADAPTATION +% +COMPUTE_METRIC= YES +NORMALIZE_METRIC= YES +METRIC_SENSOR= TOTAL_PRESSURE, MACH +METRIC_NORM= 4 +METRIC_HMAX= 10.0 +METRIC_HMIN= 1.0e-6 +METRIC_HGRAD= 1.5 +% METRIC_ARMAX= 1.0e3 +METRIC_COMPLEXITY= 10000 +NUM_METHOD_HESS= GREEN_GAUSS +ADAP_ITER= 5 +ADAP_TIME_SUBINTERVAL= 1 +ADAP_COMPLEXITIES= ( 10000, 20000, 40000 ) + +% COMPRESSIBLE FREE-STREAM +% +MACH_NUMBER= 0.8 +AOA= 1.25 +FREESTREAM_TEMPERATURE= 300.0 +REYNOLDS_NUMBER= 6.0E6 +REYNOLDS_LENGTH= 1.0 + +% REFERENCE VALUES +% +REF_ORIGIN_MOMENT_X= 0.25 +REF_ORIGIN_MOMENT_Y= 0.00 +REF_ORIGIN_MOMENT_Z= 0.00 +REF_LENGTH= 1.0 +REF_AREA= 1.0 +REF_DIMENSIONALIZATION= DIMENSIONAL + +% BOUNDARY CONDITIONS +% +MARKER_HEATFLUX= ( airfoil, 0.0 ) +MARKER_FAR= ( farfield ) +MARKER_PLOTTING= ( airfoil ) +MARKER_MONITORING= ( airfoil ) + +% DISCRETIZATION +% +NUM_METHOD_GRAD= GREEN_GAUSS +CONV_NUM_METHOD_FLOW= JST +JST_SENSOR_COEFF= ( 0.5, 0.005 ) +CONV_NUM_METHOD_TURB= SCALAR_UPWIND +MUSCL_TURB= NO + +% SOLUTION METHODS +% +TIME_DISCRE_FLOW= EULER_IMPLICIT +TIME_DISCRE_TURB= EULER_IMPLICIT +CFL_NUMBER= 100.0 +CFL_REDUCTION_TURB= 0.5 +CFL_ADAPT= NO +LINEAR_SOLVER= FGMRES +LINEAR_SOLVER_ERROR= 0.1 +LINEAR_SOLVER_ITER= 10 + +% CONVERGENCE +% +ITER= 1000 +CONV_FIELD= REL_RMS_DENSITY +CONV_RESIDUAL_MINVAL= -3 +CONV_STARTITER= 0 + +% INPUT/OUTPUT +% +% Mesh input file +MESH_FILENAME= inv_naca0012.su2 +MESH_FORMAT= SU2 +% +% Restart input files +SOLUTION_FILENAME= restart_flow +SOLUTION_ADJ_FILENAME= restart_adj +% +% Output restart files +RESTART_FILENAME= restart_flow +RESTART_ADJ_FILENAME= restart_adj +% +% Output file names +VOLUME_FILENAME= flow +SURFACE_FILENAME= surface_flow +TABULAR_FORMAT= CSV +CONV_FILENAME= history +% +SCREEN_OUTPUT= ( INNER_ITER, RMS_DENSITY, REL_RMS_DENSITY, DRAG, LIFT, CAUCHY_TAVG_DRAG, CAUCHY_TAVG_LIFT ) +HISTORY_OUTPUT= ( INNER_ITER, REL_RMS_RES, RMS_RES, AERO_COEFF, TAVG_AERO_COEFF, CAUCHY ) +VOLUME_OUTPUT= ( COORDINATES, SOLUTION, PRIMITIVE, SENSOR_VALUE, SENSOR_GRADIENT, SENSOR_HESSIAN, METRIC, VOLUME, TOTAL_VOLUME ) +% +OUTPUT_FILES= ( RESTART, PARAVIEW ) +WRT_RESTART_COMPACT= NO