From bffaaad34d982792cb710ba5baf1f690045d49ed Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 3 Nov 2025 11:55:22 -0800 Subject: [PATCH 01/36] Add metric/adaptation config options and maps --- Common/include/CConfig.hpp | 115 ++++++++++++++++++++++++++++ Common/include/option_structure.hpp | 30 ++++++++ Common/src/CConfig.cpp | 105 +++++++++++++++++++++++++ 3 files changed, 250 insertions(+) diff --git a/Common/include/CConfig.hpp b/Common/include/CConfig.hpp index 794622bb1a1e..c2d7e854bd75 100644 --- a/Common/include/CConfig.hpp +++ b/Common/include/CConfig.hpp @@ -1277,6 +1277,18 @@ 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. */ + METRIC_SENSOR* Metric_Sensor; /*!< \brief Sensors to use for adaptation. */ + unsigned short Metric_Norm; /*!< \brief Lp-norm for mesh adaptation */ + unsigned long Metric_Complexity; /*!< \brief Constraint mesh complexity */ + 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. @@ -10142,4 +10154,107 @@ class CConfig { */ const FluidFlamelet_ParsedOptions& GetFlameletParsedOptions() const { return flamelet_ParsedOptions; } + /*! + * \brief Check if a metric tensor field is being computed + * \return TRUE<\code> if a metric tensor field is being computed + */ + 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 Check if goal-oriented error estimation is being carried out + * \return TRUE<\code> if goal-oriented error estimation is taking place + */ + bool GetGoal_Oriented_Metric(void) const { return (nMetric_Sensor > 0) && (Metric_Sensor[0] == METRIC_SENSOR::GOAL); } + + /*! + * \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 adaptation sensor + */ + METRIC_SENSOR GetMetric_Sensor(unsigned short iSens) const { return Metric_Sensor[iSens]; } + + /*! + * \brief Get corresponding string from metric sensor type + */ + string GetMetric_SensorString(unsigned short iSens) const { + string sensor_name; + switch (Metric_Sensor[iSens]) { + case METRIC_SENSOR::DENSITY: + sensor_name = "Density"; + break; + case METRIC_SENSOR::MACH: + sensor_name = "Mach"; + break; + case METRIC_SENSOR::PRESSURE: + sensor_name = "Pressure"; + break; + case METRIC_SENSOR::TOTAL_PRESSURE: + sensor_name = "Total Pressure"; + break; + case METRIC_SENSOR::TEMPERATURE: + sensor_name = "Temperature"; + break; + case METRIC_SENSOR::TEMPERATURE_VE: + sensor_name = "Temperature_ve"; + break; + case METRIC_SENSOR::ENERGY: + sensor_name = "Energy"; + break; + case METRIC_SENSOR::ENERGY_VE: + sensor_name = "Energy_ve"; + break; + case METRIC_SENSOR::GOAL: + sensor_name = "Goal"; + break; + default: + SU2_MPI::Error("Unsupported metric sensor.", CURRENT_FUNCTION); + } + + return sensor_name; + } + + /*! + * \brief Get number of adaptation sensors + */ + unsigned short GetnMetric_Sensor(void) 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; } + }; diff --git a/Common/include/option_structure.hpp b/Common/include/option_structure.hpp index bf99257b1387..6b7320b62bed 100644 --- a/Common/include/option_structure.hpp +++ b/Common/include/option_structure.hpp @@ -2621,6 +2621,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). */ }; /*! @@ -2652,6 +2654,8 @@ 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. */ + GRADIENT_ADAPT , /*!< \brief Gradient vectors for anisotropic sizing metric tensor. */ + HESSIAN , /*!< \brief Hessian tensors for anisotropic sizing metric tensor. */ }; /*! @@ -2797,6 +2801,32 @@ static const MapType Sobolev_Modus_Map = { MakePair("ONLY_GRADIENT", ENUM_SOBOLEV_MODUS::ONLY_GRAD) }; +/*! + * \brief Types of sensors for anisotropic metric + */ +enum class METRIC_SENSOR { + DENSITY, /*!< \brief Density feature-based metric. */ + MACH, /*!< \brief Mach feature-based metric. */ + PRESSURE, /*!< \brief Pressure feature-based metric. */ + TOTAL_PRESSURE, /*!< \brief Total pressure feature-based metric. */ + TEMPERATURE, /*!< \brief Temperature feature-based metric. */ + TEMPERATURE_VE, /*!< \brief Vibrational/electronic temperature feature-based metric. */ + ENERGY, /*!< \brief Energy feature-based metric. */ + ENERGY_VE, /*!< \brief Vibrational/electronic energy feature-based metric. */ + GOAL, /*!< \brief Goal-oriented metric. */ +}; +static const MapType Metric_Sensor_Map = { + MakePair("DENSITY", METRIC_SENSOR::DENSITY) + MakePair("MACH", METRIC_SENSOR::MACH) + MakePair("PRESSURE", METRIC_SENSOR::PRESSURE) + MakePair("TOTAL_PRESSURE", METRIC_SENSOR::TOTAL_PRESSURE) + MakePair("TEMPERATURE", METRIC_SENSOR::TEMPERATURE) + MakePair("TEMPERATURE_VE", METRIC_SENSOR::TEMPERATURE_VE) + MakePair("ENERGY", METRIC_SENSOR::ENERGY) + MakePair("ENERGY_VE", METRIC_SENSOR::ENERGY_VE) + MakePair("GOAL", METRIC_SENSOR::GOAL) +}; + /*! * \brief Type of mesh deformation */ diff --git a/Common/src/CConfig.cpp b/Common/src/CConfig.cpp index 0a9cca0f63f7..5232edc31896 100644 --- a/Common/src/CConfig.cpp +++ b/Common/src/CConfig.cpp @@ -3034,6 +3034,55 @@ 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 a metric tensor field */ + addBoolOption("COMPUTE_METRIC", Compute_Metric, false); + /*!\brief NORMALIZE_METRIC \n DESCRIPTION: Normalize the metric tensor */ + addBoolOption("NORMALIZE_METRIC", Normalize_Metric, true); + /*!\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 */ + addEnumListOption("METRIC_SENSOR", nMetric_Sensor, Metric_Sensor, Metric_Sensor_Map); + /*!\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 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 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_ITER \n DESCRIPTION: Maximum cell size at each target complexity */ + addPythonOption("ADAP_HMAXS"); + /*!\brief ADAP_ITER \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_ITER \n DESCRIPTION: Hausdorff distance parameter for surface remeshing */ + addPythonOption("ADAP_HAUSD"); + /*!\brief ADAP_ITER \n DESCRIPTION: A mesh adaptation option */ + addPythonOption("ADAP_ANGLE"); + /* END_CONFIG_OPTIONS */ } @@ -5752,6 +5801,38 @@ 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 (auto iSensor = 0; iSensor < nMetric_Sensor; iSensor++) { + /*--- If using GOAL, it must be the only sensor and the discrete adjoint must be used ---*/ + if (Metric_Sensor[iSensor] == METRIC_SENSOR::GOAL) { + if (nMetric_Sensor != 1) + SU2_MPI::Error("Adaptation sensor GOAL cannot be used with other sensors.", CURRENT_FUNCTION); + if (!DiscreteAdjoint) + SU2_MPI::Error("Adaptation sensor GOAL can only be computed for MATH_PROBLEM = DISCRETE_ADJOINT.", CURRENT_FUNCTION); + } + + if (Kind_Solver == MAIN_SOLVER::NEMO_EULER || Kind_Solver == MAIN_SOLVER::NEMO_NAVIER_STOKES) { + if (Metric_Sensor[iSensor] == METRIC_SENSOR::GOAL || Metric_Sensor[iSensor] == METRIC_SENSOR::DENSITY) + SU2_MPI::Error(string("Adaptation sensor ") + GetMetric_SensorString(iSensor) + string(" not available for NEMO problems."), CURRENT_FUNCTION); + } + if (Kind_Solver != MAIN_SOLVER::NEMO_EULER && Kind_Solver != MAIN_SOLVER::NEMO_NAVIER_STOKES) { + if (Metric_Sensor[iSensor] == METRIC_SENSOR::TEMPERATURE_VE || Metric_Sensor[iSensor] == METRIC_SENSOR::ENERGY_VE) + SU2_MPI::Error(string("Adaptation sensor ") + GetMetric_SensorString(iSensor) + string(" not available for non-NEMO problems."), CURRENT_FUNCTION); + } + if (Kind_Solver == MAIN_SOLVER::INC_EULER || Kind_Solver == MAIN_SOLVER::INC_NAVIER_STOKES || Kind_Solver == MAIN_SOLVER::INC_RANS) { + if (Metric_Sensor[iSensor] == METRIC_SENSOR::GOAL || Metric_Sensor[iSensor] == METRIC_SENSOR::ENERGY) + SU2_MPI::Error(string("Adaptation sensor ") + GetMetric_SensorString(iSensor) + string(" not available for INC problems."), 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); + } + } + } void CConfig::SetMarkers(SU2_COMPONENT val_software) { @@ -7925,6 +8006,30 @@ 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 (auto iSensor = 0; iSensor < nMetric_Sensor; iSensor++) { + cout << GetMetric_SensorString(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; + 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, From 3f2a7ddfd182e9cc314a7e32c7687cd4339f2261 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 3 Nov 2025 11:59:11 -0800 Subject: [PATCH 02/36] Add adaptation primitive/gradient/Hessian and metric tensor to CVariable with initialization --- SU2_CFD/include/variables/CVariable.hpp | 118 ++++++++++++++++++++++++ SU2_CFD/src/variables/CFlowVariable.cpp | 7 ++ SU2_CFD/src/variables/CVariable.cpp | 8 ++ 3 files changed, 133 insertions(+) diff --git a/SU2_CFD/include/variables/CVariable.hpp b/SU2_CFD/include/variables/CVariable.hpp index dc905d79fcb4..5cd23008e271 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 Primitive_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,115 @@ 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 SetPrimitive_Adapt(unsigned long iPoint, unsigned long iVar, su2double primitive) { Primitive_Adapt(iPoint,iVar) = primitive; } + + /*! + * \brief Get the gradient of the entire solution. + * \return Reference to gradient. + */ + inline const MatrixType& GetPrimitive_Adapt(void) const { return Primitive_Adapt; } + + /*! + * \brief Get the value of the solution gradient. + * \param[in] iPoint - Point index. + * \return Value of the gradient solution. + */ + inline su2double *GetPrimitive_Adapt(unsigned long iPoint) { return Primitive_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 GetPrimitive_Adapt(unsigned long iPoint, unsigned long iVar) const { return Primitive_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); } }; diff --git a/SU2_CFD/src/variables/CFlowVariable.cpp b/SU2_CFD/src/variables/CFlowVariable.cpp index 6d07949d79f2..d9350462ae19 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->GetGoal_Oriented_Metric()? nVar : config->GetnMetric_Sensor(); + unsigned short nSymMat = 3 * (nDim - 1); + Primitive_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 111a2a17775d..8e7234b258ca 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->GetGoal_Oriented_Metric()? nVar : config->GetnMetric_Sensor(); + Gradient_Adapt.resize(nPoint,nHess,nDim,0.0); + Hessian.resize(nPoint,nHess,nSymMat,0.0); + } } void CVariable::Set_OldSolution() { From 10e16068d5776f888df6700516e065e9c3b8bcfc Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 3 Nov 2025 12:03:02 -0800 Subject: [PATCH 03/36] Add solver functionality to calculate metric tensor field - GG gradient/Hessian calculation on adaptation-specific variable containers - Metric integration and normalization for steady and unsteady flows - Storing of proper primitive variables for Euler solver --- .../gradients/computeGradientsGreenGauss.hpp | 137 +++++++ SU2_CFD/include/gradients/computeMetrics.hpp | 377 ++++++++++++++++++ SU2_CFD/include/solvers/CEulerSolver.hpp | 43 ++ SU2_CFD/include/solvers/CSolver.hpp | 63 ++- SU2_CFD/src/solvers/CBaselineSolver.cpp | 1 + SU2_CFD/src/solvers/CDiscAdjSolver.cpp | 1 + SU2_CFD/src/solvers/CEulerSolver.cpp | 1 + SU2_CFD/src/solvers/CIncEulerSolver.cpp | 1 + SU2_CFD/src/solvers/CNEMOEulerSolver.cpp | 1 + SU2_CFD/src/solvers/CSolver.cpp | 133 +++++- SU2_CFD/src/solvers/CTurbSASolver.cpp | 3 +- 11 files changed, 744 insertions(+), 17 deletions(-) create mode 100644 SU2_CFD/include/gradients/computeMetrics.hpp diff --git a/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp b/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp index 98a170cf3810..a2f984e1560a 100644 --- a/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp +++ b/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp @@ -179,6 +179,123 @@ 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); + su2double diagScale; + + /*--- 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 jDim = 0; jDim < nDim; ++jDim) { + for (size_t iVar = varBegin; iVar < varEnd; ++iVar) { + su2double flux = weight * (gradient(iPoint, iVar, jDim) + gradient(jPoint, iVar, jDim)); + for (size_t iDim = 0; iDim < nDim; ++iDim) { + size_t ind = (iDim <= jDim) ? iDim*nDim - ((iDim - 1)*iDim)/2 + jDim - iDim + : jDim*nDim - ((jDim - 1)*jDim)/2 + iDim - jDim; + diagScale = (iDim == jDim)? 1.0 : 0.5; + hessian(iPoint, iVar, ind) += diagScale * flux * area[iDim]; + } // iDims + } // variables + } // jDims + } // 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 jDim = 0; jDim < nDim; ++jDim) { + for (size_t iVar = varBegin; iVar < varEnd; iVar++) { + su2double flux = gradient(iPoint, iVar, jDim) / volume; + for (size_t iDim = 0; iDim < nDim; ++iDim) { + size_t ind = (iDim <= jDim) ? iDim * nDim - ((iDim - 1) * iDim)/2 + jDim - iDim + : jDim * nDim - ((jDim - 1) * jDim)/2 + iDim - jDim; + diagScale = (iDim == jDim)? 1.0 : 0.5; + hessian(iPoint, iVar, ind) -= diagScale * flux * area[iDim]; + } // iDims + } // variables + } // jDims + } // 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 +322,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/gradients/computeMetrics.hpp b/SU2_CFD/include/gradients/computeMetrics.hpp new file mode 100644 index 000000000000..cb437fbd29ea --- /dev/null +++ b/SU2_CFD/include/gradients/computeMetrics.hpp @@ -0,0 +1,377 @@ +/*! + * \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.3.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 "../../../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, 2); + mat[1][0] = metric_field(iPoint, 1); mat[1][1] = metric_field(iPoint, 3); mat[1][2] = metric_field(iPoint, 4); + mat[2][0] = metric_field(iPoint, 2); 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[0][2] * scale; + metric_field(iPoint, 3) = mat[1][1] * 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, 2); + mat[1][0] = metric_field(iPoint, iSensor, 1); mat[1][1] = metric_field(iPoint, iSensor, 3); mat[1][2] = metric_field(iPoint, iSensor, 4); + mat[2][0] = metric_field(iPoint, iSensor, 2); 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[0][2] * scale; + metric_field(iPoint, iSensor, 3) = mat[1][1] * scale; + metric_field(iPoint, iSensor, 4) = mat[1][2] * scale; + metric_field(iPoint, iSensor, 5) = mat[2][2] * scale; + break; + } + } + } + }; +} + +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-12; + + 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 (EigVal[iDim] != 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 m02 = metric(iPoint, 2); + const ScalarType m11 = 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(); + + const bool goal = (config.GetGoal_Oriented_Metric()); + + /*--- Constants defining normalization ---*/ + const ScalarType p = config.GetMetric_Norm(); + const ScalarType N = SU2_TYPE::GetValue(config.GetMetric_Complexity()); + 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) { + auto nodes = geometry.nodes; + + /*--- 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/solvers/CEulerSolver.hpp b/SU2_CFD/include/solvers/CEulerSolver.hpp index e3f029a37538..1883e8e26d2a 100644 --- a/SU2_CFD/include/solvers/CEulerSolver.hpp +++ b/SU2_CFD/include/solvers/CEulerSolver.hpp @@ -273,6 +273,49 @@ class CEulerSolver : public CFVMFlowSolverBaseGetGoal_Oriented_Metric()) { + const auto nSensor = config->GetnMetric_Sensor(); + for (auto iSensor = 0; iSensor < nSensor; iSensor++) { + switch (config->GetMetric_Sensor(iSensor)) { + case METRIC_SENSOR::MACH: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetVelocity2(iPoint) / nodes->GetSoundSpeed(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::PRESSURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetPressure(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::TEMPERATURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetTemperature(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::DENSITY: + default: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetDensity(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + } + } + } + else { + // TODO: store variables which need Hessian computation for goal-oriented metric + } + } + /*! * \brief Set gradients of coefficients for fixed CL mode * \param[in] config - Definition of the particular problem. diff --git a/SU2_CFD/include/solvers/CSolver.hpp b/SU2_CFD/include/solvers/CSolver.hpp index f0b3a4c493d1..9f801d22925a 100644 --- a/SU2_CFD/include/solvers/CSolver.hpp +++ b/SU2_CFD/include/solvers/CSolver.hpp @@ -85,7 +85,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. */ @@ -580,6 +581,22 @@ class CSolver { */ inline virtual void SetPrimitive_Limiter(CGeometry *geometry, const CConfig *config) { } + /*! + * \brief A virtual member. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + */ + virtual void SetPrimitive_Adapt(CGeometry *geometry, const CConfig *config) { } + + /*! + * \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). @@ -4349,24 +4366,42 @@ 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 metric tensor field. + * \param[in] solver - Physical definition of the problem. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + */ + void ComputeMetric(CSolver **solver, CGeometry *geometry, const CConfig *config); + + /*! + * \brief Add contribution to the metric field. + * \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. + */ + void AddMetrics(CSolver **solver, const CGeometry *geometry, const CConfig *config, + const unsigned short iSensor); protected: /*! diff --git a/SU2_CFD/src/solvers/CBaselineSolver.cpp b/SU2_CFD/src/solvers/CBaselineSolver.cpp index fa89ee5e4830..ef6703f7efb2 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 8bae5e6ca36f..2e8a1b3a0aaa 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 dcca14edd19d..ebb02534b492 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 3df1ffbe3dd8..53d672829298 100644 --- a/SU2_CFD/src/solvers/CIncEulerSolver.cpp +++ b/SU2_CFD/src/solvers/CIncEulerSolver.cpp @@ -104,6 +104,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 65f3ea81f123..1497f4fe0f70 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 a9c3875406bf..16bbfc1ea3b1 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -29,6 +29,7 @@ #include "../../include/solvers/CSolver.hpp" #include "../../include/gradients/computeGradientsGreenGauss.hpp" #include "../../include/gradients/computeGradientsLeastSquares.hpp" +#include "../../include/gradients/computeMetrics.hpp" #include "../../include/limiters/computeLimiters.hpp" #include "../../../Common/include/toolboxes/MMS/CIncTGVSolution.hpp" #include "../../../Common/include/toolboxes/MMS/CInviscidVortexSolution.hpp" @@ -1379,6 +1380,14 @@ void CSolver::GetCommCountAndType(const CConfig* config, COUNT_PER_POINT = nVar; MPI_TYPE = COMM_TYPE_DOUBLE; break; + case MPI_QUANTITIES::GRADIENT_ADAPT: + COUNT_PER_POINT = config->GetGoal_Oriented_Metric()? nVar*nDim : config->GetnMetric_Sensor()*nDim; + MPI_TYPE = COMM_TYPE_DOUBLE; + break; + case MPI_QUANTITIES::HESSIAN: + COUNT_PER_POINT = config->GetGoal_Oriented_Metric()? nVar*nSymMat : config->GetnMetric_Sensor()*nSymMat; + MPI_TYPE = COMM_TYPE_DOUBLE; + break; default: SU2_MPI::Error("Unrecognized quantity for point-to-point MPI comms.", CURRENT_FUNCTION); @@ -1393,6 +1402,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(); } } @@ -1409,7 +1420,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; @@ -1435,6 +1446,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); @@ -1503,10 +1515,16 @@ void CSolver::InitiateComms(CGeometry *geometry, 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); @@ -1551,7 +1569,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; @@ -1572,6 +1590,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); @@ -1651,10 +1670,16 @@ void CSolver::CompleteComms(CGeometry *geometry, 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]); @@ -2165,6 +2190,20 @@ 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::SetHessian_GG(CGeometry *geometry, const CConfig *config, short idxVel, const unsigned short Kind_Solver) { + const auto& solution = config->GetGoal_Oriented_Metric()? base_nodes->GetSolution() : base_nodes->GetPrimitive_Adapt(); + auto& gradient = base_nodes->GetGradient_Adapt(); + auto nHess = config->GetGoal_Oriented_Metric()? nVar : config->GetnMetric_Sensor(); + + 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. ---*/ @@ -4346,3 +4385,93 @@ void CSolver::SavelibROM(CGeometry *geometry, CConfig *config, bool converged) { #endif } + + +void CSolver::ComputeMetric(CSolver **solver, CGeometry *geometry, const CConfig *config) { + /*--- TODO: - goal-oriented metric ---*/ + /*--- - metric intersection ---*/ + const unsigned long nPointDomain = geometry->GetnPointDomain(); + + const bool visc = (config->GetViscous()); + const bool turb = (config->GetKind_Turb_Model() != TURB_MODEL::NONE); + + const bool goal = (config->GetGoal_Oriented_Metric()); + const bool normalize = (config->GetNormalize_Metric()); + + unsigned short nSensor = config->GetnMetric_Sensor(); + + 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); + + /*--- Integrate and normalize the metric tensor field ---*/ + vector integrals; + for (auto iSensor = 0u; iSensor < nSensor; ++iSensor) { + SU2_OMP_MASTER + + if (!goal) { + /*--- Make the Hessian eigenvalues positive definite, and add to the metric tensor ---*/ + auto& hessians = base_nodes->GetHessian(); + setPositiveDefiniteMetrics(*geometry, *config, iSensor, hessians); + AddMetrics(solver, geometry, config, iSensor); + + /*--- 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) { + cout << "Global metric normalization integral for sensor "; + cout << config->GetMetric_SensorString(iSensor) << ": " << integral << endl; + } + } + } else { + SU2_MPI::Error("Goal-oriented metric not currently implemented.", CURRENT_FUNCTION); + } + END_SU2_OMP_MASTER + SU2_OMP_BARRIER + } +} + +void CSolver::AddMetrics(CSolver **solver, const CGeometry*geometry, const CConfig *config, + const unsigned short iSensor) { + /*--- 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 short nVarFlo = solver[FLOW_SOL]->GetnVar(); + const unsigned short nSensor = config->GetnMetric_Sensor(); + + const bool turb = (config->GetKind_Turb_Model() != TURB_MODEL::NONE); + const bool goal = (config->GetGoal_Oriented_Metric()); + + 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); + 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 7649165d0be3..0124d6e887b5 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 ---*/ @@ -301,7 +302,7 @@ void CTurbSASolver::Viscous_Residual(const unsigned long iEdge, const CGeometry* /*--- Roughness heights. ---*/ numerics->SetRoughness(geometry->nodes->GetRoughnessHeight(iPoint), geometry->nodes->GetRoughnessHeight(jPoint)); }; - + /*--- Now instantiate the generic non-conservative implementation with the functor above. ---*/ Viscous_Residual_NonCons(iEdge, geometry, solver_container, numerics, config, SolverSpecificNumerics); From 14aba45873b5d1e2819a4f4714c4ffe0ae319d94 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 3 Nov 2025 12:03:16 -0800 Subject: [PATCH 04/36] Add single zone driver calls to calculate metric fiedl --- SU2_CFD/include/drivers/CSinglezoneDriver.hpp | 5 +++ SU2_CFD/src/drivers/CSinglezoneDriver.cpp | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/SU2_CFD/include/drivers/CSinglezoneDriver.hpp b/SU2_CFD/include/drivers/CSinglezoneDriver.hpp index 355369556ea5..d7e789ae66ce 100644 --- a/SU2_CFD/include/drivers/CSinglezoneDriver.hpp +++ b/SU2_CFD/include/drivers/CSinglezoneDriver.hpp @@ -110,4 +110,9 @@ class CSinglezoneDriver : public CDriver { */ bool Monitor(unsigned long TimeIter) override; + /*! + * \brief Perform all steps to compute the metric tensor. + */ + virtual void ComputeMetricField(); + }; diff --git a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp index 1270eef237ab..eec6ec19bc9a 100644 --- a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp +++ b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp @@ -175,6 +175,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 +317,33 @@ bool CSinglezoneDriver::Monitor(unsigned long TimeIter){ bool CSinglezoneDriver::GetTimeConvergence() const{ return output_container[ZONE_0]->GetCauchyCorrectedTimeConvergence(config_container[ZONE_0]); } + +void CSinglezoneDriver::ComputeMetricField() { + + 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; + } + solver_flow->InitiateComms(geometry, config, MPI_QUANTITIES::SOLUTION); + solver_flow->CompleteComms(geometry, config, MPI_QUANTITIES::SOLUTION); + solver_flow->Preprocessing(geometry, solver, config, MESH_0, NO_RK_ITER, + RUNTIME_FLOW_SYS, true); + solver_flow->SetPrimitive_Adapt(geometry, config, solver_flow->GetNodes()); + + 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); +} From 631f4b57bf6c98305786f9fbac87a94ee095f0bc Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 3 Nov 2025 12:10:26 -0800 Subject: [PATCH 05/36] Add metric tensor outputs --- SU2_CFD/include/output/CFlowOutput.hpp | 16 +++++++++++ SU2_CFD/src/output/CFlowCompOutput.cpp | 3 ++ SU2_CFD/src/output/CFlowOutput.cpp | 39 ++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/SU2_CFD/include/output/CFlowOutput.hpp b/SU2_CFD/include/output/CFlowOutput.hpp index 63867b091ae4..7c8c23af181f 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/src/output/CFlowCompOutput.cpp b/SU2_CFD/src/output/CFlowCompOutput.cpp index 2476498e84d7..6bcae5a287f0 100644 --- a/SU2_CFD/src/output/CFlowCompOutput.cpp +++ b/SU2_CFD/src/output/CFlowCompOutput.cpp @@ -303,6 +303,9 @@ void CFlowCompOutput::SetVolumeOutputFields(CConfig *config){ AddCommonFVMOutputs(config); + // Metric tensor and dual-cell volume + AddMeshAdaptationOutputs(config); + if (config->GetTime_Domain()) { SetTimeAveragedFields(); } diff --git a/SU2_CFD/src/output/CFlowOutput.cpp b/SU2_CFD/src/output/CFlowOutput.cpp index b355c0698afb..be5647734a0b 100644 --- a/SU2_CFD/src/output/CFlowOutput.cpp +++ b/SU2_CFD/src/output/CFlowOutput.cpp @@ -4109,3 +4109,42 @@ 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) { + + // Anisotropic metric tensor, and dual-cell volume + if(config->GetCompute_Metric()) { + // Common metric components for both 2D and 3D + AddVolumeOutput("METRIC_XX", "Metric_xx", "MESH_ADAPT", "x-x-component of the metric"); + AddVolumeOutput("METRIC_XY", "Metric_xy", "MESH_ADAPT", "x-y-component of the metric"); + AddVolumeOutput("METRIC_YY", "Metric_yy", "MESH_ADAPT", "y-y-component of the metric"); + + // Additional components for 3D + if (nDim == 3) { + AddVolumeOutput("METRIC_XZ", "Metric_xz", "MESH_ADAPT", "x-z-component of the metric"); + AddVolumeOutput("METRIC_YZ", "Metric_yz", "MESH_ADAPT", "y-z-component of the metric"); + AddVolumeOutput("METRIC_ZZ", "Metric_zz", "MESH_ADAPT", "z-z-component of the metric"); + } + } +} + +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()) { + // Common metric 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)); + } + } +} + From 14dddf6211ed5c6fdd9c61f6e640a9acafb78ce9 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 3 Nov 2025 12:11:56 -0800 Subject: [PATCH 06/36] Add tensor outputs to paraview filewriter --- .../filewriter/CParaviewXMLFileWriter.cpp | 178 ++++++++++++++++-- 1 file changed, 158 insertions(+), 20 deletions(-) diff --git a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp index 5a848b2e11a9..14fd3e1a3fa2 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) { + + /*--- Adjust the string name to remove the tensor component suffix ---*/ - if (output_variable && isVector) { + 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,112 @@ 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, isTensor = false; - bool output_variable = true, isVector = false; - size_t found = fieldNames[iField].find("_x"); + // 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("_y"); + found = fieldNames[iField].find("_xy"); if (found!=string::npos) { - /*--- We have found a vector, so skip the Y component. ---*/ + /*--- We have found a tensor, so skip the XY component. ---*/ output_variable = false; + isTensor = true; VarCounter++; } - found = fieldNames[iField].find("_z"); + found = fieldNames[iField].find("_yy"); if (found!=string::npos) { - /*--- We have found a vector, so skip the Z component. ---*/ + /*--- 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("_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++; + } + } - if (output_variable && isVector) { + /*--- Write the point data as a tensor, vector, or scalar. ---*/ + + if (output_variable && isTensor) { + + /*--- Resize buffer to accommodate tensor data ---*/ + dataBufferFloat.resize(myPoint*NTENSOR); + + float val = 0.0; + 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); + + 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 +434,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. ---*/ From c8420370a8e4c52fd513adfdbdf954cc4478cc4f Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 3 Nov 2025 12:32:55 -0800 Subject: [PATCH 07/36] Add Alberto/Andrea's Primitive_Adapt for NEMO and incompressible Euler --- Common/src/CConfig.cpp | 4 +- SU2_CFD/include/solvers/CEulerSolver.hpp | 21 ++++++-- SU2_CFD/include/solvers/CIncEulerSolver.hpp | 49 +++++++++++++++++ SU2_CFD/include/solvers/CNEMOEulerSolver.hpp | 55 ++++++++++++++++++++ 4 files changed, 123 insertions(+), 6 deletions(-) diff --git a/Common/src/CConfig.cpp b/Common/src/CConfig.cpp index 5232edc31896..a647545b7801 100644 --- a/Common/src/CConfig.cpp +++ b/Common/src/CConfig.cpp @@ -5814,7 +5814,7 @@ void CConfig::SetPostprocessing(SU2_COMPONENT val_software, unsigned short val_i } if (Kind_Solver == MAIN_SOLVER::NEMO_EULER || Kind_Solver == MAIN_SOLVER::NEMO_NAVIER_STOKES) { - if (Metric_Sensor[iSensor] == METRIC_SENSOR::GOAL || Metric_Sensor[iSensor] == METRIC_SENSOR::DENSITY) + if (Metric_Sensor[iSensor] == METRIC_SENSOR::GOAL || Metric_Sensor[iSensor] == METRIC_SENSOR::DENSITY || Metric_Sensor[iSensor] == METRIC_SENSOR::TOTAL_PRESSURE) SU2_MPI::Error(string("Adaptation sensor ") + GetMetric_SensorString(iSensor) + string(" not available for NEMO problems."), CURRENT_FUNCTION); } if (Kind_Solver != MAIN_SOLVER::NEMO_EULER && Kind_Solver != MAIN_SOLVER::NEMO_NAVIER_STOKES) { @@ -5822,7 +5822,7 @@ void CConfig::SetPostprocessing(SU2_COMPONENT val_software, unsigned short val_i SU2_MPI::Error(string("Adaptation sensor ") + GetMetric_SensorString(iSensor) + string(" not available for non-NEMO problems."), CURRENT_FUNCTION); } if (Kind_Solver == MAIN_SOLVER::INC_EULER || Kind_Solver == MAIN_SOLVER::INC_NAVIER_STOKES || Kind_Solver == MAIN_SOLVER::INC_RANS) { - if (Metric_Sensor[iSensor] == METRIC_SENSOR::GOAL || Metric_Sensor[iSensor] == METRIC_SENSOR::ENERGY) + if (Metric_Sensor[iSensor] == METRIC_SENSOR::GOAL || Metric_Sensor[iSensor] == METRIC_SENSOR::MACH) SU2_MPI::Error(string("Adaptation sensor ") + GetMetric_SensorString(iSensor) + string(" not available for INC problems."), CURRENT_FUNCTION); } } diff --git a/SU2_CFD/include/solvers/CEulerSolver.hpp b/SU2_CFD/include/solvers/CEulerSolver.hpp index 1883e8e26d2a..0109d461015e 100644 --- a/SU2_CFD/include/solvers/CEulerSolver.hpp +++ b/SU2_CFD/include/solvers/CEulerSolver.hpp @@ -283,9 +283,9 @@ class CEulerSolver : public CFVMFlowSolverBaseGetnMetric_Sensor(); for (auto iSensor = 0; iSensor < nSensor; iSensor++) { switch (config->GetMetric_Sensor(iSensor)) { - case METRIC_SENSOR::MACH: + case METRIC_SENSOR::DENSITY: for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetVelocity2(iPoint) / nodes->GetSoundSpeed(iPoint); + const su2double prim_var = nodes->GetDensity(iPoint); nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); } break; @@ -295,16 +295,29 @@ class CEulerSolver : public CFVMFlowSolverBaseSetPrimitive_Adapt(iPoint, iSensor, prim_var); } break; + case METRIC_SENSOR::TOTAL_PRESSURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double Mach = sqrt(nodes->GetVelocity2(iPoint))/nodes->GetSoundSpeed(iPoint); + const su2double prim_var = nodes->GetPressure(iPoint)*pow((1+0.2*Mach*Mach), 3.5); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; case METRIC_SENSOR::TEMPERATURE: for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { const su2double prim_var = nodes->GetTemperature(iPoint); nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); } break; - case METRIC_SENSOR::DENSITY: + case METRIC_SENSOR::ENERGY: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetEnergy(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::MACH: default: for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetDensity(iPoint); + const su2double prim_var = sqrt(nodes->GetVelocity2(iPoint)) / nodes->GetSoundSpeed(iPoint); nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); } break; diff --git a/SU2_CFD/include/solvers/CIncEulerSolver.hpp b/SU2_CFD/include/solvers/CIncEulerSolver.hpp index 54fd616d0de8..c9fe7191d6ff 100644 --- a/SU2_CFD/include/solvers/CIncEulerSolver.hpp +++ b/SU2_CFD/include/solvers/CIncEulerSolver.hpp @@ -86,6 +86,55 @@ class CIncEulerSolver : public CFVMFlowSolverBaseGetGoal_Oriented_Metric()) { + const auto nSensor = config->GetnMetric_Sensor(); + for (auto iSensor = 0; iSensor < nSensor; iSensor++) { + switch (config->GetMetric_Sensor(iSensor)) { + case METRIC_SENSOR::DENSITY: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetDensity(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::TOTAL_PRESSURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetPressure(iPoint) + 0.5*nodes->GetDensity(iPoint)*nodes->GetVelocity2(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::TEMPERATURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetTemperature(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::ENERGY: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetEnergy(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::PRESSURE: + default: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetPressure(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + } + } + } + else { + // TODO: store variables which need Hessian computation for goal-oriented metric + } + } + /*! * \brief Update the Beta parameter for the incompressible preconditioner. * \param[in] geometry - Geometrical definition of the problem. diff --git a/SU2_CFD/include/solvers/CNEMOEulerSolver.hpp b/SU2_CFD/include/solvers/CNEMOEulerSolver.hpp index 9077ea195d89..1f13399554c3 100644 --- a/SU2_CFD/include/solvers/CNEMOEulerSolver.hpp +++ b/SU2_CFD/include/solvers/CNEMOEulerSolver.hpp @@ -195,6 +195,61 @@ class CNEMOEulerSolver : public CFVMFlowSolverBaseGetGoal_Oriented_Metric()) { + const auto nSensor = config->GetnMetric_Sensor(); + for (auto iSensor = 0; iSensor < nSensor; iSensor++) { + switch (config->GetMetric_Sensor(iSensor)) { + case METRIC_SENSOR::PRESSURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetPressure(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::TEMPERATURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetTemperature(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::TEMPERATURE_VE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetTemperature_ve(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::ENERGY: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetEnergy(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::ENERGY_VE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = *nodes->GetEve(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::MACH: + default: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = sqrt(nodes->GetVelocity2(iPoint)) / nodes->GetSoundSpeed(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + } + } + } + else { + // TODO: store variables which need Hessian computation for goal-oriented metric + } + } + /*! * \brief Compute a suitable under-relaxation parameter to limit the change in the solution variables over * a nonlinear iteration for stability. From 10adbacfbbb26e00dba6378200f9a0f3e6ef9719 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Tue, 4 Nov 2025 10:16:25 -0800 Subject: [PATCH 08/36] Update adaptation python option descriptions --- Common/src/CConfig.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Common/src/CConfig.cpp b/Common/src/CConfig.cpp index a647545b7801..95a993c46eb6 100644 --- a/Common/src/CConfig.cpp +++ b/Common/src/CConfig.cpp @@ -3072,15 +3072,15 @@ void CConfig::SetConfig_Options() { addPythonOption("ADAP_ADJ_CFLS"); /*!\brief ADAP_RESIDUAL_REDUCTIONS \n DESCRIPTION: Residual reduction at each target complexity */ addPythonOption("ADAP_RESIDUAL_REDUCTIONS"); - /*!\brief ADAP_ITER \n DESCRIPTION: Maximum cell size at each target complexity */ + /*!\brief ADAP_HMAXS \n DESCRIPTION: Maximum cell size at each target complexity */ addPythonOption("ADAP_HMAXS"); - /*!\brief ADAP_ITER \n DESCRIPTION: Minimum cell size at each target complexity */ + /*!\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_ITER \n DESCRIPTION: Hausdorff distance parameter for surface remeshing */ + /*!\brief ADAP_HAUSD \n DESCRIPTION: Hausdorff distance parameter for surface remeshing */ addPythonOption("ADAP_HAUSD"); - /*!\brief ADAP_ITER \n DESCRIPTION: A mesh adaptation option */ + /*!\brief ADAP_ANGLE \n DESCRIPTION: Sharp angle detection parameter for surface remeshing */ addPythonOption("ADAP_ANGLE"); /* END_CONFIG_OPTIONS */ From 79da503bd55b6da4e4d1cf3b8add58d643a6b052 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Tue, 4 Nov 2025 13:45:54 -0800 Subject: [PATCH 09/36] Fix call to SetPrimitive_Adapt() --- SU2_CFD/src/drivers/CSinglezoneDriver.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp index eec6ec19bc9a..f4343ba9d150 100644 --- a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp +++ b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp @@ -334,7 +334,7 @@ void CSinglezoneDriver::ComputeMetricField() { solver_flow->CompleteComms(geometry, config, MPI_QUANTITIES::SOLUTION); solver_flow->Preprocessing(geometry, solver, config, MESH_0, NO_RK_ITER, RUNTIME_FLOW_SYS, true); - solver_flow->SetPrimitive_Adapt(geometry, config, solver_flow->GetNodes()); + solver_flow->SetPrimitive_Adapt(geometry, config); if (config->GetKind_Hessian_Method() == GREEN_GAUSS) { if(rank == MASTER_NODE) cout << "Computing Hessians using Green-Gauss." << endl; From b4c9765f1501e06ced62a6eafb8214e6686d7543 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Tue, 4 Nov 2025 14:17:27 -0800 Subject: [PATCH 10/36] Remove various unused variables and goal-oriented code paths --- Common/include/CConfig.hpp | 6 -- Common/src/CConfig.cpp | 12 ++- SU2_CFD/include/gradients/computeMetrics.hpp | 2 - SU2_CFD/include/solvers/CEulerSolver.hpp | 87 +++++++++----------- SU2_CFD/include/solvers/CIncEulerSolver.hpp | 73 ++++++++-------- SU2_CFD/include/solvers/CNEMOEulerSolver.hpp | 85 +++++++++---------- SU2_CFD/src/solvers/CSolver.cpp | 19 ++--- SU2_CFD/src/variables/CFlowVariable.cpp | 2 +- SU2_CFD/src/variables/CVariable.cpp | 2 +- 9 files changed, 127 insertions(+), 161 deletions(-) diff --git a/Common/include/CConfig.hpp b/Common/include/CConfig.hpp index c2d7e854bd75..6f837fd0bc54 100644 --- a/Common/include/CConfig.hpp +++ b/Common/include/CConfig.hpp @@ -10166,12 +10166,6 @@ class CConfig { */ bool GetNormalize_Metric(void) const { return Normalize_Metric; } - /*! - * \brief Check if goal-oriented error estimation is being carried out - * \return TRUE<\code> if goal-oriented error estimation is taking place - */ - bool GetGoal_Oriented_Metric(void) const { return (nMetric_Sensor > 0) && (Metric_Sensor[0] == METRIC_SENSOR::GOAL); } - /*! * \brief Get the kind of method for computation of Hessians used for anisotropy. * \return Numerical method for computation of Hessians used for anisotropy. diff --git a/Common/src/CConfig.cpp b/Common/src/CConfig.cpp index 95a993c46eb6..956d6bcf4653 100644 --- a/Common/src/CConfig.cpp +++ b/Common/src/CConfig.cpp @@ -5805,16 +5805,14 @@ void CConfig::SetPostprocessing(SU2_COMPONENT val_software, unsigned short val_i if (Compute_Metric) { /*--- Check that config is valid for requested sensor ---*/ for (auto iSensor = 0; iSensor < nMetric_Sensor; iSensor++) { - /*--- If using GOAL, it must be the only sensor and the discrete adjoint must be used ---*/ + /*--- TODO: If using GOAL, it must be the only sensor and the discrete adjoint must be used ---*/ + /*--- For now, goal-oriented adaptation is unsupported ---*/ if (Metric_Sensor[iSensor] == METRIC_SENSOR::GOAL) { - if (nMetric_Sensor != 1) - SU2_MPI::Error("Adaptation sensor GOAL cannot be used with other sensors.", CURRENT_FUNCTION); - if (!DiscreteAdjoint) - SU2_MPI::Error("Adaptation sensor GOAL can only be computed for MATH_PROBLEM = DISCRETE_ADJOINT.", CURRENT_FUNCTION); + SU2_MPI::Error("Adaptation sensor GOAL not yet supported.", CURRENT_FUNCTION); } if (Kind_Solver == MAIN_SOLVER::NEMO_EULER || Kind_Solver == MAIN_SOLVER::NEMO_NAVIER_STOKES) { - if (Metric_Sensor[iSensor] == METRIC_SENSOR::GOAL || Metric_Sensor[iSensor] == METRIC_SENSOR::DENSITY || Metric_Sensor[iSensor] == METRIC_SENSOR::TOTAL_PRESSURE) + if (Metric_Sensor[iSensor] == METRIC_SENSOR::DENSITY || Metric_Sensor[iSensor] == METRIC_SENSOR::TOTAL_PRESSURE) SU2_MPI::Error(string("Adaptation sensor ") + GetMetric_SensorString(iSensor) + string(" not available for NEMO problems."), CURRENT_FUNCTION); } if (Kind_Solver != MAIN_SOLVER::NEMO_EULER && Kind_Solver != MAIN_SOLVER::NEMO_NAVIER_STOKES) { @@ -5822,7 +5820,7 @@ void CConfig::SetPostprocessing(SU2_COMPONENT val_software, unsigned short val_i SU2_MPI::Error(string("Adaptation sensor ") + GetMetric_SensorString(iSensor) + string(" not available for non-NEMO problems."), CURRENT_FUNCTION); } if (Kind_Solver == MAIN_SOLVER::INC_EULER || Kind_Solver == MAIN_SOLVER::INC_NAVIER_STOKES || Kind_Solver == MAIN_SOLVER::INC_RANS) { - if (Metric_Sensor[iSensor] == METRIC_SENSOR::GOAL || Metric_Sensor[iSensor] == METRIC_SENSOR::MACH) + if (Metric_Sensor[iSensor] == METRIC_SENSOR::MACH) SU2_MPI::Error(string("Adaptation sensor ") + GetMetric_SensorString(iSensor) + string(" not available for INC problems."), CURRENT_FUNCTION); } } diff --git a/SU2_CFD/include/gradients/computeMetrics.hpp b/SU2_CFD/include/gradients/computeMetrics.hpp index cb437fbd29ea..e1b90c17cc81 100644 --- a/SU2_CFD/include/gradients/computeMetrics.hpp +++ b/SU2_CFD/include/gradients/computeMetrics.hpp @@ -252,8 +252,6 @@ void normalizeMetrics(CGeometry& geometry, const CConfig& config, const unsigned long nPointDomain = geometry.GetnPointDomain(); - const bool goal = (config.GetGoal_Oriented_Metric()); - /*--- Constants defining normalization ---*/ const ScalarType p = config.GetMetric_Norm(); const ScalarType N = SU2_TYPE::GetValue(config.GetMetric_Complexity()); diff --git a/SU2_CFD/include/solvers/CEulerSolver.hpp b/SU2_CFD/include/solvers/CEulerSolver.hpp index 0109d461015e..d8a4286d20cc 100644 --- a/SU2_CFD/include/solvers/CEulerSolver.hpp +++ b/SU2_CFD/include/solvers/CEulerSolver.hpp @@ -279,54 +279,49 @@ class CEulerSolver : public CFVMFlowSolverBaseGetGoal_Oriented_Metric()) { - const auto nSensor = config->GetnMetric_Sensor(); - for (auto iSensor = 0; iSensor < nSensor; iSensor++) { - switch (config->GetMetric_Sensor(iSensor)) { - case METRIC_SENSOR::DENSITY: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetDensity(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::PRESSURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetPressure(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::TOTAL_PRESSURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double Mach = sqrt(nodes->GetVelocity2(iPoint))/nodes->GetSoundSpeed(iPoint); - const su2double prim_var = nodes->GetPressure(iPoint)*pow((1+0.2*Mach*Mach), 3.5); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::TEMPERATURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetTemperature(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::ENERGY: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetEnergy(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::MACH: - default: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = sqrt(nodes->GetVelocity2(iPoint)) / nodes->GetSoundSpeed(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - } + const auto nSensor = config->GetnMetric_Sensor(); + for (auto iSensor = 0; iSensor < nSensor; iSensor++) { + switch (config->GetMetric_Sensor(iSensor)) { + case METRIC_SENSOR::DENSITY: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetDensity(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::PRESSURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetPressure(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::TOTAL_PRESSURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double Mach = sqrt(nodes->GetVelocity2(iPoint))/nodes->GetSoundSpeed(iPoint); + const su2double prim_var = nodes->GetPressure(iPoint)*pow((1+0.2*Mach*Mach), 3.5); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::TEMPERATURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetTemperature(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::ENERGY: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetEnergy(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::MACH: + default: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = sqrt(nodes->GetVelocity2(iPoint)) / nodes->GetSoundSpeed(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; } } - else { - // TODO: store variables which need Hessian computation for goal-oriented metric - } } /*! diff --git a/SU2_CFD/include/solvers/CIncEulerSolver.hpp b/SU2_CFD/include/solvers/CIncEulerSolver.hpp index c9fe7191d6ff..941f46c2eb52 100644 --- a/SU2_CFD/include/solvers/CIncEulerSolver.hpp +++ b/SU2_CFD/include/solvers/CIncEulerSolver.hpp @@ -92,47 +92,42 @@ class CIncEulerSolver : public CFVMFlowSolverBaseGetGoal_Oriented_Metric()) { - const auto nSensor = config->GetnMetric_Sensor(); - for (auto iSensor = 0; iSensor < nSensor; iSensor++) { - switch (config->GetMetric_Sensor(iSensor)) { - case METRIC_SENSOR::DENSITY: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetDensity(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::TOTAL_PRESSURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetPressure(iPoint) + 0.5*nodes->GetDensity(iPoint)*nodes->GetVelocity2(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::TEMPERATURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetTemperature(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::ENERGY: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetEnergy(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::PRESSURE: - default: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetPressure(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - } + const auto nSensor = config->GetnMetric_Sensor(); + for (auto iSensor = 0; iSensor < nSensor; iSensor++) { + switch (config->GetMetric_Sensor(iSensor)) { + case METRIC_SENSOR::DENSITY: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetDensity(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::TOTAL_PRESSURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetPressure(iPoint) + 0.5*nodes->GetDensity(iPoint)*nodes->GetVelocity2(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::TEMPERATURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetTemperature(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::ENERGY: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetEnergy(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::PRESSURE: + default: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetPressure(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; } } - else { - // TODO: store variables which need Hessian computation for goal-oriented metric - } } /*! diff --git a/SU2_CFD/include/solvers/CNEMOEulerSolver.hpp b/SU2_CFD/include/solvers/CNEMOEulerSolver.hpp index 1f13399554c3..3b0dc0d8389a 100644 --- a/SU2_CFD/include/solvers/CNEMOEulerSolver.hpp +++ b/SU2_CFD/include/solvers/CNEMOEulerSolver.hpp @@ -201,53 +201,48 @@ class CNEMOEulerSolver : public CFVMFlowSolverBaseGetGoal_Oriented_Metric()) { - const auto nSensor = config->GetnMetric_Sensor(); - for (auto iSensor = 0; iSensor < nSensor; iSensor++) { - switch (config->GetMetric_Sensor(iSensor)) { - case METRIC_SENSOR::PRESSURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetPressure(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::TEMPERATURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetTemperature(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::TEMPERATURE_VE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetTemperature_ve(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::ENERGY: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetEnergy(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::ENERGY_VE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = *nodes->GetEve(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::MACH: - default: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = sqrt(nodes->GetVelocity2(iPoint)) / nodes->GetSoundSpeed(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - } + const auto nSensor = config->GetnMetric_Sensor(); + for (auto iSensor = 0; iSensor < nSensor; iSensor++) { + switch (config->GetMetric_Sensor(iSensor)) { + case METRIC_SENSOR::PRESSURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetPressure(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::TEMPERATURE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetTemperature(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::TEMPERATURE_VE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetTemperature_ve(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::ENERGY: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = nodes->GetEnergy(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::ENERGY_VE: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = *nodes->GetEve(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; + case METRIC_SENSOR::MACH: + default: + for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { + const su2double prim_var = sqrt(nodes->GetVelocity2(iPoint)) / nodes->GetSoundSpeed(iPoint); + nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); + } + break; } } - else { - // TODO: store variables which need Hessian computation for goal-oriented metric - } } /*! diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index 16bbfc1ea3b1..2e47e50fdaf0 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -1381,11 +1381,11 @@ void CSolver::GetCommCountAndType(const CConfig* config, MPI_TYPE = COMM_TYPE_DOUBLE; break; case MPI_QUANTITIES::GRADIENT_ADAPT: - COUNT_PER_POINT = config->GetGoal_Oriented_Metric()? nVar*nDim : config->GetnMetric_Sensor()*nDim; + COUNT_PER_POINT = config->GetnMetric_Sensor()*nDim; MPI_TYPE = COMM_TYPE_DOUBLE; break; case MPI_QUANTITIES::HESSIAN: - COUNT_PER_POINT = config->GetGoal_Oriented_Metric()? nVar*nSymMat : config->GetnMetric_Sensor()*nSymMat; + COUNT_PER_POINT = config->GetnMetric_Sensor()*nSymMat; MPI_TYPE = COMM_TYPE_DOUBLE; break; default: @@ -2191,9 +2191,9 @@ void CSolver::SetSolution_Gradient_LS(CGeometry *geometry, const CConfig *config } void CSolver::SetHessian_GG(CGeometry *geometry, const CConfig *config, short idxVel, const unsigned short Kind_Solver) { - const auto& solution = config->GetGoal_Oriented_Metric()? base_nodes->GetSolution() : base_nodes->GetPrimitive_Adapt(); + const auto& solution = base_nodes->GetPrimitive_Adapt(); auto& gradient = base_nodes->GetGradient_Adapt(); - auto nHess = config->GetGoal_Oriented_Metric()? nVar : config->GetnMetric_Sensor(); + auto nHess = config->GetnMetric_Sensor(); computeGradientsGreenGauss(this, MPI_QUANTITIES::GRADIENT_ADAPT, PERIODIC_GRAD_ADAPT, *geometry, *config, solution, 0, nHess, idxVel, gradient); @@ -4391,14 +4391,10 @@ void CSolver::ComputeMetric(CSolver **solver, CGeometry *geometry, const CConfig /*--- TODO: - goal-oriented metric ---*/ /*--- - metric intersection ---*/ const unsigned long nPointDomain = geometry->GetnPointDomain(); + const unsigned short nSensor = config->GetnMetric_Sensor(); - const bool visc = (config->GetViscous()); - const bool turb = (config->GetKind_Turb_Model() != TURB_MODEL::NONE); - - const bool goal = (config->GetGoal_Oriented_Metric()); const bool normalize = (config->GetNormalize_Metric()); - unsigned short nSensor = config->GetnMetric_Sensor(); const unsigned long time_iter = config->GetTimeIter(); const bool steady = (config->GetTime_Marching() == TIME_MARCHING::STEADY); @@ -4452,11 +4448,6 @@ void CSolver::AddMetrics(CSolver **solver, const CGeometry*geometry, const CConf const unsigned long nPointDomain = geometry->GetnPointDomain(); const unsigned short nSymMat = 3*(nDim-1); - const unsigned short nVarFlo = solver[FLOW_SOL]->GetnVar(); - const unsigned short nSensor = config->GetnMetric_Sensor(); - - const bool turb = (config->GetKind_Turb_Model() != TURB_MODEL::NONE); - const bool goal = (config->GetGoal_Oriented_Metric()); const unsigned long time_iter = config->GetTimeIter(); const bool time_stepping = (config->GetTime_Marching() == TIME_MARCHING::DT_STEPPING_1ST) || diff --git a/SU2_CFD/src/variables/CFlowVariable.cpp b/SU2_CFD/src/variables/CFlowVariable.cpp index d9350462ae19..2723543cee21 100644 --- a/SU2_CFD/src/variables/CFlowVariable.cpp +++ b/SU2_CFD/src/variables/CFlowVariable.cpp @@ -99,7 +99,7 @@ CFlowVariable::CFlowVariable(unsigned long npoint, unsigned long ndim, unsigned } if (config->GetCompute_Metric()) { - unsigned short nHess = config->GetGoal_Oriented_Metric()? nVar : config->GetnMetric_Sensor(); + unsigned short nHess = config->GetnMetric_Sensor(); unsigned short nSymMat = 3 * (nDim - 1); Primitive_Adapt.resize(nPoint, nHess) = su2double(0.0); Metric.resize(nPoint, nSymMat) = 0.0; diff --git a/SU2_CFD/src/variables/CVariable.cpp b/SU2_CFD/src/variables/CVariable.cpp index 8e7234b258ca..7c4ba2a70c16 100644 --- a/SU2_CFD/src/variables/CVariable.cpp +++ b/SU2_CFD/src/variables/CVariable.cpp @@ -83,7 +83,7 @@ CVariable::CVariable(unsigned long npoint, unsigned long ndim, unsigned long nva /*--- Gradient and Hessian for anisotropic metric ---*/ if (config->GetCompute_Metric()) { - unsigned short nHess = config->GetGoal_Oriented_Metric()? nVar : config->GetnMetric_Sensor(); + unsigned short nHess = config->GetnMetric_Sensor(); Gradient_Adapt.resize(nPoint,nHess,nDim,0.0); Hessian.resize(nPoint,nHess,nSymMat,0.0); } From 76f7a9d15a911c810894e032f96b5b04fdc88a17 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Tue, 4 Nov 2025 14:41:43 -0800 Subject: [PATCH 11/36] Address CodeQL complaints --- SU2_CFD/include/gradients/computeMetrics.hpp | 5 ++--- SU2_CFD/src/output/CFlowOutput.cpp | 1 - SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/SU2_CFD/include/gradients/computeMetrics.hpp b/SU2_CFD/include/gradients/computeMetrics.hpp index e1b90c17cc81..8917d580973b 100644 --- a/SU2_CFD/include/gradients/computeMetrics.hpp +++ b/SU2_CFD/include/gradients/computeMetrics.hpp @@ -32,6 +32,7 @@ #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" @@ -171,7 +172,7 @@ void setPositiveDefiniteMetrics(CGeometry& geometry, const CConfig& config, /*--- 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 (EigVal[iDim] != EigVal[iDim]) { + if (std::isnan(EigVal[iDim])) { /*--- NaN detected, set to small positive value ---*/ EigVal[iDim] = eps; } else { @@ -268,8 +269,6 @@ void normalizeMetrics(CGeometry& geometry, const CConfig& config, ScalarType A[nDim][nDim], EigVec[nDim][nDim], EigVal[nDim], work[nDim]; for (auto iPoint = 0ul; iPoint < nPointDomain; ++iPoint) { - auto nodes = geometry.nodes; - /*--- Decompose metric ---*/ Tensor::get(metric, iPoint, iSensor, A, nDim); CBlasStructure::EigenDecomposition(A, EigVec, EigVal, nDim, work); diff --git a/SU2_CFD/src/output/CFlowOutput.cpp b/SU2_CFD/src/output/CFlowOutput.cpp index be5647734a0b..f6c7c38735d4 100644 --- a/SU2_CFD/src/output/CFlowOutput.cpp +++ b/SU2_CFD/src/output/CFlowOutput.cpp @@ -4132,7 +4132,6 @@ void CFlowOutput::LoadMeshAdaptationOutputs(const CConfig* config, const CSolver unsigned long iPoint) { const auto* Node_Flow = solver[FLOW_SOL]->GetNodes(); - const auto* Node_Geo = geometry->nodes; if(config->GetCompute_Metric()) { // Common metric components for both 2D and 3D SetVolumeOutputValue("METRIC_XX", iPoint, Node_Flow->GetMetric(iPoint, 0)); diff --git a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp index 14fd3e1a3fa2..da99b870fe4b 100644 --- a/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp +++ b/SU2_CFD/src/output/filewriter/CParaviewXMLFileWriter.cpp @@ -384,7 +384,6 @@ void CParaviewXMLFileWriter::WriteData(string val_filename){ /*--- Resize buffer to accommodate tensor data ---*/ dataBufferFloat.resize(myPoint*NTENSOR); - float val = 0.0; 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 From 6e4c7606ef4f7ac321630f9a86b83641afb08da6 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Tue, 4 Nov 2025 18:15:45 -0800 Subject: [PATCH 12/36] Bug fix --- SU2_CFD/src/solvers/CSolver.cpp | 47 +++++++++++++++------------------ 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index 2e47e50fdaf0..58fc19bd293f 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -4407,33 +4407,28 @@ void CSolver::ComputeMetric(CSolver **solver, CGeometry *geometry, const CConfig vector integrals; for (auto iSensor = 0u; iSensor < nSensor; ++iSensor) { SU2_OMP_MASTER - - if (!goal) { - /*--- Make the Hessian eigenvalues positive definite, and add to the metric tensor ---*/ - auto& hessians = base_nodes->GetHessian(); - setPositiveDefiniteMetrics(*geometry, *config, iSensor, hessians); - AddMetrics(solver, geometry, config, iSensor); - - /*--- 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) { - cout << "Global metric normalization integral for sensor "; - cout << config->GetMetric_SensorString(iSensor) << ": " << integral << endl; - } + /*--- Make the Hessian eigenvalues positive definite, and add to the metric tensor ---*/ + auto& hessians = base_nodes->GetHessian(); + setPositiveDefiniteMetrics(*geometry, *config, iSensor, hessians); + AddMetrics(solver, geometry, config, iSensor); + + /*--- 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) { + cout << "Global metric normalization integral for sensor "; + cout << config->GetMetric_SensorString(iSensor) << ": " << integral << endl; } - } else { - SU2_MPI::Error("Goal-oriented metric not currently implemented.", CURRENT_FUNCTION); } END_SU2_OMP_MASTER SU2_OMP_BARRIER From 1ff633b70ef20b7a74a592488e8ddef7e8a950ed Mon Sep 17 00:00:00 2001 From: bmunguia Date: Wed, 8 Apr 2026 11:51:50 -0700 Subject: [PATCH 13/36] Overhaul Hessian/metric implementation to use CPrimitiveIndices for resolving sensor sol/var indices --- Common/include/CConfig.hpp | 78 ++++---- Common/include/option_structure.hpp | 26 --- Common/src/CConfig.cpp | 51 ++--- SU2_CFD/include/drivers/CSinglezoneDriver.hpp | 3 +- .../{gradients => metrics}/computeMetrics.hpp | 6 +- SU2_CFD/include/metrics/metricUtils.hpp | 176 ++++++++++++++++++ SU2_CFD/include/solvers/CEulerSolver.hpp | 51 ----- SU2_CFD/include/solvers/CIncEulerSolver.hpp | 44 ----- SU2_CFD/include/solvers/CNEMOEulerSolver.hpp | 50 ----- SU2_CFD/include/solvers/CSolver.hpp | 69 ++++++- SU2_CFD/include/variables/CVariable.hpp | 34 +++- SU2_CFD/src/drivers/CSinglezoneDriver.cpp | 49 ++++- SU2_CFD/src/output/CFlowCompOutput.cpp | 2 + SU2_CFD/src/output/CFlowOutput.cpp | 96 +++++++++- SU2_CFD/src/solvers/CSolver.cpp | 70 +++++-- SU2_CFD/src/variables/CFlowVariable.cpp | 2 +- externals/opdi | 2 +- subprojects/MLPCpp | 2 +- 18 files changed, 527 insertions(+), 284 deletions(-) rename SU2_CFD/include/{gradients => metrics}/computeMetrics.hpp (99%) create mode 100644 SU2_CFD/include/metrics/metricUtils.hpp diff --git a/Common/include/CConfig.hpp b/Common/include/CConfig.hpp index 60b970717660..2b26d3ac70f3 100644 --- a/Common/include/CConfig.hpp +++ b/Common/include/CConfig.hpp @@ -1302,9 +1302,11 @@ class CConfig { 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. */ - METRIC_SENSOR* Metric_Sensor; /*!< \brief 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 */ @@ -10272,15 +10274,15 @@ class CConfig { const FluidFlamelet_ParsedOptions& GetFlameletParsedOptions() const { return flamelet_ParsedOptions; } /*! - * \brief Check if a metric tensor field is being computed - * \return TRUE<\code> if a metric tensor field is being computed - */ + * \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; } /*! @@ -10290,54 +10292,37 @@ class CConfig { unsigned short GetKind_Hessian_Method(void) const { return Kind_Hessian_Method; } /*! - * \brief Get adaptation sensor + * \brief Get complete array of metric sensor names + * \return Array of sensor names */ - METRIC_SENSOR GetMetric_Sensor(unsigned short iSens) const { return Metric_Sensor[iSens]; } + string* GetMetric_Sensor() const { + return Metric_Sensor; + } /*! - * \brief Get corresponding string from metric sensor type + * \brief Get metric sensor name by index + * \param[in] iSens - Index of the sensor + * \return Sensor name string */ - string GetMetric_SensorString(unsigned short iSens) const { - string sensor_name; - switch (Metric_Sensor[iSens]) { - case METRIC_SENSOR::DENSITY: - sensor_name = "Density"; - break; - case METRIC_SENSOR::MACH: - sensor_name = "Mach"; - break; - case METRIC_SENSOR::PRESSURE: - sensor_name = "Pressure"; - break; - case METRIC_SENSOR::TOTAL_PRESSURE: - sensor_name = "Total Pressure"; - break; - case METRIC_SENSOR::TEMPERATURE: - sensor_name = "Temperature"; - break; - case METRIC_SENSOR::TEMPERATURE_VE: - sensor_name = "Temperature_ve"; - break; - case METRIC_SENSOR::ENERGY: - sensor_name = "Energy"; - break; - case METRIC_SENSOR::ENERGY_VE: - sensor_name = "Energy_ve"; - break; - case METRIC_SENSOR::GOAL: - sensor_name = "Goal"; - break; - default: - SU2_MPI::Error("Unsupported metric sensor.", CURRENT_FUNCTION); - } + 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]; + } - return sensor_name; + /*! + * \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(void) const { return nMetric_Sensor; } + unsigned short GetnMetric_Sensor() const { return nMetric_Sensor; } /*! * \brief Get adaptation norm value (Lp) @@ -10368,4 +10353,11 @@ class CConfig { */ 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 de0f413da8c7..1d5940ff8420 100644 --- a/Common/include/option_structure.hpp +++ b/Common/include/option_structure.hpp @@ -2851,32 +2851,6 @@ static const MapType Sobolev_Modus_Map = { MakePair("ONLY_GRADIENT", ENUM_SOBOLEV_MODUS::ONLY_GRAD) }; -/*! - * \brief Types of sensors for anisotropic metric - */ -enum class METRIC_SENSOR { - DENSITY, /*!< \brief Density feature-based metric. */ - MACH, /*!< \brief Mach feature-based metric. */ - PRESSURE, /*!< \brief Pressure feature-based metric. */ - TOTAL_PRESSURE, /*!< \brief Total pressure feature-based metric. */ - TEMPERATURE, /*!< \brief Temperature feature-based metric. */ - TEMPERATURE_VE, /*!< \brief Vibrational/electronic temperature feature-based metric. */ - ENERGY, /*!< \brief Energy feature-based metric. */ - ENERGY_VE, /*!< \brief Vibrational/electronic energy feature-based metric. */ - GOAL, /*!< \brief Goal-oriented metric. */ -}; -static const MapType Metric_Sensor_Map = { - MakePair("DENSITY", METRIC_SENSOR::DENSITY) - MakePair("MACH", METRIC_SENSOR::MACH) - MakePair("PRESSURE", METRIC_SENSOR::PRESSURE) - MakePair("TOTAL_PRESSURE", METRIC_SENSOR::TOTAL_PRESSURE) - MakePair("TEMPERATURE", METRIC_SENSOR::TEMPERATURE) - MakePair("TEMPERATURE_VE", METRIC_SENSOR::TEMPERATURE_VE) - MakePair("ENERGY", METRIC_SENSOR::ENERGY) - MakePair("ENERGY_VE", METRIC_SENSOR::ENERGY_VE) - MakePair("GOAL", METRIC_SENSOR::GOAL) -}; - /*! * \brief Type of mesh deformation */ diff --git a/Common/src/CConfig.cpp b/Common/src/CConfig.cpp index 3161e1108341..4e75131200df 100644 --- a/Common/src/CConfig.cpp +++ b/Common/src/CConfig.cpp @@ -3077,19 +3077,22 @@ void CConfig::SetConfig_Options() { /*--- options that are used for mesh adaptation ---*/ /*!\par CONFIG_CATEGORY:Adaptation Options \ingroup Config*/ - /*!\brief COMPUTE_METRIC \n DESCRIPTION: Compute a metric tensor field */ + /*!\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, true); - /*!\brief NUM_METHOD_HESS \n DESCRIPTION: Numerical method for Hessian computation \n OPTIONS: See \link Gradient_Map \endlink. \n DEFAULT: GREEN_GAUSS. \ingroup Config*/ + 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 */ - addEnumListOption("METRIC_SENSOR", nMetric_Sensor, Metric_Sensor, Metric_Sensor_Map); + /*!\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); @@ -3097,6 +3100,8 @@ void CConfig::SetConfig_Options() { 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"); @@ -5850,31 +5855,25 @@ void CConfig::SetPostprocessing(SU2_COMPONENT val_software, unsigned short val_i /*--- Checks for mesh adaptation ---*/ if (Compute_Metric) { /*--- Check that config is valid for requested sensor ---*/ - for (auto iSensor = 0; iSensor < nMetric_Sensor; iSensor++) { - /*--- TODO: If using GOAL, it must be the only sensor and the discrete adjoint must be used ---*/ - /*--- For now, goal-oriented adaptation is unsupported ---*/ - if (Metric_Sensor[iSensor] == METRIC_SENSOR::GOAL) { + 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); } - - if (Kind_Solver == MAIN_SOLVER::NEMO_EULER || Kind_Solver == MAIN_SOLVER::NEMO_NAVIER_STOKES) { - if (Metric_Sensor[iSensor] == METRIC_SENSOR::DENSITY || Metric_Sensor[iSensor] == METRIC_SENSOR::TOTAL_PRESSURE) - SU2_MPI::Error(string("Adaptation sensor ") + GetMetric_SensorString(iSensor) + string(" not available for NEMO problems."), CURRENT_FUNCTION); - } - if (Kind_Solver != MAIN_SOLVER::NEMO_EULER && Kind_Solver != MAIN_SOLVER::NEMO_NAVIER_STOKES) { - if (Metric_Sensor[iSensor] == METRIC_SENSOR::TEMPERATURE_VE || Metric_Sensor[iSensor] == METRIC_SENSOR::ENERGY_VE) - SU2_MPI::Error(string("Adaptation sensor ") + GetMetric_SensorString(iSensor) + string(" not available for non-NEMO problems."), CURRENT_FUNCTION); - } - if (Kind_Solver == MAIN_SOLVER::INC_EULER || Kind_Solver == MAIN_SOLVER::INC_NAVIER_STOKES || Kind_Solver == MAIN_SOLVER::INC_RANS) { - if (Metric_Sensor[iSensor] == METRIC_SENSOR::MACH) - SU2_MPI::Error(string("Adaptation sensor ") + GetMetric_SensorString(iSensor) + string(" not available for INC problems."), 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); } } @@ -8083,8 +8082,8 @@ void CConfig::SetOutput(SU2_COMPONENT val_software, unsigned short val_izone) { if (Compute_Metric) { cout << endl <<"---------------- Mesh Adaptation Information ( Zone " << iZone << " ) -----------------" << endl; cout << "Adaptation sensor(s): "; - for (auto iSensor = 0; iSensor < nMetric_Sensor; iSensor++) { - cout << GetMetric_SensorString(iSensor); + for (unsigned short iSensor = 0; iSensor < nMetric_Sensor; iSensor++) { + cout << Metric_Sensor[iSensor]; if (iSensor < nMetric_Sensor - 1 ) cout << ", "; } cout << endl; @@ -8093,6 +8092,10 @@ void CConfig::SetOutput(SU2_COMPONENT val_software, unsigned short val_izone) { } 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; diff --git a/SU2_CFD/include/drivers/CSinglezoneDriver.hpp b/SU2_CFD/include/drivers/CSinglezoneDriver.hpp index a8812a37b46c..bcda037e0cff 100644 --- a/SU2_CFD/include/drivers/CSinglezoneDriver.hpp +++ b/SU2_CFD/include/drivers/CSinglezoneDriver.hpp @@ -112,7 +112,8 @@ class CSinglezoneDriver : public CDriver { /*! * \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(); + virtual void ComputeMetricField(bool restartMetric = false); }; diff --git a/SU2_CFD/include/gradients/computeMetrics.hpp b/SU2_CFD/include/metrics/computeMetrics.hpp similarity index 99% rename from SU2_CFD/include/gradients/computeMetrics.hpp rename to SU2_CFD/include/metrics/computeMetrics.hpp index 8917d580973b..e8070156764d 100644 --- a/SU2_CFD/include/gradients/computeMetrics.hpp +++ b/SU2_CFD/include/metrics/computeMetrics.hpp @@ -4,7 +4,7 @@ * \note This allows the same implementation to be used for goal-oriented * or feature-based mesh adaptation. * \author B. Munguía - * \version 8.3.0 "Harrier" + * \version 8.4.0 "Harrier" * * SU2 Project Website: https://su2code.github.io * @@ -160,7 +160,7 @@ void setPositiveDefiniteMetrics(CGeometry& geometry, const CConfig& config, ScalarType A[nDim][nDim], EigVec[nDim][nDim], EigVal[nDim], work[nDim]; /*--- Minimum eigenvalue threshold ---*/ - const ScalarType eps = 1e-12; + const ScalarType eps = 1e-20; for (auto iPoint = 0ul; iPoint < nPointDomain; ++iPoint) { /*--- Get full metric tensor ---*/ @@ -255,7 +255,7 @@ void normalizeMetrics(CGeometry& geometry, const CConfig& config, /*--- Constants defining normalization ---*/ const ScalarType p = config.GetMetric_Norm(); - const ScalarType N = SU2_TYPE::GetValue(config.GetMetric_Complexity()); + 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); diff --git a/SU2_CFD/include/metrics/metricUtils.hpp b/SU2_CFD/include/metrics/metricUtils.hpp new file mode 100644 index 000000000000..1f1761c1e901 --- /dev/null +++ b/SU2_CFD/include/metrics/metricUtils.hpp @@ -0,0 +1,176 @@ +/*! + * \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 "../../../Common/include/CConfig.hpp" +#include "../solvers/CSolver.hpp" +#include "../variables/CPrimitiveIndices.hpp" +#include "../../../Common/include/parallelization/mpi_structure.hpp" + +namespace MetricUtils { + +/*! + * \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 indices in each solver via SetMetricSensorIndices(). + * + * \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 ---*/ + std::map> sensor_indices_by_solver; + std::map> sensor_names_by_solver; + + /*--- Build a map of all available variables across all solvers ---*/ + std::map> var_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& entry : primitive_map) { + var_map[entry.first] = std::make_pair(iSol, entry.second); + } + } 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 and group by solver ---*/ + bool all_resolved = true; + for (const auto& sensor_name : sensor_names) { + auto it = var_map.find(sensor_name); + if (it != var_map.end()) { + const unsigned short iSol = it->second.first; + const unsigned short var_index = it->second.second; + + sensor_indices_by_solver[iSol].push_back(var_index); + sensor_names_by_solver[iSol].push_back(sensor_name); + + if (rank == MASTER_NODE) { + std::cout << " Resolved sensor '" << sensor_name << "' to solver " + << iSol << ", variable index " << var_index << std::endl; + } + } else { + if (rank == MASTER_NODE) { + std::cerr << "Warning: Sensor '" << sensor_name << "' not found in any solver" << std::endl; + } + all_resolved = false; + } + } + + /*--- Set sensor indices directly in each solver ---*/ + for (const auto& entry : sensor_indices_by_solver) { + const unsigned short iSol = entry.first; + solver_container[iSol]->SetMetricSensorIndices(sensor_indices_by_solver[iSol]); + solver_container[iSol]->SetMetricSensorNames(sensor_names_by_solver[iSol]); + } + + /*--- Return true if at least one sensor was resolved ---*/ + const bool any_resolved = !sensor_indices_by_solver.empty(); + if (!all_resolved && rank == MASTER_NODE) { + std::cerr << "Warning: Some sensors could not be resolved (see above). Proceeding with resolved sensors." << std::endl; + } + return any_resolved; +} + +/*! + * \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& indices = solver_container[iSol]->GetMetricSensorIndices(); + if (!indices.empty()) { + solver_container[iSol]->AllocateMetricSensorArrays(indices); + } + } +} + +} // namespace MetricUtils diff --git a/SU2_CFD/include/solvers/CEulerSolver.hpp b/SU2_CFD/include/solvers/CEulerSolver.hpp index 8672323c8e7e..0ad143edaffc 100644 --- a/SU2_CFD/include/solvers/CEulerSolver.hpp +++ b/SU2_CFD/include/solvers/CEulerSolver.hpp @@ -273,57 +273,6 @@ class CEulerSolver : public CFVMFlowSolverBaseGetnMetric_Sensor(); - for (auto iSensor = 0; iSensor < nSensor; iSensor++) { - switch (config->GetMetric_Sensor(iSensor)) { - case METRIC_SENSOR::DENSITY: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetDensity(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::PRESSURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetPressure(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::TOTAL_PRESSURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double Mach = sqrt(nodes->GetVelocity2(iPoint))/nodes->GetSoundSpeed(iPoint); - const su2double prim_var = nodes->GetPressure(iPoint)*pow((1+0.2*Mach*Mach), 3.5); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::TEMPERATURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetTemperature(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::ENERGY: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetEnergy(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::MACH: - default: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = sqrt(nodes->GetVelocity2(iPoint)) / nodes->GetSoundSpeed(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - } - } - } - /*! * \brief Set gradients of coefficients for fixed CL mode * \param[in] config - Definition of the particular problem. diff --git a/SU2_CFD/include/solvers/CIncEulerSolver.hpp b/SU2_CFD/include/solvers/CIncEulerSolver.hpp index f606cfde5ec4..a4185a5a6e77 100644 --- a/SU2_CFD/include/solvers/CIncEulerSolver.hpp +++ b/SU2_CFD/include/solvers/CIncEulerSolver.hpp @@ -86,50 +86,6 @@ class CIncEulerSolver : public CFVMFlowSolverBaseGetnMetric_Sensor(); - for (auto iSensor = 0; iSensor < nSensor; iSensor++) { - switch (config->GetMetric_Sensor(iSensor)) { - case METRIC_SENSOR::DENSITY: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetDensity(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::TOTAL_PRESSURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetPressure(iPoint) + 0.5*nodes->GetDensity(iPoint)*nodes->GetVelocity2(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::TEMPERATURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetTemperature(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::ENERGY: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetEnergy(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::PRESSURE: - default: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetPressure(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - } - } - } - /*! * \brief Update the Beta parameter for the incompressible preconditioner. * \param[in] geometry - Geometrical definition of the problem. diff --git a/SU2_CFD/include/solvers/CNEMOEulerSolver.hpp b/SU2_CFD/include/solvers/CNEMOEulerSolver.hpp index b166b26d9e3e..75bd963f7563 100644 --- a/SU2_CFD/include/solvers/CNEMOEulerSolver.hpp +++ b/SU2_CFD/include/solvers/CNEMOEulerSolver.hpp @@ -192,56 +192,6 @@ class CNEMOEulerSolver : public CFVMFlowSolverBaseGetnMetric_Sensor(); - for (auto iSensor = 0; iSensor < nSensor; iSensor++) { - switch (config->GetMetric_Sensor(iSensor)) { - case METRIC_SENSOR::PRESSURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetPressure(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::TEMPERATURE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetTemperature(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::TEMPERATURE_VE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetTemperature_ve(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::ENERGY: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = nodes->GetEnergy(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::ENERGY_VE: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = *nodes->GetEve(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - case METRIC_SENSOR::MACH: - default: - for (auto iPoint = 0ul; iPoint < nPoint; iPoint++) { - const su2double prim_var = sqrt(nodes->GetVelocity2(iPoint)) / nodes->GetSoundSpeed(iPoint); - nodes->SetPrimitive_Adapt(iPoint, iSensor, prim_var); - } - break; - } - } - } - /*! * \brief Compute a suitable under-relaxation parameter to limit the change in the solution variables over * a nonlinear iteration for stability. diff --git a/SU2_CFD/include/solvers/CSolver.hpp b/SU2_CFD/include/solvers/CSolver.hpp index a6418d3d0529..99408d54c966 100644 --- a/SU2_CFD/include/solvers/CSolver.hpp +++ b/SU2_CFD/include/solvers/CSolver.hpp @@ -208,6 +208,11 @@ class CSolver { vector fields; + /*--- Metric sensor indices for mesh adaptation ---*/ + vector MetricSensorIndices; /*!< \brief Variable indices for metric sensors in this solver. */ + vector MetricSensorNames; /*!< \brief Names of metric sensors in this solver. */ + + #ifdef HAVE_LIBROM std::unique_ptr u_basis_generator; #endif @@ -581,12 +586,25 @@ class CSolver { */ inline virtual void SetPrimitive_Limiter(CGeometry *geometry, const CConfig *config) { } - /*! - * \brief A virtual member. + /*! + * \brief Set primitive variables for adaptation using resolved sensor locations. * \param[in] geometry - Geometrical definition of the problem. * \param[in] config - Definition of the particular problem. */ - virtual void SetPrimitive_Adapt(CGeometry *geometry, const CConfig *config) { } + virtual void SetPrimitive_Adapt(CGeometry *geometry, const CConfig *config); + + /*! + * \brief Set solution variables for adaptation using resolved sensor locations. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] config - Definition of the particular problem. + */ + virtual void SetSolution_Adapt(CGeometry *geometry, const CConfig *config); + + /*! + * \brief Allocate Gradient_Adapt and Hessian arrays for specified sensor variables. + * \param[in] sensor_indices - Vector of variable indices for this solver to allocate arrays for + */ + virtual void AllocateMetricSensorArrays(const vector& sensor_indices); /*! * \brief Compute the Green-Gauss Hessian of the solution. @@ -4274,6 +4292,41 @@ 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(MetricSensorIndices.size()); } + + /*! + * \brief Get the metric sensor variable indices for this solver. + * \return Vector of variable indices used as metric sensors. + */ + inline const vector& GetMetricSensorIndices() const { return MetricSensorIndices; } + + /*! + * \brief Get the metric sensor names for this solver. + * \return Vector of sensor names. + */ + inline const vector& GetMetricSensorNames() const { return MetricSensorNames; } + + /*! + * \brief Set the metric sensor indices for this solver. + * \param[in] indices - Variable indices for metric sensors. + * \param[in] names - Names of metric sensors. + */ + inline void SetMetricSensorIndices(const vector& indices) { + MetricSensorIndices = indices; + } + + /*! + * \brief Set the metric sensor names for this solver. + * \param[in] names - Names of metric sensors. + */ + inline void SetMetricSensorNames(const vector& names) { + MetricSensorNames = names; + } + /*! * \brief A virtual member. * \param[in] geometry - Geometrical definition. @@ -4408,22 +4461,24 @@ class CSolver { } /*! - * \brief Compute the metric tensor field. + * \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); + void ComputeMetric(CSolver **solver, CGeometry *geometry, const CConfig *config, bool restartMetric); /*! - * \brief Add contribution to the metric field. + * \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); + const unsigned short iSensor, bool restartMetric); protected: /*! diff --git a/SU2_CFD/include/variables/CVariable.hpp b/SU2_CFD/include/variables/CVariable.hpp index bb5c724dc551..707de335e5d6 100644 --- a/SU2_CFD/include/variables/CVariable.hpp +++ b/SU2_CFD/include/variables/CVariable.hpp @@ -103,7 +103,7 @@ class CVariable { VectorType SolutionExtra_BGS_k; /*!< \brief Intermediate storage, enables cross term extraction as that is also pushed to Solution. */ - MatrixType Primitive_Adapt; /*!< \brief Variables for which we need gradients for anisotropy in mesh adaptation. */ + 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. */ @@ -2384,20 +2384,20 @@ class CVariable { * \param[in] iPoint - Point index. * \param[in] gradient - Gradient of the solution. */ - inline void SetPrimitive_Adapt(unsigned long iPoint, unsigned long iVar, su2double primitive) { Primitive_Adapt(iPoint,iVar) = primitive; } + 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& GetPrimitive_Adapt(void) const { return Primitive_Adapt; } + 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 *GetPrimitive_Adapt(unsigned long iPoint) { return Primitive_Adapt[iPoint]; } + inline su2double *GetSensor_Adapt(unsigned long iPoint) { return Sensor_Adapt[iPoint]; } /*! * \brief Get the value of the solution gradient. @@ -2405,7 +2405,7 @@ class CVariable { * \param[in] iVar - Variable index. * \return Value of the solution gradient. */ - inline su2double GetPrimitive_Adapt(unsigned long iPoint, unsigned long iVar) const { return Primitive_Adapt(iPoint,iVar); } + inline su2double GetSensor_Adapt(unsigned long iPoint, unsigned long iVar) const { return Sensor_Adapt(iPoint,iVar); } /*! * \brief Set the gradient of the solution. @@ -2489,4 +2489,28 @@ class CVariable { * \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/CSinglezoneDriver.cpp b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp index 4cf4519af7f6..f04d54814f15 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, @@ -49,6 +50,39 @@ void CSinglezoneDriver::StartSolver() { config_container[ZONE_0]->Set_StartTime(StartTime); + /*--- Resolve and allocate metric arrays if metric computation is enabled ---*/ + if (config_container[ZONE_0]->GetCompute_Metric()) { + /*--- Resolve sensor indices from sensor names and store in solvers ---*/ + 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) { + /*--- Allocate metric sensor arrays ---*/ + MetricUtils::InitializeMetrics(solver_container[ZONE_0][INST_0][MESH_0]); + + /*--- Count total sensors for reporting ---*/ + unsigned long total_sensors = 0; + for (unsigned short iSol = 0; iSol < MAX_SOLS; iSol++) { + if (solver_container[ZONE_0][INST_0][MESH_0][iSol] != nullptr) { + total_sensors += solver_container[ZONE_0][INST_0][MESH_0][iSol]->GetMetricSensorIndices().size(); + } + } + + if (rank == MASTER_NODE && total_sensors > 0) { + cout << "Successfully resolved " << total_sensors << " metric sensors." << endl; + } + } else if (rank == MASTER_NODE) { + cout << "Warning: COMPUTE_METRIC is enabled but no valid sensors found." << endl; + } + } + /*--- Main external loop of the solver. Runs for the number of time steps required. ---*/ if (rank == MASTER_NODE) @@ -140,6 +174,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, @@ -318,7 +363,7 @@ bool CSinglezoneDriver::GetTimeConvergence() const{ return output_container[ZONE_0]->GetCauchyCorrectedTimeConvergence(config_container[ZONE_0]); } -void CSinglezoneDriver::ComputeMetricField() { +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]; @@ -345,5 +390,5 @@ void CSinglezoneDriver::ComputeMetricField() { } if(rank == MASTER_NODE) cout << "Computing feature-based metric tensor." << endl; - solver_flow->ComputeMetric(solver, geometry, config); + solver_flow->ComputeMetric(solver, geometry, config, restartMetric); } diff --git a/SU2_CFD/src/output/CFlowCompOutput.cpp b/SU2_CFD/src/output/CFlowCompOutput.cpp index 051cf36a7a38..32e9e42cc4a6 100644 --- a/SU2_CFD/src/output/CFlowCompOutput.cpp +++ b/SU2_CFD/src/output/CFlowCompOutput.cpp @@ -397,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/CFlowOutput.cpp b/SU2_CFD/src/output/CFlowOutput.cpp index 5a462be1560b..a955a1a8b217 100644 --- a/SU2_CFD/src/output/CFlowOutput.cpp +++ b/SU2_CFD/src/output/CFlowOutput.cpp @@ -4179,19 +4179,60 @@ void CFlowOutput::AddTurboOutput(unsigned short nZone){ void CFlowOutput::AddMeshAdaptationOutputs(const CConfig* config) { - // Anisotropic metric tensor, and dual-cell volume + 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()) { - // Common metric components for both 2D and 3D - AddVolumeOutput("METRIC_XX", "Metric_xx", "MESH_ADAPT", "x-x-component of the metric"); - AddVolumeOutput("METRIC_XY", "Metric_xy", "MESH_ADAPT", "x-y-component of the metric"); - AddVolumeOutput("METRIC_YY", "Metric_yy", "MESH_ADAPT", "y-y-component of the metric"); + // 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", "MESH_ADAPT", "x-z-component of the metric"); - AddVolumeOutput("METRIC_YZ", "Metric_yz", "MESH_ADAPT", "y-z-component of the metric"); - AddVolumeOutput("METRIC_ZZ", "Metric_zz", "MESH_ADAPT", "z-z-component of the metric"); + 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"); } } @@ -4199,8 +4240,40 @@ void CFlowOutput::LoadMeshAdaptationOutputs(const CConfig* config, const CSolver unsigned long iPoint) { const auto* Node_Flow = solver[FLOW_SOL]->GetNodes(); + const auto* Node_Geo = geometry->nodes; if(config->GetCompute_Metric()) { - // Common metric components for both 2D and 3D + const auto& flow_sensor_names = solver[FLOW_SOL]->GetMetricSensorNames(); + const auto nFlowSensors = solver[FLOW_SOL]->GetnMetricSensor(); + + // Gradients + for (auto iSensor = 0u; iSensor < nFlowSensors; iSensor++){ + const string& sens_str = flow_sensor_names[iSensor]; + // 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 < nFlowSensors; iSensor++){ + const string& sens_str = flow_sensor_names[iSensor]; + // 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)); @@ -4211,6 +4284,9 @@ void CFlowOutput::LoadMeshAdaptationOutputs(const CConfig* config, const CSolver 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/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index d01b6cdf4c07..cb5bbc776b59 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -29,8 +29,8 @@ #include "../../include/solvers/CSolver.hpp" #include "../../include/gradients/computeGradientsGreenGauss.hpp" #include "../../include/gradients/computeGradientsLeastSquares.hpp" -#include "../../include/gradients/computeMetrics.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" @@ -1387,11 +1387,11 @@ void CSolver::GetCommCountAndType(const CConfig* config, MPI_TYPE = COMM_TYPE_DOUBLE; break; case MPI_QUANTITIES::GRADIENT_ADAPT: - COUNT_PER_POINT = config->GetnMetric_Sensor()*nDim; + COUNT_PER_POINT = GetnMetricSensor()*nDim; MPI_TYPE = COMM_TYPE_DOUBLE; break; case MPI_QUANTITIES::HESSIAN: - COUNT_PER_POINT = config->GetnMetric_Sensor()*nSymMat; + COUNT_PER_POINT = GetnMetricSensor()*nSymMat; MPI_TYPE = COMM_TYPE_DOUBLE; break; default: @@ -2192,10 +2192,47 @@ 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(const vector& sensor_indices) { + if (base_nodes == nullptr || sensor_indices.empty()) return; + base_nodes->AllocateMetricSensorArrays(sensor_indices.size()); +} + +void CSolver::SetPrimitive_Adapt(CGeometry *geometry, const CConfig *config) { + const auto nSensors = MetricSensorIndices.size(); + + /*--- Copy each resolved sensor variable into Sensor_Adapt ---*/ + for (size_t iSensor = 0; iSensor < nSensors; iSensor++) { + const auto var_idx = MetricSensorIndices[iSensor]; + + SU2_OMP_FOR_STAT(omp_chunk_size) + for (unsigned long iPoint = 0; iPoint < nPoint; iPoint++) { + const su2double prim_var = base_nodes->GetPrimitive(iPoint, var_idx); + base_nodes->SetSensor_Adapt(iPoint, iSensor, prim_var); + } + END_SU2_OMP_FOR + } +} + +void CSolver::SetSolution_Adapt(CGeometry *geometry, const CConfig *config) { + const auto nSensors = MetricSensorIndices.size(); + + /*--- Copy each resolved sensor variable into Sensor_Adapt ---*/ + for (size_t iSensor = 0; iSensor < nSensors; iSensor++) { + const auto var_idx = MetricSensorIndices[iSensor]; + + SU2_OMP_FOR_STAT(omp_chunk_size) + for (unsigned long iPoint = 0; iPoint < nPoint; iPoint++) { + const su2double prim_var = base_nodes->GetSolution(iPoint, var_idx); + base_nodes->SetSensor_Adapt(iPoint, iSensor, prim_var); + } + 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->GetPrimitive_Adapt(); + const auto& solution = base_nodes->GetSensor_Adapt(); auto& gradient = base_nodes->GetGradient_Adapt(); - auto nHess = config->GetnMetric_Sensor(); + auto nHess = GetnMetricSensor(); computeGradientsGreenGauss(this, MPI_QUANTITIES::GRADIENT_ADAPT, PERIODIC_GRAD_ADAPT, *geometry, *config, solution, 0, nHess, idxVel, gradient); @@ -4389,14 +4426,11 @@ void CSolver::SavelibROM(CGeometry *geometry, CConfig *config, bool converged) { } -void CSolver::ComputeMetric(CSolver **solver, CGeometry *geometry, const CConfig *config) { +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 = config->GetnMetric_Sensor(); - - const bool normalize = (config->GetNormalize_Metric()); - + const unsigned short nSensor = GetnMetricSensor(); const unsigned long time_iter = config->GetTimeIter(); const bool steady = (config->GetTime_Marching() == TIME_MARCHING::STEADY); @@ -4404,15 +4438,20 @@ void CSolver::ComputeMetric(CSolver **solver, CGeometry *geometry, const CConfig (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, and add to the metric tensor ---*/ + /*--- Make the Hessian eigenvalues positive definite ---*/ auto& hessians = base_nodes->GetHessian(); setPositiveDefiniteMetrics(*geometry, *config, iSensor, hessians); - AddMetrics(solver, geometry, config, iSensor); + + 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(); @@ -4428,8 +4467,9 @@ void CSolver::ComputeMetric(CSolver **solver, CGeometry *geometry, const CConfig if (is_last_iter) { integrals.push_back(integral); if (rank == MASTER_NODE) { + const string& sensor_name = (iSensor < MetricSensorNames.size()) ? MetricSensorNames[iSensor] : "unknown"; cout << "Global metric normalization integral for sensor "; - cout << config->GetMetric_SensorString(iSensor) << ": " << integral << endl; + cout << sensor_name << ": " << integral << endl; } } END_SU2_OMP_MASTER @@ -4438,7 +4478,7 @@ void CSolver::ComputeMetric(CSolver **solver, CGeometry *geometry, const CConfig } void CSolver::AddMetrics(CSolver **solver, const CGeometry*geometry, const CConfig *config, - const unsigned short iSensor) { + const unsigned short iSensor, bool restartMetric) { /*--- TODO: - goal-oriented metric ---*/ /*--- - metric intersection ---*/ auto varFlo = solver[FLOW_SOL]->GetNodes(); @@ -4450,7 +4490,7 @@ void CSolver::AddMetrics(CSolver **solver, const CGeometry*geometry, const CConf 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); + 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; diff --git a/SU2_CFD/src/variables/CFlowVariable.cpp b/SU2_CFD/src/variables/CFlowVariable.cpp index cb634fbad879..7558929efd85 100644 --- a/SU2_CFD/src/variables/CFlowVariable.cpp +++ b/SU2_CFD/src/variables/CFlowVariable.cpp @@ -101,7 +101,7 @@ CFlowVariable::CFlowVariable(unsigned long npoint, unsigned long ndim, unsigned if (config->GetCompute_Metric()) { unsigned short nHess = config->GetnMetric_Sensor(); unsigned short nSymMat = 3 * (nDim - 1); - Primitive_Adapt.resize(nPoint, nHess) = su2double(0.0); + Sensor_Adapt.resize(nPoint, nHess) = su2double(0.0); Metric.resize(nPoint, nSymMat) = 0.0; } } diff --git a/externals/opdi b/externals/opdi index 294807b0111c..a5e2ac47035b 160000 --- a/externals/opdi +++ b/externals/opdi @@ -1 +1 @@ -Subproject commit 294807b0111ce241cda97db62f80cdd5012d9381 +Subproject commit a5e2ac47035b6b3663f60d5f80b7a9fe62084867 diff --git a/subprojects/MLPCpp b/subprojects/MLPCpp index 02f2cb9dde79..e19ca0cafb28 160000 --- a/subprojects/MLPCpp +++ b/subprojects/MLPCpp @@ -1 +1 @@ -Subproject commit 02f2cb9dde791074858e11ac091f7c4df9c6af65 +Subproject commit e19ca0cafb28c4b7ba5b8cffef42883259b00dc0 From 09b332cd88b6ec617054406d54dee84bcfd4d0c3 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Wed, 8 Apr 2026 12:53:07 -0700 Subject: [PATCH 14/36] Helpers for number of resolved sensors --- SU2_CFD/include/metrics/metricUtils.hpp | 15 +++++++++++++++ SU2_CFD/src/drivers/CSinglezoneDriver.cpp | 15 ++++----------- SU2_CFD/src/output/CFlowOutput.cpp | 12 ++++++------ SU2_CFD/src/solvers/CSolver.cpp | 4 ++-- 4 files changed, 27 insertions(+), 19 deletions(-) diff --git a/SU2_CFD/include/metrics/metricUtils.hpp b/SU2_CFD/include/metrics/metricUtils.hpp index 1f1761c1e901..1ae22e8958d8 100644 --- a/SU2_CFD/include/metrics/metricUtils.hpp +++ b/SU2_CFD/include/metrics/metricUtils.hpp @@ -173,4 +173,19 @@ inline void InitializeMetrics(CSolver** solver_container) { } } +/*! + * \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]->GetMetricSensorIndices().size(); + } + } + return num_sensor; +} + } // namespace MetricUtils diff --git a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp index f04d54814f15..e9bc2282925c 100644 --- a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp +++ b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp @@ -50,7 +50,7 @@ void CSinglezoneDriver::StartSolver() { config_container[ZONE_0]->Set_StartTime(StartTime); - /*--- Resolve and allocate metric arrays if metric computation is enabled ---*/ + /*--- Resolve and allocate metric arrays if metric computation is enabled ---*/ if (config_container[ZONE_0]->GetCompute_Metric()) { /*--- Resolve sensor indices from sensor names and store in solvers ---*/ if (rank == MASTER_NODE) { @@ -66,17 +66,10 @@ void CSinglezoneDriver::StartSolver() { if (resolved) { /*--- Allocate metric sensor arrays ---*/ 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]); - /*--- Count total sensors for reporting ---*/ - unsigned long total_sensors = 0; - for (unsigned short iSol = 0; iSol < MAX_SOLS; iSol++) { - if (solver_container[ZONE_0][INST_0][MESH_0][iSol] != nullptr) { - total_sensors += solver_container[ZONE_0][INST_0][MESH_0][iSol]->GetMetricSensorIndices().size(); - } - } - - if (rank == MASTER_NODE && total_sensors > 0) { - cout << "Successfully resolved " << total_sensors << " metric sensors." << endl; + 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; diff --git a/SU2_CFD/src/output/CFlowOutput.cpp b/SU2_CFD/src/output/CFlowOutput.cpp index a955a1a8b217..14ebacce663c 100644 --- a/SU2_CFD/src/output/CFlowOutput.cpp +++ b/SU2_CFD/src/output/CFlowOutput.cpp @@ -4242,12 +4242,12 @@ void CFlowOutput::LoadMeshAdaptationOutputs(const CConfig* config, const CSolver const auto* Node_Flow = solver[FLOW_SOL]->GetNodes(); const auto* Node_Geo = geometry->nodes; if(config->GetCompute_Metric()) { - const auto& flow_sensor_names = solver[FLOW_SOL]->GetMetricSensorNames(); - const auto nFlowSensors = solver[FLOW_SOL]->GetnMetricSensor(); + const auto& Sensor_Names = solver[FLOW_SOL]->GetMetricSensorNames(); + const auto nSensor = solver[FLOW_SOL]->GetnMetricSensor(); // Gradients - for (auto iSensor = 0u; iSensor < nFlowSensors; iSensor++){ - const string& sens_str = flow_sensor_names[iSensor]; + for (auto iSensor = 0u; iSensor < nSensor; iSensor++){ + const string& sens_str = Sensor_Names[iSensor]; // 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)); @@ -4258,8 +4258,8 @@ void CFlowOutput::LoadMeshAdaptationOutputs(const CConfig* config, const CSolver } // Hessians - for (auto iSensor = 0u; iSensor < nFlowSensors; iSensor++){ - const string& sens_str = flow_sensor_names[iSensor]; + for (auto iSensor = 0u; iSensor < nSensor; iSensor++){ + const string& sens_str = Sensor_Names[iSensor]; // 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)); diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index cb5bbc776b59..6f91582627ac 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -2198,7 +2198,7 @@ void CSolver::AllocateMetricSensorArrays(const vector& sensor_in } void CSolver::SetPrimitive_Adapt(CGeometry *geometry, const CConfig *config) { - const auto nSensors = MetricSensorIndices.size(); + const auto nSensors = GetnMetricSensor(); /*--- Copy each resolved sensor variable into Sensor_Adapt ---*/ for (size_t iSensor = 0; iSensor < nSensors; iSensor++) { @@ -2214,7 +2214,7 @@ void CSolver::SetPrimitive_Adapt(CGeometry *geometry, const CConfig *config) { } void CSolver::SetSolution_Adapt(CGeometry *geometry, const CConfig *config) { - const auto nSensors = MetricSensorIndices.size(); + const auto nSensors = GetnMetricSensor(); /*--- Copy each resolved sensor variable into Sensor_Adapt ---*/ for (size_t iSensor = 0; iSensor < nSensors; iSensor++) { From e83414eadaeae764e2ab9a06cc22cd18ce724237 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Wed, 8 Apr 2026 15:48:52 -0700 Subject: [PATCH 15/36] Add python wrapper support for custom metric sensors, and example --- SU2_CFD/include/drivers/CDriverBase.hpp | 24 +++++ SU2_CFD/include/metrics/metricUtils.hpp | 20 +++- SU2_CFD/src/drivers/CDriverBase.cpp | 33 ++++++ SU2_CFD/src/drivers/CSinglezoneDriver.cpp | 31 +++--- SU2_CFD/src/output/CFlowOutput.cpp | 13 +++ SU2_CFD/src/solvers/CSolver.cpp | 5 +- SU2_PY/SU2/__init__.py | 1 + SU2_PY/SU2/metric/__init__.py | 1 + SU2_PY/SU2/metric/resolver.py | 91 ++++++++++++++++ SU2_PY/meson.build | 4 + TestCases/mesh_adaptation/metric.py | 48 +++++++++ TestCases/mesh_adaptation/rans_naca0012.cfg | 111 ++++++++++++++++++++ 12 files changed, 359 insertions(+), 23 deletions(-) create mode 100644 SU2_PY/SU2/metric/__init__.py create mode 100644 SU2_PY/SU2/metric/resolver.py create mode 100644 TestCases/mesh_adaptation/metric.py create mode 100644 TestCases/mesh_adaptation/rans_naca0012.cfg diff --git a/SU2_CFD/include/drivers/CDriverBase.hpp b/SU2_CFD/include/drivers/CDriverBase.hpp index 1a4101bee956..8362433b9545 100644 --- a/SU2_CFD/include/drivers/CDriverBase.hpp +++ b/SU2_CFD/include/drivers/CDriverBase.hpp @@ -515,6 +515,30 @@ 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 Set the value of a metric sensor for a single node. + * \param[in] iPoint - Node index. + * \param[in] iSensor - Local sensor index (from GetMetricSensorIndex). + * \param[in] value - Sensor value at this node. + */ + void SetSensorAdapt(unsigned long iPoint, unsigned short iSensor, passivedouble value); + + /*! + * \brief Get the value of a metric sensor at a single node. + * \param[in] iPoint - Node index. + * \param[in] iSensor - Local sensor index (from GetMetricSensorIndex). + * \return Sensor value at this node. + */ + passivedouble GetSensorAdapt(unsigned long iPoint, unsigned short iSensor) const; + /*! * \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/metrics/metricUtils.hpp b/SU2_CFD/include/metrics/metricUtils.hpp index 1ae22e8958d8..f015a27275d5 100644 --- a/SU2_CFD/include/metrics/metricUtils.hpp +++ b/SU2_CFD/include/metrics/metricUtils.hpp @@ -116,6 +116,7 @@ inline bool ResolveSensorIndices( /*--- Resolve each sensor and group by solver ---*/ bool all_resolved = true; + std::vector unresolved; for (const auto& sensor_name : sensor_names) { auto it = var_map.find(sensor_name); if (it != var_map.end()) { @@ -131,9 +132,21 @@ inline bool ResolveSensorIndices( } } else { if (rank == MASTER_NODE) { - std::cerr << "Warning: Sensor '" << sensor_name << "' not found in any solver" << std::endl; + std::cout << " Custom sensor '" << sensor_name << "' detected." << std::endl; } all_resolved = false; + unresolved.push_back(sensor_name); + } + } + + /*--- Assign unresolved sensors to FLOW_SOL with USHRT_MAX as a placeholder index. + * These slots are skipped by SetPrimitive_Adapt and must be filled externally + * (e.g. via CDriverBase::SetSensorAdapt from the Python wrapper). ---*/ + if (!unresolved.empty()) { + unsigned short flow_sol = FLOW_SOL; + for (const auto& name : unresolved) { + sensor_names_by_solver[flow_sol].push_back(name); + sensor_indices_by_solver[flow_sol].push_back(std::numeric_limits::max()); } } @@ -144,11 +157,8 @@ inline bool ResolveSensorIndices( solver_container[iSol]->SetMetricSensorNames(sensor_names_by_solver[iSol]); } - /*--- Return true if at least one sensor was resolved ---*/ + /*--- Return true if at least one sensor was resolved or reserved ---*/ const bool any_resolved = !sensor_indices_by_solver.empty(); - if (!all_resolved && rank == MASTER_NODE) { - std::cerr << "Warning: Some sensors could not be resolved (see above). Proceeding with resolved sensors." << std::endl; - } return any_resolved; } diff --git a/SU2_CFD/src/drivers/CDriverBase.cpp b/SU2_CFD/src/drivers/CDriverBase.cpp index 00f604669040..b4613b47b5fd 100644 --- a/SU2_CFD/src/drivers/CDriverBase.cpp +++ b/SU2_CFD/src/drivers/CDriverBase.cpp @@ -436,3 +436,36 @@ 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& names = flow_solver->GetMetricSensorNames(); + auto it = std::find(names.begin(), names.end(), sensor_name); + if (it == names.end()) return -1; + return static_cast(it - names.begin()); +} + +void CDriverBase::SetSensorAdapt(unsigned long iPoint, unsigned short iSensor, passivedouble value) { + auto* flow_solver = solver_container[selected_zone][INST_0][MESH_0][FLOW_SOL]; + if (flow_solver == nullptr) + SU2_MPI::Error("Flow solver does not exist.", CURRENT_FUNCTION); + if (iSensor >= flow_solver->GetnMetricSensor()) + SU2_MPI::Error("Sensor index " + to_string(iSensor) + " out of range.", CURRENT_FUNCTION); + if (iPoint >= main_geometry->GetnPoint()) + SU2_MPI::Error("Node index " + to_string(iPoint) + " out of range.", CURRENT_FUNCTION); + + flow_solver->GetNodes()->SetSensor_Adapt(iPoint, iSensor, value); +} + +passivedouble CDriverBase::GetSensorAdapt(unsigned long iPoint, unsigned short iSensor) const { + auto* flow_solver = solver_container[selected_zone][INST_0][MESH_0][FLOW_SOL]; + if (flow_solver == nullptr) + SU2_MPI::Error("Flow solver does not exist.", CURRENT_FUNCTION); + if (iSensor >= flow_solver->GetnMetricSensor()) + SU2_MPI::Error("Sensor index " + to_string(iSensor) + " out of range.", CURRENT_FUNCTION); + if (iPoint >= main_geometry->GetnPoint()) + SU2_MPI::Error("Node index " + to_string(iPoint) + " out of range.", CURRENT_FUNCTION); + + return SU2_TYPE::GetValue(flow_solver->GetNodes()->GetSensor_Adapt(iPoint, iSensor)); +} diff --git a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp index e9bc2282925c..4b9b6d3ee43c 100644 --- a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp +++ b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp @@ -40,22 +40,13 @@ CSinglezoneDriver::CSinglezoneDriver(char* confFile, /*--- Initialize the counter for TimeIter ---*/ TimeIter = 0; -} - -CSinglezoneDriver::~CSinglezoneDriver() = default; - -void CSinglezoneDriver::StartSolver() { - StartTime = SU2_MPI::Wtime(); - - config_container[ZONE_0]->Set_StartTime(StartTime); - - /*--- Resolve and allocate metric arrays if metric computation is enabled ---*/ + /*--- 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()) { - /*--- Resolve sensor indices from sensor names and store in solvers ---*/ - if (rank == MASTER_NODE) { + if (rank == MASTER_NODE) cout << "Resolving metric sensor indices." << endl; - } bool resolved = MetricUtils::ResolveSensorIndices( config_container[ZONE_0], @@ -64,17 +55,23 @@ void CSinglezoneDriver::StartSolver() { ); if (resolved) { - /*--- Allocate metric sensor arrays ---*/ 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) { + 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; + +void CSinglezoneDriver::StartSolver() { + + StartTime = SU2_MPI::Wtime(); + + config_container[ZONE_0]->Set_StartTime(StartTime); /*--- Main external loop of the solver. Runs for the number of time steps required. ---*/ diff --git a/SU2_CFD/src/output/CFlowOutput.cpp b/SU2_CFD/src/output/CFlowOutput.cpp index 14ebacce663c..626ede79bfad 100644 --- a/SU2_CFD/src/output/CFlowOutput.cpp +++ b/SU2_CFD/src/output/CFlowOutput.cpp @@ -4189,6 +4189,13 @@ void CFlowOutput::AddMeshAdaptationOutputs(const CConfig* config) { // 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); @@ -4245,6 +4252,12 @@ void CFlowOutput::LoadMeshAdaptationOutputs(const CConfig* config, const CSolver const auto& Sensor_Names = solver[FLOW_SOL]->GetMetricSensorNames(); const auto nSensor = solver[FLOW_SOL]->GetnMetricSensor(); + // Sensor values (primitive and custom) + for (auto iSensor = 0u; iSensor < nSensor; iSensor++) { + const string& sens_str = Sensor_Names[iSensor]; + SetVolumeOutputValue("SENSOR_" + sens_str, iPoint, Node_Flow->GetSensor_Adapt(iPoint, iSensor)); + } + // Gradients for (auto iSensor = 0u; iSensor < nSensor; iSensor++){ const string& sens_str = Sensor_Names[iSensor]; diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index 6f91582627ac..0332209688e7 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -2200,9 +2200,12 @@ void CSolver::AllocateMetricSensorArrays(const vector& sensor_in void CSolver::SetPrimitive_Adapt(CGeometry *geometry, const CConfig *config) { const auto nSensors = GetnMetricSensor(); - /*--- Copy each resolved sensor variable into Sensor_Adapt ---*/ + /*--- Copy each resolved sensor variable into Sensor_Adapt. + * Slots with index == USHRT_MAX are custom (Python-defined) sensors + * and must be filled externally via CDriverBase::SetSensorAdapt. ---*/ for (size_t iSensor = 0; iSensor < nSensors; iSensor++) { const auto var_idx = MetricSensorIndices[iSensor]; + if (var_idx == std::numeric_limits::max()) continue; SU2_OMP_FOR_STAT(omp_chunk_size) for (unsigned long iPoint = 0; iPoint < nPoint; iPoint++) { diff --git a/SU2_PY/SU2/__init__.py b/SU2_PY/SU2/__init__.py index a548f451104c..0d4732843e78 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 000000000000..5a82cb2c4115 --- /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 000000000000..1ff86a0f85ab --- /dev/null +++ b/SU2_PY/SU2/metric/resolver.py @@ -0,0 +1,91 @@ +#!/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 . + + +class CustomSensorRegistry: + """Registry mapping sensor names to per-node callables for mesh adaptation. + + Usage:: + + registry = CustomSensorRegistry() + registry["MACH"] = my_sensor # fn(driver, iPoint) -> float + + driver = pysu2.CSinglezoneDriver("case.cfg", 1, comm) + registry.initialize(driver) # resolve indices once after construction + + for inner_iter in range(N): + driver.Preprocess(inner_iter) + driver.Run() + registry.populate(driver) # fill sensor slots before ComputeMetricField + driver.Postprocess() + driver.Update() + driver.Output(inner_iter) + """ + + def __init__(self): + self._sensors = {} # name -> fn(driver, iPoint) -> float + self._indices = {} # name -> iSensor (cached by initialize) + + def register(self, name, fn): + """Register a sensor function ``fn(driver, iPoint) -> float``. + + Args: + name: Sensor name exactly as listed in METRIC_SENSOR in the config. + fn: Called once per node per iteration. Must cover all nodes + (domain + halo) so the Green-Gauss gradient has correct + values at MPI boundaries. + """ + self._sensors[name] = fn + + def __setitem__(self, name, fn): + self.register(name, fn) + + def __getitem__(self, name): + return self._sensors[name] + + def initialize(self, driver): + """Resolve and cache sensor indices. Call once after driver construction. + + Raises: + ValueError: if a registered name is not listed in METRIC_SENSOR. + """ + 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): + """Evaluate all sensors and push values to the driver. Call before Postprocess().""" + nNodes = driver.GetNumberNodes() + for name, fn in self._sensors.items(): + iSensor = self._indices[name] + for iPoint in range(nNodes): + driver.SetSensorAdapt(iPoint, iSensor, fn(driver, iPoint)) diff --git a/SU2_PY/meson.build b/SU2_PY/meson.build index ced812903468..5c6d772b5e41 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.py b/TestCases/mesh_adaptation/metric.py new file mode 100644 index 000000000000..81e6318651ab --- /dev/null +++ b/TestCases/mesh_adaptation/metric.py @@ -0,0 +1,48 @@ +import math + +from mpi4py import MPI + +import pysu2 +from SU2.metric import CustomSensorRegistry + +comm = MPI.COMM_WORLD + + +def mach_number(driver, iNode: int) -> float: + """Compute local Mach number at a single node for compressible flow.""" + prim_idx = driver.GetPrimitiveIndices() + nDim = driver.GetNumberDimensions() + primVars = driver.Primitives() + + vel_cols = [prim_idx["VELOCITY_X"], prim_idx["VELOCITY_Y"]] + if nDim == 3: + vel_cols.append(prim_idx["VELOCITY_Z"]) + a_col = prim_idx["SOUND_SPEED"] + + vel2 = sum(primVars.Get(iNode, k) ** 2 for k in vel_cols) + a = primVars.Get(iNode, a_col) + return math.sqrt(vel2) / max(a, 1e-20) + + +def main(): + # Intialize driver + driver = pysu2.CSinglezoneDriver("rans_naca0012.cfg", 1, comm) + + # Initialize custom metric sensors + custom_sensors = CustomSensorRegistry() + custom_sensors.register("MACH", mach_number) + custom_sensors.initialize(driver) + + driver.Preprocess(0) + driver.Run() + custom_sensors.populate(driver) # Store Mach sensor + driver.Postprocess() + driver.Update() + driver.Monitor(0) + driver.Output(0) + + driver.Finalize() + + +if __name__ == "__main__": + main() diff --git a/TestCases/mesh_adaptation/rans_naca0012.cfg b/TestCases/mesh_adaptation/rans_naca0012.cfg new file mode 100644 index 000000000000..4baf23b3d6ba --- /dev/null +++ b/TestCases/mesh_adaptation/rans_naca0012.cfg @@ -0,0 +1,111 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % +% SU2 configuration file % +% Case description: NACA0012 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= MACH, PRESSURE +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= 10.0 +CFL_REDUCTION_TURB= 0.5 +CFL_ADAPT= NO +LINEAR_SOLVER= FGMRES +LINEAR_SOLVER_ERROR= 0.1 +LINEAR_SOLVER_ITER= 10 + +% CONVERGENCE +% +ITER= 5000 +CONV_FIELD= REL_RMS_DENSITY +CONV_RESIDUAL_MINVAL= -5 +CONV_STARTITER= 0 + +% INPUT/OUTPUT +% +% Mesh input file +MESH_FILENAME= rans_tri_n0012.su2 % 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 From 48e88d7c37110f82d72bee0324087fb6f7adb3ec Mon Sep 17 00:00:00 2001 From: bmunguia Date: Thu, 9 Apr 2026 10:00:19 -0700 Subject: [PATCH 16/36] Communicate sensors after calculating so custom sensors can be set properly --- Common/include/option_structure.hpp | 1 + SU2_CFD/src/drivers/CSinglezoneDriver.cpp | 9 +-- SU2_CFD/src/solvers/CSolver.cpp | 12 ++++ SU2_PY/SU2/metric/resolver.py | 70 ++++++++++----------- TestCases/mesh_adaptation/metric.py | 11 +++- TestCases/mesh_adaptation/rans_naca0012.cfg | 8 +-- 6 files changed, 65 insertions(+), 46 deletions(-) diff --git a/Common/include/option_structure.hpp b/Common/include/option_structure.hpp index 1d5940ff8420..97c7f91ccaad 100644 --- a/Common/include/option_structure.hpp +++ b/Common/include/option_structure.hpp @@ -2704,6 +2704,7 @@ 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. */ }; diff --git a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp index 4b9b6d3ee43c..7ec1565422a9 100644 --- a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp +++ b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp @@ -365,11 +365,12 @@ void CSinglezoneDriver::ComputeMetricField(bool restartMetric) { cout << endl <<"----------------------------- Compute Metric ----------------------------" << endl; cout << "Storing primitive variables needed for gradients in metric." << endl; } - solver_flow->InitiateComms(geometry, config, MPI_QUANTITIES::SOLUTION); - solver_flow->CompleteComms(geometry, config, MPI_QUANTITIES::SOLUTION); - solver_flow->Preprocessing(geometry, solver, config, MESH_0, NO_RK_ITER, - RUNTIME_FLOW_SYS, true); + + /*--- Set primitive variable adaptation sensors ---*/ + /*--- Custom sensors should have already been set via python wrapper ---*/ solver_flow->SetPrimitive_Adapt(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; diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index 0332209688e7..48a9c65da788 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -1386,6 +1386,10 @@ 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; @@ -1516,6 +1520,10 @@ 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: @@ -1671,6 +1679,10 @@ 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: diff --git a/SU2_PY/SU2/metric/resolver.py b/SU2_PY/SU2/metric/resolver.py index 1ff86a0f85ab..641eb69b4ec5 100644 --- a/SU2_PY/SU2/metric/resolver.py +++ b/SU2_PY/SU2/metric/resolver.py @@ -25,53 +25,47 @@ # You should have received a copy of the GNU Lesser General Public # License along with SU2. If not, see . +from __future__ import annotations -class CustomSensorRegistry: - """Registry mapping sensor names to per-node callables for mesh adaptation. - - Usage:: - - registry = CustomSensorRegistry() - registry["MACH"] = my_sensor # fn(driver, iPoint) -> float - - driver = pysu2.CSinglezoneDriver("case.cfg", 1, comm) - registry.initialize(driver) # resolve indices once after construction +from collections.abc import Callable +from typing import Any - for inner_iter in range(N): - driver.Preprocess(inner_iter) - driver.Run() - registry.populate(driver) # fill sensor slots before ComputeMetricField - driver.Postprocess() - driver.Update() - driver.Output(inner_iter) - """ +SU2Driver = Any +SensorFn = Callable[[SU2Driver, int], float] - def __init__(self): - self._sensors = {} # name -> fn(driver, iPoint) -> float - self._indices = {} # name -> iSensor (cached by initialize) - def register(self, name, fn): - """Register a sensor function ``fn(driver, iPoint) -> float``. +class CustomSensorRegistry: + """Registry mapping sensor names to callables for mesh adaptation.""" + def __init__(self, sensors: dict[str, SensorFn] | None = None) -> None: + """ Args: - name: Sensor name exactly as listed in METRIC_SENSOR in the config. - fn: Called once per node per iteration. Must cover all nodes - (domain + halo) so the Green-Gauss gradient has correct - values at MPI boundaries. + sensors: Optional mapping of sensor name to callable. Each callable + must have the signature ``fn(driver, iPoint) -> float`` and names + must match entries in ``METRIC_SENSOR`` in the config exactly. """ - self._sensors[name] = fn + self._sensors: dict[str, SensorFn] = ( + dict(sensors) if sensors is not None else {} + ) + self._indices: dict[str, int] = {} - def __setitem__(self, name, fn): - self.register(name, fn) + def __setitem__(self, name: str, fn: SensorFn) -> None: + """Register or replace the sensor callable for ``name``.""" + self._sensors[name] = fn - def __getitem__(self, name): + def __getitem__(self, name: str) -> SensorFn: + """Return the sensor callable registered under ``name``.""" return self._sensors[name] - def initialize(self, driver): + 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 name is not listed in METRIC_SENSOR. + ValueError: If a registered sensor name is not listed in + ``METRIC_SENSOR`` in the config. """ for name in self._sensors: idx = driver.GetMetricSensorIndex(name) @@ -82,8 +76,14 @@ def initialize(self, driver): ) self._indices[name] = idx - def populate(self, driver): - """Evaluate all sensors and push values to the driver. Call before Postprocess().""" + def populate(self, driver: SU2Driver) -> None: + """Evaluate all sensors and push values to the driver. + + Call after ``Run()`` and before ``Postprocess()`` on each iteration. + + Args: + driver: Active SU2 driver instance (e.g. ``CSinglezoneDriver``). + """ nNodes = driver.GetNumberNodes() for name, fn in self._sensors.items(): iSensor = self._indices[name] diff --git a/TestCases/mesh_adaptation/metric.py b/TestCases/mesh_adaptation/metric.py index 81e6318651ab..1d1fde0ddc44 100644 --- a/TestCases/mesh_adaptation/metric.py +++ b/TestCases/mesh_adaptation/metric.py @@ -29,13 +29,18 @@ def main(): driver = pysu2.CSinglezoneDriver("rans_naca0012.cfg", 1, comm) # Initialize custom metric sensors - custom_sensors = CustomSensorRegistry() - custom_sensors.register("MACH", mach_number) + custom_sensors = CustomSensorRegistry({"MACH": mach_number}) + + # Initialize map of Sensor: idx custom_sensors.initialize(driver) driver.Preprocess(0) driver.Run() - custom_sensors.populate(driver) # Store Mach sensor + + # Store custom sensor + custom_sensors.populate(driver) + + # Postprocess solution/metric and output driver.Postprocess() driver.Update() driver.Monitor(0) diff --git a/TestCases/mesh_adaptation/rans_naca0012.cfg b/TestCases/mesh_adaptation/rans_naca0012.cfg index 4baf23b3d6ba..ac1deab63d87 100644 --- a/TestCases/mesh_adaptation/rans_naca0012.cfg +++ b/TestCases/mesh_adaptation/rans_naca0012.cfg @@ -69,7 +69,7 @@ MUSCL_TURB= NO % TIME_DISCRE_FLOW= EULER_IMPLICIT TIME_DISCRE_TURB= EULER_IMPLICIT -CFL_NUMBER= 10.0 +CFL_NUMBER= 100.0 CFL_REDUCTION_TURB= 0.5 CFL_ADAPT= NO LINEAR_SOLVER= FGMRES @@ -78,15 +78,15 @@ LINEAR_SOLVER_ITER= 10 % CONVERGENCE % -ITER= 5000 +ITER= 1000 CONV_FIELD= REL_RMS_DENSITY -CONV_RESIDUAL_MINVAL= -5 +CONV_RESIDUAL_MINVAL= -3 CONV_STARTITER= 0 % INPUT/OUTPUT % % Mesh input file -MESH_FILENAME= rans_tri_n0012.su2 % inv_naca0012.su2 +MESH_FILENAME= inv_naca0012.su2 MESH_FORMAT= SU2 % % Restart input files From 0c5e86eefc50193820255facb91bad33a0a13651 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Thu, 9 Apr 2026 10:04:16 -0700 Subject: [PATCH 17/36] Add metric outputs to incompressible and NEMO solvers --- SU2_CFD/src/output/CFlowCompOutput.cpp | 2 +- SU2_CFD/src/output/CFlowIncOutput.cpp | 5 +++++ SU2_CFD/src/output/CNEMOCompOutput.cpp | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/SU2_CFD/src/output/CFlowCompOutput.cpp b/SU2_CFD/src/output/CFlowCompOutput.cpp index 32e9e42cc4a6..83a17f8960fa 100644 --- a/SU2_CFD/src/output/CFlowCompOutput.cpp +++ b/SU2_CFD/src/output/CFlowCompOutput.cpp @@ -307,7 +307,7 @@ void CFlowCompOutput::SetVolumeOutputFields(CConfig *config){ AddCommonFVMOutputs(config); - // Metric tensor and dual-cell volume + // Sensor value/gradient/Hessian, metric tensor, and dual-cell volume AddMeshAdaptationOutputs(config); if (config->GetTime_Domain()) { diff --git a/SU2_CFD/src/output/CFlowIncOutput.cpp b/SU2_CFD/src/output/CFlowIncOutput.cpp index b13edb7732fb..bed54cef4752 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/CNEMOCompOutput.cpp b/SU2_CFD/src/output/CNEMOCompOutput.cpp index fb6b8f906624..fb16527ad81a 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); } From 2930c8d27c42e4eedf76c5af33b4ed6bea065433 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Thu, 9 Apr 2026 10:17:10 -0700 Subject: [PATCH 18/36] Separate examples for primitive and derived sensors --- .../{ => metric/custom}/metric.py | 0 .../{ => metric/custom}/rans_naca0012.cfg | 2 +- .../metric/primitive/inv_naca0012.cfg | 108 ++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) rename TestCases/mesh_adaptation/{ => metric/custom}/metric.py (100%) rename TestCases/mesh_adaptation/{ => metric/custom}/rans_naca0012.cfg (97%) create mode 100644 TestCases/mesh_adaptation/metric/primitive/inv_naca0012.cfg diff --git a/TestCases/mesh_adaptation/metric.py b/TestCases/mesh_adaptation/metric/custom/metric.py similarity index 100% rename from TestCases/mesh_adaptation/metric.py rename to TestCases/mesh_adaptation/metric/custom/metric.py diff --git a/TestCases/mesh_adaptation/rans_naca0012.cfg b/TestCases/mesh_adaptation/metric/custom/rans_naca0012.cfg similarity index 97% rename from TestCases/mesh_adaptation/rans_naca0012.cfg rename to TestCases/mesh_adaptation/metric/custom/rans_naca0012.cfg index ac1deab63d87..33cd9886a040 100644 --- a/TestCases/mesh_adaptation/rans_naca0012.cfg +++ b/TestCases/mesh_adaptation/metric/custom/rans_naca0012.cfg @@ -1,7 +1,7 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % % SU2 configuration file % -% Case description: NACA0012 metric computation % +% Case description: RANS NACA0012 Mach metric computation % % Author: Brian Munguía % % Institution: Stanford University % % Date: 2026.08.04 % diff --git a/TestCases/mesh_adaptation/metric/primitive/inv_naca0012.cfg b/TestCases/mesh_adaptation/metric/primitive/inv_naca0012.cfg new file mode 100644 index 000000000000..59dacf8349c0 --- /dev/null +++ b/TestCases/mesh_adaptation/metric/primitive/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 +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 From 440b815b4c682cfb69aab54c7437f9b83df9ff10 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Thu, 9 Apr 2026 15:18:23 -0700 Subject: [PATCH 19/36] Fix sensor indexing so first sensor Hessian in config is normalized --- SU2_CFD/include/metrics/metricUtils.hpp | 58 ++++++++++--------------- SU2_CFD/include/solvers/CSolver.hpp | 46 ++++++++------------ SU2_CFD/src/drivers/CDriverBase.cpp | 9 ++-- SU2_CFD/src/output/CFlowOutput.cpp | 15 +++---- SU2_CFD/src/solvers/CSolver.cpp | 30 +++++-------- 5 files changed, 64 insertions(+), 94 deletions(-) diff --git a/SU2_CFD/include/metrics/metricUtils.hpp b/SU2_CFD/include/metrics/metricUtils.hpp index f015a27275d5..d1c805c15d7b 100644 --- a/SU2_CFD/include/metrics/metricUtils.hpp +++ b/SU2_CFD/include/metrics/metricUtils.hpp @@ -45,7 +45,7 @@ namespace MetricUtils { * 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 indices in each solver via SetMetricSensorIndices(). + * Directly stores the resolved sensors in each solver via SetMetricSensors(). * * \param[in] config - Configuration containing sensor names * \param[in] geometry - Geometry for dimension info @@ -62,9 +62,8 @@ inline bool ResolveSensorIndices( /*--- Get sensor names from config ---*/ std::vector sensor_names = config->GetMetric_SensorList(); - /*--- Group sensors by solver ---*/ - std::map> sensor_indices_by_solver; - std::map> sensor_names_by_solver; + /*--- Group sensors by solver (in config order) ---*/ + std::map> sensors_by_solver; /*--- Build a map of all available variables across all solvers ---*/ std::map> var_map; @@ -91,8 +90,8 @@ inline bool ResolveSensorIndices( CPrimitiveIndices indices(incompressible, nemo, nDim, nSpecies); std::map primitive_map = PrimitiveNameToIndexMap(indices); - for (const auto& entry : primitive_map) { - var_map[entry.first] = std::make_pair(iSol, entry.second); + for (const auto& [varname, varidx] : primitive_map) { + var_map[varname] = std::make_pair(iSol, varidx); } } else { /*--- For non-flow solvers, use solution fields ---*/ @@ -114,18 +113,20 @@ inline bool ResolveSensorIndices( } } - /*--- Resolve each sensor and group by solver ---*/ + /*--- Resolve each sensor in config order, grouping by solver. + * Custom sensors (not found in var_map) are assigned to FLOW_SOL with + * prim_idx == USHRT_MAX as a placeholder. These slots are skipped by + * SetPrimitive_Adapt and must be filled externally (e.g. via + * CDriverBase::SetSensorAdapt from the Python wrapper). + * Config order is preserved so the first listed sensor is always iSensor=0 + * (the one whose Hessian is used for the metric and normalized). ---*/ bool all_resolved = true; - std::vector unresolved; for (const auto& sensor_name : sensor_names) { auto it = var_map.find(sensor_name); if (it != var_map.end()) { const unsigned short iSol = it->second.first; const unsigned short var_index = it->second.second; - - sensor_indices_by_solver[iSol].push_back(var_index); - sensor_names_by_solver[iSol].push_back(sensor_name); - + sensors_by_solver[iSol].push_back({var_index, sensor_name}); if (rank == MASTER_NODE) { std::cout << " Resolved sensor '" << sensor_name << "' to solver " << iSol << ", variable index " << var_index << std::endl; @@ -135,31 +136,18 @@ inline bool ResolveSensorIndices( std::cout << " Custom sensor '" << sensor_name << "' detected." << std::endl; } all_resolved = false; - unresolved.push_back(sensor_name); - } - } - - /*--- Assign unresolved sensors to FLOW_SOL with USHRT_MAX as a placeholder index. - * These slots are skipped by SetPrimitive_Adapt and must be filled externally - * (e.g. via CDriverBase::SetSensorAdapt from the Python wrapper). ---*/ - if (!unresolved.empty()) { - unsigned short flow_sol = FLOW_SOL; - for (const auto& name : unresolved) { - sensor_names_by_solver[flow_sol].push_back(name); - sensor_indices_by_solver[flow_sol].push_back(std::numeric_limits::max()); + sensors_by_solver[FLOW_SOL].push_back( + {std::numeric_limits::max(), sensor_name}); } } - /*--- Set sensor indices directly in each solver ---*/ - for (const auto& entry : sensor_indices_by_solver) { - const unsigned short iSol = entry.first; - solver_container[iSol]->SetMetricSensorIndices(sensor_indices_by_solver[iSol]); - solver_container[iSol]->SetMetricSensorNames(sensor_names_by_solver[iSol]); + /*--- 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 ---*/ - const bool any_resolved = !sensor_indices_by_solver.empty(); - return any_resolved; + return !sensors_by_solver.empty(); } /*! @@ -176,9 +164,9 @@ inline void InitializeMetrics(CSolver** solver_container) { for (unsigned short iSol = 0; iSol < MAX_SOLS; iSol++) { if (solver_container[iSol] == nullptr) continue; - const auto& indices = solver_container[iSol]->GetMetricSensorIndices(); - if (!indices.empty()) { - solver_container[iSol]->AllocateMetricSensorArrays(indices); + const auto nSensors = solver_container[iSol]->GetnMetricSensor(); + if (nSensors > 0) { + solver_container[iSol]->AllocateMetricSensorArrays(nSensors); } } } @@ -192,7 +180,7 @@ 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]->GetMetricSensorIndices().size(); + num_sensor += solver_container[iSol]->GetnMetricSensor(); } } return num_sensor; diff --git a/SU2_CFD/include/solvers/CSolver.hpp b/SU2_CFD/include/solvers/CSolver.hpp index 99408d54c966..f5aeb12cc62b 100644 --- a/SU2_CFD/include/solvers/CSolver.hpp +++ b/SU2_CFD/include/solvers/CSolver.hpp @@ -208,9 +208,12 @@ class CSolver { vector fields; - /*--- Metric sensor indices for mesh adaptation ---*/ - vector MetricSensorIndices; /*!< \brief Variable indices for metric sensors in this solver. */ - vector MetricSensorNames; /*!< \brief Names of metric sensors in this solver. */ + /*--- Metric sensors for mesh adaptation ---*/ + struct MetricSensorInfo { + unsigned short prim_idx; /*!< \brief Primitive variable index; USHRT_MAX for custom sensors. */ + std::string name; /*!< \brief Sensor name as specified in config. */ + }; + vector MetricSensors; /*!< \brief Sensor list in config order, one entry per sensor. */ #ifdef HAVE_LIBROM @@ -601,10 +604,10 @@ class CSolver { virtual void SetSolution_Adapt(CGeometry *geometry, const CConfig *config); /*! - * \brief Allocate Gradient_Adapt and Hessian arrays for specified sensor variables. - * \param[in] sensor_indices - Vector of variable indices for this solver to allocate arrays for + * \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(const vector& sensor_indices); + virtual void AllocateMetricSensorArrays(unsigned short nSensors); /*! * \brief Compute the Green-Gauss Hessian of the solution. @@ -4296,35 +4299,20 @@ class CSolver { * \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(MetricSensorIndices.size()); } + inline unsigned short GetnMetricSensor() const { return static_cast(MetricSensors.size()); } /*! - * \brief Get the metric sensor variable indices for this solver. - * \return Vector of variable indices used as metric sensors. + * \brief Get the metric sensor list for this solver. + * \return Vector of MetricSensorInfo entries in config order. */ - inline const vector& GetMetricSensorIndices() const { return MetricSensorIndices; } + inline const vector& GetMetricSensors() const { return MetricSensors; } /*! - * \brief Get the metric sensor names for this solver. - * \return Vector of sensor names. + * \brief Set the metric sensor list for this solver. + * \param[in] sensors - Sensor entries in config order. */ - inline const vector& GetMetricSensorNames() const { return MetricSensorNames; } - - /*! - * \brief Set the metric sensor indices for this solver. - * \param[in] indices - Variable indices for metric sensors. - * \param[in] names - Names of metric sensors. - */ - inline void SetMetricSensorIndices(const vector& indices) { - MetricSensorIndices = indices; - } - - /*! - * \brief Set the metric sensor names for this solver. - * \param[in] names - Names of metric sensors. - */ - inline void SetMetricSensorNames(const vector& names) { - MetricSensorNames = names; + inline void SetMetricSensors(const vector& sensors) { + MetricSensors = sensors; } /*! diff --git a/SU2_CFD/src/drivers/CDriverBase.cpp b/SU2_CFD/src/drivers/CDriverBase.cpp index b4613b47b5fd..e38d98eb9755 100644 --- a/SU2_CFD/src/drivers/CDriverBase.cpp +++ b/SU2_CFD/src/drivers/CDriverBase.cpp @@ -440,10 +440,11 @@ map CDriverBase::GetPrimitiveIndices() const { 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& names = flow_solver->GetMetricSensorNames(); - auto it = std::find(names.begin(), names.end(), sensor_name); - if (it == names.end()) return -1; - return static_cast(it - names.begin()); + 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; } void CDriverBase::SetSensorAdapt(unsigned long iPoint, unsigned short iSensor, passivedouble value) { diff --git a/SU2_CFD/src/output/CFlowOutput.cpp b/SU2_CFD/src/output/CFlowOutput.cpp index 626ede79bfad..f7c2c9b12851 100644 --- a/SU2_CFD/src/output/CFlowOutput.cpp +++ b/SU2_CFD/src/output/CFlowOutput.cpp @@ -4249,18 +4249,17 @@ void CFlowOutput::LoadMeshAdaptationOutputs(const CConfig* config, const CSolver const auto* Node_Flow = solver[FLOW_SOL]->GetNodes(); const auto* Node_Geo = geometry->nodes; if(config->GetCompute_Metric()) { - const auto& Sensor_Names = solver[FLOW_SOL]->GetMetricSensorNames(); - const auto nSensor = solver[FLOW_SOL]->GetnMetricSensor(); + const auto& sensors = solver[FLOW_SOL]->GetMetricSensors(); // Sensor values (primitive and custom) - for (auto iSensor = 0u; iSensor < nSensor; iSensor++) { - const string& sens_str = Sensor_Names[iSensor]; + 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 < nSensor; iSensor++){ - const string& sens_str = Sensor_Names[iSensor]; + 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)); @@ -4271,8 +4270,8 @@ void CFlowOutput::LoadMeshAdaptationOutputs(const CConfig* config, const CSolver } // Hessians - for (auto iSensor = 0u; iSensor < nSensor; iSensor++){ - const string& sens_str = Sensor_Names[iSensor]; + 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)); diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index 48a9c65da788..624fded5540b 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -2204,41 +2204,35 @@ 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(const vector& sensor_indices) { - if (base_nodes == nullptr || sensor_indices.empty()) return; - base_nodes->AllocateMetricSensorArrays(sensor_indices.size()); +void CSolver::AllocateMetricSensorArrays(unsigned short nSensors) { + if (base_nodes == nullptr || nSensors == 0) return; + base_nodes->AllocateMetricSensorArrays(nSensors); } void CSolver::SetPrimitive_Adapt(CGeometry *geometry, const CConfig *config) { - const auto nSensors = GetnMetricSensor(); - /*--- Copy each resolved sensor variable into Sensor_Adapt. - * Slots with index == USHRT_MAX are custom (Python-defined) sensors + * Slots with prim_idx == USHRT_MAX are custom (Python-defined) sensors * and must be filled externally via CDriverBase::SetSensorAdapt. ---*/ - for (size_t iSensor = 0; iSensor < nSensors; iSensor++) { - const auto var_idx = MetricSensorIndices[iSensor]; - if (var_idx == std::numeric_limits::max()) continue; + for (size_t iSensor = 0; iSensor < MetricSensors.size(); ++iSensor) { + const auto prim_idx = MetricSensors[iSensor].prim_idx; + if (prim_idx == std::numeric_limits::max()) continue; SU2_OMP_FOR_STAT(omp_chunk_size) for (unsigned long iPoint = 0; iPoint < nPoint; iPoint++) { - const su2double prim_var = base_nodes->GetPrimitive(iPoint, var_idx); - base_nodes->SetSensor_Adapt(iPoint, iSensor, prim_var); + base_nodes->SetSensor_Adapt(iPoint, iSensor, base_nodes->GetPrimitive(iPoint, prim_idx)); } END_SU2_OMP_FOR } } void CSolver::SetSolution_Adapt(CGeometry *geometry, const CConfig *config) { - const auto nSensors = GetnMetricSensor(); - /*--- Copy each resolved sensor variable into Sensor_Adapt ---*/ - for (size_t iSensor = 0; iSensor < nSensors; iSensor++) { - const auto var_idx = MetricSensorIndices[iSensor]; + for (size_t iSensor = 0; iSensor < MetricSensors.size(); ++iSensor) { + const auto prim_idx = MetricSensors[iSensor].prim_idx; SU2_OMP_FOR_STAT(omp_chunk_size) for (unsigned long iPoint = 0; iPoint < nPoint; iPoint++) { - const su2double prim_var = base_nodes->GetSolution(iPoint, var_idx); - base_nodes->SetSensor_Adapt(iPoint, iSensor, prim_var); + base_nodes->SetSensor_Adapt(iPoint, iSensor, base_nodes->GetSolution(iPoint, prim_idx)); } END_SU2_OMP_FOR } @@ -4482,7 +4476,7 @@ void CSolver::ComputeMetric(CSolver **solver, CGeometry *geometry, const CConfig if (is_last_iter) { integrals.push_back(integral); if (rank == MASTER_NODE) { - const string& sensor_name = (iSensor < MetricSensorNames.size()) ? MetricSensorNames[iSensor] : "unknown"; + const string& sensor_name = (iSensor < MetricSensors.size()) ? MetricSensors[iSensor].name : "unknown"; cout << "Global metric normalization integral for sensor "; cout << sensor_name << ": " << integral << endl; } From e3300dedd1972572d4421e7f0337d95b9cad796b Mon Sep 17 00:00:00 2001 From: bmunguia Date: Sat, 11 Apr 2026 10:25:44 -0700 Subject: [PATCH 20/36] Add built-in derived sensors for Mach and velocity magnitude --- Common/include/option_structure.hpp | 9 ++ SU2_CFD/include/drivers/CDriverBase.hpp | 19 +--- SU2_CFD/include/metrics/metricUtils.hpp | 105 +++++++++++++++--- SU2_CFD/include/solvers/CSolver.hpp | 15 ++- SU2_CFD/src/drivers/CDriverBase.cpp | 24 ---- SU2_CFD/src/drivers/CSinglezoneDriver.cpp | 8 +- SU2_CFD/src/solvers/CSolver.cpp | 21 ++-- SU2_PY/SU2/metric/resolver.py | 15 ++- .../mesh_adaptation/metric/custom/metric.py | 22 ++-- .../metric/custom/rans_naca0012.cfg | 4 +- .../metric/primitive/inv_naca0012.cfg | 2 +- 11 files changed, 155 insertions(+), 89 deletions(-) diff --git a/Common/include/option_structure.hpp b/Common/include/option_structure.hpp index 97c7f91ccaad..10f2efa55c74 100644 --- a/Common/include/option_structure.hpp +++ b/Common/include/option_structure.hpp @@ -2864,6 +2864,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/SU2_CFD/include/drivers/CDriverBase.hpp b/SU2_CFD/include/drivers/CDriverBase.hpp index 8362433b9545..043ede2e46cf 100644 --- a/SU2_CFD/include/drivers/CDriverBase.hpp +++ b/SU2_CFD/include/drivers/CDriverBase.hpp @@ -524,20 +524,13 @@ class CDriverBase { short GetMetricSensorIndex(const std::string& sensor_name) const; /*! - * \brief Set the value of a metric sensor for a single node. - * \param[in] iPoint - Node index. - * \param[in] iSensor - Local sensor index (from GetMetricSensorIndex). - * \param[in] value - Sensor value at this node. + * \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. */ - void SetSensorAdapt(unsigned long iPoint, unsigned short iSensor, passivedouble value); - - /*! - * \brief Get the value of a metric sensor at a single node. - * \param[in] iPoint - Node index. - * \param[in] iSensor - Local sensor index (from GetMetricSensorIndex). - * \return Sensor value at this node. - */ - passivedouble GetSensorAdapt(unsigned long iPoint, unsigned short iSensor) const; + 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. diff --git a/SU2_CFD/include/metrics/metricUtils.hpp b/SU2_CFD/include/metrics/metricUtils.hpp index d1c805c15d7b..6768399f8822 100644 --- a/SU2_CFD/include/metrics/metricUtils.hpp +++ b/SU2_CFD/include/metrics/metricUtils.hpp @@ -27,11 +27,13 @@ #pragma once -#include -#include +#include +#include +#include #include #include -#include +#include +#include #include "../../../Common/include/CConfig.hpp" #include "../solvers/CSolver.hpp" #include "../variables/CPrimitiveIndices.hpp" @@ -39,6 +41,53 @@ 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. * @@ -65,8 +114,11 @@ inline bool ResolveSensorIndices( /*--- Group sensors by solver (in config order) ---*/ std::map> sensors_by_solver; - /*--- Build a map of all available variables across all solvers ---*/ + /*--- 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; @@ -93,6 +145,10 @@ inline bool ResolveSensorIndices( 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(); @@ -114,30 +170,45 @@ inline bool ResolveSensorIndices( } /*--- Resolve each sensor in config order, grouping by solver. - * Custom sensors (not found in var_map) are assigned to FLOW_SOL with - * prim_idx == USHRT_MAX as a placeholder. These slots are skipped by - * SetPrimitive_Adapt and must be filled externally (e.g. via - * CDriverBase::SetSensorAdapt from the Python wrapper). - * Config order is preserved so the first listed sensor is always iSensor=0 - * (the one whose Hessian is used for the metric and normalized). ---*/ + * 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}); + sensors_by_solver[iSol].push_back( + {var_index, sensor_name, SensorType::PRIMITIVE, {}}); if (rank == MASTER_NODE) { - std::cout << " Resolved sensor '" << sensor_name << "' to solver " + std::cout << " Primitive sensor '" << sensor_name << "' resolved to solver " << iSol << ", variable index " << var_index << std::endl; } } else { - if (rank == MASTER_NODE) { - std::cout << " Custom sensor '" << sensor_name << "' detected." << std::endl; + 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, {}}); } - all_resolved = false; - sensors_by_solver[FLOW_SOL].push_back( - {std::numeric_limits::max(), sensor_name}); } } diff --git a/SU2_CFD/include/solvers/CSolver.hpp b/SU2_CFD/include/solvers/CSolver.hpp index f5aeb12cc62b..366df11e03a8 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 @@ -210,8 +211,10 @@ class CSolver { /*--- Metric sensors for mesh adaptation ---*/ struct MetricSensorInfo { - unsigned short prim_idx; /*!< \brief Primitive variable index; USHRT_MAX for custom sensors. */ - std::string name; /*!< \brief Sensor name as specified in config. */ + 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. */ @@ -590,18 +593,18 @@ class CSolver { inline virtual void SetPrimitive_Limiter(CGeometry *geometry, const CConfig *config) { } /*! - * \brief Set primitive variables for adaptation using resolved sensor locations. + * \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_Adapt(CGeometry *geometry, const CConfig *config); + virtual void SetPrimitive_SensorAdapt(CGeometry *geometry, const CConfig *config); /*! - * \brief Set solution variables for adaptation using resolved sensor locations. + * \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 SetSolution_Adapt(CGeometry *geometry, const CConfig *config); + virtual void SetDerived_SensorAdapt(CGeometry *geometry, const CConfig *config); /*! * \brief Allocate Gradient_Adapt and Hessian arrays for the sensors assigned to this solver. diff --git a/SU2_CFD/src/drivers/CDriverBase.cpp b/SU2_CFD/src/drivers/CDriverBase.cpp index e38d98eb9755..bf4d1b8b0f1d 100644 --- a/SU2_CFD/src/drivers/CDriverBase.cpp +++ b/SU2_CFD/src/drivers/CDriverBase.cpp @@ -446,27 +446,3 @@ short CDriverBase::GetMetricSensorIndex(const string& sensor_name) const { } return -1; } - -void CDriverBase::SetSensorAdapt(unsigned long iPoint, unsigned short iSensor, passivedouble value) { - auto* flow_solver = solver_container[selected_zone][INST_0][MESH_0][FLOW_SOL]; - if (flow_solver == nullptr) - SU2_MPI::Error("Flow solver does not exist.", CURRENT_FUNCTION); - if (iSensor >= flow_solver->GetnMetricSensor()) - SU2_MPI::Error("Sensor index " + to_string(iSensor) + " out of range.", CURRENT_FUNCTION); - if (iPoint >= main_geometry->GetnPoint()) - SU2_MPI::Error("Node index " + to_string(iPoint) + " out of range.", CURRENT_FUNCTION); - - flow_solver->GetNodes()->SetSensor_Adapt(iPoint, iSensor, value); -} - -passivedouble CDriverBase::GetSensorAdapt(unsigned long iPoint, unsigned short iSensor) const { - auto* flow_solver = solver_container[selected_zone][INST_0][MESH_0][FLOW_SOL]; - if (flow_solver == nullptr) - SU2_MPI::Error("Flow solver does not exist.", CURRENT_FUNCTION); - if (iSensor >= flow_solver->GetnMetricSensor()) - SU2_MPI::Error("Sensor index " + to_string(iSensor) + " out of range.", CURRENT_FUNCTION); - if (iPoint >= main_geometry->GetnPoint()) - SU2_MPI::Error("Node index " + to_string(iPoint) + " out of range.", CURRENT_FUNCTION); - - return SU2_TYPE::GetValue(flow_solver->GetNodes()->GetSensor_Adapt(iPoint, iSensor)); -} diff --git a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp index 7ec1565422a9..3741727c05cb 100644 --- a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp +++ b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp @@ -366,9 +366,11 @@ void CSinglezoneDriver::ComputeMetricField(bool restartMetric) { cout << "Storing primitive variables needed for gradients in metric." << endl; } - /*--- Set primitive variable adaptation sensors ---*/ - /*--- Custom sensors should have already been set via python wrapper ---*/ - solver_flow->SetPrimitive_Adapt(geometry, config); + /*--- 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); diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index 624fded5540b..fcae5e5fe525 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -2209,14 +2209,13 @@ void CSolver::AllocateMetricSensorArrays(unsigned short nSensors) { base_nodes->AllocateMetricSensorArrays(nSensors); } -void CSolver::SetPrimitive_Adapt(CGeometry *geometry, const CConfig *config) { - /*--- Copy each resolved sensor variable into Sensor_Adapt. - * Slots with prim_idx == USHRT_MAX are custom (Python-defined) sensors - * and must be filled externally via CDriverBase::SetSensorAdapt. ---*/ +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) { - const auto prim_idx = MetricSensors[iSensor].prim_idx; - if (prim_idx == std::numeric_limits::max()) continue; + 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)); @@ -2225,14 +2224,16 @@ void CSolver::SetPrimitive_Adapt(CGeometry *geometry, const CConfig *config) { } } -void CSolver::SetSolution_Adapt(CGeometry *geometry, const CConfig *config) { - /*--- Copy each resolved sensor variable into Sensor_Adapt ---*/ +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) { - const auto prim_idx = MetricSensors[iSensor].prim_idx; + 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, base_nodes->GetSolution(iPoint, prim_idx)); + base_nodes->SetSensor_Adapt(iPoint, iSensor, fn(base_nodes->GetPrimitive(iPoint))); } END_SU2_OMP_FOR } diff --git a/SU2_PY/SU2/metric/resolver.py b/SU2_PY/SU2/metric/resolver.py index 641eb69b4ec5..7a8dd419438b 100644 --- a/SU2_PY/SU2/metric/resolver.py +++ b/SU2_PY/SU2/metric/resolver.py @@ -31,7 +31,7 @@ from typing import Any SU2Driver = Any -SensorFn = Callable[[SU2Driver, int], float] +SensorFn = Callable[[SU2Driver], list[float]] class CustomSensorRegistry: @@ -41,7 +41,7 @@ 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, iPoint) -> float`` and names + 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] = ( @@ -77,15 +77,18 @@ def initialize(self, driver: SU2Driver) -> None: self._indices[name] = idx def populate(self, driver: SU2Driver) -> None: - """Evaluate all sensors and push values to the driver. + """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``). """ - nNodes = driver.GetNumberNodes() + sensors = driver.AdaptSensors() + for name, fn in self._sensors.items(): iSensor = self._indices[name] - for iPoint in range(nNodes): - driver.SetSensorAdapt(iPoint, iSensor, fn(driver, iPoint)) + values = fn(driver) + + for iNode, value in enumerate(values): + sensors.Set(iNode, iSensor, value) diff --git a/TestCases/mesh_adaptation/metric/custom/metric.py b/TestCases/mesh_adaptation/metric/custom/metric.py index 1d1fde0ddc44..f92d17a3ee86 100644 --- a/TestCases/mesh_adaptation/metric/custom/metric.py +++ b/TestCases/mesh_adaptation/metric/custom/metric.py @@ -8,20 +8,28 @@ comm = MPI.COMM_WORLD -def mach_number(driver, iNode: int) -> float: - """Compute local Mach number at a single node for compressible flow.""" +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() + dens_col = prim_idx["DENSITY"] + press_col = prim_idx["PRESSURE"] vel_cols = [prim_idx["VELOCITY_X"], prim_idx["VELOCITY_Y"]] if nDim == 3: vel_cols.append(prim_idx["VELOCITY_Z"]) - a_col = prim_idx["SOUND_SPEED"] - vel2 = sum(primVars.Get(iNode, k) ** 2 for k in vel_cols) - a = primVars.Get(iNode, a_col) - return math.sqrt(vel2) / max(a, 1e-20) + p_tot = [0.0] * nNodes + for iNode in range(nNodes): + row = primVars(iNode) + p = row[press_col] + r = row[dens_col] + vel2 = sum(row[k] ** 2 for k in vel_cols) + + p_tot[iNode] = p + 0.5 * r * vel2 + return p_tot def main(): @@ -29,7 +37,7 @@ def main(): driver = pysu2.CSinglezoneDriver("rans_naca0012.cfg", 1, comm) # Initialize custom metric sensors - custom_sensors = CustomSensorRegistry({"MACH": mach_number}) + custom_sensors = CustomSensorRegistry({"TOTAL_PRESSURE": total_pressure}) # Initialize map of Sensor: idx custom_sensors.initialize(driver) diff --git a/TestCases/mesh_adaptation/metric/custom/rans_naca0012.cfg b/TestCases/mesh_adaptation/metric/custom/rans_naca0012.cfg index 33cd9886a040..9c99ab1e70cc 100644 --- a/TestCases/mesh_adaptation/metric/custom/rans_naca0012.cfg +++ b/TestCases/mesh_adaptation/metric/custom/rans_naca0012.cfg @@ -1,7 +1,7 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % % % SU2 configuration file % -% Case description: RANS NACA0012 Mach metric computation % +% Case description: RANS NACA0012 total pressure metric computation % % Author: Brian Munguía % % Institution: Stanford University % % Date: 2026.08.04 % @@ -21,7 +21,7 @@ RESTART_SOL= NO % COMPUTE_METRIC= YES NORMALIZE_METRIC= YES -METRIC_SENSOR= MACH, PRESSURE +METRIC_SENSOR= TOTAL_PRESSURE, MACH METRIC_NORM= 4 METRIC_HMAX= 10.0 METRIC_HMIN= 1.0e-6 diff --git a/TestCases/mesh_adaptation/metric/primitive/inv_naca0012.cfg b/TestCases/mesh_adaptation/metric/primitive/inv_naca0012.cfg index 59dacf8349c0..d05f47ff94d6 100644 --- a/TestCases/mesh_adaptation/metric/primitive/inv_naca0012.cfg +++ b/TestCases/mesh_adaptation/metric/primitive/inv_naca0012.cfg @@ -19,7 +19,7 @@ RESTART_SOL= NO % COMPUTE_METRIC= YES NORMALIZE_METRIC= YES -METRIC_SENSOR= PRESSURE +METRIC_SENSOR= PRESSURE, MACH METRIC_NORM= 2 METRIC_HMAX= 10.0 METRIC_HMIN= 1.0e-6 From 11c7387f22c2f1911d5f001f4680f74fc46b3d97 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Sat, 11 Apr 2026 10:27:20 -0700 Subject: [PATCH 21/36] Rename test case directory --- .../metric/{primitive => built_in}/inv_naca0012.cfg | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename TestCases/mesh_adaptation/metric/{primitive => built_in}/inv_naca0012.cfg (100%) diff --git a/TestCases/mesh_adaptation/metric/primitive/inv_naca0012.cfg b/TestCases/mesh_adaptation/metric/built_in/inv_naca0012.cfg similarity index 100% rename from TestCases/mesh_adaptation/metric/primitive/inv_naca0012.cfg rename to TestCases/mesh_adaptation/metric/built_in/inv_naca0012.cfg From 97c9a264130fd0f3927bc783f327ae8ba78dd4ff Mon Sep 17 00:00:00 2001 From: bmunguia Date: Sat, 11 Apr 2026 10:50:03 -0700 Subject: [PATCH 22/36] Fix test case --- TestCases/mesh_adaptation/metric/custom/metric.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/TestCases/mesh_adaptation/metric/custom/metric.py b/TestCases/mesh_adaptation/metric/custom/metric.py index f92d17a3ee86..893daf4c0d76 100644 --- a/TestCases/mesh_adaptation/metric/custom/metric.py +++ b/TestCases/mesh_adaptation/metric/custom/metric.py @@ -15,9 +15,12 @@ def total_pressure(driver) -> list[float]: primVars = driver.Primitives() nNodes = driver.GetNumberNodes() - driver.GetNumberHaloNodes() - dens_col = prim_idx["DENSITY"] + 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"]) @@ -25,10 +28,11 @@ def total_pressure(driver) -> list[float]: for iNode in range(nNodes): row = primVars(iNode) p = row[press_col] - r = row[dens_col] + a = row[a_col] vel2 = sum(row[k] ** 2 for k in vel_cols) + mach2 = vel2 / (a * a) - p_tot[iNode] = p + 0.5 * r * vel2 + p_tot[iNode] = p * pow(1.0 + coeff * mach2, exp_fac) return p_tot From 78fedf53e60e300625041f1be8d8ceb0601ada61 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Sat, 11 Apr 2026 17:07:49 -0700 Subject: [PATCH 23/36] Fix Hessian indexing and add comment about symmetry by construction --- .../gradients/computeGradientsGreenGauss.hpp | 18 +++++++++----- SU2_CFD/include/metrics/computeMetrics.hpp | 24 +++++++++---------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp b/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp index ff2722f1ae2c..5fed79ed14bc 100644 --- a/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp +++ b/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp @@ -217,7 +217,7 @@ void computeHessiansGreenGauss(CSolver* solver, MPI_QUANTITIES kindMpiComm, PERI 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. ---*/ + * If inwards we need to flip the area vector. ---*/ su2double dir = (iPoint < jPoint)? 1.0 : -1.0; su2double weight = dir * halfOnVol; @@ -228,8 +228,11 @@ void computeHessiansGreenGauss(CSolver* solver, MPI_QUANTITIES kindMpiComm, PERI for (size_t iVar = varBegin; iVar < varEnd; ++iVar) { su2double flux = weight * (gradient(iPoint, iVar, jDim) + gradient(jPoint, iVar, jDim)); for (size_t iDim = 0; iDim < nDim; ++iDim) { - size_t ind = (iDim <= jDim) ? iDim*nDim - ((iDim - 1)*iDim)/2 + jDim - iDim - : jDim*nDim - ((jDim - 1)*jDim)/2 + iDim - jDim; + /*--- Make the Hessian symmetric by construction. + * Get the 1D index for lower triangular storage. ---*/ + const auto iMax = max(iDim, jDim); + const auto iMin = min(iDim, jDim); + const size_t ind = iMax * (iMax + 1) / 2 + iMin; diagScale = (iDim == jDim)? 1.0 : 0.5; hessian(iPoint, iVar, ind) += diagScale * flux * area[iDim]; } // iDims @@ -245,7 +248,7 @@ void computeHessiansGreenGauss(CSolver* solver, MPI_QUANTITIES kindMpiComm, PERI (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. ---*/ + * may try to update the same point. ---*/ SU2_OMP_FOR_STAT(32) for (size_t iVertex = 0; iVertex < geometry.GetnVertex(iMarker); ++iVertex) { @@ -263,8 +266,11 @@ void computeHessiansGreenGauss(CSolver* solver, MPI_QUANTITIES kindMpiComm, PERI for (size_t iVar = varBegin; iVar < varEnd; iVar++) { su2double flux = gradient(iPoint, iVar, jDim) / volume; for (size_t iDim = 0; iDim < nDim; ++iDim) { - size_t ind = (iDim <= jDim) ? iDim * nDim - ((iDim - 1) * iDim)/2 + jDim - iDim - : jDim * nDim - ((jDim - 1) * jDim)/2 + iDim - jDim; + /*--- Make the Hessian symmetric by construction. + * Get the 1D index for lower triangular storage. ---*/ + const auto iMax = max(iDim, jDim); + const auto iMin = min(iDim, jDim); + const size_t ind = iMax * (iMax + 1) / 2 + iMin; diagScale = (iDim == jDim)? 1.0 : 0.5; hessian(iPoint, iVar, ind) -= diagScale * flux * area[iDim]; } // iDims diff --git a/SU2_CFD/include/metrics/computeMetrics.hpp b/SU2_CFD/include/metrics/computeMetrics.hpp index e8070156764d..1abea265c5ad 100644 --- a/SU2_CFD/include/metrics/computeMetrics.hpp +++ b/SU2_CFD/include/metrics/computeMetrics.hpp @@ -49,9 +49,9 @@ namespace tensor { break; } case 3: { - mat[0][0] = metric_field(iPoint, 0); mat[0][1] = metric_field(iPoint, 1); mat[0][2] = metric_field(iPoint, 2); - mat[1][0] = metric_field(iPoint, 1); mat[1][1] = metric_field(iPoint, 3); mat[1][2] = metric_field(iPoint, 4); - mat[2][0] = metric_field(iPoint, 2); mat[2][1] = metric_field(iPoint, 4); mat[2][2] = metric_field(iPoint, 5); + 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; } } @@ -71,8 +71,8 @@ namespace tensor { case 3: { metric_field(iPoint, 0) = mat[0][0] * scale; metric_field(iPoint, 1) = mat[0][1] * scale; - metric_field(iPoint, 2) = mat[0][2] * scale; - metric_field(iPoint, 3) = mat[1][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; @@ -92,9 +92,9 @@ namespace tensor { 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, 2); - mat[1][0] = metric_field(iPoint, iSensor, 1); mat[1][1] = metric_field(iPoint, iSensor, 3); mat[1][2] = metric_field(iPoint, iSensor, 4); - mat[2][0] = metric_field(iPoint, iSensor, 2); mat[2][1] = metric_field(iPoint, iSensor, 4); mat[2][2] = metric_field(iPoint, iSensor, 5); + 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; } } @@ -114,8 +114,8 @@ namespace tensor { 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[0][2] * scale; - metric_field(iPoint, iSensor, 3) = mat[1][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; @@ -221,8 +221,8 @@ ScalarType integrateMetrics(CGeometry& geometry, const CConfig& config, } else if constexpr (nDim == 3) { const ScalarType m00 = metric(iPoint, 0); const ScalarType m01 = metric(iPoint, 1); - const ScalarType m02 = metric(iPoint, 2); - const ScalarType m11 = metric(iPoint, 3); + 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); From eff8e0492819753b27463b91e4754df8c9dd54b1 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Sat, 11 Apr 2026 17:09:49 -0700 Subject: [PATCH 24/36] Remove namespace indentation --- SU2_CFD/include/metrics/computeMetrics.hpp | 152 ++++++++++----------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/SU2_CFD/include/metrics/computeMetrics.hpp b/SU2_CFD/include/metrics/computeMetrics.hpp index 1abea265c5ad..9816bff3575c 100644 --- a/SU2_CFD/include/metrics/computeMetrics.hpp +++ b/SU2_CFD/include/metrics/computeMetrics.hpp @@ -38,92 +38,92 @@ #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; - } +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; - } + 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; - } + } +}; + +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; - } + 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 { From 017b3b7e090fd0291aa30a7b003ef087c9e78f20 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Sat, 11 Apr 2026 17:27:36 -0700 Subject: [PATCH 25/36] Merge upstream submodules --- externals/opdi | 2 +- subprojects/MLPCpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/externals/opdi b/externals/opdi index a5e2ac47035b..294807b0111c 160000 --- a/externals/opdi +++ b/externals/opdi @@ -1 +1 @@ -Subproject commit a5e2ac47035b6b3663f60d5f80b7a9fe62084867 +Subproject commit 294807b0111ce241cda97db62f80cdd5012d9381 diff --git a/subprojects/MLPCpp b/subprojects/MLPCpp index e19ca0cafb28..02f2cb9dde79 160000 --- a/subprojects/MLPCpp +++ b/subprojects/MLPCpp @@ -1 +1 @@ -Subproject commit e19ca0cafb28c4b7ba5b8cffef42883259b00dc0 +Subproject commit 02f2cb9dde791074858e11ac091f7c4df9c6af65 From c70dcf2625ec9a988055994aa41461eed7a27114 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Sat, 11 Apr 2026 17:37:26 -0700 Subject: [PATCH 26/36] Remove unused import --- TestCases/mesh_adaptation/metric/custom/metric.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/TestCases/mesh_adaptation/metric/custom/metric.py b/TestCases/mesh_adaptation/metric/custom/metric.py index 893daf4c0d76..42c13c22a500 100644 --- a/TestCases/mesh_adaptation/metric/custom/metric.py +++ b/TestCases/mesh_adaptation/metric/custom/metric.py @@ -1,5 +1,3 @@ -import math - from mpi4py import MPI import pysu2 From d0c2d55271841318fa3fcd0d172706e511aa836d Mon Sep 17 00:00:00 2001 From: bmunguia Date: Sat, 11 Apr 2026 17:53:37 -0700 Subject: [PATCH 27/36] Separate Hessian inner loop into diagonal and off-diagonal --- .../gradients/computeGradientsGreenGauss.hpp | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp b/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp index 5fed79ed14bc..d69af1d7edc1 100644 --- a/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp +++ b/SU2_CFD/include/gradients/computeGradientsGreenGauss.hpp @@ -193,7 +193,6 @@ void computeHessiansGreenGauss(CSolver* solver, MPI_QUANTITIES kindMpiComm, PERI #endif const size_t nSymMat = 3 * (nDim - 1); - su2double diagScale; /*--- For each (non-halo) volume integrate over its faces (edges). ---*/ @@ -224,20 +223,26 @@ void computeHessiansGreenGauss(CSolver* solver, MPI_QUANTITIES kindMpiComm, PERI const auto area = geometry.edges->GetNormal(iEdge); - for (size_t jDim = 0; jDim < nDim; ++jDim) { - for (size_t iVar = varBegin; iVar < varEnd; ++iVar) { - su2double flux = weight * (gradient(iPoint, iVar, jDim) + gradient(jPoint, iVar, jDim)); - for (size_t iDim = 0; iDim < nDim; ++iDim) { - /*--- Make the Hessian symmetric by construction. - * Get the 1D index for lower triangular storage. ---*/ - const auto iMax = max(iDim, jDim); - const auto iMin = min(iDim, jDim); - const size_t ind = iMax * (iMax + 1) / 2 + iMin; - diagScale = (iDim == jDim)? 1.0 : 0.5; - hessian(iPoint, iVar, ind) += diagScale * flux * area[iDim]; - } // iDims - } // variables - } // jDims + 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 @@ -262,20 +267,26 @@ void computeHessiansGreenGauss(CSolver* solver, MPI_QUANTITIES kindMpiComm, PERI su2double volume = nodes->GetVolume(iPoint) + nodes->GetPeriodicVolume(iPoint); const auto area = geometry.vertex[iMarker][iVertex]->GetNormal(); - for (size_t jDim = 0; jDim < nDim; ++jDim) { - for (size_t iVar = varBegin; iVar < varEnd; iVar++) { - su2double flux = gradient(iPoint, iVar, jDim) / volume; - for (size_t iDim = 0; iDim < nDim; ++iDim) { - /*--- Make the Hessian symmetric by construction. - * Get the 1D index for lower triangular storage. ---*/ - const auto iMax = max(iDim, jDim); - const auto iMin = min(iDim, jDim); - const size_t ind = iMax * (iMax + 1) / 2 + iMin; - diagScale = (iDim == jDim)? 1.0 : 0.5; - hessian(iPoint, iVar, ind) -= diagScale * flux * area[iDim]; - } // iDims - } // variables - } // jDims + 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 From c1ea0c45817c8af96a85137b1fdda3f3929a8136 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 13 Apr 2026 13:50:34 -0700 Subject: [PATCH 28/36] Rename "Derived" to "Computed" sensors --- Common/include/option_structure.hpp | 4 +- SU2_CFD/include/metrics/metricUtils.hpp | 46 +++++++++++------------ SU2_CFD/include/solvers/CSolver.hpp | 6 +-- SU2_CFD/src/drivers/CSinglezoneDriver.cpp | 4 +- SU2_CFD/src/solvers/CSolver.cpp | 8 ++-- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/Common/include/option_structure.hpp b/Common/include/option_structure.hpp index 766c2f42c904..8aab855b190d 100644 --- a/Common/include/option_structure.hpp +++ b/Common/include/option_structure.hpp @@ -2882,9 +2882,9 @@ static const MapType Deform_Kind_Map = { /*! * \brief Type of sensor for anisotropic metrics. */ -enum class SensorType : unsigned char { +enum class SensorType { PRIMITIVE, /*!< \brief Value read directly from the primitive variable array. */ - DERIVED, /*!< \brief Officially-supported computed quantity (e.g. Mach number). */ + COMPUTED, /*!< \brief Officially-supported computed quantity (e.g. Mach number). */ CUSTOM, /*!< \brief User-defined sensor populated externally via the Python wrapper. */ }; diff --git a/SU2_CFD/include/metrics/metricUtils.hpp b/SU2_CFD/include/metrics/metricUtils.hpp index 6768399f8822..a6e74ba8351c 100644 --- a/SU2_CFD/include/metrics/metricUtils.hpp +++ b/SU2_CFD/include/metrics/metricUtils.hpp @@ -42,9 +42,9 @@ namespace MetricUtils { /*! - * \brief Build a map of supported derived sensor names to their point-wise evaluator lambdas. + * \brief Build a map of supported computed sensor names to their point-wise evaluator lambdas. * - * Derived sensors are officially-supported computed quantities that are not stored directly + * Computed 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) @@ -55,13 +55,13 @@ namespace MetricUtils { * \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. + * \return Map of computed sensor names to evaluator lambdas. */ inline std::map> -DerivedNameToFunctionMap(const std::map& primitive_map, - unsigned long nDim, bool incompressible) { +ComputedNameToFunctionMap(const std::map& primitive_map, + unsigned long nDim, bool incompressible) { using SensorFn = std::function; - std::map derived_map; + std::map computed_map; const unsigned short vel_x = primitive_map.at("VELOCITY_X"); const unsigned short vel_y = primitive_map.at("VELOCITY_Y"); @@ -69,7 +69,7 @@ DerivedNameToFunctionMap(const std::map& primitive_ : 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 { + computed_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); @@ -78,14 +78,14 @@ DerivedNameToFunctionMap(const std::map& primitive_ /*--- 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 { + computed_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; + return computed_map; } /*! @@ -116,9 +116,9 @@ inline bool ResolveSensorIndices( /*--- 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 ---*/ + * computed_map: name -> evaluator lambda for COMPUTED sensors ---*/ std::map> var_map; - std::map> derived_map; + std::map> computed_map; for (unsigned short iSol = 0; iSol < MAX_SOLS; iSol++) { if (solver_container[iSol] == nullptr) continue; @@ -146,9 +146,9 @@ inline bool ResolveSensorIndices( 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); + /*--- Build computed sensor map. VELOCITY is supported by all flow solvers; + * MACH is excluded for incompressible (see ComputedNameToFunctionMap). ---*/ + computed_map = ComputedNameToFunctionMap(primitive_map, nDim, incompressible); } else { /*--- For non-flow solvers, use solution fields ---*/ std::vector solution_fields = solver_container[iSol]->GetSolutionFields(); @@ -171,9 +171,9 @@ inline bool ResolveSensorIndices( /*--- 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). + * PRIMITIVE: index into primitive variable array (prim_idx is valid). + * COMPUTED: 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; @@ -190,17 +190,17 @@ inline bool ResolveSensorIndices( << iSol << ", variable index " << var_index << std::endl; } } else { - auto dit = derived_map.find(sensor_name); - if (dit != derived_map.end()) { - /*--- DERIVED sensor ---*/ + auto dit = computed_map.find(sensor_name); + if (dit != computed_map.end()) { + /*--- COMPUTED sensor ---*/ sensors_by_solver[FLOW_SOL].push_back( {std::numeric_limits::max(), sensor_name, - SensorType::DERIVED, dit->second}); + SensorType::COMPUTED, dit->second}); if (rank == MASTER_NODE) { - std::cout << " Derived sensor '" << sensor_name << "' registered." << std::endl; + std::cout << " Computed sensor '" << sensor_name << "' registered." << std::endl; } } else { - /*--- CUSTOM sensor — must be populated via Python wrapper ---*/ + /*--- CUSTOM sensor (must be populated via Python wrapper) ---*/ if (rank == MASTER_NODE) { std::cout << " Custom sensor '" << sensor_name << "' detected." << std::endl; } diff --git a/SU2_CFD/include/solvers/CSolver.hpp b/SU2_CFD/include/solvers/CSolver.hpp index 366df11e03a8..4a9d19a1eba1 100644 --- a/SU2_CFD/include/solvers/CSolver.hpp +++ b/SU2_CFD/include/solvers/CSolver.hpp @@ -214,7 +214,7 @@ class CSolver { 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. */ + std::function fn; /*!< \brief Point-wise evaluator for COMPUTED sensors. */ }; vector MetricSensors; /*!< \brief Sensor list in config order, one entry per sensor. */ @@ -600,11 +600,11 @@ class CSolver { virtual void SetPrimitive_SensorAdapt(CGeometry *geometry, const CConfig *config); /*! - * \brief Evaluate DERIVED sensors (e.g. Mach number) and store results in Sensor_Adapt. + * \brief Evaluate COMPUTED 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); + virtual void SetComputed_SensorAdapt(CGeometry *geometry, const CConfig *config); /*! * \brief Allocate Gradient_Adapt and Hessian arrays for the sensors assigned to this solver. diff --git a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp index c8fc42cdbdc9..ade324b70045 100644 --- a/SU2_CFD/src/drivers/CSinglezoneDriver.cpp +++ b/SU2_CFD/src/drivers/CSinglezoneDriver.cpp @@ -375,11 +375,11 @@ void CSinglezoneDriver::ComputeMetricField(bool restartMetric) { cout << "Storing primitive variables needed for gradients in metric." << endl; } - /*--- Populate primitive and derived sensor slots in Sensor_Adapt. + /*--- Populate primitive and computed 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->SetComputed_SensorAdapt(geometry, config); solver_flow->InitiateComms(geometry, config, MPI_QUANTITIES::SENSOR_ADAPT); solver_flow->CompleteComms(geometry, config, MPI_QUANTITIES::SENSOR_ADAPT); diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index 9f8c8b7dd8a4..431597adae45 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -2228,7 +2228,7 @@ void CSolver::AllocateMetricSensorArrays(unsigned short 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. ---*/ + * COMPUTED and CUSTOM slots are skipped here. ---*/ for (size_t iSensor = 0; iSensor < MetricSensors.size(); ++iSensor) { if (MetricSensors[iSensor].type != SensorType::PRIMITIVE) continue; @@ -2241,11 +2241,11 @@ void CSolver::SetPrimitive_SensorAdapt(CGeometry *geometry, const CConfig *confi } } -void CSolver::SetDerived_SensorAdapt(CGeometry *geometry, const CConfig *config) { - /*--- Evaluate DERIVED sensors via their stored lambdas (e.g. Mach number). +void CSolver::SetComputed_SensorAdapt(CGeometry *geometry, const CConfig *config) { + /*--- Evaluate COMPUTED 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; + if (MetricSensors[iSensor].type != SensorType::COMPUTED) continue; const auto& fn = MetricSensors[iSensor].fn; SU2_OMP_FOR_STAT(omp_chunk_size) From f5089339644208e56c6f54d593000d5b3842f3a4 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 13 Apr 2026 14:22:39 -0700 Subject: [PATCH 29/36] Fix sensor allocation --- SU2_CFD/include/variables/CVariable.hpp | 22 ++------------------- SU2_CFD/src/variables/CFlowVariable.cpp | 7 ------- SU2_CFD/src/variables/CVariable.cpp | 26 +++++++++++++++++++------ 3 files changed, 22 insertions(+), 33 deletions(-) diff --git a/SU2_CFD/include/variables/CVariable.hpp b/SU2_CFD/include/variables/CVariable.hpp index 707de335e5d6..5247de9cfd8a 100644 --- a/SU2_CFD/include/variables/CVariable.hpp +++ b/SU2_CFD/include/variables/CVariable.hpp @@ -2491,26 +2491,8 @@ class CVariable { 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. + * \brief Allocate Sensor_Adapt, Gradient_Adapt, Hessian, and Metric 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; - } - } + void AllocateMetricSensorArrays(unsigned short nSensors); }; diff --git a/SU2_CFD/src/variables/CFlowVariable.cpp b/SU2_CFD/src/variables/CFlowVariable.cpp index ac548611dc14..06f90656450c 100644 --- a/SU2_CFD/src/variables/CFlowVariable.cpp +++ b/SU2_CFD/src/variables/CFlowVariable.cpp @@ -97,13 +97,6 @@ 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 81640adc4a93..f7456f5c3824 100644 --- a/SU2_CFD/src/variables/CVariable.cpp +++ b/SU2_CFD/src/variables/CVariable.cpp @@ -81,12 +81,6 @@ 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() { @@ -141,3 +135,23 @@ void CVariable::RegisterSolution_time_n1() { void CVariable::RegisterUserDefinedSource() { RegisterContainer(true, UserDefinedSource); } + +void CVariable::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) = su2double(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; + } +} \ No newline at end of file From 49b16819ff3c80a064387e6552f08e2c6dee1ec7 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 13 Apr 2026 15:00:16 -0700 Subject: [PATCH 30/36] Resolve more compilation issues --- SU2_CFD/include/metrics/computeMetrics.hpp | 2 +- SU2_CFD/src/solvers/CSolver.cpp | 61 ++++++++++------------ 2 files changed, 30 insertions(+), 33 deletions(-) diff --git a/SU2_CFD/include/metrics/computeMetrics.hpp b/SU2_CFD/include/metrics/computeMetrics.hpp index 9816bff3575c..ecb496f495b0 100644 --- a/SU2_CFD/include/metrics/computeMetrics.hpp +++ b/SU2_CFD/include/metrics/computeMetrics.hpp @@ -285,7 +285,7 @@ void normalizeMetrics(CGeometry& geometry, const CConfig& config, /*--- Clip by user-specified aspect ratio ---*/ unsigned short iMax = 0; - for (auto iDim = 1; iDim < nDim; ++iDim) + for (auto iDim = 1u; iDim < nDim; ++iDim) iMax = (EigVal[iDim] > EigVal[iMax])? iDim : iMax; for (auto iDim = 0u; iDim < nDim; ++iDim) diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index 431597adae45..d633c728a8e3 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -2233,7 +2233,7 @@ void CSolver::SetPrimitive_SensorAdapt(CGeometry *geometry, const CConfig *confi if (MetricSensors[iSensor].type != SensorType::PRIMITIVE) continue; const auto prim_idx = MetricSensors[iSensor].prim_idx; - SU2_OMP_FOR_STAT(omp_chunk_size) + SU2_OMP_FOR_DYN(256) for (unsigned long iPoint = 0; iPoint < nPoint; iPoint++) { base_nodes->SetSensor_Adapt(iPoint, iSensor, base_nodes->GetPrimitive(iPoint, prim_idx)); } @@ -2248,7 +2248,7 @@ void CSolver::SetComputed_SensorAdapt(CGeometry *geometry, const CConfig *config if (MetricSensors[iSensor].type != SensorType::COMPUTED) continue; const auto& fn = MetricSensors[iSensor].fn; - SU2_OMP_FOR_STAT(omp_chunk_size) + SU2_OMP_FOR_DYN(256) for (unsigned long iPoint = 0; iPoint < nPoint; iPoint++) { base_nodes->SetSensor_Adapt(iPoint, iSensor, fn(base_nodes->GetPrimitive(iPoint))); } @@ -4479,51 +4479,48 @@ void CSolver::SavelibROM(CGeometry *geometry, CConfig *config, bool converged) { 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 + BEGIN_SU2_OMP_SAFE_GLOBAL_ACCESS + { /*--- 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; + if (iSensor == 0) { + /*--- 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 + } // iSensor == 0 + } // BEGIN_SU2_OMP_SAFE_GLOBAL_ACCESS + END_SU2_OMP_SAFE_GLOBAL_ACCESS } } From 82bee37a677e42fb0ff549cdd37ec82273fb747c Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 13 Apr 2026 15:12:28 -0700 Subject: [PATCH 31/36] More bug fixes --- SU2_CFD/include/metrics/computeMetrics.hpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/SU2_CFD/include/metrics/computeMetrics.hpp b/SU2_CFD/include/metrics/computeMetrics.hpp index ecb496f495b0..a3f33479c9df 100644 --- a/SU2_CFD/include/metrics/computeMetrics.hpp +++ b/SU2_CFD/include/metrics/computeMetrics.hpp @@ -33,6 +33,7 @@ #include #include #include +#include "../../../Common/include/parallelization/mpi_structure.hpp" #include "../../../Common/include/parallelization/omp_structure.hpp" #include "../../../Common/include/linear_algebra/blas_structure.hpp" #include "../../../Common/include/toolboxes/geometry_toolbox.hpp" @@ -171,7 +172,7 @@ void setPositiveDefiniteMetrics(CGeometry& geometry, const CConfig& config, /*--- 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++) { + for (auto iDim = 0u; iDim < nDim; iDim++) { if (std::isnan(EigVal[iDim])) { /*--- NaN detected, set to small positive value ---*/ EigVal[iDim] = eps; @@ -233,7 +234,7 @@ ScalarType integrateMetrics(CGeometry& geometry, const CConfig& config, localIntegral += pow(abs(det), normExp) * Vol; } - CBaseMPIWrapper::Allreduce(&localIntegral, &globalIntegral, 1, MPI_DOUBLE, MPI_SUM, SU2_MPI::GetComm()); + SU2_MPI::Allreduce(&localIntegral, &globalIntegral, 1, MPI_DOUBLE, MPI_SUM, SU2_MPI::GetComm()); return globalIntegral; } From e9677a5ff661dcd2db0cc8b2594774d27aa68d60 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 13 Apr 2026 15:23:21 -0700 Subject: [PATCH 32/36] More bug fixes --- SU2_CFD/include/metrics/computeMetrics.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SU2_CFD/include/metrics/computeMetrics.hpp b/SU2_CFD/include/metrics/computeMetrics.hpp index a3f33479c9df..e594a15f626a 100644 --- a/SU2_CFD/include/metrics/computeMetrics.hpp +++ b/SU2_CFD/include/metrics/computeMetrics.hpp @@ -333,7 +333,7 @@ void setPositiveDefiniteMetrics(CGeometry& geometry, const CConfig& config, template ScalarType integrateMetrics(CGeometry& geometry, const CConfig& config, unsigned short iSensor, MetricType& metric) { - su2double integral; + su2double integral = 0.0; switch (geometry.GetnDim()) { case 2: integral = detail::integrateMetrics<2, ScalarType>(geometry, config, iSensor, metric); From 6769658c6118ce59c13a05db48bba493546bc155 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 13 Apr 2026 15:40:18 -0700 Subject: [PATCH 33/36] More compiler warning fixes --- SU2_CFD/include/metrics/metricUtils.hpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/SU2_CFD/include/metrics/metricUtils.hpp b/SU2_CFD/include/metrics/metricUtils.hpp index a6e74ba8351c..06f6f8e000eb 100644 --- a/SU2_CFD/include/metrics/metricUtils.hpp +++ b/SU2_CFD/include/metrics/metricUtils.hpp @@ -176,7 +176,6 @@ inline bool ResolveSensorIndices( * 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()) { @@ -204,7 +203,6 @@ inline bool ResolveSensorIndices( 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, {}}); From bb679ea658746ed74095a5956ea48914cf59b150 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 13 Apr 2026 15:51:41 -0700 Subject: [PATCH 34/36] More bug fixes --- SU2_CFD/include/metrics/computeMetrics.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SU2_CFD/include/metrics/computeMetrics.hpp b/SU2_CFD/include/metrics/computeMetrics.hpp index e594a15f626a..a185b305f693 100644 --- a/SU2_CFD/include/metrics/computeMetrics.hpp +++ b/SU2_CFD/include/metrics/computeMetrics.hpp @@ -333,7 +333,7 @@ void setPositiveDefiniteMetrics(CGeometry& geometry, const CConfig& config, template ScalarType integrateMetrics(CGeometry& geometry, const CConfig& config, unsigned short iSensor, MetricType& metric) { - su2double integral = 0.0; + ScalarType integral = ScalarType(0.0); switch (geometry.GetnDim()) { case 2: integral = detail::integrateMetrics<2, ScalarType>(geometry, config, iSensor, metric); From 6f33d2e8406b5150a65bef640f87dc2f754687a9 Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 13 Apr 2026 17:25:01 -0700 Subject: [PATCH 35/36] Fix divide-by-zero when Compute_Metric False --- SU2_CFD/src/solvers/CAdjEulerSolver.cpp | 1 + SU2_CFD/src/solvers/CAdjTurbSolver.cpp | 1 + SU2_CFD/src/solvers/CBaselineSolver_FEM.cpp | 1 + SU2_CFD/src/solvers/CDiscAdjFEASolver.cpp | 1 + SU2_CFD/src/solvers/CDiscAdjMeshSolver.cpp | 1 + SU2_CFD/src/solvers/CHeatSolver.cpp | 1 + SU2_CFD/src/solvers/CSolver.cpp | 6 ++---- SU2_CFD/src/solvers/CSpeciesSolver.cpp | 1 + SU2_CFD/src/solvers/CTransLMSolver.cpp | 1 + SU2_CFD/src/solvers/CTurbSSTSolver.cpp | 1 + 10 files changed, 11 insertions(+), 4 deletions(-) diff --git a/SU2_CFD/src/solvers/CAdjEulerSolver.cpp b/SU2_CFD/src/solvers/CAdjEulerSolver.cpp index bfe558952e84..b438682c61ef 100644 --- a/SU2_CFD/src/solvers/CAdjEulerSolver.cpp +++ b/SU2_CFD/src/solvers/CAdjEulerSolver.cpp @@ -92,6 +92,7 @@ CAdjEulerSolver::CAdjEulerSolver(CGeometry *geometry, CConfig *config, unsigned /*--- Define geometry constans in the solver structure ---*/ nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); nMarker = config->GetnMarker_All(); nPoint = geometry->GetnPoint(); nPointDomain = geometry->GetnPointDomain(); diff --git a/SU2_CFD/src/solvers/CAdjTurbSolver.cpp b/SU2_CFD/src/solvers/CAdjTurbSolver.cpp index 4e331049c020..f83263db75b2 100644 --- a/SU2_CFD/src/solvers/CAdjTurbSolver.cpp +++ b/SU2_CFD/src/solvers/CAdjTurbSolver.cpp @@ -36,6 +36,7 @@ CAdjTurbSolver::CAdjTurbSolver(CGeometry *geometry, CConfig *config, unsigned sh adjoint = true; nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); Gamma = config->GetGamma(); Gamma_Minus_One = Gamma - 1.0; diff --git a/SU2_CFD/src/solvers/CBaselineSolver_FEM.cpp b/SU2_CFD/src/solvers/CBaselineSolver_FEM.cpp index 5990270c85d4..f8b4d828a8b1 100644 --- a/SU2_CFD/src/solvers/CBaselineSolver_FEM.cpp +++ b/SU2_CFD/src/solvers/CBaselineSolver_FEM.cpp @@ -37,6 +37,7 @@ CBaselineSolver_FEM::CBaselineSolver_FEM(CGeometry *geometry, CConfig *config) { /*--- Define geometry constants in the solver structure ---*/ nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); /*--- Create an object of the class CMeshFEM_DG and retrieve the necessary geometrical information for the FEM DG solver. If necessary, it is diff --git a/SU2_CFD/src/solvers/CDiscAdjFEASolver.cpp b/SU2_CFD/src/solvers/CDiscAdjFEASolver.cpp index d7177bbb50ce..9be866665184 100644 --- a/SU2_CFD/src/solvers/CDiscAdjFEASolver.cpp +++ b/SU2_CFD/src/solvers/CDiscAdjFEASolver.cpp @@ -39,6 +39,7 @@ CDiscAdjFEASolver::CDiscAdjFEASolver(CGeometry *geometry, CConfig *config, CSolv nVar = dynamic? 3*direct_solver->GetnVar() : direct_solver->GetnVar(); nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); /*-- Store some information about direct solver ---*/ this->KindDirect_Solver = Kind_Solver; diff --git a/SU2_CFD/src/solvers/CDiscAdjMeshSolver.cpp b/SU2_CFD/src/solvers/CDiscAdjMeshSolver.cpp index cdfade30aa55..6926746a8e7c 100644 --- a/SU2_CFD/src/solvers/CDiscAdjMeshSolver.cpp +++ b/SU2_CFD/src/solvers/CDiscAdjMeshSolver.cpp @@ -33,6 +33,7 @@ CDiscAdjMeshSolver::CDiscAdjMeshSolver(CGeometry *geometry, CConfig *config, CSo nVar = geometry->GetnDim(); nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); /*-- Store some information about direct solver ---*/ this->direct_solver = direct_solver; diff --git a/SU2_CFD/src/solvers/CHeatSolver.cpp b/SU2_CFD/src/solvers/CHeatSolver.cpp index 49235895307d..a1d2447be36f 100644 --- a/SU2_CFD/src/solvers/CHeatSolver.cpp +++ b/SU2_CFD/src/solvers/CHeatSolver.cpp @@ -52,6 +52,7 @@ CHeatSolver::CHeatSolver(CGeometry *geometry, CConfig *config, unsigned short iM /*--- Define geometry constants in the solver structure ---*/ nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); /*--- Define some structures for locating max residuals ---*/ diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index d633c728a8e3..40d6ad11ea09 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -1463,7 +1463,6 @@ 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); @@ -1542,7 +1541,7 @@ void CSolver::InitiateComms(CGeometry *geometry, bufDSend[buf_offset+iVar*nDim+iDim] = gradient(iPoint, iVar, iDim); break; case MPI_QUANTITIES::HESSIAN: - for (iVar = 0; iVar < nVarHess; iVar++) + for (iVar = 0; iVar < GetnMetricSensor(); iVar++) for (iMat = 0; iMat < nSymMat; iMat++) bufDSend[buf_offset+iVar*nSymMat+iMat] = gradient(iPoint, iVar, iMat); break; @@ -1612,7 +1611,6 @@ 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); @@ -1702,7 +1700,7 @@ void CSolver::CompleteComms(CGeometry *geometry, gradient(iPoint,iVar,iDim) = bufDRecv[buf_offset+iVar*nDim+iDim]; break; case MPI_QUANTITIES::HESSIAN: - for (iVar = 0; iVar < nVarHess; iVar++) + for (iVar = 0; iVar < GetnMetricSensor(); iVar++) for (iMat = 0; iMat < nSymMat; iMat++) gradient(iPoint, iVar, iMat) = bufDRecv[buf_offset+iVar*nSymMat+iMat]; break; diff --git a/SU2_CFD/src/solvers/CSpeciesSolver.cpp b/SU2_CFD/src/solvers/CSpeciesSolver.cpp index 4e629d1d2ada..a5151cb19cac 100644 --- a/SU2_CFD/src/solvers/CSpeciesSolver.cpp +++ b/SU2_CFD/src/solvers/CSpeciesSolver.cpp @@ -112,6 +112,7 @@ void CSpeciesSolver::Initialize(CGeometry* geometry, CConfig* config, unsigned s /*--- Define geometry constants in the solver structure ---*/ nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); AllocVectorOfMatrices( nVertex, nVar,CustomBoundaryScalar); diff --git a/SU2_CFD/src/solvers/CTransLMSolver.cpp b/SU2_CFD/src/solvers/CTransLMSolver.cpp index 21469b71d507..f2576398d783 100644 --- a/SU2_CFD/src/solvers/CTransLMSolver.cpp +++ b/SU2_CFD/src/solvers/CTransLMSolver.cpp @@ -60,6 +60,7 @@ CTransLMSolver::CTransLMSolver(CGeometry *geometry, CConfig *config, unsigned sh /*--- Define geometry constants in the solver structure ---*/ nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); /*--- Define variables needed for transition from config file */ options = config->GetLMParsedOptions(); diff --git a/SU2_CFD/src/solvers/CTurbSSTSolver.cpp b/SU2_CFD/src/solvers/CTurbSSTSolver.cpp index 4f1212d62867..1ace6dc254a8 100644 --- a/SU2_CFD/src/solvers/CTurbSSTSolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSSTSolver.cpp @@ -56,6 +56,7 @@ CTurbSSTSolver::CTurbSSTSolver(CGeometry *geometry, CConfig *config, unsigned sh /*--- Define geometry constants in the solver structure ---*/ nDim = geometry->GetnDim(); + nSymMat = 3 * (nDim - 1); /*--- Single grid simulation ---*/ From 83a29b5b0d6f0d95788893f13e740e32bd0e9d8a Mon Sep 17 00:00:00 2001 From: bmunguia Date: Mon, 13 Apr 2026 18:35:39 -0700 Subject: [PATCH 36/36] Address CodeQL complaints --- Common/src/CConfig.cpp | 4 +--- SU2_CFD/src/solvers/CSolver.cpp | 16 ++++++++++------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Common/src/CConfig.cpp b/Common/src/CConfig.cpp index fa7712f3d042..7611282a5956 100644 --- a/Common/src/CConfig.cpp +++ b/Common/src/CConfig.cpp @@ -7995,9 +7995,7 @@ void CConfig::SetOutput(SU2_COMPONENT val_software, unsigned short val_izone) { if (iSensor < nMetric_Sensor - 1 ) cout << ", "; } cout << endl; - switch (Kind_Hessian_Method) { - case GREEN_GAUSS: cout << "Hessian for adaptive metric: Green-Gauss." << endl; break; - } + cout << "Hessian for adaptive metric: Green-Gauss." << endl; if (Normalize_Metric) { cout << "Target complexity: " << Metric_Complexity << endl; if (TimeMarching != TIME_MARCHING::STEADY) { diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp index 40d6ad11ea09..204a03807621 100644 --- a/SU2_CFD/src/solvers/CSolver.cpp +++ b/SU2_CFD/src/solvers/CSolver.cpp @@ -1462,7 +1462,7 @@ void CSolver::InitiateComms(CGeometry *geometry, /*--- Handle the different types of gradient and limiter. ---*/ - const auto nVarGrad = COUNT_PER_POINT / nDim; + const unsigned short nVarGrad = COUNT_PER_POINT / nDim; auto& gradient = CommHelpers::selectGradient(base_nodes, commType); auto& limiter = CommHelpers::selectLimiter(base_nodes, commType); @@ -1542,8 +1542,10 @@ void CSolver::InitiateComms(CGeometry *geometry, break; case MPI_QUANTITIES::HESSIAN: for (iVar = 0; iVar < GetnMetricSensor(); iVar++) - for (iMat = 0; iMat < nSymMat; iMat++) - bufDSend[buf_offset+iVar*nSymMat+iMat] = gradient(iPoint, iVar, iMat); + for (iMat = 0; iMat < nSymMat; iMat++) { + const auto buf_idx = buf_offset + static_cast(iVar)*nSymMat + iMat; + bufDSend[buf_idx] = gradient(iPoint, iVar, iMat); + } break; case MPI_QUANTITIES::SOLUTION_FEA: for (iVar = 0; iVar < nVar; iVar++) { @@ -1610,7 +1612,7 @@ void CSolver::CompleteComms(CGeometry *geometry, /*--- Handle the different types of gradient and limiter. ---*/ - const auto nVarGrad = COUNT_PER_POINT / nDim; + const unsigned short nVarGrad = COUNT_PER_POINT / nDim; auto& gradient = CommHelpers::selectGradient(base_nodes, commType); auto& limiter = CommHelpers::selectLimiter(base_nodes, commType); @@ -1701,8 +1703,10 @@ void CSolver::CompleteComms(CGeometry *geometry, break; case MPI_QUANTITIES::HESSIAN: for (iVar = 0; iVar < GetnMetricSensor(); iVar++) - for (iMat = 0; iMat < nSymMat; iMat++) - gradient(iPoint, iVar, iMat) = bufDRecv[buf_offset+iVar*nSymMat+iMat]; + for (iMat = 0; iMat < nSymMat; iMat++) { + const auto buf_idx = buf_offset + static_cast(iVar)*nSymMat + iMat; + gradient(iPoint, iVar, iMat) = bufDRecv[buf_idx]; + } break; case MPI_QUANTITIES::SOLUTION_FEA: for (iVar = 0; iVar < nVar; iVar++) {