diff --git a/SU2_CFD/src/numerics/flow/convection/roe.cpp b/SU2_CFD/src/numerics/flow/convection/roe.cpp index 73114576d40..3fd23d64458 100644 --- a/SU2_CFD/src/numerics/flow/convection/roe.cpp +++ b/SU2_CFD/src/numerics/flow/convection/roe.cpp @@ -334,9 +334,12 @@ void CUpwL2Roe_Flow::FinalizeResidual(su2double *val_residual, su2double **val_J delta_wave[2] = proj_delta_vel + delta_p/(RoeDensity*RoeSoundSpeed); delta_wave[3] = -proj_delta_vel + delta_p/(RoeDensity*RoeSoundSpeed); } else { - delta_wave[0] = delta_rho - delta_p/RoeSoundSpeed2; - delta_wave[1] = (UnitNormal[0]*delta_vel[2]-UnitNormal[2]*delta_vel[0])*zeta; - delta_wave[2] = (UnitNormal[1]*delta_vel[0]-UnitNormal[0]*delta_vel[1])*zeta; + /*--- The first three columns of the 3D P matrix combine the entropy wave (weighted by the normal) with the + shear waves, so their strengths are n_j (Delta rho - Delta p / c^2) + (Delta u x n)_j. ---*/ + const auto delta_entropy = delta_rho - delta_p/RoeSoundSpeed2; + delta_wave[0] = UnitNormal[0]*delta_entropy + (delta_vel[1]*UnitNormal[2]-delta_vel[2]*UnitNormal[1])*zeta; + delta_wave[1] = UnitNormal[1]*delta_entropy + (delta_vel[2]*UnitNormal[0]-delta_vel[0]*UnitNormal[2])*zeta; + delta_wave[2] = UnitNormal[2]*delta_entropy + (delta_vel[0]*UnitNormal[1]-delta_vel[1]*UnitNormal[0])*zeta; delta_wave[3] = proj_delta_vel + delta_p/(RoeDensity*RoeSoundSpeed); delta_wave[4] = -proj_delta_vel + delta_p/(RoeDensity*RoeSoundSpeed); } @@ -407,9 +410,12 @@ void CUpwLMRoe_Flow::FinalizeResidual(su2double *val_residual, su2double **val_J delta_wave[2] = proj_delta_vel + delta_p/(RoeDensity*RoeSoundSpeed); delta_wave[3] = -proj_delta_vel + delta_p/(RoeDensity*RoeSoundSpeed); } else { - delta_wave[0] = delta_rho - delta_p/RoeSoundSpeed2; - delta_wave[1] = (UnitNormal[0]*delta_vel[2]-UnitNormal[2]*delta_vel[0]); - delta_wave[2] = (UnitNormal[1]*delta_vel[0]-UnitNormal[0]*delta_vel[1]); + /*--- The first three columns of the 3D P matrix combine the entropy wave (weighted by the normal) with the + shear waves, so their strengths are n_j (Delta rho - Delta p / c^2) + (Delta u x n)_j. ---*/ + const auto delta_entropy = delta_rho - delta_p/RoeSoundSpeed2; + delta_wave[0] = UnitNormal[0]*delta_entropy + (delta_vel[1]*UnitNormal[2]-delta_vel[2]*UnitNormal[1]); + delta_wave[1] = UnitNormal[1]*delta_entropy + (delta_vel[2]*UnitNormal[0]-delta_vel[0]*UnitNormal[2]); + delta_wave[2] = UnitNormal[2]*delta_entropy + (delta_vel[0]*UnitNormal[1]-delta_vel[1]*UnitNormal[0]); delta_wave[3] = proj_delta_vel + delta_p/(RoeDensity*RoeSoundSpeed); delta_wave[4] = -proj_delta_vel + delta_p/(RoeDensity*RoeSoundSpeed); } @@ -811,8 +817,8 @@ CNumerics::ResidualType<> CUpwGeneralRoe_Flow::ComputeResidual(const CConfig* co for (iVar = 0; iVar < nVar; iVar++) { Flux[iVar] = 0.0; for (jVar = 0; jVar < nVar; jVar++) { - Jacobian_i[iVar][iVar] = 0.0; - Jacobian_j[iVar][iVar] = 0.0; + Jacobian_i[iVar][jVar] = 0.0; + Jacobian_j[iVar][jVar] = 0.0; } } AD::SetPreaccOut(Flux, nVar); @@ -901,9 +907,12 @@ CNumerics::ResidualType<> CUpwGeneralRoe_Flow::ComputeResidual(const CConfig* co delta_wave[2] = proj_delta_vel + delta_p/(RoeDensity*RoeSoundSpeed); delta_wave[3] = -proj_delta_vel + delta_p/(RoeDensity*RoeSoundSpeed); } else { - delta_wave[0] = delta_rho - delta_p/(RoeSoundSpeed*RoeSoundSpeed); - delta_wave[1] = UnitNormal[0]*delta_vel[2]-UnitNormal[2]*delta_vel[0]; - delta_wave[2] = UnitNormal[1]*delta_vel[0]-UnitNormal[0]*delta_vel[1]; + /*--- The first three columns of the 3D P matrix combine the entropy wave (weighted by the normal) with the + shear waves, so their strengths are n_j (Delta rho - Delta p / c^2) + (Delta u x n)_j. ---*/ + const auto delta_entropy = delta_rho - delta_p/(RoeSoundSpeed*RoeSoundSpeed); + delta_wave[0] = UnitNormal[0]*delta_entropy + delta_vel[1]*UnitNormal[2]-delta_vel[2]*UnitNormal[1]; + delta_wave[1] = UnitNormal[1]*delta_entropy + delta_vel[2]*UnitNormal[0]-delta_vel[0]*UnitNormal[2]; + delta_wave[2] = UnitNormal[2]*delta_entropy + delta_vel[0]*UnitNormal[1]-delta_vel[1]*UnitNormal[0]; delta_wave[3] = proj_delta_vel + delta_p/(RoeDensity*RoeSoundSpeed); delta_wave[4] = -proj_delta_vel + delta_p/(RoeDensity*RoeSoundSpeed); } diff --git a/UnitTests/SU2_CFD/numerics/roe_contact_tests.cpp b/UnitTests/SU2_CFD/numerics/roe_contact_tests.cpp new file mode 100644 index 00000000000..9c99a461f21 --- /dev/null +++ b/UnitTests/SU2_CFD/numerics/roe_contact_tests.cpp @@ -0,0 +1,125 @@ +/*! + * \file roe_contact_tests.cpp + * \brief Roe-type schemes must resolve a contact discontinuity and a shear layer exactly (upwind flux), in 2D + * and 3D, on faces not aligned with the axes. + * \version 8.5.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 . + */ + +#include "catch.hpp" +#include +#include +#include +#include "../../../SU2_CFD/include/numerics/flow/convection/roe.hpp" + +namespace { + +constexpr su2double gamma = 1.4; + +std::unique_ptr MakeConfig(bool implicit) { + std::stringstream options; + options << "SOLVER= EULER\nCONV_NUM_METHOD_FLOW= ROE\nENTROPY_FIX_COEFF= 0.0\nROE_KAPPA= 0.5\n"; + options << "TIME_DISCRE_FLOW= " << (implicit ? "EULER_IMPLICIT" : "EULER_EXPLICIT") << "\n"; + return std::make_unique(options, SU2_COMPONENT::SU2_CFD, false); +} + +/*--- Primitive (T, u, v, [w], p, rho, h, c) and secondary (dp/drho_e, dp/de_rho) variables of an ideal gas. ---*/ +void State(unsigned short nDim, su2double rho, const su2double* vel, su2double p, su2double* V, su2double* S) { + su2double q2 = 0.0; + for (unsigned short iDim = 0; iDim < nDim; iDim++) { + V[iDim + 1] = vel[iDim]; + q2 += vel[iDim] * vel[iDim]; + } + V[0] = p / (rho * 287.058); + V[nDim + 1] = p; + V[nDim + 2] = rho; + V[nDim + 3] = gamma / (gamma - 1) * p / rho + 0.5 * q2; + V[nDim + 4] = sqrt(gamma * p / rho); + S[0] = p / rho; + S[1] = (gamma - 1) * rho; +} + +template +std::unique_ptr MakeScheme(unsigned short nDim, const CConfig* config) { + return std::make_unique(nDim, nDim + 2, config); +} +template <> +std::unique_ptr MakeScheme(unsigned short nDim, const CConfig* config) { + return std::make_unique(nDim, nDim + 2, config, false); +} + +/*--- Two states with the same pressure and normal velocity, different density and (with shear) tangential + velocity: a contact discontinuity, whose exact (upwind) flux is the flux of the upstream state. ---*/ +template +void CheckContact(const char* name, unsigned short nDim, const su2double* vel, bool implicit, bool shear) { + auto config = MakeConfig(implicit); + auto numerics = MakeScheme(nDim, config.get()); + + const su2double p = 1.0, rho_i = 1.4, rho_j = 0.7; + const su2double n2[2] = {0.6, 0.8}, n3[3] = {2.0 / 7, 3.0 / 7, 6.0 / 7}; + const su2double* n = (nDim == 2) ? n2 : n3; + const su2double t2[2] = {0.16, -0.12}, t3[3] = {0.15, -0.1, 0.0}; // tangential velocity jumps + const su2double* t = (nDim == 2) ? t2 : t3; + su2double vel_j[3] = {0.0}; + for (unsigned short iDim = 0; iDim < nDim; iDim++) vel_j[iDim] = vel[iDim] + (shear ? t[iDim] : 0.0); + + su2double Vi[10] = {0.0}, Vj[10] = {0.0}, Si[2], Sj[2]; + State(nDim, rho_i, vel, p, Vi, Si); + State(nDim, rho_j, vel_j, p, Vj, Sj); + numerics->SetPrimitive(Vi, Vj); + numerics->SetSecondary(Si, Sj); + numerics->SetNormal(n); + const auto res = numerics->ComputeResidual(config.get()); + + su2double un = 0.0; + for (unsigned short iDim = 0; iDim < nDim; iDim++) un += vel[iDim] * n[iDim]; + const su2double rho_up = (un >= 0.0) ? rho_i : rho_j; + const su2double* V_up = (un >= 0.0) ? Vi : Vj; + + CAPTURE(name, nDim, un, implicit, shear); + CHECK(res.residual[0] == Approx(rho_up * un).margin(1e-12)); + for (unsigned short iDim = 0; iDim < nDim; iDim++) + CHECK(res.residual[iDim + 1] == Approx(rho_up * V_up[iDim + 1] * un + p * n[iDim]).margin(1e-12)); + CHECK(res.residual[nDim + 1] == Approx(rho_up * V_up[nDim + 3] * un).margin(1e-12)); +} + +template +void CheckContacts(const char* name, bool implicit = true, bool shear = true) { + const su2double zero[3] = {0.0, 0.0, 0.0}, moving[3] = {0.3, -0.1, 0.2}, reverse[3] = {-0.3, 0.1, -0.2}; + for (const unsigned short nDim : {2, 3}) { + for (const auto* vel : {zero, moving, reverse}) { + CheckContact(name, nDim, vel, implicit, false); + if (shear) CheckContact(name, nDim, vel, implicit, true); + } + } +} + +} // namespace + +TEST_CASE("Roe schemes resolve a contact and a shear layer exactly", "[Roe]") { + CheckContacts("Roe"); + CheckContacts("general Roe", true); + CheckContacts("general Roe", false); + CheckContacts("LMRoe"); + /*--- L2Roe scales the tangential velocity jump at low Mach number (not upwind by design). ---*/ + CheckContacts("L2Roe", true, false); +} diff --git a/UnitTests/meson.build b/UnitTests/meson.build index 176abed8dba..85b96280544 100644 --- a/UnitTests/meson.build +++ b/UnitTests/meson.build @@ -15,6 +15,7 @@ su2_cfd_tests = files(['Common/CConfig_tests.cpp', 'Common/containers/CLookupTable_tests.cpp', 'Common/toolboxes/multilayer_perceptron/CLookUp_ANN_tests.cpp', 'SU2_CFD/numerics/CNumerics_tests.cpp', + 'SU2_CFD/numerics/roe_contact_tests.cpp', 'SU2_CFD/numerics/hllc_jacobian_tests.cpp', 'SU2_CFD/fluid/CFluidModel_tests.cpp', 'SU2_CFD/output/CNEMOCompOutput_tests.cpp',