From 547e120277994338547e8f3c92166881e2a9a86e Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Wed, 26 Aug 2026 21:31:34 -0700 Subject: [PATCH 01/20] Promote generic scalar edge-flux building blocks to numerics/util.hpp Moves the value-type-generic parts of numerics_simd/util.hpp (Vector/Matrix aliases, gatherVariables, distanceVector, the MUSCL reconstruction helpers, and the limiter-type dispatcher) to a new numerics/util.hpp so they are usable outside the SIMD flow path, with numerics_simd/util.hpp reduced to a thin include plus its VectorDbl/MatrixDbl aliases. Adds two independent-block SetBlocks/SetOffDiagBlocks overloads to CSysMatrix (with a unit test), extends CMatrixView with the get<>() gather interface C2DContainer already has, and introduces EdgeResidual, EdgeSide, ScalarFluxOptions and the CAvgGradScalarBase/CUpwScalarFlux/CUpwScalarBase CRTP chain in numerics/scalar/scalar_edge_flux.hpp: a model-agnostic convection+diffusion kernel meant to be shared by every scalar solver. CScalarSolver's writers are adjusted to accumulate into two per-row edge flux containers (EdgeFluxes/EdgeFluxesDiff) instead of deriving one side's contribution from the other's sign, which is what the new diffusion kernel needs and also fixes a latent gap where the flamelet preferential-diffusion terms were never accumulated into the diff container under the reducer strategy. No caller uses the new scalar_edge_flux.hpp kernel yet. --- .../containers/container_decorators.hpp | 27 + Common/include/linear_algebra/CSysMatrix.hpp | 74 +++ .../numerics/scalar/scalar_edge_flux.hpp | 325 ++++++++++ SU2_CFD/include/numerics/util.hpp | 593 ++++++++++++++++++ .../include/numerics_simd/CNumericsSIMD.hpp | 18 +- .../numerics_simd/flow/convection/common.hpp | 155 +---- SU2_CFD/include/numerics_simd/util.hpp | 250 +------- SU2_CFD/include/solvers/CScalarSolver.hpp | 7 +- SU2_CFD/include/solvers/CScalarSolver.inl | 12 +- SU2_CFD/src/solvers/CHeatSolver.cpp | 15 +- .../src/solvers/CSpeciesFlameletSolver.cpp | 2 + SU2_CFD/src/solvers/CSpeciesSolver.cpp | 5 +- SU2_CFD/src/solvers/CTransLMSolver.cpp | 4 +- .../edge_residual_blocks_tests.cpp | 109 ++++ UnitTests/meson.build | 3 +- 15 files changed, 1169 insertions(+), 430 deletions(-) create mode 100644 SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp create mode 100644 SU2_CFD/include/numerics/util.hpp create mode 100644 UnitTests/Common/linear_algebra/edge_residual_blocks_tests.cpp diff --git a/Common/include/containers/container_decorators.hpp b/Common/include/containers/container_decorators.hpp index a9e66857eb3..331e87b9a0f 100644 --- a/Common/include/containers/container_decorators.hpp +++ b/Common/include/containers/container_decorators.hpp @@ -62,6 +62,33 @@ class CMatrixView { const Scalar* operator[](Index i) const noexcept { return &m_ptr[i * m_cols]; } const Scalar& operator()(Index i, Index j) const noexcept { return m_ptr[i * m_cols + j]; } + /*! + * \brief Return copy of data in a static size container (see C2DContainer::get). + * \param[in] i - Row of the view (e.g. point index, whole-mesh usage). + * \param[in] start - Starting column to copy the data (amount determined by container size). + */ + template + StaticContainer get(Index i, Index start = 0) const noexcept { + constexpr size_t Size = StaticContainer::StaticSize; + static_assert(Size, "This method requires a static output type."); + StaticContainer ret; + for (size_t k = 0; k < Size; ++k) ret.data()[k] = m_ptr[i * m_cols + start + k]; + return ret; + } + + /*! + * \brief SIMD gather version of get, one row per lane. + */ + template + StaticContainer get(simd::Array i, Index start = 0) const noexcept { + constexpr size_t Size = StaticContainer::StaticSize; + static_assert(Size, "This method requires a static output type."); + StaticContainer ret; + for (size_t lane = 0; lane < N; ++lane) + for (size_t k = 0; k < Size; ++k) ret.data()[k][lane] = m_ptr[i[lane] * m_cols + start + k]; + return ret; + } + template ::value> = 0> Scalar* operator[](Index i) noexcept { return &m_ptr[i * m_cols]; diff --git a/Common/include/linear_algebra/CSysMatrix.hpp b/Common/include/linear_algebra/CSysMatrix.hpp index abecc767e26..84f15b58b8a 100644 --- a/Common/include/linear_algebra/CSysMatrix.hpp +++ b/Common/include/linear_algebra/CSysMatrix.hpp @@ -1027,6 +1027,80 @@ class CSysMatrix { SetBlocks(iEdge, block_i, block_j, -1); } + /*! + * \brief Set the four blocks of an edge, for fluxes whose i and j contributions are independent. + * \note The diagonal blocks are accumulated, the off-diagonal blocks are set. + */ + template + inline void SetBlocks(unsigned long iEdge, unsigned long iPoint, unsigned long jPoint, const MatrixType& jac_ii, + const MatrixType& jac_ij, const MatrixType& jac_ji, const MatrixType& jac_jj, + OtherType mask = 1) { + const auto blkSz = nVar * nEqn; + auto* bii = &mat.d[iPoint * blkSz]; + auto* bjj = &mat.d[jPoint * blkSz]; + unsigned long iVar, jVar, offset = 0; + + if (quantized_mode) { + ScalarType bij_buf[MAXNVAR * MAXNVAR], bji_buf[MAXNVAR * MAXNVAR]; + for (iVar = 0; iVar < nVar; iVar++) + for (jVar = 0; jVar < nEqn; jVar++, ++offset) { + bii[offset] += PassiveAssign(jac_ii[iVar][jVar] * mask); + bjj[offset] += PassiveAssign(jac_jj[iVar][jVar] * mask); + bij_buf[offset] = PassiveAssign(jac_ij[iVar][jVar] * mask); + bji_buf[offset] = PassiveAssign(jac_ji[iVar][jVar] * mask); + } + QuantizeBlock(bij_buf, &q_scale.u[iEdge * nVar], &q_blocks.u[iEdge * blkSz]); + const auto k_l = edge_ptr_l[iEdge]; + QuantizeBlock(bji_buf, &q_scale.l[k_l * nVar], &q_blocks.l[k_l * blkSz]); + return; + } + + auto* bij = &mat.u[iEdge * blkSz]; + auto* bji = &mat.l[edge_ptr_l[iEdge] * blkSz]; + for (iVar = 0; iVar < nVar; iVar++) { + for (jVar = 0; jVar < nEqn; jVar++) { + bii[offset] += PassiveAssign(jac_ii[iVar][jVar] * mask); + bjj[offset] += PassiveAssign(jac_jj[iVar][jVar] * mask); + bij[offset] = PassiveAssign(jac_ij[iVar][jVar] * mask); + bji[offset] = PassiveAssign(jac_ji[iVar][jVar] * mask); + ++offset; + } + } + } + + /*! + * \brief Set the off-diagonal blocks of an edge, the diagonal being assembled elsewhere. + */ + template + inline void SetOffDiagBlocks(unsigned long iEdge, const MatrixType& jac_ij, const MatrixType& jac_ji, + OtherType mask = 1) { + const auto blkSz = nVar * nEqn; + unsigned long iVar, jVar, offset = 0; + + if (quantized_mode) { + ScalarType bij_buf[MAXNVAR * MAXNVAR], bji_buf[MAXNVAR * MAXNVAR]; + for (iVar = 0; iVar < nVar; iVar++) + for (jVar = 0; jVar < nEqn; jVar++, ++offset) { + bij_buf[offset] = PassiveAssign(jac_ij[iVar][jVar] * mask); + bji_buf[offset] = PassiveAssign(jac_ji[iVar][jVar] * mask); + } + QuantizeBlock(bij_buf, &q_scale.u[iEdge * nVar], &q_blocks.u[iEdge * blkSz]); + const auto k_l = edge_ptr_l[iEdge]; + QuantizeBlock(bji_buf, &q_scale.l[k_l * nVar], &q_blocks.l[k_l * blkSz]); + return; + } + + auto* bij = &mat.u[iEdge * blkSz]; + auto* bji = &mat.l[edge_ptr_l[iEdge] * blkSz]; + for (iVar = 0; iVar < nVar; iVar++) { + for (jVar = 0; jVar < nEqn; jVar++) { + bij[offset] = PassiveAssign(jac_ij[iVar][jVar] * mask); + bji[offset] = PassiveAssign(jac_ji[iVar][jVar] * mask); + ++offset; + } + } + } + /*! * \brief SIMD version, does the update for multiple edges. * \note Nothing is updated if the mask is 0. diff --git a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp new file mode 100644 index 00000000000..61847a9774a --- /dev/null +++ b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp @@ -0,0 +1,325 @@ +/*! + * \file scalar_edge_flux.hpp + * \brief Model-agnostic convection and diffusion of a transported scalar, shared by every + * scalar solver (turbulence, transition, species, flamelet, heat). + * \author P. Gomes + * \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 . + */ + +#pragma once + +#include "../../../../Common/include/CConfig.hpp" +#include "../../../../Common/include/containers/container_decorators.hpp" +#include "../util.hpp" +#include "../../variables/CFlowVariable.hpp" + +/*! + * \brief Locates the data of one endpoint of an edge. + * \note The j side of a boundary flux indexes per-marker ghost containers, which have the + * same types as the solver's own, so the kernels read both through one code path. + */ +template +struct EdgeSide { + const VariableType& scalarNodes; /*!< \brief Scalar solver variables. */ + const CFlowVariable* flowNodes; /*!< \brief Flow variables, null for solid heat transfer. */ + CMatrixView coord; /*!< \brief Point coordinates. */ + CMatrixView gridVel; /*!< \brief Empty when the grid is static. */ +}; + +/*! + * \brief Loop invariant flags for a scalar edge flux, built once outside the edge loop so the + * compiler can unswitch the branches they guard. + */ +struct ScalarFluxOptions { + bool dynamicGrid, boundedScalar, correctGradient, accurateJacobians; + bool convective; /*!< \brief Whether the convective scheme contributes. */ + bool viscous; /*!< \brief Whether the diffusion term contributes. */ + bool oneSided; /*!< \brief Whether only the row of i is assembled. */ +}; + +/*! + * \brief Thin wrapper giving a Vector the all(iVar) accessor the MUSCL reconstruction helpers + * expect (see CCompressiblePrimitives in numerics_simd/flow/variables.hpp). + */ +template +struct CScalarValues { + Vector all; +}; + +/*! + * \brief Diffusion of a transported scalar, driven by model-supplied coefficients. + * \note The derived class returns the coefficients of both orientations of the edge, as one + * object, so that whatever the two share is computed once. A model whose coefficients + * are the same in both orientations returns the same value twice. A model whose matrix + * is diagonal declares DiagonalDiffusion and returns a vector instead of a matrix. + */ +template +class CAvgGradScalarBase { + protected: + using Int = typename CLaneTraits::Int; + + template + FORCEINLINE void diffusionTerms(const FlowIndices& idx, const ScalarFluxOptions& opt, Int iPoint, + const EdgeSide& side_i, Int jPoint, + const EdgeSide& side_j, const Vector& normal, + const Vector& vector_ij, EdgeResidual& res) const { + if (!opt.viscous) return; + + constexpr size_t Size = EdgeResidual::Size; + + const Double dist2_ij = squaredNorm(vector_ij); + const Double proj_vector_ij = dot(vector_ij, normal) / dist2_ij; + + /*--- Average gradient, corrected for skewness when asked. + * \note Gathered one variable at a time, bounded by res.nVar rather than Size: a static + * model with nVar 1 has Size 2 (the Matrix degeneracy floor), and a + * dynamic one has Size MaxScalarVar, so a single Size-wide read would run past the + * actual width of the gradient container in either case. ---*/ + Matrix avgGrad; + for (size_t iVar = 0; iVar < res.nVar; ++iVar) { + const auto grad_i = gatherVariables<1, nDim>(iPoint, side_i.scalarNodes.GetGradient(), iVar); + const auto grad_j = gatherVariables<1, nDim>(jPoint, side_j.scalarNodes.GetGradient(), iVar); + for (int iDim = 0; iDim < nDim; ++iDim) avgGrad(iVar, iDim) = 0.5 * (grad_i(iDim) + grad_j(iDim)); + } + + if (opt.correctGradient) { + for (size_t iVar = 0; iVar < res.nVar; ++iVar) { + const Double phi_i = gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), iVar); + const Double phi_j = gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), iVar); + const Double corr = (dot(avgGrad[iVar], vector_ij) - phi_j + phi_i) / dist2_ij; + for (int iDim = 0; iDim < nDim; ++iDim) avgGrad(iVar, iDim) -= corr * vector_ij(iDim); + } + } + + Vector projGrad; + for (size_t iVar = 0; iVar < res.nVar; ++iVar) projGrad(iVar) = dot(avgGrad[iVar], normal); + + /*--- The Jacobians of a conservative model are w.r.t. the conserved (density-weighted) + * variable, which divides the geometric projection by the density of the row being written. ---*/ + Double w_i = 1.0, w_j = 1.0; + if constexpr (Derived::Conservative) { + w_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); + w_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); + } + const Double proj_on_w_i = proj_vector_ij / w_i; + const Double proj_on_w_j = proj_vector_ij / w_j; + + const auto* self = static_cast(this); + const auto D = self->coefficients(idx, iPoint, side_i, jPoint, side_j); + + for (size_t iVar = 0; iVar < res.nVar; ++iVar) { + if constexpr (Derived::DiagonalDiffusion) { + res.flux_i(iVar) -= D.i(iVar) * projGrad(iVar); + res.jac_ii(iVar, iVar) += D.i(iVar) * proj_on_w_i; + res.jac_ij(iVar, iVar) -= D.i(iVar) * proj_on_w_j; + + if (!opt.oneSided) { + res.flux_j(iVar) += D.j(iVar) * projGrad(iVar); + res.jac_ji(iVar, iVar) -= D.j(iVar) * proj_on_w_i; + res.jac_jj(iVar, iVar) += D.j(iVar) * proj_on_w_j; + } + } else { + for (size_t jVar = 0; jVar < res.nVar; ++jVar) { + res.flux_i(iVar) -= D.i(iVar, jVar) * projGrad(jVar); + res.jac_ii(iVar, jVar) += D.i(iVar, jVar) * proj_on_w_i; + res.jac_ij(iVar, jVar) -= D.i(iVar, jVar) * proj_on_w_j; + + if (!opt.oneSided) { + res.flux_j(iVar) += D.j(iVar, jVar) * projGrad(jVar); + res.jac_ji(iVar, jVar) -= D.j(iVar, jVar) * proj_on_w_i; + res.jac_jj(iVar, jVar) += D.j(iVar, jVar) * proj_on_w_j; + } + } + } + } + + if (opt.accurateJacobians) { + /*--- Coefficients that depend on the transported variables contribute here. ---*/ + self->coefficientJacobians(projGrad, res); + } + + self->extraDiffusionTerms(idx, iPoint, side_i, jPoint, side_j, normal, vector_ij, res); + } + + /*! + * \brief Contribution of the derivatives of the coefficients themselves. + */ + template + FORCEINLINE void coefficientJacobians(Ts&...) const {} + + /*! + * \brief Diffusion of a model that transports more than one gradient, of states it + * synthesises from its own containers. + */ + template + FORCEINLINE void extraDiffusionTerms(Ts&...) const {} +}; + +/*! + * \brief Convective flux shared by every model whose transport equation has the shape + * flux(iVar) = a0 * w_i * phi_i(iVar) + a1 * w_j * phi_j(iVar), which is every + * model except SA and stochastic backscatter (see CScalarFlux_SA). + * \note The weight w is 1 for a non-conservative model, the density for a conservative one. + */ +template +class CUpwScalarFlux : public CAvgGradScalarBase { + protected: + using Int = typename CLaneTraits::Int; + + explicit CUpwScalarFlux(const CConfig&) {} + + template + FORCEINLINE void finalizeFlux(const FlowIndices& idx, const ScalarFluxOptions&, Int iPoint, + const EdgeSide& side_i, Int jPoint, + const EdgeSide& side_j, const Double& a0, const Double& a1, + EdgeResidual& res) const { + Double w0 = a0, w1 = a1; + if constexpr (Derived::Conservative) { + w0 *= gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); + w1 *= gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); + } + + for (size_t iVar = 0; iVar < res.nVar; ++iVar) { + const Double phi_i = gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), iVar); + const Double phi_j = gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), iVar); + const Double flux = w0 * phi_i + w1 * phi_j; + + res.flux_i(iVar) += flux; + res.flux_j(iVar) -= flux; + + res.jac_ii(iVar, iVar) += w0; + res.jac_ij(iVar, iVar) += w1; + res.jac_ji(iVar, iVar) -= w0; + res.jac_jj(iVar, iVar) -= w1; + } + } +}; + +/*! + * \brief Upwind convection and diffusion of a transported scalar, accumulated into one + * residual, each term contributing or not according to the options. + */ +template +class CUpwScalarBase : public CUpwScalarFlux { + public: + using Double = Double_; + using Int = typename CLaneTraits::Int; + static constexpr int nDim = nDim_; + static constexpr size_t nVar = nVar_; + + /*! + * \brief Backing size, for the model to size the containers its coefficients() returns; + * nVar itself is Dynamic for a runtime model and never usable as a container size. + */ + static constexpr size_t Size = EdgeResidual::Size; + + protected: + using Base = CUpwScalarFlux; + + const FlowIndices idx; + const size_t nEqn; /*!< \brief Equations of the model, which a dynamic one gives to its base. */ + + public: + /*! + * \brief Constructor, inherited by the model with `using Base::Base`. + * \note Public, not protected: a using-declaration that inherits a constructor keeps the + * base's own access, so the solver that builds the concrete model needs this public + * to build it at all. + */ + explicit CUpwScalarBase(const CConfig& config, size_t nEqn_ = nVar_) + : Base(config), idx(nDim, config.GetnSpecies()), nEqn(nEqn_) {} + + template + FORCEINLINE EdgeResidual ComputeFlux(const ScalarFluxOptions& opt, Int iPoint, + const EdgeSide& side_i, Int jPoint, + const EdgeSide& side_j, + const Vector& normal, + const Double& massFlux) const { + /*--- Inputs are registered as they are read, by each of the two terms. ---*/ + AD::StartPreacc(); + AD::SetPreaccIn(normal, nDim); + + EdgeResidual res(nEqn); + + /*--- Read once by the reconstruction (added alongside the first model that uses it) and + * by the diffusion. ---*/ + Vector vector_ij; + if (muscl || opt.viscous) { + vector_ij = distanceVector(iPoint, side_i.coord, jPoint, side_j.coord); + } + + if (opt.convective) { + /*--- Upwinding weights of the face normal mass or volume flux. ---*/ + Double a0, a1; + if (opt.boundedScalar) { + AD::SetPreaccIn(massFlux); + const Double rho_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); + const Double rho_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); + a0 = fmax(0.0, massFlux) / rho_i; + a1 = fmin(0.0, massFlux) / rho_j; + } else { + const auto u_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Velocity()); + const auto u_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Velocity()); + + /*--- Face normal velocity of the mean of the two points, relative to the grid. ---*/ + Vector vel_ij; + for (int iDim = 0; iDim < nDim; ++iDim) vel_ij(iDim) = 0.5 * (u_i(iDim) + u_j(iDim)); + + if (opt.dynamicGrid) { + const auto ug_i = gatherVariables(iPoint, side_i.gridVel); + const auto ug_j = gatherVariables(jPoint, side_j.gridVel); + for (int iDim = 0; iDim < nDim; ++iDim) vel_ij(iDim) -= 0.5 * (ug_i(iDim) + ug_j(iDim)); + } + + const Double q_ij = dot(vel_ij, normal); + a0 = fmax(0.0, q_ij); + a1 = fmin(0.0, q_ij); + } + + static_cast(this)->finalizeFlux(idx, opt, iPoint, side_i, jPoint, side_j, a0, a1, res); + } + + Base::diffusionTerms(idx, opt, iPoint, side_i, jPoint, side_j, normal, vector_ij, res); + + AD::SetPreaccOut(res.flux_i, res.nVar); + if (!opt.oneSided) AD::SetPreaccOut(res.flux_j, res.nVar); + AD::EndPreacc(); + + return res; + } + + /*! + * \brief Compute the flux of an edge and write it to the linear system. + */ + template + FORCEINLINE void ComputeFlux(const ScalarFluxOptions& opt, Int iEdge, Int iPoint, + const EdgeSide& side_i, Int jPoint, + const EdgeSide& side_j, const Vector& normal, + const Double& massFlux, bool implicit, UpdateType updateType, Double updateMask, + CSysVector& vector, CSysVector& vectorDiff, + SparseMatrixType& matrix) const { + const auto res = ComputeFlux(opt, iPoint, side_i, jPoint, side_j, normal, massFlux); + + updateLinearSystem(iEdge, iPoint, jPoint, implicit, updateType, updateMask, res, vector, vectorDiff, matrix); + } +}; diff --git a/SU2_CFD/include/numerics/util.hpp b/SU2_CFD/include/numerics/util.hpp new file mode 100644 index 00000000000..5003fc4652d --- /dev/null +++ b/SU2_CFD/include/numerics/util.hpp @@ -0,0 +1,593 @@ +/*! + * \file util.hpp + * \brief Generic auxiliary functions. + * \author P. Gomes + * \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 . + */ + +#pragma once + +#include + +#include "../../../Common/include/option_structure.hpp" +#include "../../../Common/include/parallelization/vectorization.hpp" +#include "../../../Common/include/containers/C2DContainer.hpp" +#include "../../../Common/include/linear_algebra/CSysVector.hpp" +#include "../../../Common/include/linear_algebra/CSysMatrix.hpp" + +/*! + * \enum UpdateType + * \brief Ways to update vectors and system matrices. + * COLORING is the typical i/j update, whereas for REDUCTION + * the fluxes are stored and the matrix diagonal is not modified. + */ +enum class UpdateType { COLORING, REDUCTION }; + +#ifdef CODI_FORWARD_TYPE +using SparseMatrixType = CSysMatrix; +#else +using SparseMatrixType = CSysMatrix; +#endif + +/*! + * \brief Alignment of the static containers backing a flux value type. + * \note Yields the type's own alignment for a SIMD array, and the container default (0) + * for a plain scalar, which has no alignment of its own. + */ +template +struct CAlignTraits { + enum : size_t { Align = 0 }; +}; + +template +struct CAlignTraits> { + enum : size_t { Align = simd::Array::Align }; +}; + +/*! + * \brief Static vector and matrix types. + * \note These should be used instead of C-style arrays. + */ +template +using Vector = C2DContainer::Align, Size, 1>; + +template +using Matrix = C2DContainer::Align, Rows, Cols>; + +/*! + * \brief The flux value type and lane count that go with an index type. + * \note There is exactly one floating type in play, su2double, active under AD; a plain + * integral index reads one of it, a lane-vector index reads a lane-vector of it. This + * is what lets every helper below deduce its value type from the index it is handed, + * instead of a caller naming it explicitly. + */ +template +struct CValueTraits; + +template <> +struct CValueTraits { + using Double = su2double; + static constexpr size_t Size = 1; +}; + +template +struct CValueTraits> { + using Double = simd::Array; + static constexpr size_t Size = N; +}; + +/*! + * \brief Index type and lane count that go with a flux value type, the converse of + * CValueTraits, used where the value type is already known (e.g. a class template + * parameter) and the index type is what needs deriving. + */ +template +struct CLaneTraits; + +template <> +struct CLaneTraits { + using Int = unsigned long; + static constexpr size_t Size = 1; +}; + +template +struct CLaneTraits> { + using Int = simd::Array; + static constexpr size_t Size = N; +}; + +/*! + * \brief Constexpr version of max. + */ +inline constexpr size_t Max(size_t a, size_t b) { return a > b ? a : b; } + +/*! + * \brief Simple pair type for i/j variables. + */ +template +struct CPair { + T i, j; +}; + +/*! + * \brief Blocks a template parameter from participating in argument deduction. + * \note Deduction never applies a user conversion, so a parameter typed plain Double would + * force a caller passing a bare su2double constant (kappa, a limiter ramp) to have + * already broadcast it. Wrapping the parameter type here defers Double entirely to + * the other, genuinely deduced arguments, and the broadcast then happens as an + * ordinary implicit conversion at the call. + */ +template +struct CIdentity { + using type = T; +}; +template +using CNonDeduced = typename CIdentity::type; + +/*! + * \brief Equation count of a model whose value is only known at runtime. + */ +constexpr size_t Dynamic = size_t(-1); + +/*! + * \brief Backing size of the static arrays of a dynamic model. + * \note The scalar numerics cap the equation count at this value and error above it, so a + * configuration that fits them fits these kernels. + */ +constexpr size_t MaxScalarVar = 8; + +/*! + * \brief Residual of one edge, accumulated by the convective and the diffusive terms. + * \note flux_i and flux_j are the contributions to the rows of i and j. They are opposite + * for a conservative term and independent for a non-conservative one. The Jacobians + * map onto the ii, ij, ji and jj blocks of the edge. A dynamic model sizes the storage + * with the maximum and iterates to nVar, which the scheme sets from the solver. + */ +template +struct EdgeResidual { + /*!< \brief The Matrix a static nVar==1 model would otherwise need degenerates + * to vector-only indexing in C2DContainer (its RowMajor, one-row specialization), so the + * backing is never smaller than 2; the unused padding row/column is simply never visited, + * every loop here and in the model bounds itself to nVar, not Size. */ + static constexpr size_t Size = Max(2, (nVar_ == Dynamic) ? MaxScalarVar : nVar_); + + Vector flux_i, flux_j; + Matrix jac_ii, jac_ij, jac_ji, jac_jj; + const size_t nVar; + + /*! + * \brief Zero the terms of the equations in use, so that both terms can accumulate into them. + * \note A static model zeroes its whole storage with constant trip counts; a dynamic one + * zeroes the leading nVar rows and columns and leaves the rest of the backing untouched. + */ + FORCEINLINE explicit EdgeResidual(size_t nEqn = Size) : nVar(nEqn) { + for (size_t iVar = 0; iVar < nVar; ++iVar) { + flux_i(iVar) = 0.0; + flux_j(iVar) = 0.0; + for (size_t jVar = 0; jVar < nVar; ++jVar) { + jac_ii(iVar, jVar) = 0.0; + jac_ij(iVar, jVar) = 0.0; + jac_ji(iVar, jVar) = 0.0; + jac_jj(iVar, jVar) = 0.0; + } + } + } +}; + +/*! + * \brief Dot product. + */ +template +FORCEINLINE auto dot(ForwardIterator iterator, const T* ptr) -> typename std::decay::type { + typename std::decay::type sum = 0.0; + for (size_t iDim = 0; iDim < nDim; ++iDim) { + sum += *(iterator++) * ptr[iDim]; + } + return sum; +} + +/*! + * \overload Dot product. + */ +template +FORCEINLINE Double dot(ForwardIterator iterator, const Vector& vector) { + return dot(iterator, vector.data()); +} + +/*! + * \overload Dot product. + */ +template +FORCEINLINE Double dot(const Vector& a, const Vector& b) { + return dot(a.data(), b.data()); +} + +/*! + * \brief Squared norm. + */ +template +FORCEINLINE auto squaredNorm(ForwardIterator iterator) -> typename std::decay::type { + typename std::decay::type sum = 0.0; + for (size_t iDim = 0; iDim < nDim; ++iDim) { + sum += pow(*(iterator++), 2); + } + return sum; +} + +/*! + * \overload Squared norm. + */ +template +FORCEINLINE Double squaredNorm(const Vector& vector) { + return squaredNorm(vector.data()); +} + +/*! + * \brief Tangential projection. + */ +template +FORCEINLINE Vector tangentProjection(const Matrix& tensor, + const Vector& unitVector) { + Vector proj; + for (size_t iDim = 0; iDim < nDim; ++iDim) proj(iDim) = dot(tensor[iDim], unitVector); + + Double normalProj = dot(proj, unitVector); + + for (size_t iDim = 0; iDim < nDim; ++iDim) proj(iDim) -= normalProj * unitVector(iDim); + + return proj; +} + +/*! + * \brief Vector norm. + */ +template +FORCEINLINE Double norm(const Vector& vector) { + return sqrt(squaredNorm(vector)); +} + +#ifndef CODI_REVERSE_TYPE +/*! + * \brief Gather a single variable, from column iVar (0 by default) of row iPoint of a + * 2D container, or from index iPoint of a 1D container. + */ +template ::Double> +FORCEINLINE Double gatherVariables(Int iPoint, const Container& vars, size_t iVar = 0) { + return vars.template get>(iPoint, iVar)(0); +} + +/*! + * \brief Gather nVar contiguous variables starting at column iVar (0 by default) of row + * iPoint of a 2D container. + */ +template ::Double> +FORCEINLINE Vector gatherVariables(Int iPoint, const Container& vars, size_t iVar = 0) { + return vars.template get>(iPoint, iVar); +} + +/*! + * \brief Gather an nRows x nCols block of a 3D container, from outer index iPoint and + * starting at middle index iRow. + */ +template ::Double> +FORCEINLINE Matrix gatherVariables(Int iPoint, const Container& vars, size_t iRow = 0) { + return vars.template get>(iPoint, iRow); +} +#else + +namespace { +template = 0> +FORCEINLINE const su2double& get(const Container& vars, unsigned long iPoint) { + return vars(iPoint); +} + +/*--- When getting 1 variable from a matrix container, we assume it is the first. ---*/ +template = 0> +FORCEINLINE const su2double& get(const Container& vars, unsigned long iPoint, size_t iVar = 0) { + return vars(iPoint, iVar); +} +} // namespace + +template ::Double> +FORCEINLINE Double gatherVariables(Int iPoint, const Container& vars, size_t iVar = 0) { + Double x; + for (size_t k = 0; k < CValueTraits::Size; ++k) { + AD::SetPreaccIn(get(vars, iPoint[k], iVar)); + x[k] = get(vars, iPoint[k], iVar); + } + return x; +} + +template ::Double> +FORCEINLINE Vector gatherVariables(Int iPoint, const Container& vars, size_t iVar = 0) { + Vector x; + for (size_t i = 0; i < nVar; ++i) { + for (size_t k = 0; k < CValueTraits::Size; ++k) { + AD::SetPreaccIn(vars(iPoint[k], iVar + i)); + x[i][k] = vars(iPoint[k], iVar + i); + } + } + return x; +} + +template ::Double> +FORCEINLINE Matrix gatherVariables(Int iPoint, const Container& vars, size_t iRow = 0) { + Matrix x; + for (size_t i = 0; i < nRows; ++i) { + for (size_t j = 0; j < nCols; ++j) { + for (size_t k = 0; k < CValueTraits::Size; ++k) { + AD::SetPreaccIn(vars(iPoint[k], iRow + i, j)); + x(i, j)[k] = vars(iPoint[k], iRow + i, j); + } + } + } + return x; +} +#endif + +/*! + * \brief Stop the AD preaccumulation. + */ +template +FORCEINLINE void stopPreacc(Vector& x) { + AD::SetPreaccOut(x, nVar, CLaneTraits::Size); + AD::EndPreacc(); +} + +/*! + * \brief Distance vector, from point i to point j of one container. + */ +template ::Double> +FORCEINLINE Vector distanceVector(Int iPoint, Int jPoint, const Container& coords) { + return distanceVector(iPoint, coords, jPoint, coords); +} + +/*! + * \brief Distance vector, from point i of one container to point j of another. + * \note The two endpoints of a boundary flux read different containers, the solver's own + * and the marker's ghost one; the interior edge loop passes the same container twice. + */ +template ::Double> +FORCEINLINE Vector distanceVector(Int iPoint, const Container& coords_i, Int jPoint, + const Container& coords_j) { + auto coord_i = gatherVariables(iPoint, coords_i); + auto coord_j = gatherVariables(jPoint, coords_j); + Vector vector_ij; + for (size_t iDim = 0; iDim < nDim; ++iDim) { + vector_ij(iDim) = coord_j(iDim) - coord_i(iDim); + } + return vector_ij; +} + +/*! + * \brief Blended difference for U-MUSCL reconstruction. + * \param[in] gradProj - Gradient projection at point i: dot(grad_i, vector_ij). + * \param[in] delta - Centered difference: V_j - V_i. + * \param[in] kappa - Blending parameter. + * \return Blended difference for reconstruction from point i. + */ +template +FORCEINLINE Double umusclProjection(const Double& gradProj, const Double& delta, const CNonDeduced& kappa) { + /*-------------------------------------------------------------------*/ + /*--- The MUSCL kappa-scheme reconstruction is typically written: ---*/ + /*--- V_L = V_i + 0.25 * dV_ij^kap, where ---*/ + /*--- dV_ij^kap = (1-kappa) dV_ij^upw + (1+kappa) dV_ij^cen, ---*/ + /*--- dV_ij^cen = V_j - V_i, ---*/ + /*--- dV_ij^upw = 2 grad(Vi) dot vector_ij - dV_ij^cen. ---*/ + /*--- To maintain proper scaling for edge limiters, the result of ---*/ + /*--- this function is 0.5 * dV_ij^kap. ---*/ + /*-------------------------------------------------------------------*/ + return (1.0 - kappa) * gradProj + kappa * delta; +} + +/*! + * \brief MUSCL reconstruction of the specified variable. + * \note The result should be halved when added to i (or subtracted from j). + */ +template +FORCEINLINE Double musclReconstruction(const GradType& grad, const Vector& vector_ij, + const Double& delta, const size_t iVar, const CNonDeduced& kappa, + const CNonDeduced& umusclRamp) { + const Double proj = dot(grad[iVar], vector_ij); + return umusclRamp * umusclProjection(proj, delta, kappa); +} + +/*! + * \brief Unlimited reconstruction. + * \param[in] iRow - Starting row of gradient to read, for reconstructing a slice of a + * larger set of gradients (e.g. only the velocity out of the primitives). + */ +template +FORCEINLINE void musclUnlimited(typename CLaneTraits::Int iPoint, typename CLaneTraits::Int jPoint, + const Vector& vector_ij, const Gradient_t& gradient, CPair& V, + const CNonDeduced& kappa, const CNonDeduced& umusclRamp, + size_t iRow = 0) { + constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; + + auto grad_i = gatherVariables(iPoint, gradient, iRow); + auto grad_j = gatherVariables(jPoint, gradient, iRow); + + for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { + /*--- Centered difference, needed for U-MUSCL projection ---*/ + const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); + + /*--- U-MUSCL reconstructed variables ---*/ + const Double proj_i = musclReconstruction(grad_i, vector_ij, delta_ij, iVar, kappa, umusclRamp); + const Double proj_j = musclReconstruction(grad_j, vector_ij, delta_ij, iVar, kappa, umusclRamp); + + /*--- Apply reconstruction: V_L = V_i + 0.5 * dV_ij^kap ---*/ + V.i.all(iVar) += 0.5 * proj_i; + V.j.all(iVar) -= 0.5 * proj_j; + } +} + +/*! + * \brief Limited reconstruction with point-based limiter. + */ +template +FORCEINLINE void musclPointLimited(typename CLaneTraits::Int iPoint, + typename CLaneTraits::Int jPoint, const Vector& vector_ij, + const Limiter_t& limiter, const Gradient_t& gradient, CPair& V, + const CNonDeduced& kappa, const CNonDeduced& umusclRamp, + size_t iRow = 0) { + constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; + + auto lim_i = gatherVariables(iPoint, limiter, iRow); + auto lim_j = gatherVariables(jPoint, limiter, iRow); + + auto grad_i = gatherVariables(iPoint, gradient, iRow); + auto grad_j = gatherVariables(jPoint, gradient, iRow); + + for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { + /*--- Centered difference, needed for U-MUSCL projection ---*/ + const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); + + /*--- U-MUSCL reconstructed variables ---*/ + const Double proj_i = musclReconstruction(grad_i, vector_ij, delta_ij, iVar, kappa, umusclRamp); + const Double proj_j = musclReconstruction(grad_j, vector_ij, delta_ij, iVar, kappa, umusclRamp); + + /*--- Apply reconstruction: V_L = V_i + 0.5 * lim * dV_ij^kap ---*/ + V.i.all(iVar) += 0.5 * lim_i(iVar) * proj_i; + V.j.all(iVar) -= 0.5 * lim_j(iVar) * proj_j; + } +} + +/*! + * \brief Limited reconstruction with edge-based limiter. + */ +template +FORCEINLINE void musclEdgeLimited(typename CLaneTraits::Int iPoint, + typename CLaneTraits::Int jPoint, const Vector& vector_ij, + const Gradient_t& gradient, CPair& V, const CNonDeduced& kappa, + const CNonDeduced& umusclRamp, size_t iRow = 0) { + constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; + + auto grad_i = gatherVariables(iPoint, gradient, iRow); + auto grad_j = gatherVariables(jPoint, gradient, iRow); + + for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { + /*--- Centered difference, needed for U-MUSCL projection and limiter ---*/ + const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); + const Double delta_ij_2 = pow(delta_ij, 2) + 1e-6; + + /*--- U-MUSCL reconstructed variables ---*/ + const Double proj_i = musclReconstruction(grad_i, vector_ij, delta_ij, iVar, kappa, umusclRamp); + const Double proj_j = musclReconstruction(grad_j, vector_ij, delta_ij, iVar, kappa, umusclRamp); + + const Double lim_i = (delta_ij_2 + proj_i * delta_ij) / (pow(proj_i, 2) + delta_ij_2); + const Double lim_j = (delta_ij_2 + proj_j * delta_ij) / (pow(proj_j, 2) + delta_ij_2); + + /*--- Apply reconstruction: V_L = V_i + 0.5 * lim * dV_ij^kap ---*/ + V.i.all(iVar) += 0.5 * lim_i * proj_i; + V.j.all(iVar) -= 0.5 * lim_j * proj_j; + } +} + +/*! + * \brief Reconstruct a slice of nVarGrad variables starting at column iRow, dispatching on the + * limiter type. This is the switch `reconstructPrimitives` used to perform inline; lifted + * here so both the flow and the scalar reconstructions call the same body. + */ +template +FORCEINLINE void reconstruct(typename CLaneTraits::Int iPoint, typename CLaneTraits::Int jPoint, + const Vector& vector_ij, const Gradient_t& gradient, + const Limiter_t& limiter, LIMITER limiterType, size_t iRow, CPair& V, + const CNonDeduced& kappa, const CNonDeduced& umusclRamp) { + switch (limiterType) { + case LIMITER::NONE: + musclUnlimited(iPoint, jPoint, vector_ij, gradient, V, kappa, umusclRamp, iRow); + break; + case LIMITER::VAN_ALBADA_EDGE: + musclEdgeLimited(iPoint, jPoint, vector_ij, gradient, V, kappa, umusclRamp, iRow); + break; + default: + musclPointLimited(iPoint, jPoint, vector_ij, limiter, gradient, V, kappa, umusclRamp, iRow); + break; + } +} + +/*! + * \brief Update the matrix and right-hand-side of a linear system with one conservative flux. + */ +template ::Int> +FORCEINLINE void updateLinearSystem(Int iEdge, Int iPoint, Int jPoint, bool implicit, UpdateType updateType, + Double updateMask, const Vector& flux, + const Matrix& jac_i, const Matrix& jac_j, + CSysVector& vector, SparseMatrixType& matrix) { + if (updateType == UpdateType::COLORING) { + vector.UpdateBlocks(iPoint, jPoint, flux, updateMask); + if (implicit) { + auto wasActive = AD::BeginPassive(); + matrix.SetBlocks(iEdge, iPoint, jPoint, jac_i, jac_j, updateMask); + AD::EndPassive(wasActive); + } + } else { + vector.SetBlock(iEdge, flux, updateMask); + if (implicit) { + auto wasActive = AD::BeginPassive(); + matrix.SetBlocks(iEdge, jac_i, jac_j, updateMask); + AD::EndPassive(wasActive); + } + } +} + +/*! + * \brief Update the matrix and right-hand-side of a linear system with two independent row + * contributions and four independent Jacobian blocks. + * \note It carries a second CSysVector, the target of flux_j under UpdateType::REDUCTION and + * unused under COLORING, where both rows are written directly. + */ +template ::Int> +FORCEINLINE void updateLinearSystem(Int iEdge, Int iPoint, Int jPoint, bool implicit, UpdateType updateType, + Double updateMask, const EdgeResidual& res, + CSysVector& vector, CSysVector& vectorDiff, + SparseMatrixType& matrix) { + if (updateType == UpdateType::COLORING) { + vector.AddBlock(iPoint, res.flux_i, updateMask); + vector.AddBlock(jPoint, res.flux_j, updateMask); + if (implicit) { + auto wasActive = AD::BeginPassive(); + matrix.SetBlocks(iEdge, iPoint, jPoint, res.jac_ii, res.jac_ij, res.jac_ji, res.jac_jj, updateMask); + AD::EndPassive(wasActive); + } + } else { + vector.SetBlock(iEdge, res.flux_i, updateMask); + vectorDiff.SetBlock(iEdge, res.flux_j, updateMask); + if (implicit) { + auto wasActive = AD::BeginPassive(); + matrix.SetOffDiagBlocks(iEdge, res.jac_ij, res.jac_ji, updateMask); + AD::EndPassive(wasActive); + } + } +} + +/*! + * \brief Store the (scalar) mass flux of an edge, e.g. for "bounded scalar" transport equations. + * \note No-op if "target" is null. As with CEdge's Nodes/Normal, edges within a SIMD group are + * contiguous (coloring groups are multiples of the SIMD size), so this is a plain vectorized store + * starting at iEdge[0], relying on "target" being padded to a multiple of the SIMD size. + */ +template ::Int> +FORCEINLINE void updateEdgeMassFlux(Int iEdge, const Double& massFlux, su2activevector* target) { + if (target) massFlux.store(&(*target)[iEdge[0]]); +} diff --git a/SU2_CFD/include/numerics_simd/CNumericsSIMD.hpp b/SU2_CFD/include/numerics_simd/CNumericsSIMD.hpp index a7726a6436a..076c08517b4 100644 --- a/SU2_CFD/include/numerics_simd/CNumericsSIMD.hpp +++ b/SU2_CFD/include/numerics_simd/CNumericsSIMD.hpp @@ -28,15 +28,7 @@ #pragma once #include "../../../Common/include/parallelization/vectorization.hpp" -#include "../../../Common/include/containers/C2DContainer.hpp" - -/*! - * \enum UpdateType - * \brief Ways to update vectors and system matrices. - * COLORING is the typical i/j update, whereas for REDUCTION - * the fluxes are stored and the matrix diagonal is not modified. - */ -enum class UpdateType {COLORING, REDUCTION}; +#include "../numerics/util.hpp" /*! * \brief Define Double and Int SIMD types. @@ -45,18 +37,10 @@ using Double = simd::Array; using Int = simd::Array; /*--- Forward declare a few classes used in name only by the interface. ---*/ -template class CSysVector; -template class CSysMatrix; class CConfig; class CGeometry; class CVariable; -#ifdef CODI_FORWARD_TYPE -using SparseMatrixType = CSysMatrix; -#else -using SparseMatrixType = CSysMatrix; -#endif - /*! * \class CNumericsSIMD * \ingroup ConvDiscr diff --git a/SU2_CFD/include/numerics_simd/flow/convection/common.hpp b/SU2_CFD/include/numerics_simd/flow/convection/common.hpp index f3669d45423..800167d4b0f 100644 --- a/SU2_CFD/include/numerics_simd/flow/convection/common.hpp +++ b/SU2_CFD/include/numerics_simd/flow/convection/common.hpp @@ -32,149 +32,6 @@ #include "../variables.hpp" #include "../../../variables/CNSVariable.hpp" -/*! - * \brief Blended difference for U-MUSCL reconstruction. - * \param[in] gradProj - Gradient projection at point i: dot(grad_i, vector_ij). - * \param[in] delta - Centered difference: V_j - V_i. - * \param[in] kappa - Blending parameter. - * \return Blended difference for reconstruction from point i. - */ -FORCEINLINE Double umusclProjection(const Double& gradProj, - const Double& delta, - const Double& kappa) { - /*-------------------------------------------------------------------*/ - /*--- The MUSCL kappa-scheme reconstruction is typically written: ---*/ - /*--- V_L = V_i + 0.25 * dV_ij^kap, where ---*/ - /*--- dV_ij^kap = (1-kappa) dV_ij^upw + (1+kappa) dV_ij^cen, ---*/ - /*--- dV_ij^cen = V_j - V_i, ---*/ - /*--- dV_ij^upw = 2 grad(Vi) dot vector_ij - dV_ij^cen. ---*/ - /*--- To maintain proper scaling for edge limiters, the result of ---*/ - /*--- this function is 0.5 * dV_ij^kap. ---*/ - /*-------------------------------------------------------------------*/ - return (1.0 - kappa) * gradProj + kappa * delta; -} - -/*! - * \brief MUSCL reconstruction of the specified variable. - * \note The result should be halved when added to i (or subtracted from j). - * \param[in] grad_i - Gradient vector at point i. - * \param[in] vector_ij - Distance vector from i to j. - * \param[in] delta - Centered difference: V_j - V_i. - * \param[in] iVar - Variable index. - * \param[in] kappa - Blending coefficient. - * \param[in] umusclRamp - MUSCL 1st-2nd order ramp times Newton-Krylov relaxation. - * \return Variable reconstructed from point i. - */ -template -FORCEINLINE Double musclReconstruction(const GradType& grad, - const VectorDbl& vector_ij, - const Double& delta, - const size_t iVar, - const Double& kappa, - const Double& umusclRamp) { - const Double proj = dot(grad[iVar], vector_ij); - return umusclRamp * umusclProjection(proj, delta, kappa); -} - -/*! - * \brief Unlimited reconstruction. - */ -template -FORCEINLINE void musclUnlimited(const Int& iPoint, - const Int& jPoint, - const VectorDbl& vector_ij, - const Gradient_t& gradient, - CPair& V, - const Double& kappa, - const Double& umusclRamp) { - constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; - - auto grad_i = gatherVariables(iPoint, gradient); - auto grad_j = gatherVariables(jPoint, gradient); - - for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { - /*--- Centered difference, needed for U-MUSCL projection ---*/ - const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); - - /*--- U-MUSCL reconstructed variables ---*/ - const Double proj_i = musclReconstruction(grad_i, vector_ij, delta_ij, iVar, kappa, umusclRamp); - const Double proj_j = musclReconstruction(grad_j, vector_ij, delta_ij, iVar, kappa, umusclRamp); - - /*--- Apply reconstruction: V_L = V_i + 0.5 * dV_ij^kap ---*/ - V.i.all(iVar) += 0.5 * proj_i; - V.j.all(iVar) -= 0.5 * proj_j; - } -} - -/*! - * \brief Limited reconstruction with point-based limiter. - */ -template -FORCEINLINE void musclPointLimited(const Int& iPoint, - const Int& jPoint, - const VectorDbl& vector_ij, - const Limiter_t& limiter, - const Gradient_t& gradient, - CPair& V, - const Double& kappa, - const Double& umusclRamp) { - constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; - - auto lim_i = gatherVariables(iPoint, limiter); - auto lim_j = gatherVariables(jPoint, limiter); - - auto grad_i = gatherVariables(iPoint, gradient); - auto grad_j = gatherVariables(jPoint, gradient); - - for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { - /*--- Centered difference, needed for U-MUSCL projection ---*/ - const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); - - /*--- U-MUSCL reconstructed variables ---*/ - const Double proj_i = musclReconstruction(grad_i, vector_ij, delta_ij, iVar, kappa, umusclRamp); - const Double proj_j = musclReconstruction(grad_j, vector_ij, delta_ij, iVar, kappa, umusclRamp); - - /*--- Apply reconstruction: V_L = V_i + 0.5 * lim * dV_ij^kap ---*/ - V.i.all(iVar) += 0.5 * lim_i(iVar) * proj_i; - V.j.all(iVar) -= 0.5 * lim_j(iVar) * proj_j; - } -} - -/*! - * \brief Limited reconstruction with edge-based limiter. - */ -template -FORCEINLINE void musclEdgeLimited(const Int& iPoint, - const Int& jPoint, - const VectorDbl& vector_ij, - const Gradient_t& gradient, - CPair& V, - const Double& kappa, - const Double& umusclRamp) { - constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; - - auto grad_i = gatherVariables(iPoint, gradient); - auto grad_j = gatherVariables(jPoint, gradient); - - for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { - /*--- Centered difference, needed for U-MUSCL projection and limiter ---*/ - const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); - const Double delta_ij_2 = pow(delta_ij, 2) + 1e-6; - - /*--- U-MUSCL reconstructed variables ---*/ - const Double proj_i = musclReconstruction(grad_i, vector_ij, delta_ij, iVar, kappa, umusclRamp); - const Double proj_j = musclReconstruction(grad_j, vector_ij, delta_ij, iVar, kappa, umusclRamp); - - /// TODO: Customize the limiter function. - const Double lim_i = (delta_ij_2 + proj_i*delta_ij) / (pow(proj_i,2) + delta_ij_2); - const Double lim_j = (delta_ij_2 + proj_j*delta_ij) / (pow(proj_j,2) + delta_ij_2); - - /*--- Apply reconstruction: V_L = V_i + 0.5 * lim * dV_ij^kap ---*/ - V.i.all(iVar) += 0.5 * lim_i * proj_i; - V.j.all(iVar) -= 0.5 * lim_j * proj_j; - } -} - /*! * \brief Retrieve primitive variables for points i/j, reconstructing them if needed. * \note Density and enthalpy are recomputed from ideal gas EOS. @@ -219,17 +76,7 @@ FORCEINLINE CPair reconstructPrimitives(const Int& iEdge, if (muscl) { /*--- Reconstruct density and enthalpy without using their gradients. ---*/ constexpr auto nVarGrad = ReconVarType::nVar - 2; - switch (limiterType) { - case LIMITER::NONE: - musclUnlimited(iPoint, jPoint, vector_ij, gradients, V, kappa, umusclRamp); - break; - case LIMITER::VAN_ALBADA_EDGE: - musclEdgeLimited(iPoint, jPoint, vector_ij, gradients, V, kappa, umusclRamp); - break; - default: - musclPointLimited(iPoint, jPoint, vector_ij, limiters, gradients, V, kappa, umusclRamp); - break; - } + reconstruct(iPoint, jPoint, vector_ij, gradients, limiters, limiterType, 0, V, kappa, umusclRamp); /*--- Recompute density using the reconstructed pressure and temperature. ---*/ V.i.density() = V.i.pressure() / (gasConst * V.i.temperature()); V.j.density() = V.j.pressure() / (gasConst * V.j.temperature()); diff --git a/SU2_CFD/include/numerics_simd/util.hpp b/SU2_CFD/include/numerics_simd/util.hpp index 79594268be8..9db7721803f 100644 --- a/SU2_CFD/include/numerics_simd/util.hpp +++ b/SU2_CFD/include/numerics_simd/util.hpp @@ -1,6 +1,6 @@ /*! * \file util.hpp - * \brief Generic auxiliary functions. + * \brief Vector, matrix and index types bound to the SIMD Double/Int of CNumericsSIMD.hpp. * \author P. Gomes * \version 8.5.0 "Harrier" * @@ -28,248 +28,10 @@ #pragma once #include "CNumericsSIMD.hpp" -#include "../../../Common/include/containers/C2DContainer.hpp" -#include "../../../Common/include/linear_algebra/CSysVector.hpp" -#include "../../../Common/include/linear_algebra/CSysMatrix.hpp" +#include "../numerics/util.hpp" -/*! - * \brief Static vector and matrix types. - * \note These should be used instead of C-style arrays. - */ -template -using Vector = C2DContainer; - -template using VectorInt = Vector; -template using VectorDbl = Vector; - -template -using Matrix = C2DContainer; - -template using MatrixInt = Matrix; -template using MatrixDbl = Matrix; - -/*! - * \brief Constexpr version of max. - */ -inline constexpr size_t Max(size_t a, size_t b) { return a>b? a : b; } - -/*! - * \brief Simple pair type for i/j variables. - */ -template -struct CPair { - T i, j; -}; - -/*! - * \brief Dot product. - */ -template -FORCEINLINE Double dot(ForwardIterator iterator, const T* ptr) { - Double sum = 0.0; - for (size_t iDim = 0; iDim < nDim; ++iDim) { - sum += *(iterator++) * ptr[iDim]; - } - return sum; -} - -/*! - * \overload Dot product. - */ -template -FORCEINLINE Double dot(ForwardIterator iterator, const VectorDbl& vector) { - return dot(iterator, vector.data()); -} - -/*! - * \overload Dot product. - */ -template -FORCEINLINE Double dot(const VectorDbl& a, const VectorDbl& b) { - return dot(a.data(), b.data()); -} - -/*! - * \brief Squared norm. - */ -template -FORCEINLINE Double squaredNorm(ForwardIterator iterator) { - Double sum = 0.0; - for (size_t iDim = 0; iDim < nDim; ++iDim) { - sum += pow(*(iterator++),2); - } - return sum; -} - -/*! - * \overload Squared norm. - */ -template -FORCEINLINE Double squaredNorm(const VectorDbl& vector) { - return squaredNorm(vector.data()); -} - -/*! - * \brief Tangential projection. - */ -template -FORCEINLINE VectorDbl tangentProjection(const MatrixDbl& tensor, - const VectorDbl& unitVector) { - VectorDbl proj; - for (size_t iDim = 0; iDim < nDim; ++iDim) - proj(iDim) = dot(tensor[iDim], unitVector); - - Double normalProj = dot(proj, unitVector); - - for (size_t iDim = 0; iDim < nDim; ++iDim) - proj(iDim) -= normalProj * unitVector(iDim); +template using VectorInt = Vector; +template using VectorDbl = Vector; - return proj; -} - -/*! - * \brief Vector norm. - */ -template -FORCEINLINE Double norm(const VectorDbl& vector) { return sqrt(squaredNorm(vector)); } - -#ifndef CODI_REVERSE_TYPE -/*! - * \brief Gather a single variable from index iPoint of a 1D container. - */ -template -FORCEINLINE Double gatherVariables(Int iPoint, const Container& vars) { - return *vars.innerIter(iPoint); -} - -/*! - * \brief Gather a vector of variables (size nVar) from row iPoint of a 2D container. - */ -template -FORCEINLINE VectorDbl gatherVariables(Int iPoint, const Container& vars) { - return vars.template get >(iPoint); -} - -/*! - * \brief Gather a matrix of variables from outer index iPoint of a 3D container. - */ -template -FORCEINLINE MatrixDbl gatherVariables(Int iPoint, const Container& vars) { - return vars.template get >(iPoint); -} -#else - -namespace { - template = 0> - FORCEINLINE const su2double& get(const Container& vars, unsigned long iPoint) { return vars(iPoint); } - - /*--- When getting 1 variable from a matrix container, we assume it is the first. ---*/ - template = 0> - FORCEINLINE const su2double& get(const Container& vars, unsigned long iPoint) { return vars(iPoint,0); } -} - -template -FORCEINLINE Double gatherVariables(Int iPoint, const Container& vars) { - Double x; - for (size_t k=0; k -FORCEINLINE VectorDbl gatherVariables(Int iPoint, const Container& vars) { - VectorDbl x; - for (size_t i=0; i -FORCEINLINE MatrixDbl gatherVariables(Int iPoint, const Container& vars) { - MatrixDbl x; - for (size_t i=0; i -FORCEINLINE void stopPreacc(VectorDbl& x) { - AD::SetPreaccOut(x, nVar, Double::Size); - AD::EndPreacc(); -} - -/*! - * \brief Distance vector, from point i to point j. - */ -template -FORCEINLINE VectorDbl distanceVector(Int iPoint, Int jPoint, - const Container& coords) { - auto coord_i = gatherVariables(iPoint, coords); - auto coord_j = gatherVariables(jPoint, coords); - VectorDbl vector_ij; - for (size_t iDim = 0; iDim < nDim; ++iDim) { - vector_ij(iDim) = coord_j(iDim) - coord_i(iDim); - } - return vector_ij; -} - -/*! - * \brief Update the matrix and right-hand-side of a linear system. - */ -template -FORCEINLINE void updateLinearSystem(Int iEdge, - Int iPoint, - Int jPoint, - bool implicit, - UpdateType updateType, - Double updateMask, - const VectorDbl& flux, - const MatrixDbl& jac_i, - const MatrixDbl& jac_j, - CSysVector& vector, - SparseMatrixType& matrix) { - if (updateType == UpdateType::COLORING) { - vector.UpdateBlocks(iPoint, jPoint, flux, updateMask); - if(implicit) { - auto wasActive = AD::BeginPassive(); - matrix.SetBlocks(iEdge, iPoint, jPoint, jac_i, jac_j, updateMask); - AD::EndPassive(wasActive); - } - } - else { - vector.SetBlock(iEdge, flux, updateMask); - if(implicit) { - auto wasActive = AD::BeginPassive(); - matrix.SetBlocks(iEdge, jac_i, jac_j, updateMask); - AD::EndPassive(wasActive); - } - } -} - -/*! - * \brief Store the (scalar) mass flux of an edge, e.g. for "bounded scalar" transport equations. - * \note No-op if "target" is null. As with CEdge's Nodes/Normal, edges within a SIMD group are - * contiguous (coloring groups are multiples of the SIMD size), so this is a plain vectorized store - * starting at iEdge[0], relying on "target" being padded to a multiple of the SIMD size. - */ -FORCEINLINE void updateEdgeMassFlux(Int iEdge, - const Double& massFlux, - su2activevector* target) { - if (target) massFlux.store(&(*target)[iEdge[0]]); -} +template using MatrixInt = Matrix; +template using MatrixDbl = Matrix; diff --git a/SU2_CFD/include/solvers/CScalarSolver.hpp b/SU2_CFD/include/solvers/CScalarSolver.hpp index 500233645bf..e941faacdd7 100644 --- a/SU2_CFD/include/solvers/CScalarSolver.hpp +++ b/SU2_CFD/include/solvers/CScalarSolver.hpp @@ -138,7 +138,9 @@ class CScalarSolver : public CSolver { auto residual = numerics->ComputeResidual(config); if (ReducerStrategy) { + /*--- Accumulates onto the convective contribution the interior loop already wrote. ---*/ EdgeFluxes.SubtractBlock(iEdge, residual); + EdgeFluxesDiff.AddBlock(iEdge, residual); if (implicit) Jacobian.UpdateBlocksSub(iEdge, residual.jacobian_i, residual.jacobian_j); } else { LinSysRes.SubtractBlock(iPoint, residual); @@ -194,8 +196,9 @@ class CScalarSolver : public CSolver { Jacobian.GetBlocks(iEdge, iPoint, jPoint, Block_ii, Block_ij, Block_ji, Block_jj); } if (ReducerStrategy) { + /*--- i's row takes its contribution from residual_ij alone, accumulated onto what the + * convective term already wrote; j's row is accumulated once residual_ji is known, below. ---*/ EdgeFluxes.SubtractBlock(iEdge, residual_ij); - EdgeFluxesDiff.SetBlock(iEdge, residual_ij); if (implicit) { /*--- For the reducer strategy the Jacobians are averaged for simplicity. ---*/ for (int iVar=0; iVar::Upwind_Residual(CGeometry* geometry, CSolver** auto residual = numerics->ComputeResidual(config); if (ReducerStrategy) { + /*--- A conservative flux writes opposite contributions into the two containers; the + * viscous term, computed below on the same edge, accumulates onto them. ---*/ EdgeFluxes.SetBlock(iEdge, residual); + EdgeFluxesDiff.SetBlock(iEdge, residual, -1); if (implicit) Jacobian.SetBlocks(iEdge, residual.jacobian_i, residual.jacobian_j); } else { LinSysRes.AddBlock(iPoint, residual); @@ -351,8 +354,8 @@ template void CScalarSolver::SumEdgeFluxes(CGeometry* geometry) { SU2_ZONE_SCOPED - const bool nonConservative = EdgeFluxesDiff.GetLocSize() > 0; - + /*--- EdgeFluxes and EdgeFluxesDiff hold the two row contributions of an edge directly, + * flux_i and flux_j, so each point simply accumulates its own side of every incident edge. ---*/ SU2_OMP_FOR_STAT(omp_chunk_size) for (unsigned long iPoint = 0; iPoint < nPoint; ++iPoint) { LinSysRes.SetBlock_Zero(iPoint); @@ -361,10 +364,7 @@ void CScalarSolver::SumEdgeFluxes(CGeometry* geometry) { if (iPoint == geometry->edges->GetNode(iEdge, 0)) { LinSysRes.AddBlock(iPoint, EdgeFluxes.GetBlock(iEdge)); } else { - LinSysRes.SubtractBlock(iPoint, EdgeFluxes.GetBlock(iEdge)); - if (nonConservative) { - LinSysRes.SubtractBlock(iPoint, EdgeFluxesDiff.GetBlock(iEdge)); - } + LinSysRes.AddBlock(iPoint, EdgeFluxesDiff.GetBlock(iEdge)); } } } diff --git a/SU2_CFD/src/solvers/CHeatSolver.cpp b/SU2_CFD/src/solvers/CHeatSolver.cpp index d944255a694..cb087023acd 100644 --- a/SU2_CFD/src/solvers/CHeatSolver.cpp +++ b/SU2_CFD/src/solvers/CHeatSolver.cpp @@ -66,7 +66,10 @@ CHeatSolver::CHeatSolver(CGeometry *geometry, CConfig *config, unsigned short iM Jacobian.Initialize(nPoint, nPointDomain, nVar, nVar, true, geometry, config, ReducerStrategy); LinSysSol.Initialize(nPoint, nPointDomain, nVar, 0.0); LinSysRes.Initialize(nPoint, nPointDomain, nVar, 0.0); - if (ReducerStrategy) EdgeFluxes.Initialize(geometry->GetnEdge(), geometry->GetnEdge(), nVar, nullptr); + if (ReducerStrategy) { + EdgeFluxes.Initialize(geometry->GetnEdge(), geometry->GetnEdge(), nVar, nullptr); + EdgeFluxesDiff.Initialize(geometry->GetnEdge(), geometry->GetnEdge(), nVar, nullptr); + } if (config->GetExtraOutput()) { if (nDim == 2) { nOutputVariables = 13; } @@ -178,10 +181,14 @@ void CHeatSolver::Preprocessing(CGeometry *geometry, CSolver **solver_container, SU2_OMP_SAFE_GLOBAL_ACCESS(config->SetGlobalParam(config->GetKind_Solver(), RunTime_EqSystem);) CommonPreprocessing(geometry, config, Output); - /*--- Need to clear EdgeFluxes and Jacobian when only the viscous part is called for solid heat transfer, - * for the weakly coupled energy equation the convection part does this by setting instead of incrementing. ---*/ + /*--- Need to clear EdgeFluxes, EdgeFluxesDiff and Jacobian when only the viscous part is called for + * solid heat transfer, for the weakly coupled energy equation the convection part does this by + * setting instead of incrementing. ---*/ if (!Output && !flow) { - if (ReducerStrategy) EdgeFluxes.SetValZero(); + if (ReducerStrategy) { + EdgeFluxes.SetValZero(); + EdgeFluxesDiff.SetValZero(); + } if (config->GetKind_TimeIntScheme() == EULER_IMPLICIT) Jacobian.SetValZero(); SU2_OMP_BARRIER diff --git a/SU2_CFD/src/solvers/CSpeciesFlameletSolver.cpp b/SU2_CFD/src/solvers/CSpeciesFlameletSolver.cpp index fee5a0702e2..9e60f3c40d8 100644 --- a/SU2_CFD/src/solvers/CSpeciesFlameletSolver.cpp +++ b/SU2_CFD/src/solvers/CSpeciesFlameletSolver.cpp @@ -798,6 +798,7 @@ void CSpeciesFlameletSolver::Viscous_Residual(const unsigned long iEdge, const C if (ReducerStrategy) { EdgeFluxes.SubtractBlock(iEdge, residual_PD); + EdgeFluxesDiff.AddBlock(iEdge, residual_PD); if (implicit) Jacobian.UpdateBlocksSub(iEdge, residual_PD.jacobian_i, residual_PD.jacobian_j); } else { @@ -845,6 +846,7 @@ void CSpeciesFlameletSolver::Viscous_Residual(const unsigned long iEdge, const C if (ReducerStrategy) { EdgeFluxes.SubtractBlock(iEdge, residual_thermal); + EdgeFluxesDiff.AddBlock(iEdge, residual_thermal); } else { LinSysRes.SubtractBlock(iPoint, residual_thermal); LinSysRes.AddBlock(jPoint, residual_thermal); diff --git a/SU2_CFD/src/solvers/CSpeciesSolver.cpp b/SU2_CFD/src/solvers/CSpeciesSolver.cpp index 38ad833f65e..0c3c2502849 100644 --- a/SU2_CFD/src/solvers/CSpeciesSolver.cpp +++ b/SU2_CFD/src/solvers/CSpeciesSolver.cpp @@ -140,7 +140,10 @@ void CSpeciesSolver::Initialize(CGeometry* geometry, CConfig* config, unsigned s LinSysRes.Initialize(nPoint, nPointDomain, nVar, 0.0); System.SetxIsZero(true); - if (ReducerStrategy) EdgeFluxes.Initialize(geometry->GetnEdge(), geometry->GetnEdge(), nVar, nullptr); + if (ReducerStrategy) { + EdgeFluxes.Initialize(geometry->GetnEdge(), geometry->GetnEdge(), nVar, nullptr); + EdgeFluxesDiff.Initialize(geometry->GetnEdge(), geometry->GetnEdge(), nVar, nullptr); + } } /*--- Initialize lower and upper limits---*/ diff --git a/SU2_CFD/src/solvers/CTransLMSolver.cpp b/SU2_CFD/src/solvers/CTransLMSolver.cpp index bb8a53cda12..0261e5e093f 100644 --- a/SU2_CFD/src/solvers/CTransLMSolver.cpp +++ b/SU2_CFD/src/solvers/CTransLMSolver.cpp @@ -85,8 +85,10 @@ CTransLMSolver::CTransLMSolver(CGeometry *geometry, CConfig *config, unsigned sh LinSysRes.Initialize(nPoint, nPointDomain, nVar, 0.0); System.SetxIsZero(true); - if (ReducerStrategy) + if (ReducerStrategy) { EdgeFluxes.Initialize(geometry->GetnEdge(), geometry->GetnEdge(), nVar, nullptr); + EdgeFluxesDiff.Initialize(geometry->GetnEdge(), geometry->GetnEdge(), nVar, nullptr); + } /*--- Initialize the BGS residuals in multizone problems. ---*/ if (multizone){ diff --git a/UnitTests/Common/linear_algebra/edge_residual_blocks_tests.cpp b/UnitTests/Common/linear_algebra/edge_residual_blocks_tests.cpp new file mode 100644 index 00000000000..7f7ee0c5b6f --- /dev/null +++ b/UnitTests/Common/linear_algebra/edge_residual_blocks_tests.cpp @@ -0,0 +1,109 @@ +/*! + * \file edge_residual_blocks_tests.cpp + * \brief Unit tests for CSysMatrix::SetBlocks (four independent blocks) and SetOffDiagBlocks. + * \author P. Gomes + * \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 "../../UnitQuadTestCase.hpp" + +/*--- A block whose entries are all different, so a mixed-up index reads back wrong. ---*/ +static void FillBlock(su2double block[][8], unsigned long nVar, su2double base) { + for (auto i = 0u; i < nVar; ++i) + for (auto j = 0u; j < nVar; ++j) block[i][j] = base + 0.1 * i + 0.01 * j; +} + +static void CheckBlock(const CSysMatrix& matrix, unsigned long i, unsigned long j, unsigned long nVar, + const su2double block[][8], double tol) { + auto view = matrix.GetBlockView(i, j); + for (auto iVar = 0u; iVar < nVar; ++iVar) + for (auto jVar = 0u; jVar < nVar; ++jVar) + CHECK(SU2_TYPE::GetValue(view(iVar, jVar)) == Approx(block[iVar][jVar]).margin(tol)); +} + +TEST_CASE("SetBlocks and SetOffDiagBlocks assemble four independent blocks", "[LinearAlgebra]") { + cout.rdbuf(nullptr); + + UnitQuadTestCase testCase; + testCase.InitConfig(); + testCase.InitGeometry(); + testCase.InitSolver(); + + cout.rdbuf(testCase.orig_buf); + + auto* solver = testCase.solver[FLOW_SOL]; + auto& matrix = solver->Jacobian; + const auto nVar = solver->GetnVar(); + REQUIRE(testCase.geometry->GetnEdge() > 0); + + const auto iEdge = 0ul; + const auto iPoint = testCase.geometry->edges->GetNode(iEdge, 0); + const auto jPoint = testCase.geometry->edges->GetNode(iEdge, 1); + + su2double jac_ii[8][8], jac_ij[8][8], jac_ji[8][8], jac_jj[8][8]; + FillBlock(jac_ii, nVar, 1.0); + FillBlock(jac_ij, nVar, 2.0); + FillBlock(jac_ji, nVar, 3.0); + FillBlock(jac_jj, nVar, 4.0); + + SECTION("SetBlocks: diagonal accumulates, off-diagonal is set") { + matrix.SetValZero(); + + matrix.SetBlocks(iEdge, iPoint, jPoint, jac_ii, jac_ij, jac_ji, jac_jj); + CheckBlock(matrix, iPoint, iPoint, nVar, jac_ii, 1e-6); + CheckBlock(matrix, jPoint, jPoint, nVar, jac_jj, 1e-6); + CheckBlock(matrix, iPoint, jPoint, nVar, jac_ij, 1e-6); + CheckBlock(matrix, jPoint, iPoint, nVar, jac_ji, 1e-6); + + /*--- A second call must double the diagonal (accumulated) and leave the + * off-diagonal exactly as set (overwritten, not doubled). ---*/ + matrix.SetBlocks(iEdge, iPoint, jPoint, jac_ii, jac_ij, jac_ji, jac_jj); + + su2double jac_ii_2x[8][8], jac_jj_2x[8][8]; + for (auto i = 0u; i < nVar; ++i) + for (auto j = 0u; j < nVar; ++j) { + jac_ii_2x[i][j] = 2 * jac_ii[i][j]; + jac_jj_2x[i][j] = 2 * jac_jj[i][j]; + } + CheckBlock(matrix, iPoint, iPoint, nVar, jac_ii_2x, 1e-6); + CheckBlock(matrix, jPoint, jPoint, nVar, jac_jj_2x, 1e-6); + CheckBlock(matrix, iPoint, jPoint, nVar, jac_ij, 1e-6); + CheckBlock(matrix, jPoint, iPoint, nVar, jac_ji, 1e-6); + } + + SECTION("SetOffDiagBlocks leaves the diagonal untouched") { + matrix.SetValZero(); + matrix.SetBlocks(iEdge, iPoint, jPoint, jac_ii, jac_ij, jac_ji, jac_jj); + + su2double jac_ij_new[8][8], jac_ji_new[8][8]; + FillBlock(jac_ij_new, nVar, 5.0); + FillBlock(jac_ji_new, nVar, 6.0); + matrix.SetOffDiagBlocks(iEdge, jac_ij_new, jac_ji_new); + + CheckBlock(matrix, iPoint, iPoint, nVar, jac_ii, 1e-6); + CheckBlock(matrix, jPoint, jPoint, nVar, jac_jj, 1e-6); + CheckBlock(matrix, iPoint, jPoint, nVar, jac_ij_new, 1e-6); + CheckBlock(matrix, jPoint, iPoint, nVar, jac_ji_new, 1e-6); + } +} diff --git a/UnitTests/meson.build b/UnitTests/meson.build index f8c22511b5f..00ad6a764cd 100644 --- a/UnitTests/meson.build +++ b/UnitTests/meson.build @@ -18,7 +18,8 @@ su2_cfd_tests = files(['Common/geometry/primal_grid/CPrimalGrid_tests.cpp', 'SU2_CFD/gradients.cpp', 'SU2_CFD/windowing.cpp', 'Common/toolboxes/random_toolbox_tests.cpp', - 'Common/linear_algebra/quantization_tests.cpp']) + 'Common/linear_algebra/quantization_tests.cpp', + 'Common/linear_algebra/edge_residual_blocks_tests.cpp']) # Reverse-mode (algorithmic differentiation) tests: su2_cfd_tests_ad = files(['Common/simple_ad_test.cpp', From f8913ff797ffcb0a783db91908c019b59c0ae6b6 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Wed, 26 Aug 2026 21:51:48 -0700 Subject: [PATCH 02/20] SA turbulence model as a CUpwScalarBase third-layer model, driving the interior edge loop Adds CScalarFlux_SA (numerics/turbulent/turb_sa_edge_flux.hpp), the SA convective and diffusive flux expressed through the CUpwScalarBase/CAvgGradScalarBase CRTP chain, and CScalarSolver::EdgeFluxResidual, the generic interior edge loop that drives any such model under both the coloring and reducer update strategies. CTurbSASolver::Upwind_Residual now resolves the compile-time flow indices, dimension, backscatter equation count (1 or 4) and MUSCL setting at runtime and calls it directly; CTurbSASolver has no Viscous_Residual of its own any more (its SetRoughness call was dead code, roughness is only read by the source term). This also finishes wiring MUSCL reconstruction into CUpwScalarBase::ComputeFlux, deferred since the header was first promoted: the convective term's transported variable and, when the flow scheme also reconstructs, its face-normal velocity are now reconstructed before the upwinding weights are formed, with kappa/the U-MUSCL ramp/limiter types read once from CConfig in the model's constructor (rebuilt every nonlinear iteration) rather than per edge. Two related bugs turned up while exercising this for real (the scalar_edge_flux.hpp kernels had only been syntax-checked via explicit instantiation until now, never executed): - musclUnlimited/musclPointLimited/musclEdgeLimited pre-gathered a whole nVarGrad x nDim gradient block and indexed it by row; for nVarGrad 1 (a non-backscatter SA equation) that block is a Matrix, which degenerates to plain per-element indexing in C2DContainer's one-row specialization instead of the row-pointer access the callers assumed. Reworked to gather one variable's gradient row at a time as a Vector, which never degenerates regardless of size. - CVariable::GetGradient() and GetLimiter() (whole-container overloads) had no const version, breaking EdgeSide's read-only access pattern the first time a real caller needed it. Verified against turb_ONERAM6 (MUSCL on and off, 1 and 16 OpenMP threads, though the reducer strategy did not trigger on this mesh at either thread count): residuals match the pre-SA baseline to ~1e-8 relative after 19 iterations, consistent with the floating-point reassociation drift already characterized in earlier steps, not a behavioral change. The backscatter (nVar 4) path compiles but was not exercised at runtime in this pass. --- .../numerics/scalar/scalar_edge_flux.hpp | 72 ++++++++-- .../numerics/turbulent/turb_sa_edge_flux.hpp | 136 ++++++++++++++++++ SU2_CFD/include/numerics/util.hpp | 40 +++--- SU2_CFD/include/solvers/CScalarSolver.hpp | 14 +- SU2_CFD/include/solvers/CScalarSolver.inl | 85 ++++++++++- SU2_CFD/include/solvers/CTurbSASolver.hpp | 29 +++- SU2_CFD/include/variables/CVariable.hpp | 2 + SU2_CFD/src/solvers/CTurbSASolver.cpp | 57 ++++++-- 8 files changed, 387 insertions(+), 48 deletions(-) create mode 100644 SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp diff --git a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp index 61847a9774a..47df0a95a0d 100644 --- a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp +++ b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp @@ -63,6 +63,7 @@ struct ScalarFluxOptions { */ template struct CScalarValues { + static constexpr size_t nVar = Size; /*!< \brief Only used as VarType::nVar when reconstruct's own nVarGrad_ default applies; every call site here passes an explicit nVarGrad_ instead. */ Vector all; }; @@ -188,10 +189,16 @@ class CUpwScalarFlux : public CAvgGradScalarBase + /*! + * \param[in] phi - Transported variable of both endpoints, reconstructed if the scheme is + * instantiated with muscl; read from here rather than side_i/side_j.scalarNodes + * directly so a model needs no reconstruction logic of its own. + */ + template FORCEINLINE void finalizeFlux(const FlowIndices& idx, const ScalarFluxOptions&, Int iPoint, const EdgeSide& side_i, Int jPoint, const EdgeSide& side_j, const Double& a0, const Double& a1, + const CPair>& phi, EdgeResidual& res) const { Double w0 = a0, w1 = a1; if constexpr (Derived::Conservative) { @@ -200,9 +207,7 @@ class CUpwScalarFlux : public CAvgGradScalarBase FORCEINLINE EdgeResidual ComputeFlux(const ScalarFluxOptions& opt, Int iPoint, @@ -261,8 +287,7 @@ class CUpwScalarBase : public CUpwScalarFlux res(nEqn); - /*--- Read once by the reconstruction (added alongside the first model that uses it) and - * by the diffusion. ---*/ + /*--- Read once by the reconstruction and by the diffusion. ---*/ Vector vector_ij; if (muscl || opt.viscous) { vector_ij = distanceVector(iPoint, side_i.coord, jPoint, side_j.coord); @@ -278,12 +303,23 @@ class CUpwScalarBase : public CUpwScalarFlux(iPoint, side_i.flowNodes->GetPrimitive(), idx.Velocity()); - const auto u_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Velocity()); + /*--- The mass-flux branch above reads the edge flux computed from unreconstructed flow + * primitives directly, so only this branch needs a reconstructed velocity. ---*/ + CPair> u; + u.i.all = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Velocity()); + u.j.all = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Velocity()); + + if constexpr (muscl) { + if (musclFlow) { + reconstruct(iPoint, jPoint, vector_ij, side_i.flowNodes->GetGradient_Reconstruction(), + side_i.flowNodes->GetLimiter_Primitive(), limiterTypeFlow, idx.Velocity(), u, kappaFlow, + umusclRamp); + } + } /*--- Face normal velocity of the mean of the two points, relative to the grid. ---*/ Vector vel_ij; - for (int iDim = 0; iDim < nDim; ++iDim) vel_ij(iDim) = 0.5 * (u_i(iDim) + u_j(iDim)); + for (int iDim = 0; iDim < nDim; ++iDim) vel_ij(iDim) = 0.5 * (u.i.all(iDim) + u.j.all(iDim)); if (opt.dynamicGrid) { const auto ug_i = gatherVariables(iPoint, side_i.gridVel); @@ -296,7 +332,21 @@ class CUpwScalarBase : public CUpwScalarFlux(this)->finalizeFlux(idx, opt, iPoint, side_i, jPoint, side_j, a0, a1, res); + /*--- Transported variable of both endpoints, reconstructed if the scheme has muscl on. + * Gathered one variable at a time (like the diffusion gradients above) so a static model + * with nVar 1 never reads past the single column its solution container actually has. ---*/ + CPair> phi; + for (size_t iVar = 0; iVar < res.nVar; ++iVar) { + phi.i.all(iVar) = gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), iVar); + phi.j.all(iVar) = gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), iVar); + } + + if constexpr (muscl && nVar != Dynamic) { + reconstruct(iPoint, jPoint, vector_ij, side_i.scalarNodes.GetGradient_Reconstruction(), + side_i.scalarNodes.GetLimiter(), limiterType, 0, phi, kappa, umusclRamp); + } + + static_cast(this)->finalizeFlux(idx, opt, iPoint, side_i, jPoint, side_j, a0, a1, phi, res); } Base::diffusionTerms(idx, opt, iPoint, side_i, jPoint, side_j, normal, vector_ij, res); diff --git a/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp b/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp new file mode 100644 index 00000000000..e6dc95bd60d --- /dev/null +++ b/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp @@ -0,0 +1,136 @@ +/*! + * \file turb_sa_edge_flux.hpp + * \brief Spalart-Allmaras model as a third-layer scalar flux, see numerics/scalar/scalar_edge_flux.hpp. + * \author P. Gomes + * \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 . + */ + +#pragma once + +#include "../scalar/scalar_edge_flux.hpp" + +/*! + * \class CScalarFlux_SA + * \brief Convection and diffusion of the Spalart-Allmaras model, non-conservative and with a + * diagonal (but asymmetric) diffusion coefficient. + * \note SA writes its own convective term rather than using the inherited CUpwScalarFlux one, + * because with stochastic backscatter active (nVar 4) the three Langevin equations are + * advected with a centered flux, unlike the plain upwind SA equation itself. + */ +template +class CScalarFlux_SA : public CUpwScalarBase, + FlowIndices, nDim, nVar, muscl> { + public: + static constexpr bool Conservative = false; + static constexpr bool DiagonalDiffusion = true; + + using Base = CUpwScalarBase; + using Int = typename Base::Int; + using Base::Base; + + private: + static constexpr passivedouble sigma = 2.0 / 3.0; /*!< \brief Constant of the diffusion term. */ + static constexpr passivedouble cb2 = 0.622; /*!< \brief Constant of the diffusion term. */ + + public: + /*! + * \brief SA convection, plus the centered advection of the backscatter equations when nVar > 1. + */ + template + FORCEINLINE void finalizeFlux(const FlowIndices&, const ScalarFluxOptions&, Int, const EdgeSide&, + Int, const EdgeSide&, const Double& a0, const Double& a1, + const CPair>& phi, + EdgeResidual& res) const { + const Double flux = a0 * phi.i.all(0) + a1 * phi.j.all(0); + + res.flux_i(0) += flux; + res.flux_j(0) -= flux; + + res.jac_ii(0, 0) += a0; + res.jac_ij(0, 0) += a1; + res.jac_ji(0, 0) -= a0; + res.jac_jj(0, 0) -= a1; + + /*--- Stochastic backscatter: three Langevin equations, advected with the mean of the two + * upwinding weights and with no diffusion. ---*/ + const Double avg = 0.5 * (a0 + a1); + for (size_t iVar = 1; iVar < res.nVar; ++iVar) { + const Double flux_bs = avg * (phi.i.all(iVar) + phi.j.all(iVar)); + + res.flux_i(iVar) += flux_bs; + res.flux_j(iVar) -= flux_bs; + + res.jac_ii(iVar, iVar) += avg; + res.jac_ij(iVar, iVar) += avg; + res.jac_ji(iVar, iVar) -= avg; + res.jac_jj(iVar, iVar) -= avg; + } + } + + /*! + * \brief Diffusion coefficients of both orientations of the edge, see CAvgGrad_TurbSA. + * \note The coefficient is not symmetric: it uses the transported variable of the row it is + * going to be used for (the quadratic, non-conservative part of the diffusion term). + * Coefficients past index 0 are left at zero, the backscatter equations have no diffusion. + */ + template + FORCEINLINE CPair> coefficients(const FlowIndices& idx, Int iPoint, + const EdgeSide& side_i, Int jPoint, + const EdgeSide& side_j) const { + const Double nu_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.LaminarViscosity()) / + gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); + const Double nu_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.LaminarViscosity()) / + gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); + + const Double nuTilde_i = gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), 0); + const Double nuTilde_j = gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), 0); + + const Double nu_e = 0.5 * (nu_i + nu_j + (1.0 + cb2) * (nuTilde_i + nuTilde_j)); + + Vector D_i, D_j; + D_i(0) = (nu_e - cb2 * nuTilde_i) / sigma; + D_j(0) = (nu_e - cb2 * nuTilde_j) / sigma; + for (size_t iVar = 1; iVar < nVar; ++iVar) { + D_i(iVar) = 0.0; + D_j(iVar) = 0.0; + } + return {D_i, D_j}; + } + + /*! + * \brief Extra Jacobian terms from the dependence of the diffusion coefficient on nu_tilde. + */ + template + FORCEINLINE void coefficientJacobians(const Vector& projGrad, EdgeResidual& res) const { + /*--- d(diffusion coefficient of i)/d(nu_tilde_i), and its counterpart w.r.t. nu_tilde_j; + * the coefficient of j is the same expression with i and j swapped, so the same two + * derivatives apply to both orientations. ---*/ + const Double dDC_dNuTilde_i = ((1.0 + cb2) * 0.5 - cb2) / sigma; + const Double dDC_dNuTilde_j = (1.0 + cb2) * 0.5 / sigma; + + res.jac_ii(0, 0) -= dDC_dNuTilde_i * projGrad(0); + res.jac_ij(0, 0) -= dDC_dNuTilde_j * projGrad(0); + res.jac_ji(0, 0) += dDC_dNuTilde_j * projGrad(0); + res.jac_jj(0, 0) += dDC_dNuTilde_i * projGrad(0); + } +}; diff --git a/SU2_CFD/include/numerics/util.hpp b/SU2_CFD/include/numerics/util.hpp index 5003fc4652d..f35b159e6ba 100644 --- a/SU2_CFD/include/numerics/util.hpp +++ b/SU2_CFD/include/numerics/util.hpp @@ -403,12 +403,19 @@ FORCEINLINE Double umusclProjection(const Double& gradProj, const Double& delta, /*! * \brief MUSCL reconstruction of the specified variable. * \note The result should be halved when added to i (or subtracted from j). - */ -template -FORCEINLINE Double musclReconstruction(const GradType& grad, const Vector& vector_ij, - const Double& delta, const size_t iVar, const CNonDeduced& kappa, - const CNonDeduced& umusclRamp) { - const Double proj = dot(grad[iVar], vector_ij); + * \note Reads its own row of the gradient container (rather than being handed an already + * gathered nVarGrad x nDim block, as it once was) so that a caller reconstructing a + * single variable, e.g. a scalar with nVar 1, never gathers a Matrix: that + * shape is the same RowMajor, one-row degeneracy that forces EdgeResidual's Size floor + * (see numerics/util.hpp), and here it would silently turn a row into a lone scalar + * instead of failing to compile, since Matrix still satisfies IsVector. + */ +template ::Int> +FORCEINLINE Double musclReconstruction(Int iPoint, const Gradient_t& gradient, size_t iRow, + const Vector& vector_ij, const Double& delta, + const CNonDeduced& kappa, const CNonDeduced& umusclRamp) { + const auto grad = gatherVariables(iPoint, gradient, iRow); + const Double proj = dot(grad, vector_ij); return umusclRamp * umusclProjection(proj, delta, kappa); } @@ -424,16 +431,13 @@ FORCEINLINE void musclUnlimited(typename CLaneTraits::Int iPoint, typena size_t iRow = 0) { constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; - auto grad_i = gatherVariables(iPoint, gradient, iRow); - auto grad_j = gatherVariables(jPoint, gradient, iRow); - for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { /*--- Centered difference, needed for U-MUSCL projection ---*/ const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); /*--- U-MUSCL reconstructed variables ---*/ - const Double proj_i = musclReconstruction(grad_i, vector_ij, delta_ij, iVar, kappa, umusclRamp); - const Double proj_j = musclReconstruction(grad_j, vector_ij, delta_ij, iVar, kappa, umusclRamp); + const Double proj_i = musclReconstruction(iPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); + const Double proj_j = musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); /*--- Apply reconstruction: V_L = V_i + 0.5 * dV_ij^kap ---*/ V.i.all(iVar) += 0.5 * proj_i; @@ -455,16 +459,13 @@ FORCEINLINE void musclPointLimited(typename CLaneTraits::Int iPoint, auto lim_i = gatherVariables(iPoint, limiter, iRow); auto lim_j = gatherVariables(jPoint, limiter, iRow); - auto grad_i = gatherVariables(iPoint, gradient, iRow); - auto grad_j = gatherVariables(jPoint, gradient, iRow); - for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { /*--- Centered difference, needed for U-MUSCL projection ---*/ const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); /*--- U-MUSCL reconstructed variables ---*/ - const Double proj_i = musclReconstruction(grad_i, vector_ij, delta_ij, iVar, kappa, umusclRamp); - const Double proj_j = musclReconstruction(grad_j, vector_ij, delta_ij, iVar, kappa, umusclRamp); + const Double proj_i = musclReconstruction(iPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); + const Double proj_j = musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); /*--- Apply reconstruction: V_L = V_i + 0.5 * lim * dV_ij^kap ---*/ V.i.all(iVar) += 0.5 * lim_i(iVar) * proj_i; @@ -482,17 +483,14 @@ FORCEINLINE void musclEdgeLimited(typename CLaneTraits::Int iPoint, const CNonDeduced& umusclRamp, size_t iRow = 0) { constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; - auto grad_i = gatherVariables(iPoint, gradient, iRow); - auto grad_j = gatherVariables(jPoint, gradient, iRow); - for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { /*--- Centered difference, needed for U-MUSCL projection and limiter ---*/ const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); const Double delta_ij_2 = pow(delta_ij, 2) + 1e-6; /*--- U-MUSCL reconstructed variables ---*/ - const Double proj_i = musclReconstruction(grad_i, vector_ij, delta_ij, iVar, kappa, umusclRamp); - const Double proj_j = musclReconstruction(grad_j, vector_ij, delta_ij, iVar, kappa, umusclRamp); + const Double proj_i = musclReconstruction(iPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); + const Double proj_j = musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); const Double lim_i = (delta_ij_2 + proj_i * delta_ij) / (pow(proj_i, 2) + delta_ij_2); const Double lim_j = (delta_ij_2 + proj_j * delta_ij) / (pow(proj_j, 2) + delta_ij_2); diff --git a/SU2_CFD/include/solvers/CScalarSolver.hpp b/SU2_CFD/include/solvers/CScalarSolver.hpp index e941faacdd7..8900355f068 100644 --- a/SU2_CFD/include/solvers/CScalarSolver.hpp +++ b/SU2_CFD/include/solvers/CScalarSolver.hpp @@ -30,6 +30,7 @@ #include "../../../Common/include/parallelization/omp_structure.hpp" #include "../../../Common/include/toolboxes/geometry_toolbox.hpp" +#include "../numerics/scalar/scalar_edge_flux.hpp" #include "../variables/CScalarVariable.hpp" #include "../variables/CFlowVariable.hpp" #include "../variables/CPrimitiveIndices.hpp" @@ -402,7 +403,18 @@ class CScalarSolver : public CSolver { * \brief Sum the edge fluxes for each cell to populate the residual vector, only used on coarse grids. * \param[in] geometry - Geometrical definition of the problem. */ - void SumEdgeFluxes(CGeometry* geometry); + void SumEdgeFluxes(const CGeometry* geometry); + + /*! + * \brief Generic interior edge loop for a scalar model expressed through the CUpwScalarBase + * CRTP chain (numerics/scalar/scalar_edge_flux.hpp), driving both the convective and + * the diffusive term of every edge with a single kernel, under either update strategy. + * \tparam Scheme - A model instantiated from CUpwScalarBase, e.g. CScalarFlux_SA<...>. + * \param[in] opt - Loop invariant flags built by the caller from the current CConfig state. + */ + template + void EdgeFluxResidual(const CGeometry* geometry, CSolver** solver_container, const CConfig* config, + const ScalarFluxOptions& opt); private: /*! diff --git a/SU2_CFD/include/solvers/CScalarSolver.inl b/SU2_CFD/include/solvers/CScalarSolver.inl index d508c29d15d..155bb72de93 100644 --- a/SU2_CFD/include/solvers/CScalarSolver.inl +++ b/SU2_CFD/include/solvers/CScalarSolver.inl @@ -351,7 +351,7 @@ void CScalarSolver::Upwind_Residual(CGeometry* geometry, CSolver** } template -void CScalarSolver::SumEdgeFluxes(CGeometry* geometry) { +void CScalarSolver::SumEdgeFluxes(const CGeometry* geometry) { SU2_ZONE_SCOPED /*--- EdgeFluxes and EdgeFluxesDiff hold the two row contributions of an edge directly, @@ -371,6 +371,89 @@ void CScalarSolver::SumEdgeFluxes(CGeometry* geometry) { END_SU2_OMP_FOR } +template +template +void CScalarSolver::EdgeFluxResidual(const CGeometry* geometry, CSolver** solver_container, + const CConfig* config, const ScalarFluxOptions& opt) { + SU2_ZONE_SCOPED + + using Double = typename Scheme::Double; + constexpr int nDim = Scheme::nDim; + + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + + const Scheme flux(*config); + + auto* flowNodes = su2staticcast_p(solver_container[FLOW_SOL]->GetNodes()); + const auto* edgeMassFluxes = solver_container[FLOW_SOL]->GetEdgeMassFluxes(); + + const EdgeSide side{*nodes, flowNodes, CMatrixView(geometry->nodes->GetCoord()), + dynamic_grid ? CMatrixView(geometry->nodes->GetGridVel()) + : CMatrixView()}; + + const auto updateType = ReducerStrategy ? UpdateType::REDUCTION : UpdateType::COLORING; + auto& target = ReducerStrategy ? EdgeFluxes : LinSysRes; + + /*--- Under the reducer the edges of a thread are not disjoint in their points, so + * preaccumulation is paused; under coloring they are, and the faster adjoint evaluation + * mode applies. ---*/ + bool pausePreacc = false; + if (ReducerStrategy) pausePreacc = AD::PausePreaccumulation(); + else AD::StartNoSharedReading(); + + for (auto color : EdgeColoring) { + SU2_OMP_FOR_DYN(nextMultiple(OMP_MIN_SIZE, color.groupSize)) + for (auto k = 0ul; k < color.size; ++k) { + const unsigned long iEdge = color.indices[k]; + const auto iPoint = geometry->edges->GetNode(iEdge, 0); + const auto jPoint = geometry->edges->GetNode(iEdge, 1); + const auto normal = gatherVariables(iEdge, geometry->edges->GetNormal()); + + const Double massFlux = opt.boundedScalar ? gatherVariables(iEdge, *edgeMassFluxes) : Double(0.0); + + flux.ComputeFlux(opt, iEdge, iPoint, side, jPoint, side, normal, massFlux, implicit, updateType, 1.0, target, + EdgeFluxesDiff, Jacobian); + + /*--- Bounded scalar divergence correction, per edge; the ReducerStrategy equivalent runs + * in a per-point pass below, where the diagonal is not written from the edge loop. ---*/ + if (opt.boundedScalar && !ReducerStrategy) { + LinSysRes.AddBlock(iPoint, nodes->GetSolution(iPoint), -massFlux); + LinSysRes.AddBlock(jPoint, nodes->GetSolution(jPoint), massFlux); + if (implicit) { + Jacobian.AddVal2Diag(iPoint, -massFlux); + Jacobian.AddVal2Diag(jPoint, massFlux); + } + } + } + END_SU2_OMP_FOR + } + + AD::ResumePreaccumulation(pausePreacc); + if (!ReducerStrategy) AD::EndNoSharedReading(); + + if (ReducerStrategy) { + SumEdgeFluxes(geometry); + if (implicit) Jacobian.SetDiagonalAsColumnSum(); + + if (opt.boundedScalar) { + SU2_OMP_FOR_STAT(omp_chunk_size) + for (unsigned long iPoint = 0; iPoint < nPoint; ++iPoint) { + const auto* solution = nodes->GetSolution(iPoint); + su2double divergence = 0; + + for (auto iEdge : geometry->nodes->GetEdges(iPoint)) { + const auto sign = (iPoint == geometry->edges->GetNode(iEdge, 0)) ? 1 : -1; + const su2double edgeMassFlux = sign * (*edgeMassFluxes)[iEdge]; + divergence += edgeMassFlux; + LinSysRes.AddBlock(iPoint, solution, -edgeMassFlux); + } + if (implicit) Jacobian.AddVal2Diag(iPoint, -divergence); + } + END_SU2_OMP_FOR + } + } +} + template void CScalarSolver::BC_Riemann(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED diff --git a/SU2_CFD/include/solvers/CTurbSASolver.hpp b/SU2_CFD/include/solvers/CTurbSASolver.hpp index fa2cc654019..939cfa2b642 100644 --- a/SU2_CFD/include/solvers/CTurbSASolver.hpp +++ b/SU2_CFD/include/solvers/CTurbSASolver.hpp @@ -95,6 +95,22 @@ class CTurbSASolver final : public CTurbSolver { */ void ComputeUnderRelaxationFactor(CSolver** solver_container, const CConfig *config) final; + /*! + * \brief Resolve the compile-time flow indices, dimension, backscatter equation count and + * MUSCL setting, and run the interior edge loop with the matching CScalarFlux_SA + * instantiation. Each overload resolves one more of those from CConfig/CGeometry and + * recurses into the next, so the runtime-to-compile-time dispatch stays linear in the + * number of axes instead of enumerating every combination by hand. + */ + template + void RunSA(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt); + + template + void RunSA(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt); + + template + void RunSA(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt); + public: /*! * \brief Constructor. @@ -140,16 +156,17 @@ class CTurbSASolver final : public CTurbSolver { unsigned short iMesh) override; /*! - * \brief Compute the viscous flux for the turbulent equation at a particular edge. - * \param[in] iEdge - Edge for which we want to compute the flux + * \brief Compute the spatial integration using the CScalarFlux_SA edge kernel, which computes + * and writes both the convective and the diffusive term of every edge; this solver has + * no Viscous_Residual of its own any more. * \param[in] geometry - Geometrical definition of the problem. * \param[in] solver_container - Container vector with all the solutions. - * \param[in] numerics - Description of the numerical method. + * \param[in] numerics_container - Unused, kept only for the boundary conditions. * \param[in] config - Definition of the particular problem. - * \note Calls a generic implementation after defining a SolverSpecificNumerics object. + * \param[in] iMesh - Index of the mesh in multigrid computations. */ - void Viscous_Residual(const unsigned long iEdge, const CGeometry* geometry, CSolver** solver_container, - CNumerics* numerics, const CConfig* config) override; + void Upwind_Residual(CGeometry* geometry, CSolver** solver_container, CNumerics** numerics_container, + CConfig* config, unsigned short iMesh) override; /*! * \brief Source term computation. diff --git a/SU2_CFD/include/variables/CVariable.hpp b/SU2_CFD/include/variables/CVariable.hpp index 528adc139db..cfacf76af96 100644 --- a/SU2_CFD/include/variables/CVariable.hpp +++ b/SU2_CFD/include/variables/CVariable.hpp @@ -789,6 +789,7 @@ class CVariable { * \return Reference to gradient. */ inline CVectorOfMatrix& GetGradient(void) { return Gradient; } + inline const CVectorOfMatrix& GetGradient(void) const { return Gradient; } /*! * \brief Get the value of the solution gradient. @@ -835,6 +836,7 @@ class CVariable { * \return Reference to the limiters vector. */ inline MatrixType& GetLimiter(void) { return Limiter; } + inline const MatrixType& GetLimiter(void) const { return Limiter; } /*! * \brief Get the value of the slope limiter. diff --git a/SU2_CFD/src/solvers/CTurbSASolver.cpp b/SU2_CFD/src/solvers/CTurbSASolver.cpp index 8e2b7fb3c61..ffa23737ed5 100644 --- a/SU2_CFD/src/solvers/CTurbSASolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSASolver.cpp @@ -26,8 +26,13 @@ */ #include "../../include/solvers/CTurbSASolver.hpp" +#include "../../include/solvers/CScalarSolver.inl" #include "../../include/variables/CTurbSAVariable.hpp" #include "../../include/variables/CFlowVariable.hpp" +#include "../../include/numerics/turbulent/turb_sa_edge_flux.hpp" +#include "../../include/variables/CEulerVariable.hpp" +#include "../../include/variables/CIncEulerVariable.hpp" +#include "../../include/variables/CNEMOEulerVariable.hpp" #include "../../../Common/include/parallelization/omp_structure.hpp" #include "../../../Common/include/toolboxes/geometry_toolbox.hpp" #include "../../../Common/include/toolboxes/random_toolbox.hpp" @@ -338,18 +343,54 @@ void CTurbSASolver::Postprocessing(CGeometry *geometry, CSolver **solver_contain AD::EndNoSharedReading(); } -void CTurbSASolver::Viscous_Residual(const unsigned long iEdge, const CGeometry* geometry, CSolver** solver_container, - CNumerics* numerics, const CConfig* config) { +void CTurbSASolver::Upwind_Residual(CGeometry* geometry, CSolver** solver_container, CNumerics** numerics_container, + CConfig* config, unsigned short iMesh) { + SU2_ZONE_SCOPED - /*--- Define an object to set solver specific numerics contribution. ---*/ - auto SolverSpecificNumerics = [&](unsigned long iPoint, unsigned long jPoint) { - /*--- Roughness heights. ---*/ - numerics->SetRoughness(geometry->nodes->GetRoughnessHeight(iPoint), geometry->nodes->GetRoughnessHeight(jPoint)); + const ScalarFluxOptions opt{ + dynamic_grid, /*--- dynamicGrid ---*/ + config->GetBounded_Turb(), /*--- boundedScalar ---*/ + true, /*--- correctGradient, as CAvgGrad_TurbSA is built today ---*/ + config->GetUse_Accurate_Turb_Jacobians(), /*--- accurateJacobians ---*/ + true, /*--- convective ---*/ + true, /*--- viscous ---*/ + false, /*--- oneSided, this is the interior loop ---*/ }; - /*--- Now instantiate the generic non-conservative implementation with the functor above. ---*/ - Viscous_Residual_NonCons(iEdge, geometry, solver_container, numerics, config, SolverSpecificNumerics); + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + RunSA>(geometry, solver_container, config, opt); + } else if (config->GetNEMOProblem()) { + RunSA>(geometry, solver_container, config, opt); + } else { + RunSA>(geometry, solver_container, config, opt); + } +} + +template +void CTurbSASolver::RunSA(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt) { + if (nDim == 2) RunSA(geometry, solver_container, config, opt); + else RunSA(geometry, solver_container, config, opt); +} + +template +void CTurbSASolver::RunSA(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt) { + /*--- nVar is 1, or 4 with the three Langevin equations of stochastic backscatter, see the + * solver constructor; either way it is fixed for the lifetime of the solver, not per call. ---*/ + if (nVar == 1) RunSA(geometry, solver_container, config, opt); + else RunSA(geometry, solver_container, config, opt); +} +template +void CTurbSASolver::RunSA(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt) { + if (config->GetMUSCL()) { + EdgeFluxResidual>(geometry, solver_container, config, opt); + } else { + EdgeFluxResidual>(geometry, solver_container, config, + opt); + } } void CTurbSASolver::Source_Residual(CGeometry *geometry, CSolver **solver_container, From dd8829b88975f05478b80f98b50f3ad28bd41147 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Wed, 26 Aug 2026 22:29:55 -0700 Subject: [PATCH 03/20] Ghost containers and BoundaryFluxResidual; SA BC_Far_Field and BC_Inlet Adds CGhostFlowVariable, a minimal CFlowVariable that exists only to expose its protected constructor, plus ghostNodes/ghostFlowNodes/ghostNormal/ghostCoord/ghostSkip to CScalarSolver: ghost-point containers sized to the largest marker, holding the same types the interior edge loop reads through EdgeSide, so the flux kernels have no separate boundary code path. ghostFlowNodes is generic and allocated lazily by the new EnsureGhostFlowContainers (its sizes come from the flow solver, which the derived solver's constructor is never handed); ghostNodes is model-specific and each solver allocates its own, same as nodes. CScalarSolver::BoundaryFluxResidual is the shared flux pass every boundary can call once its fill pass has written the ghost row, the outward normal and (for the diffusion sites, not used yet) the ghost gradient of each vertex; the ghost point has no row, so only the interior point's contribution is assembled. BC_Far_Field, previously final and shared unconditionally by every scalar solver through CNumerics, is now overridable so a migrated solver can supply its own. CTurbSASolver::BC_Far_Field and BC_Inlet are rewritten on this pattern: a fill pass over the marker's vertices followed by a BoundaryFluxResidual call, dispatched through the same RunSA-style recursive template resolution Upwind_Residual uses (RunSA_Boundary), instantiated with muscl always false since boundaries never reconstruct. Both keep opt.viscous false, matching the numerics they replace (no diffusive term at the far field, and the inlet's was already disabled there for convergence reasons). Verified against turb_ONERAM6 (far field, compressible) and the compressible and incompressible wallfunctions flat plate cases (far field and inlet, both regimes, MUSCL on and off): output is bit-identical to the pre-boundary-migration binary in every case, no reassociation drift at all this time. Incidentally hit a segfault on the incompressible flat plate case under the OpenMP reducer strategy fallback, reproduced on the pre-migration binary too (stack trace is entirely inside CIncEulerSolver::SetTime_Step / CMultiGridIntegration, nothing this branch touches) -- pre-existing and unrelated, worked around with a different incompressible case for this step's verification rather than investigated further. --- SU2_CFD/include/solvers/CScalarSolver.hpp | 46 +++- SU2_CFD/include/solvers/CScalarSolver.inl | 62 +++++ SU2_CFD/include/solvers/CTurbSASolver.hpp | 28 +++ .../include/variables/CGhostFlowVariable.hpp | 52 ++++ SU2_CFD/src/solvers/CTurbSASolver.cpp | 229 +++++++++--------- 5 files changed, 308 insertions(+), 109 deletions(-) create mode 100644 SU2_CFD/include/variables/CGhostFlowVariable.hpp diff --git a/SU2_CFD/include/solvers/CScalarSolver.hpp b/SU2_CFD/include/solvers/CScalarSolver.hpp index 8900355f068..c1feaa2ba9c 100644 --- a/SU2_CFD/include/solvers/CScalarSolver.hpp +++ b/SU2_CFD/include/solvers/CScalarSolver.hpp @@ -33,6 +33,7 @@ #include "../numerics/scalar/scalar_edge_flux.hpp" #include "../variables/CScalarVariable.hpp" #include "../variables/CFlowVariable.hpp" +#include "../variables/CGhostFlowVariable.hpp" #include "../variables/CPrimitiveIndices.hpp" #include "CSolver.hpp" @@ -81,6 +82,17 @@ class CScalarSolver : public CSolver { CSysVector EdgeFluxes; /*!< \brief Flux across each edge. */ CSysVector EdgeFluxesDiff; /*!< \brief Flux difference between ij and ji for non-conservative discretisation. */ + /*--- Ghost states of the marker currently being processed by a boundary, indexed by vertex + * and sized to the largest marker; same container types as the interior ones, so the flux + * kernels read a boundary through the same accessors as an interior edge. Boundary loops run + * one marker at a time, parallel over its vertices, so the buffers are written and consumed + * before the next marker reaches them (see BoundaryFluxResidual). ---*/ + unique_ptr ghostNodes; /*!< \brief Allocated by the derived solver, whose VariableType constructor it alone knows how to call. */ + unique_ptr ghostFlowNodes; /*!< \brief Allocated once here, sizes coming from the flow solver. */ + su2activematrix ghostNormal; /*!< \brief Outward normals, sign flipped from the vertex normals. */ + su2activematrix ghostCoord; /*!< \brief Reflected coordinates, read by the diffusion sites. */ + su2vector ghostSkip; /*!< \brief Whether a vertex contributes no flux, set by the fill pass. */ + /*! * \brief The highest level in the variable hierarchy this solver can safely use. */ @@ -416,6 +428,38 @@ class CScalarSolver : public CSolver { void EdgeFluxResidual(const CGeometry* geometry, CSolver** solver_container, const CConfig* config, const ScalarFluxOptions& opt); + /*! + * \brief Allocate the ghost flow container and the per-vertex buffers boundaries share, the + * first time a boundary needs them; a no-op on every call after the first. + * \note Sizes come from the flow solver, so this cannot run at construction time the way + * ghostNodes does: the derived solver's constructor is not handed solver_container. + */ + void EnsureGhostFlowContainers(CSolver** solver_container, const CConfig* config); + + /*! + * \brief Write the four flow primitives the flux kernels read into one row of ghostFlowNodes. + * \param[in] iVertex - Vertex of the marker currently being processed. + * \param[in] V - Row of flow primitives to copy from (e.g. GetCharacPrimVar's or a sliding state's). + */ + inline void SetGhostPrimitives(unsigned long iVertex, const su2double* V) { + auto* ghostV = ghostFlowNodes->GetPrimitive(iVertex); + ghostV[prim_idx.Density()] = V[prim_idx.Density()]; + for (auto iDim = 0u; iDim < nDim; ++iDim) ghostV[prim_idx.Velocity() + iDim] = V[prim_idx.Velocity() + iDim]; + ghostV[prim_idx.LaminarViscosity()] = V[prim_idx.LaminarViscosity()]; + ghostV[prim_idx.EddyViscosity()] = V[prim_idx.EddyViscosity()]; + } + + /*! + * \brief Generic boundary flux pass, run after a boundary's fill pass has written the ghost + * row, the outward normal and (for the diffusion sites) the ghost gradient of every + * vertex of the marker. The ghost point has no row, so only the contribution to the + * interior point is assembled. + * \tparam Scheme - Same model the interior loop uses, instantiated with muscl false. + */ + template + void BoundaryFluxResidual(const CGeometry* geometry, CSolver** solver_container, const CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit); + private: /*! * \brief Compute the viscous flux for the scalar equation at a particular edge. @@ -479,7 +523,7 @@ class CScalarSolver : public CSolver { * \param[in] val_marker - Surface marker where the boundary condition is applied. */ void BC_Far_Field(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, - CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) final; + CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) override; /*! * \brief Impose the Symmetry Plane boundary condition. diff --git a/SU2_CFD/include/solvers/CScalarSolver.inl b/SU2_CFD/include/solvers/CScalarSolver.inl index 155bb72de93..105d253e3c3 100644 --- a/SU2_CFD/include/solvers/CScalarSolver.inl +++ b/SU2_CFD/include/solvers/CScalarSolver.inl @@ -454,6 +454,68 @@ void CScalarSolver::EdgeFluxResidual(const CGeometry* geometry, CS } } +template +void CScalarSolver::EnsureGhostFlowContainers(CSolver** solver_container, const CConfig* config) { + if (ghostFlowNodes) return; + + SU2_OMP_SAFE_GLOBAL_ACCESS( + if (!ghostFlowNodes) { + unsigned long maxMarkerVertices = 0; + for (auto iMarker = 0u; iMarker < nMarker; ++iMarker) maxMarkerVertices = max(maxMarkerVertices, nVertex[iMarker]); + + auto* flowSolver = solver_container[FLOW_SOL]; + ghostFlowNodes = make_unique(maxMarkerVertices, nDim, flowSolver->GetnVar(), + flowSolver->GetnPrimVar(), flowSolver->GetnPrimVarGrad(), + config); + + ghostNormal.resize(maxMarkerVertices, nDim); + ghostCoord.resize(maxMarkerVertices, nDim); + ghostSkip.resize(maxMarkerVertices); + } + ) +} + +template +template +void CScalarSolver::BoundaryFluxResidual(const CGeometry* geometry, CSolver** solver_container, + const CConfig* config, const ScalarFluxOptions& opt, + unsigned short val_marker, bool implicit) { + using Double = typename Scheme::Double; + constexpr int nDim = Scheme::nDim; + + const Scheme flux(*config); + + auto* flowNodes = su2staticcast_p(solver_container[FLOW_SOL]->GetNodes()); + + const EdgeSide side_i{*nodes, flowNodes, CMatrixView(geometry->nodes->GetCoord()), + dynamic_grid ? CMatrixView(geometry->nodes->GetGridVel()) + : CMatrixView()}; + + const EdgeSide side_j{*ghostNodes, ghostFlowNodes.get(), CMatrixView(ghostCoord), + side_i.gridVel}; + + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (unsigned long iVertex = 0; iVertex < geometry->nVertex[val_marker]; iVertex++) { + const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); + if (!geometry->nodes->GetDomain(iPoint) || ghostSkip[iVertex]) continue; + + const auto normal = gatherVariables(iVertex, ghostNormal); + + Double massFlux = 0.0; + if (opt.boundedScalar) { + massFlux = BoundedScalarBCFlux(iPoint, implicit, flowNodes->GetDensity(iPoint), + &ghostFlowNodes->GetPrimitive(iVertex)[prim_idx.Velocity()], normal.data()); + } + + const auto res = flux.ComputeFlux(opt, iPoint, side_i, iVertex, side_j, normal, massFlux); + + /*--- The ghost point has no row, only the contribution to i is assembled. ---*/ + for (auto iVar = 0ul; iVar < res.nVar; ++iVar) LinSysRes(iPoint, iVar) += res.flux_i(iVar); + if (implicit) Jacobian.AddBlock2Diag(iPoint, res.jac_ii); + } + END_SU2_OMP_FOR +} + template void CScalarSolver::BC_Riemann(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED diff --git a/SU2_CFD/include/solvers/CTurbSASolver.hpp b/SU2_CFD/include/solvers/CTurbSASolver.hpp index 939cfa2b642..1e1a7baeaf0 100644 --- a/SU2_CFD/include/solvers/CTurbSASolver.hpp +++ b/SU2_CFD/include/solvers/CTurbSASolver.hpp @@ -111,6 +111,22 @@ class CTurbSASolver final : public CTurbSolver { template void RunSA(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt); + /*! + * \brief Same dispatch as RunSA, for a boundary's call into BoundaryFluxResidual; a boundary + * always reconstructs nothing, so muscl is not one of the axes resolved here. + */ + template + void RunSA_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit); + + template + void RunSA_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit); + + template + void RunSA_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit); + public: /*! * \brief Constructor. @@ -168,6 +184,18 @@ class CTurbSASolver final : public CTurbSolver { void Upwind_Residual(CGeometry* geometry, CSolver** solver_container, CNumerics** numerics_container, CConfig* config, unsigned short iMesh) override; + /*! + * \brief Impose the Far Field boundary condition, via the CScalarFlux_SA edge kernel. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] solver_container - Container vector with all the solutions. + * \param[in] conv_numerics - Unused, kept only for the boundary condition dispatch. + * \param[in] visc_numerics - Unused, kept only for the boundary condition dispatch. + * \param[in] config - Definition of the particular problem. + * \param[in] val_marker - Surface marker where the boundary condition is applied. + */ + void BC_Far_Field(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, + CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) override; + /*! * \brief Source term computation. * \param[in] geometry - Geometrical definition of the problem. diff --git a/SU2_CFD/include/variables/CGhostFlowVariable.hpp b/SU2_CFD/include/variables/CGhostFlowVariable.hpp new file mode 100644 index 00000000000..c53d06c7c5c --- /dev/null +++ b/SU2_CFD/include/variables/CGhostFlowVariable.hpp @@ -0,0 +1,52 @@ +/*! + * \file CGhostFlowVariable.hpp + * \brief Flow variables of the ghost points of one scalar solver's boundary marker. + * \author P. Gomes + * \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 . + */ + +#pragma once + +#include "CFlowVariable.hpp" + +/*! + * \class CGhostFlowVariable + * \brief Flow variables of the ghost points of one marker, indexed by vertex. + * \note Sized to the largest marker and to the primitive layout of the flow solver, so one set + * of indices reads the ghost points and the interior ones. Only the primitives the flux + * kernels read are filled by the boundary's fill pass (density, velocity, laminar and + * eddy viscosity); the gradients, the limiters and the non-physical edge counter are not, + * and the first order boundary path does not read them. + */ +class CGhostFlowVariable final : public CFlowVariable { + public: + CGhostFlowVariable(unsigned long npoint, unsigned long ndim, unsigned long nvar, unsigned long nprimvar, + unsigned long nprimvargrad, const CConfig* config) + : CFlowVariable(npoint, ndim, nvar, nprimvar, nprimvargrad, config) {} + + /*! + * \brief Never read: a ghost is not part of the dual-time residual, only its primitives are. + */ + inline su2double GetDensity_time_n(unsigned long) const override { return 0.0; } + inline su2double GetDensity_time_n1(unsigned long) const override { return 0.0; } +}; diff --git a/SU2_CFD/src/solvers/CTurbSASolver.cpp b/SU2_CFD/src/solvers/CTurbSASolver.cpp index ffa23737ed5..dc9f9e05583 100644 --- a/SU2_CFD/src/solvers/CTurbSASolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSASolver.cpp @@ -157,6 +157,12 @@ CTurbSASolver::CTurbSASolver(CGeometry *geometry, CConfig *config, unsigned shor nodes = new CTurbSAVariable(nu_tilde_Inf, muT_Inf, nPoint, nDim, nVar, config); SetBaseClassPointerToNodes(); + /*--- Ghost states for boundary conditions, sized to the largest marker (see BoundaryFluxResidual). ---*/ + unsigned long maxMarkerVertices = 0; + for (unsigned long iMarker = 0; iMarker < nMarker; iMarker++) + maxMarkerVertices = max(maxMarkerVertices, nVertex[iMarker]); + ghostNodes = make_unique(nu_tilde_Inf, muT_Inf, maxMarkerVertices, nDim, nVar, config); + /*--- MPI solution ---*/ InitiateComms(geometry, config, MPI_QUANTITIES::SOLUTION_EDDY); @@ -214,6 +220,8 @@ void CTurbSASolver::Preprocessing(CGeometry *geometry, CSolver **solver_containe /*--- Clear Residual and Jacobian. Upwind second order reconstruction and gradients ---*/ CommonPreprocessing(geometry, config, Output); + EnsureGhostFlowContainers(solver_container, config); + if (kind_hybridRANSLES != NO_HYBRIDRANSLES) { /*--- Set the vortex tilting coefficient at every node if required ---*/ @@ -393,6 +401,69 @@ void CTurbSASolver::RunSA(CGeometry* geometry, CSolver** solver_container, CConf } } +void CTurbSASolver::BC_Far_Field(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, + CConfig *config, unsigned short val_marker) { + SU2_ZONE_SCOPED + + EnsureGhostFlowContainers(solver_container, config); + + auto* flowSolver = solver_container[FLOW_SOL]; + + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, Solution_Inf[iVar]); + + SetGhostPrimitives(iVertex, flowSolver->GetCharacPrimVar(val_marker, iVertex)); + + /*--- Vertex normals point into the domain, the flux convention needs them outward. ---*/ + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); + + ghostSkip[iVertex] = false; + } + END_SU2_OMP_FOR + + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous, far field has no diffusive flux, matching the old numerics path*/, + true /*oneSided, the ghost point has no row*/, + }; + + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else if (config->GetNEMOProblem()) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } +} + +template +void CTurbSASolver::RunSA_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit) { + if (nDim == 2) RunSA_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + else RunSA_Boundary(geometry, solver_container, config, opt, val_marker, implicit); +} + +template +void CTurbSASolver::RunSA_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit) { + if (nVar == 1) RunSA_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + else RunSA_Boundary(geometry, solver_container, config, opt, val_marker, implicit); +} + +template +void CTurbSASolver::RunSA_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit) { + /*--- Boundaries never reconstruct: muscl is false here regardless of config->GetMUSCL(). ---*/ + BoundaryFluxResidual>(geometry, solver_container, config, + opt, val_marker, implicit); +} + void CTurbSASolver::Source_Residual(CGeometry *geometry, CSolver **solver_container, CNumerics **numerics_container, CConfig *config, unsigned short iMesh) { SU2_ZONE_SCOPED @@ -619,129 +690,71 @@ void CTurbSASolver::BC_Isothermal_Wall(CGeometry *geometry, CSolver **solver_con } -void CTurbSASolver::BC_Inlet(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, - CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { +void CTurbSASolver::BC_Inlet(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, + CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + EnsureGhostFlowContainers(solver_container, config); - /*--- Loop over all the vertices on this boundary marker ---*/ + auto* flowSolver = solver_container[FLOW_SOL]; + CFluidModel* FluidModel = flowSolver->GetFluidModel(); + const su2double* Turb_Properties = config->GetInlet_TurbVal(config->GetMarker_All_TagBound(val_marker)); + const su2double Nu_Factor = Turb_Properties[0]; + const su2double* Scalar_Inlet = config->GetKind_Species_Model() != SPECIES_MODEL::NONE + ? config->GetInlet_SpeciesVal(config->GetMarker_All_TagBound(val_marker)) + : nullptr; SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { + su2double nuTilde = Inlet_TurbVars[val_marker][iVertex][0]; + const auto* V_inlet = flowSolver->GetCharacPrimVar(val_marker, iVertex); - const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); - - /*--- Check if the node belongs to the domain (i.e., not a halo node) ---*/ - - if (geometry->nodes->GetDomain(iPoint)) { - - /*--- Normal vector for this vertex (negate for outward convention) ---*/ - - su2double Normal[MAXNDIM] = {0.0}; - for (auto iDim = 0u; iDim < nDim; iDim++) - Normal[iDim] = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - - /*--- Allocate the value at the inlet ---*/ - - auto V_inlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, iVertex); - - /*--- Retrieve solution at the farfield boundary node ---*/ - - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); - - /*--- Set various quantities in the solver class ---*/ - - conv_numerics->SetPrimitive(V_domain, V_inlet); - - /*--- Non-dimensionalize Inlet_TurbVars if Inlet-Files are used. ---*/ - su2double Inlet_Vars[MAXNVAR] = {0.0}; - Inlet_Vars[0] = Inlet_TurbVars[val_marker][iVertex][0]; - if (config->GetInlet_Profile_From_File()) { - Inlet_Vars[0] *= config->GetDensity_Ref() / config->GetViscosity_Ref(); + /*--- Non-dimensionalize Inlet_TurbVars if Inlet-Files are used. ---*/ + if (config->GetInlet_Profile_From_File()) { + nuTilde *= config->GetDensity_Ref() / config->GetViscosity_Ref(); + } else { + /*--- Fluid model evaluation of the inlet nu tilde. ---*/ + su2double Density_Inlet; + if (config->GetKind_Regime() == ENUM_REGIME::COMPRESSIBLE) { + Density_Inlet = V_inlet[prim_idx.Density()]; + FluidModel->SetTDState_Prho(V_inlet[prim_idx.Pressure()], Density_Inlet); } else { - /*--- Obtain fluid model for computing the nu tilde to impose at the inlet boundary. ---*/ - CFluidModel* FluidModel = solver_container[FLOW_SOL]->GetFluidModel(); - - /*--- Obtain density and laminar viscosity at inlet boundary node ---*/ - - su2double Density_Inlet; - if (config->GetKind_Regime() == ENUM_REGIME::COMPRESSIBLE) { - Density_Inlet = V_inlet[prim_idx.Density()]; - FluidModel->SetTDState_Prho(V_inlet[prim_idx.Pressure()], Density_Inlet); - } else { - const su2double* Scalar_Inlet = nullptr; - if (config->GetKind_Species_Model() != SPECIES_MODEL::NONE) { - Scalar_Inlet = config->GetInlet_SpeciesVal(config->GetMarker_All_TagBound(val_marker)); - } - FluidModel->SetTDState_T(V_inlet[prim_idx.Temperature()], Scalar_Inlet); - Density_Inlet = FluidModel->GetDensity(); - } - const su2double Laminar_Viscosity_Inlet = FluidModel->GetLaminarViscosity(); - const su2double* Turb_Properties = config->GetInlet_TurbVal(config->GetMarker_All_TagBound(val_marker)); - const su2double Nu_Factor = Turb_Properties[0]; - Inlet_Vars[0] = Nu_Factor * Laminar_Viscosity_Inlet / Density_Inlet; - if (config->GetSAParsedOptions().bc) { - Inlet_Vars[0] *= 0.005; - } + FluidModel->SetTDState_T(V_inlet[prim_idx.Temperature()], Scalar_Inlet); + Density_Inlet = FluidModel->GetDensity(); } + const su2double Laminar_Viscosity_Inlet = FluidModel->GetLaminarViscosity(); + nuTilde = Nu_Factor * Laminar_Viscosity_Inlet / Density_Inlet; + if (config->GetSAParsedOptions().bc) nuTilde *= 0.005; + } + ghostNodes->SetSolution(iVertex, 0, nuTilde); - /*--- Load the inlet turbulence variable (uniform by default). ---*/ - - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), Inlet_Vars); - - /*--- Set various other quantities in the conv_numerics class ---*/ - - conv_numerics->SetNormal(Normal); - - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), - geometry->nodes->GetGridVel(iPoint)); - - if (conv_numerics->GetBoundedScalar()) { - const su2double* velocity = &V_inlet[prim_idx.Velocity()]; - const su2double density = solver_container[FLOW_SOL]->GetNodes()->GetDensity(iPoint); - conv_numerics->SetMassFlux(BoundedScalarBCFlux(iPoint, implicit, density, velocity, Normal)); - } - - /*--- Compute the residual using an upwind scheme ---*/ - - auto residual = conv_numerics->ComputeResidual(config); - LinSysRes.AddBlock(iPoint, residual); + SetGhostPrimitives(iVertex, V_inlet); - /*--- Jacobian contribution for implicit integration ---*/ + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - if (implicit) Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i); + ghostSkip[iVertex] = false; + } + END_SU2_OMP_FOR -// /*--- Viscous contribution, commented out because serious convergence problems ---*/ -// -// su2double Coord_Reflected[MAXNDIM]; -// GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), -// geometry->nodes->GetCoord(iPoint), Coord_Reflected); -// visc_numerics->SetCoord(geometry->nodes->GetCoord(iPoint), Coord_Reflected); -// visc_numerics->SetNormal(Normal); -// -// /*--- Conservative variables w/o reconstruction ---*/ -// -// visc_numerics->SetPrimitive(V_domain, V_inlet); -// -// /*--- Turbulent variables w/o reconstruction, and its gradients ---*/ -// -// visc_numerics->SetScalarVar(Solution_i, Solution_j); -// visc_numerics->SetScalarVarGradient(node[iPoint]->GetGradient(), node[iPoint]->GetGradient()); -// -// /*--- Compute residual, and Jacobians ---*/ -// -// auto residual = visc_numerics->ComputeResidual(config); -// -// /*--- Subtract residual, and update Jacobians ---*/ -// -// LinSysRes.SubtractBlock(iPoint, residual); -// Jacobian.SubtractBlock2Diag(iPoint, residual.jacobian_i); + /*--- The diffusive term at the inlet is disabled: it caused serious convergence problems in + * the numerics this replaces, so opt.viscous stays false here too. ---*/ + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, + }; - } + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else if (config->GetNEMOProblem()) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); } - END_SU2_OMP_FOR } void CTurbSASolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, From 80139aa0198be05d5b7d40bfdcd06ed220525022 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Thu, 27 Aug 2026 16:49:22 -0700 Subject: [PATCH 04/20] Make MUSCL a runtime flag for the scalar edge-flux kernel; fix reverse-mode AD build muscl was a template parameter of CUpwScalarBase/CScalarFlux_SA, doubling instantiations and needing a compile-time dispatch axis in CTurbSASolver for no benefit over an ordinary loop-invariant flag; it moves into ScalarFluxOptions alongside convective/viscous/oneSided. Also fixes found while updating that path: - The CODI_REVERSE_TYPE gatherVariables overloads didn't compile for a scalar edge loop (only ever exercised simd::Array indices before) and broke on 3D gradient containers and the Matrix degenerate accessor; rewritten to gather through each container's own get(), matching the direct-mode path, with explicit AD::SetPreaccIn per lane. - A boundary's ghost point read grid velocity at its vertex index into the whole-mesh container; it now reuses the interior point's velocity, matching the numerics this replaces. - The flow limiter was incorrectly frozen by LimiterIter; only the scalar's own limiter freezes that way. - CAlignTraits yielded 0, alignas(0) on the static Vector/Matrix specializations, warned by gcc; now alignof(Type). - Missing EPS floor on the edge length in the diffusion term. Co-Authored-By: Claude Sonnet 5 --- .../numerics/scalar/scalar_edge_flux.hpp | 74 ++++++------ .../numerics/turbulent/turb_sa_edge_flux.hpp | 17 ++- SU2_CFD/include/numerics/util.hpp | 108 ++++++++---------- SU2_CFD/include/solvers/CTurbSASolver.hpp | 18 +-- SU2_CFD/src/solvers/CTurbSASolver.cpp | 24 ++-- 5 files changed, 113 insertions(+), 128 deletions(-) diff --git a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp index 47df0a95a0d..012f92230d3 100644 --- a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp +++ b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp @@ -52,9 +52,10 @@ struct EdgeSide { */ struct ScalarFluxOptions { bool dynamicGrid, boundedScalar, correctGradient, accurateJacobians; - bool convective; /*!< \brief Whether the convective scheme contributes. */ - bool viscous; /*!< \brief Whether the diffusion term contributes. */ - bool oneSided; /*!< \brief Whether only the row of i is assembled. */ + bool convective; /*!< \brief Whether the convective scheme contributes. */ + bool viscous; /*!< \brief Whether the diffusion term contributes. */ + bool oneSided; /*!< \brief Whether only the row of i is assembled. */ + bool muscl; /*!< \brief Whether the convective scheme reconstructs. A boundary clears it. */ }; /*! @@ -63,7 +64,8 @@ struct ScalarFluxOptions { */ template struct CScalarValues { - static constexpr size_t nVar = Size; /*!< \brief Only used as VarType::nVar when reconstruct's own nVarGrad_ default applies; every call site here passes an explicit nVarGrad_ instead. */ + static constexpr size_t nVar = Size; /*!< \brief Only used as VarType::nVar when reconstruct's own nVarGrad_ default + applies; every call site here passes an explicit nVarGrad_ instead. */ Vector all; }; @@ -88,7 +90,7 @@ class CAvgGradScalarBase { constexpr size_t Size = EdgeResidual::Size; - const Double dist2_ij = squaredNorm(vector_ij); + const Double dist2_ij = fmax(squaredNorm(vector_ij), EPS); const Double proj_vector_ij = dot(vector_ij, normal) / dist2_ij; /*--- Average gradient, corrected for skewness when asked. @@ -190,15 +192,14 @@ class CUpwScalarFlux : public CAvgGradScalarBase FORCEINLINE void finalizeFlux(const FlowIndices& idx, const ScalarFluxOptions&, Int iPoint, - const EdgeSide& side_i, Int jPoint, - const EdgeSide& side_j, const Double& a0, const Double& a1, - const CPair>& phi, + const EdgeSide& side_i, Int jPoint, const EdgeSide& side_j, + const Double& a0, const Double& a1, const CPair>& phi, EdgeResidual& res) const { Double w0 = a0, w1 = a1; if constexpr (Derived::Conservative) { @@ -224,7 +225,7 @@ class CUpwScalarFlux : public CAvgGradScalarBase +template class CUpwScalarBase : public CUpwScalarFlux { public: using Double = Double_; @@ -247,8 +248,9 @@ class CUpwScalarBase : public CUpwScalarFlux FORCEINLINE EdgeResidual ComputeFlux(const ScalarFluxOptions& opt, Int iPoint, const EdgeSide& side_i, Int jPoint, const EdgeSide& side_j, - const Vector& normal, - const Double& massFlux) const { + const Vector& normal, const Double& massFlux) const { /*--- Inputs are registered as they are read, by each of the two terms. ---*/ AD::StartPreacc(); AD::SetPreaccIn(normal, nDim); @@ -289,7 +288,7 @@ class CUpwScalarBase : public CUpwScalarFlux vector_ij; - if (muscl || opt.viscous) { + if (opt.muscl || opt.viscous) { vector_ij = distanceVector(iPoint, side_i.coord, jPoint, side_j.coord); } @@ -309,12 +308,10 @@ class CUpwScalarBase : public CUpwScalarFlux(iPoint, side_i.flowNodes->GetPrimitive(), idx.Velocity()); u.j.all = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Velocity()); - if constexpr (muscl) { - if (musclFlow) { - reconstruct(iPoint, jPoint, vector_ij, side_i.flowNodes->GetGradient_Reconstruction(), - side_i.flowNodes->GetLimiter_Primitive(), limiterTypeFlow, idx.Velocity(), u, kappaFlow, - umusclRamp); - } + if (opt.muscl && musclFlow) { + reconstruct(iPoint, jPoint, vector_ij, side_i.flowNodes->GetGradient_Reconstruction(), + side_i.flowNodes->GetLimiter_Primitive(), limiterTypeFlow, idx.Velocity(), u, kappaFlow, + umusclRamp); } /*--- Face normal velocity of the mean of the two points, relative to the grid. ---*/ @@ -323,7 +320,9 @@ class CUpwScalarBase : public CUpwScalarFlux(iPoint, side_i.gridVel); - const auto ug_j = gatherVariables(jPoint, side_j.gridVel); + /*--- A boundary's ghost point has no grid velocity of its own: it is spatially + * coincident with i, so it moves with it. ---*/ + const auto ug_j = opt.oneSided ? ug_i : gatherVariables(jPoint, side_j.gridVel); for (int iDim = 0; iDim < nDim; ++iDim) vel_ij(iDim) -= 0.5 * (ug_i(iDim) + ug_j(iDim)); } @@ -332,7 +331,7 @@ class CUpwScalarBase : public CUpwScalarFlux> phi; @@ -341,9 +340,11 @@ class CUpwScalarBase : public CUpwScalarFlux(iPoint, jPoint, vector_ij, side_i.scalarNodes.GetGradient_Reconstruction(), - side_i.scalarNodes.GetLimiter(), limiterType, 0, phi, kappa, umusclRamp); + if constexpr (nVar != Dynamic) { + if (opt.muscl) { + reconstruct(iPoint, jPoint, vector_ij, side_i.scalarNodes.GetGradient_Reconstruction(), + side_i.scalarNodes.GetLimiter(), limiterType, 0, phi, kappa, umusclRamp); + } } static_cast(this)->finalizeFlux(idx, opt, iPoint, side_i, jPoint, side_j, a0, a1, phi, res); @@ -363,11 +364,10 @@ class CUpwScalarBase : public CUpwScalarFlux FORCEINLINE void ComputeFlux(const ScalarFluxOptions& opt, Int iEdge, Int iPoint, - const EdgeSide& side_i, Int jPoint, - const EdgeSide& side_j, const Vector& normal, - const Double& massFlux, bool implicit, UpdateType updateType, Double updateMask, - CSysVector& vector, CSysVector& vectorDiff, - SparseMatrixType& matrix) const { + const EdgeSide& side_i, Int jPoint, const EdgeSide& side_j, + const Vector& normal, const Double& massFlux, bool implicit, + UpdateType updateType, Double updateMask, CSysVector& vector, + CSysVector& vectorDiff, SparseMatrixType& matrix) const { const auto res = ComputeFlux(opt, iPoint, side_i, jPoint, side_j, normal, massFlux); updateLinearSystem(iEdge, iPoint, jPoint, implicit, updateType, updateMask, res, vector, vectorDiff, matrix); diff --git a/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp b/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp index e6dc95bd60d..2bbca14ff38 100644 --- a/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp +++ b/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp @@ -37,30 +37,29 @@ * because with stochastic backscatter active (nVar 4) the three Langevin equations are * advected with a centered flux, unlike the plain upwind SA equation itself. */ -template -class CScalarFlux_SA : public CUpwScalarBase, - FlowIndices, nDim, nVar, muscl> { +template +class CScalarFlux_SA + : public CUpwScalarBase, FlowIndices, nDim, nVar> { public: static constexpr bool Conservative = false; static constexpr bool DiagonalDiffusion = true; - using Base = CUpwScalarBase; + using Base = CUpwScalarBase; using Int = typename Base::Int; using Base::Base; private: static constexpr passivedouble sigma = 2.0 / 3.0; /*!< \brief Constant of the diffusion term. */ - static constexpr passivedouble cb2 = 0.622; /*!< \brief Constant of the diffusion term. */ + static constexpr passivedouble cb2 = 0.622; /*!< \brief Constant of the diffusion term. */ public: /*! * \brief SA convection, plus the centered advection of the backscatter equations when nVar > 1. */ template - FORCEINLINE void finalizeFlux(const FlowIndices&, const ScalarFluxOptions&, Int, const EdgeSide&, - Int, const EdgeSide&, const Double& a0, const Double& a1, - const CPair>& phi, - EdgeResidual& res) const { + FORCEINLINE void finalizeFlux(const FlowIndices&, const ScalarFluxOptions&, Int, const EdgeSide&, Int, + const EdgeSide&, const Double& a0, const Double& a1, + const CPair>& phi, EdgeResidual& res) const { const Double flux = a0 * phi.i.all(0) + a1 * phi.j.all(0); res.flux_i(0) += flux; diff --git a/SU2_CFD/include/numerics/util.hpp b/SU2_CFD/include/numerics/util.hpp index f35b159e6ba..a376f4572af 100644 --- a/SU2_CFD/include/numerics/util.hpp +++ b/SU2_CFD/include/numerics/util.hpp @@ -51,12 +51,14 @@ using SparseMatrixType = CSysMatrix; /*! * \brief Alignment of the static containers backing a flux value type. - * \note Yields the type's own alignment for a SIMD array, and the container default (0) - * for a plain scalar, which has no alignment of its own. + * \note Yields the type's own alignment for a SIMD array, and the plain type's natural + * alignment for a scalar; C2DContainer's AlignSize also accepts 0 to mean its own + * default, but that reaches `alignas(0)` on the static specializations, which some + * compilers warn about even though it is a no-op, so a real value is passed instead. */ template struct CAlignTraits { - enum : size_t { Align = 0 }; + enum : size_t { Align = alignof(Type) }; }; template @@ -247,7 +249,7 @@ FORCEINLINE Double squaredNorm(const Vector& vector) { */ template FORCEINLINE Vector tangentProjection(const Matrix& tensor, - const Vector& unitVector) { + const Vector& unitVector) { Vector proj; for (size_t iDim = 0; iDim < nDim; ++iDim) proj(iDim) = dot(tensor[iDim], unitVector); @@ -296,51 +298,37 @@ FORCEINLINE Matrix gatherVariables(Int iPoint, const Conta #else namespace { -template = 0> -FORCEINLINE const su2double& get(const Container& vars, unsigned long iPoint) { - return vars(iPoint); -} - -/*--- When getting 1 variable from a matrix container, we assume it is the first. ---*/ -template = 0> -FORCEINLINE const su2double& get(const Container& vars, unsigned long iPoint, size_t iVar = 0) { - return vars(iPoint, iVar); +/*--- Register every lane of one gathered scalar as a preaccumulation input: the value itself + * for a scalar Double, one call per lane for a simd::Array one. The gather already happened + * through the container's own get(), which packs SIMD lanes and flattens a 3D container's + * (row, column) offset the same way the direct-mode branch above does, so this only adds the + * bookkeeping reverse mode needs on top of that shared read. ---*/ +FORCEINLINE void registerPreaccIn(const su2double& value) { AD::SetPreaccIn(value); } + +template +FORCEINLINE void registerPreaccIn(const simd::Array& value) { + for (size_t k = 0; k < N; ++k) AD::SetPreaccIn(value[k]); } } // namespace template ::Double> FORCEINLINE Double gatherVariables(Int iPoint, const Container& vars, size_t iVar = 0) { - Double x; - for (size_t k = 0; k < CValueTraits::Size; ++k) { - AD::SetPreaccIn(get(vars, iPoint[k], iVar)); - x[k] = get(vars, iPoint[k], iVar); - } - return x; + const auto x = vars.template get>(iPoint, iVar); + registerPreaccIn(x(0)); + return x(0); } template ::Double> FORCEINLINE Vector gatherVariables(Int iPoint, const Container& vars, size_t iVar = 0) { - Vector x; - for (size_t i = 0; i < nVar; ++i) { - for (size_t k = 0; k < CValueTraits::Size; ++k) { - AD::SetPreaccIn(vars(iPoint[k], iVar + i)); - x[i][k] = vars(iPoint[k], iVar + i); - } - } + auto x = vars.template get>(iPoint, iVar); + for (size_t i = 0; i < nVar; ++i) registerPreaccIn(x[i]); return x; } template ::Double> FORCEINLINE Matrix gatherVariables(Int iPoint, const Container& vars, size_t iRow = 0) { - Matrix x; - for (size_t i = 0; i < nRows; ++i) { - for (size_t j = 0; j < nCols; ++j) { - for (size_t k = 0; k < CValueTraits::Size; ++k) { - AD::SetPreaccIn(vars(iPoint[k], iRow + i, j)); - x(i, j)[k] = vars(iPoint[k], iRow + i, j); - } - } - } + auto x = vars.template get>(iPoint, iRow); + for (size_t i = 0; i < nRows * nCols; ++i) registerPreaccIn(x.data()[i]); return x; } #endif @@ -403,12 +391,12 @@ FORCEINLINE Double umusclProjection(const Double& gradProj, const Double& delta, /*! * \brief MUSCL reconstruction of the specified variable. * \note The result should be halved when added to i (or subtracted from j). - * \note Reads its own row of the gradient container (rather than being handed an already - * gathered nVarGrad x nDim block, as it once was) so that a caller reconstructing a - * single variable, e.g. a scalar with nVar 1, never gathers a Matrix: that - * shape is the same RowMajor, one-row degeneracy that forces EdgeResidual's Size floor - * (see numerics/util.hpp), and here it would silently turn a row into a lone scalar - * instead of failing to compile, since Matrix still satisfies IsVector. + * \note Reads its own row of the gradient container, rather than taking an already gathered + * nVarGrad x nDim block, so that a caller reconstructing a single variable, e.g. a scalar + * with nVar 1, never gathers a Matrix: that shape is the same RowMajor, + * one-row degeneracy that forces EdgeResidual's Size floor above, and here it would + * silently turn a row into a lone scalar instead of failing to compile, since + * Matrix still satisfies IsVector. */ template ::Int> FORCEINLINE Double musclReconstruction(Int iPoint, const Gradient_t& gradient, size_t iRow, @@ -436,8 +424,10 @@ FORCEINLINE void musclUnlimited(typename CLaneTraits::Int iPoint, typena const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); /*--- U-MUSCL reconstructed variables ---*/ - const Double proj_i = musclReconstruction(iPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); - const Double proj_j = musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); + const Double proj_i = + musclReconstruction(iPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); + const Double proj_j = + musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); /*--- Apply reconstruction: V_L = V_i + 0.5 * dV_ij^kap ---*/ V.i.all(iVar) += 0.5 * proj_i; @@ -449,11 +439,10 @@ FORCEINLINE void musclUnlimited(typename CLaneTraits::Int iPoint, typena * \brief Limited reconstruction with point-based limiter. */ template -FORCEINLINE void musclPointLimited(typename CLaneTraits::Int iPoint, - typename CLaneTraits::Int jPoint, const Vector& vector_ij, - const Limiter_t& limiter, const Gradient_t& gradient, CPair& V, - const CNonDeduced& kappa, const CNonDeduced& umusclRamp, - size_t iRow = 0) { +FORCEINLINE void musclPointLimited(typename CLaneTraits::Int iPoint, typename CLaneTraits::Int jPoint, + const Vector& vector_ij, const Limiter_t& limiter, + const Gradient_t& gradient, CPair& V, const CNonDeduced& kappa, + const CNonDeduced& umusclRamp, size_t iRow = 0) { constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; auto lim_i = gatherVariables(iPoint, limiter, iRow); @@ -464,8 +453,10 @@ FORCEINLINE void musclPointLimited(typename CLaneTraits::Int iPoint, const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); /*--- U-MUSCL reconstructed variables ---*/ - const Double proj_i = musclReconstruction(iPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); - const Double proj_j = musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); + const Double proj_i = + musclReconstruction(iPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); + const Double proj_j = + musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); /*--- Apply reconstruction: V_L = V_i + 0.5 * lim * dV_ij^kap ---*/ V.i.all(iVar) += 0.5 * lim_i(iVar) * proj_i; @@ -477,10 +468,10 @@ FORCEINLINE void musclPointLimited(typename CLaneTraits::Int iPoint, * \brief Limited reconstruction with edge-based limiter. */ template -FORCEINLINE void musclEdgeLimited(typename CLaneTraits::Int iPoint, - typename CLaneTraits::Int jPoint, const Vector& vector_ij, - const Gradient_t& gradient, CPair& V, const CNonDeduced& kappa, - const CNonDeduced& umusclRamp, size_t iRow = 0) { +FORCEINLINE void musclEdgeLimited(typename CLaneTraits::Int iPoint, typename CLaneTraits::Int jPoint, + const Vector& vector_ij, const Gradient_t& gradient, CPair& V, + const CNonDeduced& kappa, const CNonDeduced& umusclRamp, + size_t iRow = 0) { constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { @@ -489,8 +480,10 @@ FORCEINLINE void musclEdgeLimited(typename CLaneTraits::Int iPoint, const Double delta_ij_2 = pow(delta_ij, 2) + 1e-6; /*--- U-MUSCL reconstructed variables ---*/ - const Double proj_i = musclReconstruction(iPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); - const Double proj_j = musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); + const Double proj_i = + musclReconstruction(iPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); + const Double proj_j = + musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); const Double lim_i = (delta_ij_2 + proj_i * delta_ij) / (pow(proj_i, 2) + delta_ij_2); const Double lim_j = (delta_ij_2 + proj_j * delta_ij) / (pow(proj_j, 2) + delta_ij_2); @@ -503,8 +496,7 @@ FORCEINLINE void musclEdgeLimited(typename CLaneTraits::Int iPoint, /*! * \brief Reconstruct a slice of nVarGrad variables starting at column iRow, dispatching on the - * limiter type. This is the switch `reconstructPrimitives` used to perform inline; lifted - * here so both the flow and the scalar reconstructions call the same body. + * limiter type; shared by the flow and the scalar reconstructions so both call one body. */ template FORCEINLINE void reconstruct(typename CLaneTraits::Int iPoint, typename CLaneTraits::Int jPoint, diff --git a/SU2_CFD/include/solvers/CTurbSASolver.hpp b/SU2_CFD/include/solvers/CTurbSASolver.hpp index 1e1a7baeaf0..0544d82477d 100644 --- a/SU2_CFD/include/solvers/CTurbSASolver.hpp +++ b/SU2_CFD/include/solvers/CTurbSASolver.hpp @@ -96,11 +96,13 @@ class CTurbSASolver final : public CTurbSolver { void ComputeUnderRelaxationFactor(CSolver** solver_container, const CConfig *config) final; /*! - * \brief Resolve the compile-time flow indices, dimension, backscatter equation count and - * MUSCL setting, and run the interior edge loop with the matching CScalarFlux_SA - * instantiation. Each overload resolves one more of those from CConfig/CGeometry and - * recurses into the next, so the runtime-to-compile-time dispatch stays linear in the - * number of axes instead of enumerating every combination by hand. + * \brief Resolve the compile-time flow indices, dimension and backscatter equation count, and + * run the interior edge loop with the matching CScalarFlux_SA instantiation. Whether the + * scheme reconstructs is a runtime flag carried in opt (see ScalarFluxOptions::muscl), + * not an axis of this dispatch. Each overload resolves one more of the remaining axes + * from CConfig/CGeometry and recurses into the next, so the runtime-to-compile-time + * dispatch stays linear in the number of axes instead of enumerating every combination + * by hand. */ template void RunSA(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt); @@ -112,8 +114,7 @@ class CTurbSASolver final : public CTurbSolver { void RunSA(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt); /*! - * \brief Same dispatch as RunSA, for a boundary's call into BoundaryFluxResidual; a boundary - * always reconstructs nothing, so muscl is not one of the axes resolved here. + * \brief Same dispatch as RunSA, for a boundary's call into BoundaryFluxResidual. */ template void RunSA_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, @@ -173,8 +174,7 @@ class CTurbSASolver final : public CTurbSolver { /*! * \brief Compute the spatial integration using the CScalarFlux_SA edge kernel, which computes - * and writes both the convective and the diffusive term of every edge; this solver has - * no Viscous_Residual of its own any more. + * and writes both the convective and the diffusive term of every edge. * \param[in] geometry - Geometrical definition of the problem. * \param[in] solver_container - Container vector with all the solutions. * \param[in] numerics_container - Unused, kept only for the boundary conditions. diff --git a/SU2_CFD/src/solvers/CTurbSASolver.cpp b/SU2_CFD/src/solvers/CTurbSASolver.cpp index dc9f9e05583..1052b3b1ec5 100644 --- a/SU2_CFD/src/solvers/CTurbSASolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSASolver.cpp @@ -358,11 +358,12 @@ void CTurbSASolver::Upwind_Residual(CGeometry* geometry, CSolver** solver_contai const ScalarFluxOptions opt{ dynamic_grid, /*--- dynamicGrid ---*/ config->GetBounded_Turb(), /*--- boundedScalar ---*/ - true, /*--- correctGradient, as CAvgGrad_TurbSA is built today ---*/ + true, /*--- correctGradient ---*/ config->GetUse_Accurate_Turb_Jacobians(), /*--- accurateJacobians ---*/ true, /*--- convective ---*/ true, /*--- viscous ---*/ false, /*--- oneSided, this is the interior loop ---*/ + config->GetMUSCL(), /*--- muscl ---*/ }; if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { @@ -393,12 +394,7 @@ void CTurbSASolver::RunSA(CGeometry* geometry, CSolver** solver_container, CConf template void CTurbSASolver::RunSA(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt) { - if (config->GetMUSCL()) { - EdgeFluxResidual>(geometry, solver_container, config, opt); - } else { - EdgeFluxResidual>(geometry, solver_container, config, - opt); - } + EdgeFluxResidual>(geometry, solver_container, config, opt); } void CTurbSASolver::BC_Far_Field(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, @@ -426,8 +422,8 @@ void CTurbSASolver::BC_Far_Field(CGeometry *geometry, CSolver **solver_container const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, - true /*convective*/, false /*viscous, far field has no diffusive flux, matching the old numerics path*/, - true /*oneSided, the ghost point has no row*/, + true /*convective*/, false /*viscous*/, + true /*oneSided, the ghost point has no row*/, false /*muscl, a boundary never reconstructs*/, }; if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { @@ -459,9 +455,8 @@ void CTurbSASolver::RunSA_Boundary(CGeometry* geometry, CSolver** solver_contain template void CTurbSASolver::RunSA_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit) { - /*--- Boundaries never reconstruct: muscl is false here regardless of config->GetMUSCL(). ---*/ - BoundaryFluxResidual>(geometry, solver_container, config, - opt, val_marker, implicit); + BoundaryFluxResidual>(geometry, solver_container, config, opt, + val_marker, implicit); } void CTurbSASolver::Source_Residual(CGeometry *geometry, CSolver **solver_container, @@ -737,12 +732,11 @@ void CTurbSASolver::BC_Inlet(CGeometry *geometry, CSolver **solver_container, CN } END_SU2_OMP_FOR - /*--- The diffusive term at the inlet is disabled: it caused serious convergence problems in - * the numerics this replaces, so opt.viscous stays false here too. ---*/ + /*--- The diffusive term at the inlet causes serious convergence problems, so it stays off. ---*/ const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, - true /*convective*/, false /*viscous*/, true /*oneSided*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { From 1d2ef5bace1aadc394c6321537e4ea610d81d2d0 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Thu, 27 Aug 2026 21:55:51 -0700 Subject: [PATCH 05/20] SIMD binding for the scalar interior edge loop (Section 10 prototype) CScalarSolver::EdgeFluxResidual now steps by CLaneTraits::Size and builds a masked Int/Double lane group per iteration, matching CFVMFlowSolverBase's own masked edge loop; at Size 1 (su2double) it degenerates back to exactly the previous scalar loop. CTurbSASolver's interior loop now instantiates CScalarFlux_SA with simd::Array instead of su2double, which is its native SIMD width in primal mode and width 1 under reverse AD (preferredLen), so this needs no build-mode branch. Boundaries keep the su2double binding, per the design. This required two linear-algebra gaps to close: - CSysMatrix gained SIMD overloads of the four-independent-block SetBlocks and of SetOffDiagBlocks. Unlike the existing two-block SIMD SetBlocks (which flow numerics already use safely), these read through the matrix's own runtime nVar/nEqn rather than the block type's static size, because EdgeResidual floors its Jacobian storage to a minimum static size of 2 for a static nVar 1 model (see EdgeResidual's own comment) - reading the block's static size instead silently wrote a 2x2 block into an nVar=1 matrix and corrupted the heap, surfacing much later as a free() abort in unrelated flow numerics. - CSysVector::UnpackBlock and its new AddBlock overload (needed since flux_i and flux_j of an EdgeResidual are independent, unlike UpdateBlocks' shared block) have the same fix: nVar is now a runtime argument bounding a fixed MAXNVAR-sized buffer, not deduced from the vector type's static size. Verified against the pre-vectorization binary on turb_ONERAM6 (far field): bit-identical single-threaded (natural coloring), 4-thread coloring, and 32-thread reducer-strategy fallback. A synthetic BOUNDED_SCALAR turbulence case (compressible flatplate, not itself a reference case) matches to the last few digits rather than bit-for-bit, consistent with FMA/reassociation differences from real vectorization rather than a correctness issue. Co-Authored-By: Claude Sonnet 5 --- Common/include/linear_algebra/CSysMatrix.hpp | 114 +++++++++++ Common/include/linear_algebra/CSysVector.hpp | 50 +++-- SU2_CFD/include/solvers/CScalarSolver.inl | 55 +++++- SU2_CFD/src/solvers/CTurbSASolver.cpp | 7 +- .../edge_residual_blocks_simd_tests.cpp | 186 ++++++++++++++++++ UnitTests/meson.build | 3 +- 6 files changed, 391 insertions(+), 24 deletions(-) create mode 100644 UnitTests/Common/linear_algebra/edge_residual_blocks_simd_tests.cpp diff --git a/Common/include/linear_algebra/CSysMatrix.hpp b/Common/include/linear_algebra/CSysMatrix.hpp index 84f15b58b8a..53bc704946f 100644 --- a/Common/include/linear_algebra/CSysMatrix.hpp +++ b/Common/include/linear_algebra/CSysMatrix.hpp @@ -1150,6 +1150,120 @@ class CSysMatrix { } } + /*! + * \brief SIMD version, sets the four blocks of an edge for multiple edges at once. + * \note Nothing is updated if the mask is 0. As with the scalar overload, the diagonal + * blocks are accumulated and the off-diagonal blocks are set. + */ + template + FORCEINLINE void SetBlocks(simd::Array iEdge, simd::Array iPoint, simd::Array jPoint, + const MatTypeSIMD& jac_ii, const MatTypeSIMD& jac_ij, const MatTypeSIMD& jac_ji, + const MatTypeSIMD& jac_jj, simd::Array mask = 1) { + static_assert(MatTypeSIMD::StaticSize, "This method requires static size blocks."); + static_assert(MatTypeSIMD::IsRowMajor, "Block storage is not compatible with matrix."); + /*--- Read through (iVar, jVar) rather than a flat MatTypeSIMD::StaticSize/StaticNRows walk: + * EdgeResidual's Jacobian blocks are floored to a minimum static size of 2 (see its own + * comment), so a static nVar 1 model still hands in a 2x2 block here and blkSz must come + * from the matrix's own (runtime) nVar/nEqn, not from the possibly padded block type. ---*/ + const auto blkSz = nVar * nEqn; + assert(blkSz <= MatTypeSIMD::StaticSize); + + /*--- "Transpose" the four blocks, scale, and possibly convert types, + * giving the compiler the chance to vectorize all of these. ---*/ + ScalarType blk_ii[N][MAXNVAR * MAXNVAR], blk_ij[N][MAXNVAR * MAXNVAR]; + ScalarType blk_ji[N][MAXNVAR * MAXNVAR], blk_jj[N][MAXNVAR * MAXNVAR]; + + unsigned long offset = 0; + for (auto iVar = 0ul; iVar < nVar; ++iVar) { + for (auto jVar = 0ul; jVar < nEqn; ++jVar, ++offset) { + SU2_OMP_SIMD_IF_NOT_AD + for (size_t k = 0; k < N; ++k) { + blk_ii[k][offset] = PassiveAssign(mask[k] * jac_ii(iVar, jVar)[k]); + blk_ij[k][offset] = PassiveAssign(mask[k] * jac_ij(iVar, jVar)[k]); + blk_ji[k][offset] = PassiveAssign(mask[k] * jac_ji(iVar, jVar)[k]); + blk_jj[k][offset] = PassiveAssign(mask[k] * jac_jj(iVar, jVar)[k]); + } + } + } + + /*--- Update one by one skipping if mask is 0. ---*/ + for (size_t k = 0; k < N; ++k) { + if (mask[k] == 0) continue; + + auto bii = &mat.d[iPoint[k] * blkSz]; + auto bjj = &mat.d[jPoint[k] * blkSz]; + + if (quantized_mode) { + SU2_OMP_SIMD + for (size_t i = 0; i < blkSz; ++i) { + bii[i] += blk_ii[k][i]; + bjj[i] += blk_jj[k][i]; + } + EncodeQuantBlock([&, k](unsigned long r, unsigned long c) { return blk_ij[k][r * nVar + c]; }, + &q_scale.u[iEdge[k] * nVar], &q_blocks.u[iEdge[k] * blkSz], nVar); + const auto k_l = edge_ptr_l[iEdge[k]]; + EncodeQuantBlock([&, k](unsigned long r, unsigned long c) { return blk_ji[k][r * nVar + c]; }, + &q_scale.l[k_l * nVar], &q_blocks.l[k_l * blkSz], nVar); + } else { + auto bij = &mat.u[iEdge[k] * blkSz]; + auto bji = &mat.l[edge_ptr_l[iEdge[k]] * blkSz]; + SU2_OMP_SIMD + for (size_t i = 0; i < blkSz; ++i) { + bii[i] += blk_ii[k][i]; + bjj[i] += blk_jj[k][i]; + bij[i] = blk_ij[k][i]; + bji[i] = blk_ji[k][i]; + } + } + } + } + + /*! + * \brief SIMD version, sets the off-diagonal blocks of an edge for multiple edges at once. + */ + template + FORCEINLINE void SetOffDiagBlocks(simd::Array iEdge, const MatTypeSIMD& jac_ij, const MatTypeSIMD& jac_ji, + simd::Array mask = 1) { + static_assert(MatTypeSIMD::StaticSize, "This method requires static size blocks."); + static_assert(MatTypeSIMD::IsRowMajor, "Block storage is not compatible with matrix."); + /*--- See the note in the four-block SetBlocks above about reading via (iVar, jVar). ---*/ + const auto blkSz = nVar * nEqn; + assert(blkSz <= MatTypeSIMD::StaticSize); + + ScalarType blk_ij[N][MAXNVAR * MAXNVAR], blk_ji[N][MAXNVAR * MAXNVAR]; + + unsigned long offset = 0; + for (auto iVar = 0ul; iVar < nVar; ++iVar) { + for (auto jVar = 0ul; jVar < nEqn; ++jVar, ++offset) { + SU2_OMP_SIMD_IF_NOT_AD + for (size_t k = 0; k < N; ++k) { + blk_ij[k][offset] = PassiveAssign(mask[k] * jac_ij(iVar, jVar)[k]); + blk_ji[k][offset] = PassiveAssign(mask[k] * jac_ji(iVar, jVar)[k]); + } + } + } + + for (size_t k = 0; k < N; ++k) { + if (mask[k] == 0) continue; + + if (quantized_mode) { + EncodeQuantBlock([&, k](unsigned long r, unsigned long c) { return blk_ij[k][r * nVar + c]; }, + &q_scale.u[iEdge[k] * nVar], &q_blocks.u[iEdge[k] * blkSz], nVar); + const auto k_l = edge_ptr_l[iEdge[k]]; + EncodeQuantBlock([&, k](unsigned long r, unsigned long c) { return blk_ji[k][r * nVar + c]; }, + &q_scale.l[k_l * nVar], &q_blocks.l[k_l * blkSz], nVar); + } else { + ScalarType* bij = &mat.u[iEdge[k] * blkSz]; + ScalarType* bji = &mat.l[edge_ptr_l[iEdge[k]] * blkSz]; + SU2_OMP_SIMD + for (size_t i = 0; i < blkSz; ++i) { + bij[i] = blk_ij[k][i]; + bji[i] = blk_ji[k][i]; + } + } + } + } + /*! * \brief Sets the specified block to the (i, i) subblock of the sparse matrix. * Scales the input block by factor alpha. If the Overwrite parameter is diff --git a/Common/include/linear_algebra/CSysVector.hpp b/Common/include/linear_algebra/CSysVector.hpp index 28c75004958..9d05d9941c8 100644 --- a/Common/include/linear_algebra/CSysVector.hpp +++ b/Common/include/linear_algebra/CSysVector.hpp @@ -225,13 +225,21 @@ class CSysVector : public VecExpr::CVecExpr, ScalarType> void Initialize(unsigned long numBlk, unsigned long numBlkDomain, unsigned long numVar, const ScalarType* val, bool valIsArray, bool errorIfParallel = true); + enum : size_t { MAXNVAR = 20 }; /*!< \brief Upper bound for the block buffers below, matches CSysMatrix's. */ + /*! * \brief Helper to unpack (transpose) a SIMD input block. - */ - template - FORCEINLINE static void UnpackBlock(const VecTypeSIMD& in, simd::Array mask, ScalarType out[][nVar]) { + * \note "nVar" is a runtime argument, not deduced from VecTypeSIMD: an EdgeResidual's flux_i/ + * flux_j are floored to a minimum static size of 2 (see its own comment), so a static + * nVar 1 model still hands in a size-2 Vector here, and this vector's own (runtime) nVar + * is the one that must bound the transpose and the later read/write. + */ + template + FORCEINLINE static void UnpackBlock(const VecTypeSIMD& in, simd::Array mask, unsigned long nVar, + ScalarType out[][MAXNVAR]) { static_assert(VecTypeSIMD::StaticSize, "This method requires static size vectors."); - for (size_t i = 0; i < nVar; ++i) { + assert(nVar <= MAXNVAR && nVar <= VecTypeSIMD::StaticSize); + for (auto i = 0ul; i < nVar; ++i) { SU2_OMP_SIMD_IF_NOT_AD for (size_t k = 0; k < N; ++k) out[k][i] = mask[k] * in[i][k]; } @@ -660,16 +668,32 @@ class CSysVector : public VecExpr::CVecExpr, ScalarType> template FORCEINLINE void SetBlock(simd::Array iPoint, const VecTypeSIMD& vector, simd::Array mask = 1) { /*--- "Transpose" and scale input vector. ---*/ - constexpr size_t nVar = VecTypeSIMD::StaticSize; - assert(nVar == this->nVar); - ScalarType vec[N][nVar]; - UnpackBlock(vector, mask, vec); + ScalarType vec[N][MAXNVAR]; + UnpackBlock(vector, mask, nVar, vec); + + /*--- Update one by one skipping if mask is 0. ---*/ + for (size_t k = 0; k < N; ++k) { + if (mask[k] == 0) continue; + SU2_OMP_SIMD + for (auto i = 0ul; i < nVar; ++i) vec_val[iPoint[k] * nVar + i] = vec[k][i]; + } + } + + /*! + * \brief Vectorized version of AddBlock, adds to multiple iPoint's. + * \note Unlike UpdateBlocks, "vector" is independent at every iPoint (e.g. flux_i and flux_j + * of an EdgeResidual), so this takes one iPoint and one vector, called once per side. + */ + template + FORCEINLINE void AddBlock(simd::Array iPoint, const VecTypeSIMD& vector, simd::Array mask = 1) { + ScalarType vec[N][MAXNVAR]; + UnpackBlock(vector, mask, nVar, vec); /*--- Update one by one skipping if mask is 0. ---*/ for (size_t k = 0; k < N; ++k) { if (mask[k] == 0) continue; SU2_OMP_SIMD - for (size_t i = 0; i < nVar; ++i) vec_val[iPoint[k] * nVar + i] = vec[k][i]; + for (auto i = 0ul; i < nVar; ++i) vec_val[iPoint[k] * nVar + i] += vec[k][i]; } } @@ -681,16 +705,14 @@ class CSysVector : public VecExpr::CVecExpr, ScalarType> FORCEINLINE void UpdateBlocks(simd::Array iPoint, simd::Array jPoint, const VecTypeSIMD& vector, simd::Array mask = 1) { /*--- "Transpose" and scale input vector. ---*/ - constexpr size_t nVar = VecTypeSIMD::StaticSize; - assert(nVar == this->nVar); - ScalarType vec[N][nVar]; - UnpackBlock(vector, mask, vec); + ScalarType vec[N][MAXNVAR]; + UnpackBlock(vector, mask, nVar, vec); /*--- Update one by one skipping if mask is 0. ---*/ for (size_t k = 0; k < N; ++k) { if (mask[k] == 0) continue; SU2_OMP_SIMD - for (size_t i = 0; i < nVar; ++i) { + for (auto i = 0ul; i < nVar; ++i) { vec_val[iPoint[k] * nVar + i] += vec[k][i]; vec_val[jPoint[k] * nVar + i] -= vec[k][i]; } diff --git a/SU2_CFD/include/solvers/CScalarSolver.inl b/SU2_CFD/include/solvers/CScalarSolver.inl index 105d253e3c3..56e27ddc017 100644 --- a/SU2_CFD/include/solvers/CScalarSolver.inl +++ b/SU2_CFD/include/solvers/CScalarSolver.inl @@ -378,7 +378,19 @@ void CScalarSolver::EdgeFluxResidual(const CGeometry* geometry, CS SU2_ZONE_SCOPED using Double = typename Scheme::Double; + using Int = typename CLaneTraits::Int; constexpr int nDim = Scheme::nDim; + constexpr size_t Width = CLaneTraits::Size; + + /*--- Scheme::Double picks the binding: su2double (Width 1) degenerates the loop below to the + * plain scalar form, a simd::Array Double drives it several edges at a time. One loop body + * serves both, matching the flow solver's own masked edge loop. ---*/ + if constexpr (Width > 1) { + if (!ReducerStrategy && (omp_get_max_threads() > 1) && (config->GetEdgeColoringGroupSize() % Width != 0)) { + SU2_MPI::Error("When using vectorization, the EDGE_COLORING_GROUP_SIZE must be divisible " + "by the SIMD length (2, 4, or 8).", CURRENT_FUNCTION); + } + } const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; @@ -403,25 +415,52 @@ void CScalarSolver::EdgeFluxResidual(const CGeometry* geometry, CS for (auto color : EdgeColoring) { SU2_OMP_FOR_DYN(nextMultiple(OMP_MIN_SIZE, color.groupSize)) - for (auto k = 0ul; k < color.size; ++k) { - const unsigned long iEdge = color.indices[k]; + for (auto k = 0ul; k < color.size; k += Width) { + Int iEdge; + Double mask; + if constexpr (Width == 1) { + iEdge = color.indices[k]; + mask = 1.0; + } else { + for (auto j = 0ul; j < Width; ++j) { + const bool in = (k + j < color.size); + mask[j] = in; + iEdge[j] = color.indices[k + j * in]; + } + } + const auto iPoint = geometry->edges->GetNode(iEdge, 0); const auto jPoint = geometry->edges->GetNode(iEdge, 1); const auto normal = gatherVariables(iEdge, geometry->edges->GetNormal()); const Double massFlux = opt.boundedScalar ? gatherVariables(iEdge, *edgeMassFluxes) : Double(0.0); - flux.ComputeFlux(opt, iEdge, iPoint, side, jPoint, side, normal, massFlux, implicit, updateType, 1.0, target, + flux.ComputeFlux(opt, iEdge, iPoint, side, jPoint, side, normal, massFlux, implicit, updateType, mask, target, EdgeFluxesDiff, Jacobian); /*--- Bounded scalar divergence correction, per edge; the ReducerStrategy equivalent runs * in a per-point pass below, where the diagonal is not written from the edge loop. ---*/ if (opt.boundedScalar && !ReducerStrategy) { - LinSysRes.AddBlock(iPoint, nodes->GetSolution(iPoint), -massFlux); - LinSysRes.AddBlock(jPoint, nodes->GetSolution(jPoint), massFlux); - if (implicit) { - Jacobian.AddVal2Diag(iPoint, -massFlux); - Jacobian.AddVal2Diag(jPoint, massFlux); + if constexpr (Width == 1) { + LinSysRes.AddBlock(iPoint, nodes->GetSolution(iPoint), -massFlux); + LinSysRes.AddBlock(jPoint, nodes->GetSolution(jPoint), massFlux); + if (implicit) { + Jacobian.AddVal2Diag(iPoint, -massFlux); + Jacobian.AddVal2Diag(jPoint, massFlux); + } + } else { + for (auto j = 0ul; j < Width; ++j) { + if (mask[j] == 0) continue; + const auto i = iPoint[j]; + const auto jp = jPoint[j]; + const su2double mf = massFlux[j]; + LinSysRes.AddBlock(i, nodes->GetSolution(i), -mf); + LinSysRes.AddBlock(jp, nodes->GetSolution(jp), mf); + if (implicit) { + Jacobian.AddVal2Diag(i, -mf); + Jacobian.AddVal2Diag(jp, mf); + } + } } } } diff --git a/SU2_CFD/src/solvers/CTurbSASolver.cpp b/SU2_CFD/src/solvers/CTurbSASolver.cpp index 1052b3b1ec5..11eae2549c3 100644 --- a/SU2_CFD/src/solvers/CTurbSASolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSASolver.cpp @@ -394,7 +394,12 @@ void CTurbSASolver::RunSA(CGeometry* geometry, CSolver** solver_container, CConf template void CTurbSASolver::RunSA(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt) { - EdgeFluxResidual>(geometry, solver_container, config, opt); + /*--- simd::Array is its preferred SIMD width in primal mode, and width 1 (i.e. the + * scalar loop, unchanged) under reverse AD (see preferredLen in vectorization.hpp), + * so this one binding covers both without a branch here. Boundaries stay on the su2double + * binding, see RunSA_Boundary: their loops are over vertices, not the hot edge loop. ---*/ + EdgeFluxResidual, Indices, nDim, nVarSA>>(geometry, solver_container, config, + opt); } void CTurbSASolver::BC_Far_Field(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, diff --git a/UnitTests/Common/linear_algebra/edge_residual_blocks_simd_tests.cpp b/UnitTests/Common/linear_algebra/edge_residual_blocks_simd_tests.cpp new file mode 100644 index 00000000000..187d3348aa6 --- /dev/null +++ b/UnitTests/Common/linear_algebra/edge_residual_blocks_simd_tests.cpp @@ -0,0 +1,186 @@ +/*! + * \file edge_residual_blocks_simd_tests.cpp + * \brief Unit tests for the SIMD overloads of CSysMatrix::SetBlocks (four independent blocks), + * SetOffDiagBlocks, and CSysVector::AddBlock, added for the vectorized scalar edge loop. + * \author P. Gomes + * \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 + +#include "catch.hpp" +#include "../../UnitQuadTestCase.hpp" +#include "../../../SU2_CFD/include/numerics/util.hpp" + +/*--- UnitQuadTestCase's fixed config: 3D compressible Euler, nVar = nDim + 2. ---*/ +constexpr size_t nVar = 5; +using Double = simd::Array; +constexpr size_t N = Double::Size; + +static void FillBlock(Matrix& block, size_t lane, su2double base) { + for (auto i = 0u; i < nVar; ++i) + for (auto j = 0u; j < nVar; ++j) block(i, j)[lane] = base + 0.1 * i + 0.01 * j + lane; +} + +static void CheckBlock(const CSysMatrix& matrix, unsigned long i, unsigned long j, + const Matrix& block, size_t lane, double tol) { + auto view = matrix.GetBlockView(i, j); + for (auto iVar = 0u; iVar < nVar; ++iVar) + for (auto jVar = 0u; jVar < nVar; ++jVar) + CHECK(SU2_TYPE::GetValue(view(iVar, jVar)) == Approx(SU2_TYPE::GetValue(block(iVar, jVar)[lane])).margin(tol)); +} + +TEST_CASE("SIMD SetBlocks, SetOffDiagBlocks and AddBlock match their scalar counterparts", "[LinearAlgebra]") { + cout.rdbuf(nullptr); + + UnitQuadTestCase testCase; + testCase.InitConfig(); + testCase.InitGeometry(); + testCase.InitSolver(); + + cout.rdbuf(testCase.orig_buf); + + auto* solver = testCase.solver[FLOW_SOL]; + auto& matrix = solver->Jacobian; + REQUIRE(solver->GetnVar() == nVar); + + /*--- N edges with pairwise disjoint endpoints, so a lane's diagonal block never accumulates + * a second edge's contribution and CheckBlock can compare it to a single lane's own fill. + * (The production code relies on edges within a SIMD group being contiguous for the SIMD + * GetNode gather, but this test builds iPoint/jPoint with the scalar GetNode overload, one + * edge at a time, so that constraint does not apply here.) ---*/ + simd::Array iEdge, iPoint, jPoint; + std::set used; + size_t found = 0; + for (unsigned long e = 0; e < testCase.geometry->GetnEdge() && found < N; ++e) { + const auto p0 = testCase.geometry->edges->GetNode(e, 0); + const auto p1 = testCase.geometry->edges->GetNode(e, 1); + if (used.count(p0) || used.count(p1)) continue; + iEdge[found] = e; + iPoint[found] = p0; + jPoint[found] = p1; + used.insert(p0); + used.insert(p1); + ++found; + } + REQUIRE(found == N); + + Matrix jac_ii, jac_ij, jac_ji, jac_jj; + for (size_t k = 0; k < N; ++k) { + FillBlock(jac_ii, k, 1.0); + FillBlock(jac_ij, k, 2.0); + FillBlock(jac_ji, k, 3.0); + FillBlock(jac_jj, k, 4.0); + } + + SECTION("SetBlocks: diagonal accumulates, off-diagonal is set, one lane at a time via the scalar overload") { + matrix.SetValZero(); + matrix.SetBlocks(iEdge, iPoint, jPoint, jac_ii, jac_ij, jac_ji, jac_jj); + + for (size_t k = 0; k < N; ++k) { + CheckBlock(matrix, iPoint[k], iPoint[k], jac_ii, k, 1e-6); + CheckBlock(matrix, jPoint[k], jPoint[k], jac_jj, k, 1e-6); + CheckBlock(matrix, iPoint[k], jPoint[k], jac_ij, k, 1e-6); + CheckBlock(matrix, jPoint[k], iPoint[k], jac_ji, k, 1e-6); + } + } + + SECTION("SetBlocks: a lane whose mask is 0 is left untouched") { + matrix.SetValZero(); + matrix.SetBlocks(iEdge, iPoint, jPoint, jac_ii, jac_ij, jac_ji, jac_jj); + + Matrix jac_ii2 = jac_ii, jac_ij2 = jac_ij, jac_ji2 = jac_ji, jac_jj2 = jac_jj; + for (size_t k = 0; k < N; ++k) FillBlock(jac_ii2, k, 100.0); + + simd::Array mask = 1; + mask[0] = 0; + matrix.SetBlocks(iEdge, iPoint, jPoint, jac_ii2, jac_ij2, jac_ji2, jac_jj2, mask); + + /*--- Lane 0: SetBlocks was skipped, diagonal is the single accumulation from the first + * call above, off-diagonal is still what that first call set. ---*/ + CheckBlock(matrix, iPoint[0], iPoint[0], jac_ii, 0, 1e-6); + CheckBlock(matrix, iPoint[0], jPoint[0], jac_ij, 0, 1e-6); + + /*--- Lane 1 (points disjoint from every other lane's, by construction): diagonal + * accumulated twice, off-diagonal overwritten by the second call. ---*/ + if (N > 1) { + Matrix jac_ii_2x = jac_ii; + for (auto i = 0u; i < nVar; ++i) + for (auto j = 0u; j < nVar; ++j) jac_ii_2x(i, j)[1] = jac_ii(i, j)[1] + jac_ii2(i, j)[1]; + CheckBlock(matrix, iPoint[1], iPoint[1], jac_ii_2x, 1, 1e-6); + CheckBlock(matrix, iPoint[1], jPoint[1], jac_ij2, 1, 1e-6); + } + } + + SECTION("SetOffDiagBlocks leaves the diagonal untouched") { + matrix.SetValZero(); + matrix.SetBlocks(iEdge, iPoint, jPoint, jac_ii, jac_ij, jac_ji, jac_jj); + + Matrix jac_ij_new, jac_ji_new; + for (size_t k = 0; k < N; ++k) { + FillBlock(jac_ij_new, k, 5.0); + FillBlock(jac_ji_new, k, 6.0); + } + matrix.SetOffDiagBlocks(iEdge, jac_ij_new, jac_ji_new); + + for (size_t k = 0; k < N; ++k) { + CheckBlock(matrix, iPoint[k], iPoint[k], jac_ii, k, 1e-6); + CheckBlock(matrix, jPoint[k], jPoint[k], jac_jj, k, 1e-6); + CheckBlock(matrix, iPoint[k], jPoint[k], jac_ij_new, k, 1e-6); + CheckBlock(matrix, jPoint[k], iPoint[k], jac_ji_new, k, 1e-6); + } + } +} + +TEST_CASE("SIMD CSysVector::AddBlock writes independent values to independent points", "[LinearAlgebra]") { + cout.rdbuf(nullptr); + + UnitQuadTestCase testCase; + testCase.InitConfig(); + testCase.InitGeometry(); + testCase.InitSolver(); + + cout.rdbuf(testCase.orig_buf); + + auto* solver = testCase.solver[FLOW_SOL]; + auto& vector = solver->LinSysRes; + REQUIRE(solver->GetnVar() == nVar); + REQUIRE(testCase.geometry->GetnPoint() >= static_cast(N)); + + simd::Array iPoint; + Vector block; + for (size_t k = 0; k < N; ++k) { + iPoint[k] = k; + for (auto i = 0u; i < nVar; ++i) block(i)[k] = 1.0 + 0.1 * i + k; + } + + vector.SetValZero(); + vector.AddBlock(iPoint, block); + vector.AddBlock(iPoint, block); + + for (size_t k = 0; k < N; ++k) { + const auto* row = vector.GetBlock(iPoint[k]); + for (auto i = 0u; i < nVar; ++i) + CHECK(SU2_TYPE::GetValue(row[i]) == Approx(2.0 * SU2_TYPE::GetValue(block(i)[k])).margin(1e-6)); + } +} diff --git a/UnitTests/meson.build b/UnitTests/meson.build index 00ad6a764cd..69623b6b786 100644 --- a/UnitTests/meson.build +++ b/UnitTests/meson.build @@ -19,7 +19,8 @@ su2_cfd_tests = files(['Common/geometry/primal_grid/CPrimalGrid_tests.cpp', 'SU2_CFD/windowing.cpp', 'Common/toolboxes/random_toolbox_tests.cpp', 'Common/linear_algebra/quantization_tests.cpp', - 'Common/linear_algebra/edge_residual_blocks_tests.cpp']) + 'Common/linear_algebra/edge_residual_blocks_tests.cpp', + 'Common/linear_algebra/edge_residual_blocks_simd_tests.cpp']) # Reverse-mode (algorithmic differentiation) tests: su2_cfd_tests_ad = files(['Common/simple_ad_test.cpp', From 696423db21ae70f7a3814a19f0fdf26ea12d80f1 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Fri, 28 Aug 2026 21:21:18 -0700 Subject: [PATCH 06/20] Revert "SIMD binding for the scalar interior edge loop (Section 10 prototype)" SA transports one variable, too little arithmetic per edge to amortize the gather/scatter cost the vectorized binding adds; not worth carrying forward. Reverting 1d2ef5bace before continuing with the sequential migration (step 8). Co-Authored-By: Claude Sonnet 5 --- Common/include/linear_algebra/CSysMatrix.hpp | 114 ----------- Common/include/linear_algebra/CSysVector.hpp | 50 ++--- SU2_CFD/include/solvers/CScalarSolver.inl | 55 +----- SU2_CFD/src/solvers/CTurbSASolver.cpp | 7 +- .../edge_residual_blocks_simd_tests.cpp | 186 ------------------ UnitTests/meson.build | 3 +- 6 files changed, 24 insertions(+), 391 deletions(-) delete mode 100644 UnitTests/Common/linear_algebra/edge_residual_blocks_simd_tests.cpp diff --git a/Common/include/linear_algebra/CSysMatrix.hpp b/Common/include/linear_algebra/CSysMatrix.hpp index 53bc704946f..84f15b58b8a 100644 --- a/Common/include/linear_algebra/CSysMatrix.hpp +++ b/Common/include/linear_algebra/CSysMatrix.hpp @@ -1150,120 +1150,6 @@ class CSysMatrix { } } - /*! - * \brief SIMD version, sets the four blocks of an edge for multiple edges at once. - * \note Nothing is updated if the mask is 0. As with the scalar overload, the diagonal - * blocks are accumulated and the off-diagonal blocks are set. - */ - template - FORCEINLINE void SetBlocks(simd::Array iEdge, simd::Array iPoint, simd::Array jPoint, - const MatTypeSIMD& jac_ii, const MatTypeSIMD& jac_ij, const MatTypeSIMD& jac_ji, - const MatTypeSIMD& jac_jj, simd::Array mask = 1) { - static_assert(MatTypeSIMD::StaticSize, "This method requires static size blocks."); - static_assert(MatTypeSIMD::IsRowMajor, "Block storage is not compatible with matrix."); - /*--- Read through (iVar, jVar) rather than a flat MatTypeSIMD::StaticSize/StaticNRows walk: - * EdgeResidual's Jacobian blocks are floored to a minimum static size of 2 (see its own - * comment), so a static nVar 1 model still hands in a 2x2 block here and blkSz must come - * from the matrix's own (runtime) nVar/nEqn, not from the possibly padded block type. ---*/ - const auto blkSz = nVar * nEqn; - assert(blkSz <= MatTypeSIMD::StaticSize); - - /*--- "Transpose" the four blocks, scale, and possibly convert types, - * giving the compiler the chance to vectorize all of these. ---*/ - ScalarType blk_ii[N][MAXNVAR * MAXNVAR], blk_ij[N][MAXNVAR * MAXNVAR]; - ScalarType blk_ji[N][MAXNVAR * MAXNVAR], blk_jj[N][MAXNVAR * MAXNVAR]; - - unsigned long offset = 0; - for (auto iVar = 0ul; iVar < nVar; ++iVar) { - for (auto jVar = 0ul; jVar < nEqn; ++jVar, ++offset) { - SU2_OMP_SIMD_IF_NOT_AD - for (size_t k = 0; k < N; ++k) { - blk_ii[k][offset] = PassiveAssign(mask[k] * jac_ii(iVar, jVar)[k]); - blk_ij[k][offset] = PassiveAssign(mask[k] * jac_ij(iVar, jVar)[k]); - blk_ji[k][offset] = PassiveAssign(mask[k] * jac_ji(iVar, jVar)[k]); - blk_jj[k][offset] = PassiveAssign(mask[k] * jac_jj(iVar, jVar)[k]); - } - } - } - - /*--- Update one by one skipping if mask is 0. ---*/ - for (size_t k = 0; k < N; ++k) { - if (mask[k] == 0) continue; - - auto bii = &mat.d[iPoint[k] * blkSz]; - auto bjj = &mat.d[jPoint[k] * blkSz]; - - if (quantized_mode) { - SU2_OMP_SIMD - for (size_t i = 0; i < blkSz; ++i) { - bii[i] += blk_ii[k][i]; - bjj[i] += blk_jj[k][i]; - } - EncodeQuantBlock([&, k](unsigned long r, unsigned long c) { return blk_ij[k][r * nVar + c]; }, - &q_scale.u[iEdge[k] * nVar], &q_blocks.u[iEdge[k] * blkSz], nVar); - const auto k_l = edge_ptr_l[iEdge[k]]; - EncodeQuantBlock([&, k](unsigned long r, unsigned long c) { return blk_ji[k][r * nVar + c]; }, - &q_scale.l[k_l * nVar], &q_blocks.l[k_l * blkSz], nVar); - } else { - auto bij = &mat.u[iEdge[k] * blkSz]; - auto bji = &mat.l[edge_ptr_l[iEdge[k]] * blkSz]; - SU2_OMP_SIMD - for (size_t i = 0; i < blkSz; ++i) { - bii[i] += blk_ii[k][i]; - bjj[i] += blk_jj[k][i]; - bij[i] = blk_ij[k][i]; - bji[i] = blk_ji[k][i]; - } - } - } - } - - /*! - * \brief SIMD version, sets the off-diagonal blocks of an edge for multiple edges at once. - */ - template - FORCEINLINE void SetOffDiagBlocks(simd::Array iEdge, const MatTypeSIMD& jac_ij, const MatTypeSIMD& jac_ji, - simd::Array mask = 1) { - static_assert(MatTypeSIMD::StaticSize, "This method requires static size blocks."); - static_assert(MatTypeSIMD::IsRowMajor, "Block storage is not compatible with matrix."); - /*--- See the note in the four-block SetBlocks above about reading via (iVar, jVar). ---*/ - const auto blkSz = nVar * nEqn; - assert(blkSz <= MatTypeSIMD::StaticSize); - - ScalarType blk_ij[N][MAXNVAR * MAXNVAR], blk_ji[N][MAXNVAR * MAXNVAR]; - - unsigned long offset = 0; - for (auto iVar = 0ul; iVar < nVar; ++iVar) { - for (auto jVar = 0ul; jVar < nEqn; ++jVar, ++offset) { - SU2_OMP_SIMD_IF_NOT_AD - for (size_t k = 0; k < N; ++k) { - blk_ij[k][offset] = PassiveAssign(mask[k] * jac_ij(iVar, jVar)[k]); - blk_ji[k][offset] = PassiveAssign(mask[k] * jac_ji(iVar, jVar)[k]); - } - } - } - - for (size_t k = 0; k < N; ++k) { - if (mask[k] == 0) continue; - - if (quantized_mode) { - EncodeQuantBlock([&, k](unsigned long r, unsigned long c) { return blk_ij[k][r * nVar + c]; }, - &q_scale.u[iEdge[k] * nVar], &q_blocks.u[iEdge[k] * blkSz], nVar); - const auto k_l = edge_ptr_l[iEdge[k]]; - EncodeQuantBlock([&, k](unsigned long r, unsigned long c) { return blk_ji[k][r * nVar + c]; }, - &q_scale.l[k_l * nVar], &q_blocks.l[k_l * blkSz], nVar); - } else { - ScalarType* bij = &mat.u[iEdge[k] * blkSz]; - ScalarType* bji = &mat.l[edge_ptr_l[iEdge[k]] * blkSz]; - SU2_OMP_SIMD - for (size_t i = 0; i < blkSz; ++i) { - bij[i] = blk_ij[k][i]; - bji[i] = blk_ji[k][i]; - } - } - } - } - /*! * \brief Sets the specified block to the (i, i) subblock of the sparse matrix. * Scales the input block by factor alpha. If the Overwrite parameter is diff --git a/Common/include/linear_algebra/CSysVector.hpp b/Common/include/linear_algebra/CSysVector.hpp index 9d05d9941c8..28c75004958 100644 --- a/Common/include/linear_algebra/CSysVector.hpp +++ b/Common/include/linear_algebra/CSysVector.hpp @@ -225,21 +225,13 @@ class CSysVector : public VecExpr::CVecExpr, ScalarType> void Initialize(unsigned long numBlk, unsigned long numBlkDomain, unsigned long numVar, const ScalarType* val, bool valIsArray, bool errorIfParallel = true); - enum : size_t { MAXNVAR = 20 }; /*!< \brief Upper bound for the block buffers below, matches CSysMatrix's. */ - /*! * \brief Helper to unpack (transpose) a SIMD input block. - * \note "nVar" is a runtime argument, not deduced from VecTypeSIMD: an EdgeResidual's flux_i/ - * flux_j are floored to a minimum static size of 2 (see its own comment), so a static - * nVar 1 model still hands in a size-2 Vector here, and this vector's own (runtime) nVar - * is the one that must bound the transpose and the later read/write. - */ - template - FORCEINLINE static void UnpackBlock(const VecTypeSIMD& in, simd::Array mask, unsigned long nVar, - ScalarType out[][MAXNVAR]) { + */ + template + FORCEINLINE static void UnpackBlock(const VecTypeSIMD& in, simd::Array mask, ScalarType out[][nVar]) { static_assert(VecTypeSIMD::StaticSize, "This method requires static size vectors."); - assert(nVar <= MAXNVAR && nVar <= VecTypeSIMD::StaticSize); - for (auto i = 0ul; i < nVar; ++i) { + for (size_t i = 0; i < nVar; ++i) { SU2_OMP_SIMD_IF_NOT_AD for (size_t k = 0; k < N; ++k) out[k][i] = mask[k] * in[i][k]; } @@ -668,32 +660,16 @@ class CSysVector : public VecExpr::CVecExpr, ScalarType> template FORCEINLINE void SetBlock(simd::Array iPoint, const VecTypeSIMD& vector, simd::Array mask = 1) { /*--- "Transpose" and scale input vector. ---*/ - ScalarType vec[N][MAXNVAR]; - UnpackBlock(vector, mask, nVar, vec); - - /*--- Update one by one skipping if mask is 0. ---*/ - for (size_t k = 0; k < N; ++k) { - if (mask[k] == 0) continue; - SU2_OMP_SIMD - for (auto i = 0ul; i < nVar; ++i) vec_val[iPoint[k] * nVar + i] = vec[k][i]; - } - } - - /*! - * \brief Vectorized version of AddBlock, adds to multiple iPoint's. - * \note Unlike UpdateBlocks, "vector" is independent at every iPoint (e.g. flux_i and flux_j - * of an EdgeResidual), so this takes one iPoint and one vector, called once per side. - */ - template - FORCEINLINE void AddBlock(simd::Array iPoint, const VecTypeSIMD& vector, simd::Array mask = 1) { - ScalarType vec[N][MAXNVAR]; - UnpackBlock(vector, mask, nVar, vec); + constexpr size_t nVar = VecTypeSIMD::StaticSize; + assert(nVar == this->nVar); + ScalarType vec[N][nVar]; + UnpackBlock(vector, mask, vec); /*--- Update one by one skipping if mask is 0. ---*/ for (size_t k = 0; k < N; ++k) { if (mask[k] == 0) continue; SU2_OMP_SIMD - for (auto i = 0ul; i < nVar; ++i) vec_val[iPoint[k] * nVar + i] += vec[k][i]; + for (size_t i = 0; i < nVar; ++i) vec_val[iPoint[k] * nVar + i] = vec[k][i]; } } @@ -705,14 +681,16 @@ class CSysVector : public VecExpr::CVecExpr, ScalarType> FORCEINLINE void UpdateBlocks(simd::Array iPoint, simd::Array jPoint, const VecTypeSIMD& vector, simd::Array mask = 1) { /*--- "Transpose" and scale input vector. ---*/ - ScalarType vec[N][MAXNVAR]; - UnpackBlock(vector, mask, nVar, vec); + constexpr size_t nVar = VecTypeSIMD::StaticSize; + assert(nVar == this->nVar); + ScalarType vec[N][nVar]; + UnpackBlock(vector, mask, vec); /*--- Update one by one skipping if mask is 0. ---*/ for (size_t k = 0; k < N; ++k) { if (mask[k] == 0) continue; SU2_OMP_SIMD - for (auto i = 0ul; i < nVar; ++i) { + for (size_t i = 0; i < nVar; ++i) { vec_val[iPoint[k] * nVar + i] += vec[k][i]; vec_val[jPoint[k] * nVar + i] -= vec[k][i]; } diff --git a/SU2_CFD/include/solvers/CScalarSolver.inl b/SU2_CFD/include/solvers/CScalarSolver.inl index 56e27ddc017..105d253e3c3 100644 --- a/SU2_CFD/include/solvers/CScalarSolver.inl +++ b/SU2_CFD/include/solvers/CScalarSolver.inl @@ -378,19 +378,7 @@ void CScalarSolver::EdgeFluxResidual(const CGeometry* geometry, CS SU2_ZONE_SCOPED using Double = typename Scheme::Double; - using Int = typename CLaneTraits::Int; constexpr int nDim = Scheme::nDim; - constexpr size_t Width = CLaneTraits::Size; - - /*--- Scheme::Double picks the binding: su2double (Width 1) degenerates the loop below to the - * plain scalar form, a simd::Array Double drives it several edges at a time. One loop body - * serves both, matching the flow solver's own masked edge loop. ---*/ - if constexpr (Width > 1) { - if (!ReducerStrategy && (omp_get_max_threads() > 1) && (config->GetEdgeColoringGroupSize() % Width != 0)) { - SU2_MPI::Error("When using vectorization, the EDGE_COLORING_GROUP_SIZE must be divisible " - "by the SIMD length (2, 4, or 8).", CURRENT_FUNCTION); - } - } const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; @@ -415,52 +403,25 @@ void CScalarSolver::EdgeFluxResidual(const CGeometry* geometry, CS for (auto color : EdgeColoring) { SU2_OMP_FOR_DYN(nextMultiple(OMP_MIN_SIZE, color.groupSize)) - for (auto k = 0ul; k < color.size; k += Width) { - Int iEdge; - Double mask; - if constexpr (Width == 1) { - iEdge = color.indices[k]; - mask = 1.0; - } else { - for (auto j = 0ul; j < Width; ++j) { - const bool in = (k + j < color.size); - mask[j] = in; - iEdge[j] = color.indices[k + j * in]; - } - } - + for (auto k = 0ul; k < color.size; ++k) { + const unsigned long iEdge = color.indices[k]; const auto iPoint = geometry->edges->GetNode(iEdge, 0); const auto jPoint = geometry->edges->GetNode(iEdge, 1); const auto normal = gatherVariables(iEdge, geometry->edges->GetNormal()); const Double massFlux = opt.boundedScalar ? gatherVariables(iEdge, *edgeMassFluxes) : Double(0.0); - flux.ComputeFlux(opt, iEdge, iPoint, side, jPoint, side, normal, massFlux, implicit, updateType, mask, target, + flux.ComputeFlux(opt, iEdge, iPoint, side, jPoint, side, normal, massFlux, implicit, updateType, 1.0, target, EdgeFluxesDiff, Jacobian); /*--- Bounded scalar divergence correction, per edge; the ReducerStrategy equivalent runs * in a per-point pass below, where the diagonal is not written from the edge loop. ---*/ if (opt.boundedScalar && !ReducerStrategy) { - if constexpr (Width == 1) { - LinSysRes.AddBlock(iPoint, nodes->GetSolution(iPoint), -massFlux); - LinSysRes.AddBlock(jPoint, nodes->GetSolution(jPoint), massFlux); - if (implicit) { - Jacobian.AddVal2Diag(iPoint, -massFlux); - Jacobian.AddVal2Diag(jPoint, massFlux); - } - } else { - for (auto j = 0ul; j < Width; ++j) { - if (mask[j] == 0) continue; - const auto i = iPoint[j]; - const auto jp = jPoint[j]; - const su2double mf = massFlux[j]; - LinSysRes.AddBlock(i, nodes->GetSolution(i), -mf); - LinSysRes.AddBlock(jp, nodes->GetSolution(jp), mf); - if (implicit) { - Jacobian.AddVal2Diag(i, -mf); - Jacobian.AddVal2Diag(jp, mf); - } - } + LinSysRes.AddBlock(iPoint, nodes->GetSolution(iPoint), -massFlux); + LinSysRes.AddBlock(jPoint, nodes->GetSolution(jPoint), massFlux); + if (implicit) { + Jacobian.AddVal2Diag(iPoint, -massFlux); + Jacobian.AddVal2Diag(jPoint, massFlux); } } } diff --git a/SU2_CFD/src/solvers/CTurbSASolver.cpp b/SU2_CFD/src/solvers/CTurbSASolver.cpp index 11eae2549c3..1052b3b1ec5 100644 --- a/SU2_CFD/src/solvers/CTurbSASolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSASolver.cpp @@ -394,12 +394,7 @@ void CTurbSASolver::RunSA(CGeometry* geometry, CSolver** solver_container, CConf template void CTurbSASolver::RunSA(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt) { - /*--- simd::Array is its preferred SIMD width in primal mode, and width 1 (i.e. the - * scalar loop, unchanged) under reverse AD (see preferredLen in vectorization.hpp), - * so this one binding covers both without a branch here. Boundaries stay on the su2double - * binding, see RunSA_Boundary: their loops are over vertices, not the hot edge loop. ---*/ - EdgeFluxResidual, Indices, nDim, nVarSA>>(geometry, solver_container, config, - opt); + EdgeFluxResidual>(geometry, solver_container, config, opt); } void CTurbSASolver::BC_Far_Field(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, diff --git a/UnitTests/Common/linear_algebra/edge_residual_blocks_simd_tests.cpp b/UnitTests/Common/linear_algebra/edge_residual_blocks_simd_tests.cpp deleted file mode 100644 index 187d3348aa6..00000000000 --- a/UnitTests/Common/linear_algebra/edge_residual_blocks_simd_tests.cpp +++ /dev/null @@ -1,186 +0,0 @@ -/*! - * \file edge_residual_blocks_simd_tests.cpp - * \brief Unit tests for the SIMD overloads of CSysMatrix::SetBlocks (four independent blocks), - * SetOffDiagBlocks, and CSysVector::AddBlock, added for the vectorized scalar edge loop. - * \author P. Gomes - * \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 - -#include "catch.hpp" -#include "../../UnitQuadTestCase.hpp" -#include "../../../SU2_CFD/include/numerics/util.hpp" - -/*--- UnitQuadTestCase's fixed config: 3D compressible Euler, nVar = nDim + 2. ---*/ -constexpr size_t nVar = 5; -using Double = simd::Array; -constexpr size_t N = Double::Size; - -static void FillBlock(Matrix& block, size_t lane, su2double base) { - for (auto i = 0u; i < nVar; ++i) - for (auto j = 0u; j < nVar; ++j) block(i, j)[lane] = base + 0.1 * i + 0.01 * j + lane; -} - -static void CheckBlock(const CSysMatrix& matrix, unsigned long i, unsigned long j, - const Matrix& block, size_t lane, double tol) { - auto view = matrix.GetBlockView(i, j); - for (auto iVar = 0u; iVar < nVar; ++iVar) - for (auto jVar = 0u; jVar < nVar; ++jVar) - CHECK(SU2_TYPE::GetValue(view(iVar, jVar)) == Approx(SU2_TYPE::GetValue(block(iVar, jVar)[lane])).margin(tol)); -} - -TEST_CASE("SIMD SetBlocks, SetOffDiagBlocks and AddBlock match their scalar counterparts", "[LinearAlgebra]") { - cout.rdbuf(nullptr); - - UnitQuadTestCase testCase; - testCase.InitConfig(); - testCase.InitGeometry(); - testCase.InitSolver(); - - cout.rdbuf(testCase.orig_buf); - - auto* solver = testCase.solver[FLOW_SOL]; - auto& matrix = solver->Jacobian; - REQUIRE(solver->GetnVar() == nVar); - - /*--- N edges with pairwise disjoint endpoints, so a lane's diagonal block never accumulates - * a second edge's contribution and CheckBlock can compare it to a single lane's own fill. - * (The production code relies on edges within a SIMD group being contiguous for the SIMD - * GetNode gather, but this test builds iPoint/jPoint with the scalar GetNode overload, one - * edge at a time, so that constraint does not apply here.) ---*/ - simd::Array iEdge, iPoint, jPoint; - std::set used; - size_t found = 0; - for (unsigned long e = 0; e < testCase.geometry->GetnEdge() && found < N; ++e) { - const auto p0 = testCase.geometry->edges->GetNode(e, 0); - const auto p1 = testCase.geometry->edges->GetNode(e, 1); - if (used.count(p0) || used.count(p1)) continue; - iEdge[found] = e; - iPoint[found] = p0; - jPoint[found] = p1; - used.insert(p0); - used.insert(p1); - ++found; - } - REQUIRE(found == N); - - Matrix jac_ii, jac_ij, jac_ji, jac_jj; - for (size_t k = 0; k < N; ++k) { - FillBlock(jac_ii, k, 1.0); - FillBlock(jac_ij, k, 2.0); - FillBlock(jac_ji, k, 3.0); - FillBlock(jac_jj, k, 4.0); - } - - SECTION("SetBlocks: diagonal accumulates, off-diagonal is set, one lane at a time via the scalar overload") { - matrix.SetValZero(); - matrix.SetBlocks(iEdge, iPoint, jPoint, jac_ii, jac_ij, jac_ji, jac_jj); - - for (size_t k = 0; k < N; ++k) { - CheckBlock(matrix, iPoint[k], iPoint[k], jac_ii, k, 1e-6); - CheckBlock(matrix, jPoint[k], jPoint[k], jac_jj, k, 1e-6); - CheckBlock(matrix, iPoint[k], jPoint[k], jac_ij, k, 1e-6); - CheckBlock(matrix, jPoint[k], iPoint[k], jac_ji, k, 1e-6); - } - } - - SECTION("SetBlocks: a lane whose mask is 0 is left untouched") { - matrix.SetValZero(); - matrix.SetBlocks(iEdge, iPoint, jPoint, jac_ii, jac_ij, jac_ji, jac_jj); - - Matrix jac_ii2 = jac_ii, jac_ij2 = jac_ij, jac_ji2 = jac_ji, jac_jj2 = jac_jj; - for (size_t k = 0; k < N; ++k) FillBlock(jac_ii2, k, 100.0); - - simd::Array mask = 1; - mask[0] = 0; - matrix.SetBlocks(iEdge, iPoint, jPoint, jac_ii2, jac_ij2, jac_ji2, jac_jj2, mask); - - /*--- Lane 0: SetBlocks was skipped, diagonal is the single accumulation from the first - * call above, off-diagonal is still what that first call set. ---*/ - CheckBlock(matrix, iPoint[0], iPoint[0], jac_ii, 0, 1e-6); - CheckBlock(matrix, iPoint[0], jPoint[0], jac_ij, 0, 1e-6); - - /*--- Lane 1 (points disjoint from every other lane's, by construction): diagonal - * accumulated twice, off-diagonal overwritten by the second call. ---*/ - if (N > 1) { - Matrix jac_ii_2x = jac_ii; - for (auto i = 0u; i < nVar; ++i) - for (auto j = 0u; j < nVar; ++j) jac_ii_2x(i, j)[1] = jac_ii(i, j)[1] + jac_ii2(i, j)[1]; - CheckBlock(matrix, iPoint[1], iPoint[1], jac_ii_2x, 1, 1e-6); - CheckBlock(matrix, iPoint[1], jPoint[1], jac_ij2, 1, 1e-6); - } - } - - SECTION("SetOffDiagBlocks leaves the diagonal untouched") { - matrix.SetValZero(); - matrix.SetBlocks(iEdge, iPoint, jPoint, jac_ii, jac_ij, jac_ji, jac_jj); - - Matrix jac_ij_new, jac_ji_new; - for (size_t k = 0; k < N; ++k) { - FillBlock(jac_ij_new, k, 5.0); - FillBlock(jac_ji_new, k, 6.0); - } - matrix.SetOffDiagBlocks(iEdge, jac_ij_new, jac_ji_new); - - for (size_t k = 0; k < N; ++k) { - CheckBlock(matrix, iPoint[k], iPoint[k], jac_ii, k, 1e-6); - CheckBlock(matrix, jPoint[k], jPoint[k], jac_jj, k, 1e-6); - CheckBlock(matrix, iPoint[k], jPoint[k], jac_ij_new, k, 1e-6); - CheckBlock(matrix, jPoint[k], iPoint[k], jac_ji_new, k, 1e-6); - } - } -} - -TEST_CASE("SIMD CSysVector::AddBlock writes independent values to independent points", "[LinearAlgebra]") { - cout.rdbuf(nullptr); - - UnitQuadTestCase testCase; - testCase.InitConfig(); - testCase.InitGeometry(); - testCase.InitSolver(); - - cout.rdbuf(testCase.orig_buf); - - auto* solver = testCase.solver[FLOW_SOL]; - auto& vector = solver->LinSysRes; - REQUIRE(solver->GetnVar() == nVar); - REQUIRE(testCase.geometry->GetnPoint() >= static_cast(N)); - - simd::Array iPoint; - Vector block; - for (size_t k = 0; k < N; ++k) { - iPoint[k] = k; - for (auto i = 0u; i < nVar; ++i) block(i)[k] = 1.0 + 0.1 * i + k; - } - - vector.SetValZero(); - vector.AddBlock(iPoint, block); - vector.AddBlock(iPoint, block); - - for (size_t k = 0; k < N; ++k) { - const auto* row = vector.GetBlock(iPoint[k]); - for (auto i = 0u; i < nVar; ++i) - CHECK(SU2_TYPE::GetValue(row[i]) == Approx(2.0 * SU2_TYPE::GetValue(block(i)[k])).margin(1e-6)); - } -} diff --git a/UnitTests/meson.build b/UnitTests/meson.build index 69623b6b786..00ad6a764cd 100644 --- a/UnitTests/meson.build +++ b/UnitTests/meson.build @@ -19,8 +19,7 @@ su2_cfd_tests = files(['Common/geometry/primal_grid/CPrimalGrid_tests.cpp', 'SU2_CFD/windowing.cpp', 'Common/toolboxes/random_toolbox_tests.cpp', 'Common/linear_algebra/quantization_tests.cpp', - 'Common/linear_algebra/edge_residual_blocks_tests.cpp', - 'Common/linear_algebra/edge_residual_blocks_simd_tests.cpp']) + 'Common/linear_algebra/edge_residual_blocks_tests.cpp']) # Reverse-mode (algorithmic differentiation) tests: su2_cfd_tests_ad = files(['Common/simple_ad_test.cpp', From 539830ad222dd16e3b7fa0f014f935432f0443d8 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Fri, 28 Aug 2026 21:39:46 -0700 Subject: [PATCH 07/20] Remaining SA boundary sites; delete the old SA numerics classes and the NEMO turbulence path Migrates BC_Outlet, BC_Engine_Inflow, BC_Engine_Exhaust, BC_ActDisk, and BC_Fluid_Interface to the CScalarFlux_SA edge kernel via ghost fill + either BoundaryFluxResidual (through RunSA_Boundary, same pattern as BC_Far_Field and BC_Inlet) or, for BC_Fluid_Interface, a bespoke RunSA_FluidInterface: its per-donor weighted convective average and single post-loop diffusive term (from the ghost state the last donor left behind) don't fit the fill-then-flux shape the other boundaries share, so it drives the kernel directly. Also migrates the two turbomachinery sites that carry a real diffusive term, BC_Inlet_MixingPlane and BC_Inlet_Turbo: their fill pass additionally writes ghostCoord (the coordinate reflected through the interior neighbor, as the old visc_numerics->SetCoord did) and mirrors the interior gradient into the ghost row, and their ScalarFluxOptions carry correctGradient true, matching what CDriver::InstantiateTurbulentNumerics used to pass for the visc_bound_term instantiation of CAvgGrad_TurbSA (correct_grad was always true there, both for interior and boundary use). With every SA boundary now on the new kernel, CUpwSca_TurbSA, CAvgGrad_TurbSA and CAvgGrad_TurbSA_Neg are deleted, along with their branch of CDriver::InstantiateTurbulentNumerics (SST is untouched, still on the old CNumerics path pending its own migration in a later step) and the NEMO explicit instantiation. NEMO with a turbulence model is now rejected at configuration (SetPostprocessing), matching the branch CDriver:: InstantiateTurbulentNumerics's call site no longer has. Verified against the pre-migration binary: bit-identical on turb_ONERAM6 (interior + far field) and on the compressible and incompressible flatplate wallfunctions cases (BC_Outlet, MARKER_OUTLET) after every change in this commit, including after the old numerics classes were deleted. BC_Engine_*, BC_ActDisk and BC_Fluid_Interface/the two turbomachinery sites could not be verified the same way: no test case exercising them ships with a mesh in this checkout (the turbomachinery ones need a CI-downloaded mesh). They follow the same ghost-fill pattern already verified for BC_Outlet and BC_Far_Field/ BC_Inlet, and the diffusive-term additions were checked by hand against what CAvgGrad_TurbSA's boundary instantiation did, but this is a real verification gap flagged for follow-up, not a claim of bit-identical output. Co-Authored-By: Claude Sonnet 5 --- Common/src/CConfig.cpp | 4 + .../numerics/turbulent/turb_convection.hpp | 56 -- .../numerics/turbulent/turb_diffusion.hpp | 183 ----- SU2_CFD/include/solvers/CTurbSASolver.hpp | 29 + SU2_CFD/src/drivers/CDriver.cpp | 38 +- SU2_CFD/src/solvers/CTurbSASolver.cpp | 745 ++++++++---------- 6 files changed, 360 insertions(+), 695 deletions(-) diff --git a/Common/src/CConfig.cpp b/Common/src/CConfig.cpp index f272a84ab6f..9dc77359e05 100644 --- a/Common/src/CConfig.cpp +++ b/Common/src/CConfig.cpp @@ -4338,6 +4338,10 @@ void CConfig::SetPostprocessing(SU2_COMPONENT val_software, unsigned short val_i } /* --- Check for NEMO compatibility issues ---*/ + if (nemo && Kind_Turb_Model != TURB_MODEL::NONE) { + SU2_MPI::Error("A turbulence model is not yet available for the NEMO solver.", CURRENT_FUNCTION); + } + if (Kind_FluidModel == SU2_NONEQ && (Kind_TransCoeffModel != TRANSCOEFFMODEL::WILKE && Kind_TransCoeffModel != TRANSCOEFFMODEL::SUTHERLAND && Kind_TransCoeffModel != TRANSCOEFFMODEL::GUPTAYOS) ) { SU2_MPI::Error("Transport model not available for NEMO solver using SU2TCLIB. Please use the WILKE, SUTHERLAND or GUPTAYOS transport model instead.", CURRENT_FUNCTION); } diff --git a/SU2_CFD/include/numerics/turbulent/turb_convection.hpp b/SU2_CFD/include/numerics/turbulent/turb_convection.hpp index 6c1641db87d..f17dd4dac0c 100644 --- a/SU2_CFD/include/numerics/turbulent/turb_convection.hpp +++ b/SU2_CFD/include/numerics/turbulent/turb_convection.hpp @@ -30,62 +30,6 @@ #include "../scalar/scalar_convection.hpp" -/*! - * \class CUpwSca_TurbSA - * \brief Class for doing a scalar upwind solver for the Spalar-Allmaras turbulence model equations. - * \ingroup ConvDiscr - * \author A. Bueno. - */ -template -class CUpwSca_TurbSA final : public CUpwScalar { -private: - using Base = CUpwScalar; - using Base::a0; - using Base::a1; - using Base::Flux; - using Base::Jacobian_i; - using Base::Jacobian_j; - using Base::ScalarVar_i; - using Base::ScalarVar_j; - using Base::bounded_scalar; - using Base::V_i; - using Base::V_j; - using Base::idx; - using Base::nVar; - - /*! - * \brief Adds any extra variables to AD. - */ - void ExtraADPreaccIn() override {} - - /*! - * \brief SA specific steps in the ComputeResidual method - * \param[in] config - Definition of the particular problem. - */ - void FinishResidualCalc(const CConfig* config) override { - if (config->GetSBSParam().StochasticBackscatter && config->GetSBSParam().SBS_Ctau > 0.0) { - for (unsigned short iVar = 1; iVar < nVar; iVar++) { - Flux[iVar] = (a0 + a1) * 0.5 * (ScalarVar_i[iVar] + ScalarVar_j[iVar]); - Jacobian_i[iVar][iVar] = 0.5 * (a0+a1); - Jacobian_j[iVar][iVar] = 0.5 * (a0+a1); - } - } - Flux[0] = a0*ScalarVar_i[0] + a1*ScalarVar_j[0]; - Jacobian_i[0][0] = a0; - Jacobian_j[0][0] = a1; - } - -public: - /*! - * \brief Constructor of the class. - * \param[in] val_nDim - Number of dimensions of the problem. - * \param[in] val_nVar - Number of variables of the problem. - * \param[in] config - Definition of the particular problem. - */ - CUpwSca_TurbSA(unsigned short val_nDim, unsigned short val_nVar, const CConfig* config) - : CUpwScalar(val_nDim, val_nVar, config) { bounded_scalar = config->GetBounded_Turb(); } -}; - /*! * \class CUpwSca_TurbSST * \brief Class for doing a scalar upwind solver for the Menter SST turbulence model equations. diff --git a/SU2_CFD/include/numerics/turbulent/turb_diffusion.hpp b/SU2_CFD/include/numerics/turbulent/turb_diffusion.hpp index a4b2bbe264b..76225bee26f 100644 --- a/SU2_CFD/include/numerics/turbulent/turb_diffusion.hpp +++ b/SU2_CFD/include/numerics/turbulent/turb_diffusion.hpp @@ -29,189 +29,6 @@ #include "../scalar/scalar_diffusion.hpp" -/*! - * \class CAvgGrad_TurbSA - * \brief Class for computing viscous term using average of gradients (Spalart-Allmaras Turbulence model). - * \ingroup ViscDiscr - * \author A. Bueno. - */ -template -class CAvgGrad_TurbSA final : public CAvgGrad_Scalar { -private: - using Base = CAvgGrad_Scalar; - using Base::Laminar_Viscosity_i; - using Base::Laminar_Viscosity_j; - using Base::Density_i; - using Base::Density_j; - using Base::ScalarVar_i; - using Base::ScalarVar_j; - using Base::Proj_Mean_GradScalarVar; - using Base::proj_vector_ij; - using Base::Flux; - using Base::Jacobian_i; - using Base::Jacobian_j; - - const su2double sigma = 2.0/3.0; - const su2double cb2 = 0.622; - - const bool use_accurate_jacobians; - - /*! - * \brief Adds any extra variables to AD - */ - void ExtraADPreaccIn() override {} - - /*! - * \brief SA specific steps in the ComputeResidual method - * \param[in] config - Definition of the particular problem. - */ - void FinishResidualCalc(const CConfig* config) override { - const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; - - /*--- Compute mean effective viscosity ---*/ - - /*--- First Term. Normal diffusion, and conservative part of the quadratic diffusion. - * ||grad nu_t||^2 = div(nu_t grad nu_t) - nu_t div grad nu_t ---*/ - const su2double nu_i = Laminar_Viscosity_i/Density_i; - const su2double nu_j = Laminar_Viscosity_j/Density_j; - const su2double nu_e = 0.5 * (nu_i + nu_j + (1 + cb2) * (ScalarVar_i[0] + ScalarVar_j[0])); - const su2double term_1 = nu_e; - - /* Second Term (quadratic diffusion, non conservative). */ - const su2double nu_tilde_i = ScalarVar_i[0]; - const su2double term_2 = cb2 * nu_tilde_i; - - const su2double diffusion_coefficient = term_1 - term_2; - Flux[0] = diffusion_coefficient * Proj_Mean_GradScalarVar[0] / sigma; - - if (implicit) { - /*--- For Jacobians -> Use of TSL approx. to compute derivatives of the gradients ---*/ - Jacobian_i[0][0] = -diffusion_coefficient * proj_vector_ij / sigma; - Jacobian_j[0][0] = diffusion_coefficient * proj_vector_ij / sigma; - - if (use_accurate_jacobians) { - /*--- The diffusion coefficient is also a function of nu_t. ---*/ - const su2double dTerm1_dnut_i = (1 + cb2) * 0.5; - const su2double dTerm1_dnut_j = (1 + cb2) * 0.5; - - const su2double dTerm2_dnut_i = cb2; - const su2double dTerm2_dnut_j = 0.0; - - const su2double dDC_dnut_i = dTerm1_dnut_i - dTerm2_dnut_i; - const su2double dDC_dnut_j = dTerm1_dnut_j - dTerm2_dnut_j; - - Jacobian_i[0][0] += dDC_dnut_i * Proj_Mean_GradScalarVar[0] / sigma; - Jacobian_j[0][0] += dDC_dnut_j * Proj_Mean_GradScalarVar[0] / sigma; - } - } - } - -public: - /*! - * \brief Constructor of the class. - * \param[in] val_nDim - Number of dimensions of the problem. - * \param[in] val_nVar - Number of variables of the problem. - * \param[in] correct_grad - Whether to correct gradient for skewness. - * \param[in] config - Definition of the particular problem. - */ - CAvgGrad_TurbSA(unsigned short val_nDim, unsigned short val_nVar, - bool correct_grad, const CConfig* config) - : CAvgGrad_Scalar(val_nDim, val_nVar, correct_grad, config), - use_accurate_jacobians(config->GetUse_Accurate_Turb_Jacobians()) {} -}; - -/*! - * \class CAvgGrad_TurbSA_Neg - * \brief Class for computing viscous term using average of gradients (Spalart-Allmaras Turbulence model). - * \ingroup ViscDiscr - * \author F. Palacios - */ -template -class CAvgGrad_TurbSA_Neg final : public CAvgGrad_Scalar { -private: - using Base = CAvgGrad_Scalar; - using Base::Laminar_Viscosity_i; - using Base::Laminar_Viscosity_j; - using Base::Density_i; - using Base::Density_j; - using Base::ScalarVar_i; - using Base::ScalarVar_j; - using Base::Proj_Mean_GradScalarVar; - using Base::proj_vector_ij; - using Base::Flux; - using Base::Jacobian_i; - using Base::Jacobian_j; - - const su2double sigma = 2.0/3.0; - const su2double cn1 = 16.0; - const su2double cb2 = 0.622; - - /*! - * \brief Adds any extra variables to AD - */ - void ExtraADPreaccIn() override {} - - /*! - * \brief SA-neg specific steps in the ComputeResidual method - * \param[in] config - Definition of the particular problem. - */ - void FinishResidualCalc(const CConfig* config) override { - const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; - - /*--- Compute mean effective viscosity ---*/ - - const su2double nu_i = Laminar_Viscosity_i/Density_i; - const su2double nu_j = Laminar_Viscosity_j/Density_j; - - const su2double nu_ij = 0.5 * (nu_i + nu_j); - const su2double nu_tilde_i = ScalarVar_i[0]; - const su2double nu_tilde_j = ScalarVar_j[0]; - const su2double nu_tilde_ij = 0.5 * (nu_tilde_i + nu_tilde_j); - - /*--- Following Diskin's implementation from 10.2514/1.J064629, they propose a new fn function - * to be evaluated at the cell to maintain positivity in the diffusion coefficient, which is - * used in both terms. The new fn term averaged across the face reverts to the original fn - * function. ---*/ - - /*--- Second Term (LHS) ---*/ - const su2double zeta_i = ((1 + cb2) * nu_tilde_ij - cb2 * nu_tilde_i) / nu_ij; - su2double fn_i = 1.0; - if (zeta_i < 0.0) { - fn_i = (cn1 + pow(zeta_i,3)) / (cn1 - pow(zeta_i,3)); - } - - const su2double term_1 = (nu_ij + (1 + cb2) * nu_tilde_ij * fn_i); - const su2double term_2 = cb2 * nu_tilde_i * fn_i; - Flux[0] = (term_1 - term_2) * Proj_Mean_GradScalarVar[0] / sigma; - - /*--- For Jacobians -> Use of TSL approx. to compute derivatives of the gradients - * Exact Jacobians were tested on multiple cases but resulted in divergence of all - * simulations, hence only frozen diffusion coefficient (approximate) Jacobians are used. ---*/ - - if (implicit) { - const su2double diffusion_coefficient = (term_1 - term_2); - - const su2double dGrad_dnut_i = -proj_vector_ij; - const su2double dGrad_dnut_j = proj_vector_ij; - - Jacobian_i[0][0] = diffusion_coefficient * dGrad_dnut_i / sigma; - Jacobian_j[0][0] = diffusion_coefficient * dGrad_dnut_j / sigma; - } - } - -public: - /*! - * \brief Constructor of the class. - * \param[in] val_nDim - Number of dimensions of the problem. - * \param[in] val_nVar - Number of variables of the problem. - * \param[in] correct_grad - Whether to correct gradient for skewness. - * \param[in] config - Definition of the particular problem. - */ - CAvgGrad_TurbSA_Neg(unsigned short val_nDim, unsigned short val_nVar, - bool correct_grad, const CConfig* config) - : CAvgGrad_Scalar(val_nDim, val_nVar, correct_grad, config) {} -}; - /*! * \class CAvgGrad_TurbSST * \brief Class for computing viscous term using average of gradient with correction (Menter SST turbulence model). diff --git a/SU2_CFD/include/solvers/CTurbSASolver.hpp b/SU2_CFD/include/solvers/CTurbSASolver.hpp index 0544d82477d..e24052e7047 100644 --- a/SU2_CFD/include/solvers/CTurbSASolver.hpp +++ b/SU2_CFD/include/solvers/CTurbSASolver.hpp @@ -128,6 +128,21 @@ class CTurbSASolver final : public CTurbSolver { void RunSA_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit); + /*! + * \brief Same dispatch as RunSA, for BC_Fluid_Interface's combined fill-and-flux donor loop. + */ + template + void RunSA_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, bool implicit); + + template + void RunSA_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, bool implicit); + + template + void RunSA_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, bool implicit); + public: /*! * \brief Constructor. @@ -402,6 +417,20 @@ class CTurbSASolver final : public CTurbSolver { unsigned short val_marker, bool val_inlet_surface) override; + /*! + * \brief Impose the fluid interface (sliding mesh) boundary condition, via the CScalarFlux_SA + * edge kernel. The convective term is a per-donor weighted average, computed in the same + * pass that fills the ghost row of each donor; the diffusive term is computed once per + * vertex, after the donor loop, from the ghost state the last donor left behind. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] solver_container - Container vector with all the solutions. + * \param[in] conv_numerics - Unused, kept only for the boundary condition dispatch. + * \param[in] visc_numerics - Unused, kept only for the boundary condition dispatch. + * \param[in] config - Definition of the particular problem. + */ + void BC_Fluid_Interface(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, + CNumerics *visc_numerics, CConfig *config) override; + /*! * \brief Store of a set of provided inlet profile values at a vertex. * \param[in] val_inlet - vector containing the inlet values for the current vertex. diff --git a/SU2_CFD/src/drivers/CDriver.cpp b/SU2_CFD/src/drivers/CDriver.cpp index 5904ed9febb..550e4b98f63 100644 --- a/SU2_CFD/src/drivers/CDriver.cpp +++ b/SU2_CFD/src/drivers/CDriver.cpp @@ -1238,10 +1238,7 @@ void CDriver::InstantiateTurbulentNumerics(unsigned short nVar_Turb, int offset, break; case SPACE_UPWIND : for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { - if (spalart_allmaras) { - numerics[iMGlevel][TURB_SOL][conv_term] = new CUpwSca_TurbSA(nDim, nVar_Turb, config); - } - else if (menter_sst) + if (menter_sst) numerics[iMGlevel][TURB_SOL][conv_term] = new CUpwSca_TurbSST(nDim, nVar_Turb, config); } break; @@ -1253,14 +1250,7 @@ void CDriver::InstantiateTurbulentNumerics(unsigned short nVar_Turb, int offset, /*--- Definition of the viscous scheme for each equation and mesh level ---*/ for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { - if (spalart_allmaras) { - if (config->GetSAParsedOptions().version == SA_OPTIONS::NEG) { - numerics[iMGlevel][TURB_SOL][visc_term] = new CAvgGrad_TurbSA_Neg(nDim, nVar_Turb, true, config); - } else { - numerics[iMGlevel][TURB_SOL][visc_term] = new CAvgGrad_TurbSA(nDim, nVar_Turb, true, config); - } - } - else if (menter_sst) + if (menter_sst) numerics[iMGlevel][TURB_SOL][visc_term] = new CAvgGrad_TurbSST(nDim, nVar_Turb, constants, true, config); } @@ -1278,35 +1268,26 @@ void CDriver::InstantiateTurbulentNumerics(unsigned short nVar_Turb, int offset, numerics[iMGlevel][TURB_SOL][source_second_term] = new CSourceNothing(nDim, nVar_Turb, config); } - /*--- Definition of the boundary condition method ---*/ + /*--- Definition of the boundary condition method. SA drives its own boundaries through the + * CScalarFlux_SA edge kernel (see CTurbSASolver), so it needs no conv_bound_term/visc_bound_term + * here; menter_sst is unchanged, still on the CNumerics path. ---*/ for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { - if (spalart_allmaras) { - numerics[iMGlevel][TURB_SOL][conv_bound_term] = new CUpwSca_TurbSA(nDim, nVar_Turb, config); - - if (config->GetSAParsedOptions().version == SA_OPTIONS::NEG) { - numerics[iMGlevel][TURB_SOL][visc_bound_term] = new CAvgGrad_TurbSA_Neg(nDim, nVar_Turb, true, config); - } else { - numerics[iMGlevel][TURB_SOL][visc_bound_term] = new CAvgGrad_TurbSA(nDim, nVar_Turb, true, config); - } - } - else if (menter_sst) { + if (menter_sst) { numerics[iMGlevel][TURB_SOL][conv_bound_term] = new CUpwSca_TurbSST(nDim, nVar_Turb, config); numerics[iMGlevel][TURB_SOL][visc_bound_term] = new CAvgGrad_TurbSST(nDim, nVar_Turb, constants, true, config); } } } -/*--- Explicit instantiation of the template above, needed because it is defined in a cpp file, instead of hpp. ---*/ +/*--- Explicit instantiation of the template above, needed because it is defined in a cpp file, instead of hpp. + * NEMO has no explicit instantiation: NEMO with a turbulence model is rejected at configuration. ---*/ template void CDriver::InstantiateTurbulentNumerics>( unsigned short, int, const CConfig*, const CSolver*, CNumerics****&) const; template void CDriver::InstantiateTurbulentNumerics>( unsigned short, int, const CConfig*, const CSolver*, CNumerics****&) const; -template void CDriver::InstantiateTurbulentNumerics>( - unsigned short, int, const CConfig*, const CSolver*, CNumerics****&) const; - template void CDriver::InstantiateTransitionNumerics(unsigned short nVar_Trans, int offset, const CConfig *config, const CSolver* trans_solver, CNumerics ****&numerics) const { @@ -2038,9 +2019,6 @@ void CDriver::InitializeNumerics(CConfig *config, CGeometry **geometry, CSolver if (incompressible) InstantiateTurbulentNumerics >(nVar_Turb, offset, config, solver[MESH_0][TURB_SOL], numerics); - else if (NEMO_ns) - InstantiateTurbulentNumerics >(nVar_Turb, offset, config, - solver[MESH_0][TURB_SOL], numerics); else InstantiateTurbulentNumerics >(nVar_Turb, offset, config, solver[MESH_0][TURB_SOL], numerics); diff --git a/SU2_CFD/src/solvers/CTurbSASolver.cpp b/SU2_CFD/src/solvers/CTurbSASolver.cpp index 1052b3b1ec5..3500916b41b 100644 --- a/SU2_CFD/src/solvers/CTurbSASolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSASolver.cpp @@ -751,276 +751,131 @@ void CTurbSASolver::BC_Inlet(CGeometry *geometry, CSolver **solver_container, CN } } -void CTurbSASolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, - CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { +void CTurbSASolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, + CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + EnsureGhostFlowContainers(solver_container, config); - /*--- Loop over all the vertices on this boundary marker ---*/ + auto* flowSolver = solver_container[FLOW_SOL]; SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { - const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); - /*--- Check if the node belongs to the domain (i.e., not a halo node) ---*/ - - if (geometry->nodes->GetDomain(iPoint)) { - - /*--- Allocate the value at the outlet ---*/ - - auto V_outlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, iVertex); - - /*--- Retrieve solution at the farfield boundary node ---*/ - - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); + /*--- Neumann: the turbulent variable is copied from the interior before computing the flux. ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, nodes->GetSolution(iPoint, iVar)); - /*--- Set various quantities in the solver class ---*/ - - conv_numerics->SetPrimitive(V_domain, V_outlet); - - /*--- Set the turbulent variables. Here we use a Neumann BC such - that the turbulent variable is copied from the interior of the - domain to the outlet before computing the residual. ---*/ - - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), nodes->GetSolution(iPoint)); - - /*--- Set Normal (negate for outward convention) ---*/ - - su2double Normal[MAXNDIM] = {0.0}; - for (auto iDim = 0u; iDim < nDim; iDim++) - Normal[iDim] = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); + SetGhostPrimitives(iVertex, flowSolver->GetCharacPrimVar(val_marker, iVertex)); - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), - geometry->nodes->GetGridVel(iPoint)); + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - if (conv_numerics->GetBoundedScalar()) { - const su2double* velocity = &V_outlet[prim_idx.Velocity()]; - const su2double density = solver_container[FLOW_SOL]->GetNodes()->GetDensity(iPoint); - conv_numerics->SetMassFlux(BoundedScalarBCFlux(iPoint, implicit, density, velocity, Normal)); - } + ghostSkip[iVertex] = false; + } + END_SU2_OMP_FOR - /*--- Compute the residual using an upwind scheme ---*/ - - auto residual = conv_numerics->ComputeResidual(config); - LinSysRes.AddBlock(iPoint, residual); - - /*--- Jacobian contribution for implicit integration ---*/ - - if (implicit) Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i); - -// /*--- Viscous contribution, commented out because serious convergence problems ---*/ -// -// su2double Coord_Reflected[MAXNDIM]; -// GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), -// geometry->nodes->GetCoord(iPoint), Coord_Reflected); -// visc_numerics->SetCoord(geometry->nodes->GetCoord(iPoint), Coord_Reflected); -// visc_numerics->SetNormal(Normal); -// -// /*--- Conservative variables w/o reconstruction ---*/ -// -// visc_numerics->SetPrimitive(V_domain, V_outlet); -// -// /*--- Turbulent variables w/o reconstruction, and its gradients ---*/ -// -// visc_numerics->SetScalarVar(Solution_i, Solution_j); -// visc_numerics->SetScalarVarGradient(node[iPoint]->GetGradient(), node[iPoint]->GetGradient()); -// -// /*--- Compute residual, and Jacobians ---*/ -// -// auto residual = visc_numerics->ComputeResidual(config); -// -// /*--- Subtract residual, and update Jacobians ---*/ -// -// LinSysRes.SubtractBlock(iPoint, residual); -// Jacobian.SubtractBlock2Diag(iPoint, residual.jacobian_i); + /*--- The diffusive term causes serious convergence problems, so it stays off, as it did before. ---*/ + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; - } + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else if (config->GetNEMOProblem()) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); } - END_SU2_OMP_FOR } -void CTurbSASolver::BC_Engine_Inflow(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, - CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { +void CTurbSASolver::BC_Engine_Inflow(CGeometry *geometry, CSolver **solver_container, CNumerics*, + CNumerics*, CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + EnsureGhostFlowContainers(solver_container, config); - /*--- Loop over all the vertices on this boundary marker ---*/ + auto* flowSolver = solver_container[FLOW_SOL]; SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { - const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); - /*--- Check if the node belongs to the domain (i.e., not a halo node) ---*/ - - if (geometry->nodes->GetDomain(iPoint)) { - - /*--- Allocate the value at the infinity ---*/ - - auto V_inflow = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, iVertex); - - /*--- Retrieve solution at the farfield boundary node ---*/ - - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); - - /*--- Set various quantities in the solver class ---*/ - - conv_numerics->SetPrimitive(V_domain, V_inflow); + /*--- Neumann: the turbulent variable is copied from the interior before computing the flux. ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, nodes->GetSolution(iPoint, iVar)); - /*--- Set the turbulent variables. Here we use a Neumann BC such - that the turbulent variable is copied from the interior of the - domain to the outlet before computing the residual. ---*/ - - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), nodes->GetSolution(iPoint)); - - /*--- Set Normal (negate for outward convention) ---*/ - - su2double Normal[MAXNDIM] = {0.0}; - for (auto iDim = 0u; iDim < nDim; iDim++) - Normal[iDim] = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); - - /*--- Set grid movement ---*/ - - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), - geometry->nodes->GetGridVel(iPoint)); - - /*--- Compute the residual using an upwind scheme ---*/ - - auto residual = conv_numerics->ComputeResidual(config); - LinSysRes.AddBlock(iPoint, residual); - - /*--- Jacobian contribution for implicit integration ---*/ - - if (implicit) Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i); - -// /*--- Viscous contribution, commented out because serious convergence problems ---*/ -// -// su2double Coord_Reflected[MAXNDIM]; -// GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), -// geometry->nodes->GetCoord(iPoint), Coord_Reflected); -// visc_numerics->SetCoord(geometry->nodes->GetCoord(iPoint), Coord_Reflected); -// visc_numerics->SetNormal(Normal); -// -// /*--- Conservative variables w/o reconstruction ---*/ -// -// visc_numerics->SetPrimitive(V_domain, V_inflow); -// -// /*--- Turbulent variables w/o reconstruction, and its gradients ---*/ -// -// visc_numerics->SetScalarVar(node[iPoint]->GetSolution(), node[iPoint]->GetSolution()); -// visc_numerics->SetScalarVarGradient(node[iPoint]->GetGradient(), node[iPoint]->GetGradient()); -// -// /*--- Compute residual, and Jacobians ---*/ -// -// auto residual = visc_numerics->ComputeResidual(config); -// -// /*--- Subtract residual, and update Jacobians ---*/ -// -// LinSysRes.SubtractBlock(iPoint, residual); -// Jacobian.SubtractBlock2Diag(iPoint, residual.jacobian_i); + SetGhostPrimitives(iVertex, flowSolver->GetCharacPrimVar(val_marker, iVertex)); - } + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); + ghostSkip[iVertex] = false; } END_SU2_OMP_FOR + /*--- The diffusive term causes serious convergence problems, so it stays off, as it did before. ---*/ + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; + + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else if (config->GetNEMOProblem()) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } } -void CTurbSASolver::BC_Engine_Exhaust(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, - CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { +void CTurbSASolver::BC_Engine_Exhaust(CGeometry *geometry, CSolver **solver_container, CNumerics*, + CNumerics*, CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + EnsureGhostFlowContainers(solver_container, config); - /*--- Loop over all the vertices on this boundary marker ---*/ + auto* flowSolver = solver_container[FLOW_SOL]; SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { + /*--- Prescribed turbulent state for an inflow. ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, nu_tilde_Engine[iVar]); - const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); - - /*--- Check if the node belongs to the domain (i.e., not a halo node) ---*/ - - if (geometry->nodes->GetDomain(iPoint)) { - - /*--- Normal vector for this vertex (negate for outward convention) ---*/ - - su2double Normal[MAXNDIM] = {0.0}; - for (auto iDim = 0u; iDim < nDim; iDim++) - Normal[iDim] = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - - /*--- Allocate the value at the infinity ---*/ - - auto V_exhaust = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, iVertex); - - /*--- Retrieve solution at the farfield boundary node ---*/ - - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); - - /*--- Set various quantities in the solver class ---*/ - - conv_numerics->SetPrimitive(V_domain, V_exhaust); - - /*--- Set the turbulent variable states (prescribed for an inflow) ---*/ - - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), nu_tilde_Engine); - - /*--- Set various other quantities in the conv_numerics class ---*/ - - conv_numerics->SetNormal(Normal); - - /*--- Set grid movement ---*/ - - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), - geometry->nodes->GetGridVel(iPoint)); - - /*--- Compute the residual using an upwind scheme ---*/ - - auto residual = conv_numerics->ComputeResidual(config); - LinSysRes.AddBlock(iPoint, residual); - - /*--- Jacobian contribution for implicit integration ---*/ - - if (implicit) Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i); + SetGhostPrimitives(iVertex, flowSolver->GetCharacPrimVar(val_marker, iVertex)); -// /*--- Viscous contribution, commented out because serious convergence problems ---*/ -// -// su2double Coord_Reflected[MAXNDIM]; -// GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), -// geometry->nodes->GetCoord(iPoint), Coord_Reflected); -// visc_numerics->SetCoord(geometry->nodes->GetCoord(iPoint), Coord_Reflected); -// visc_numerics->SetNormal(Normal); -// -// /*--- Conservative variables w/o reconstruction ---*/ -// -// visc_numerics->SetPrimitive(V_domain, V_exhaust); -// -// /*--- Turbulent variables w/o reconstruction, and its gradients ---*/ -// -// visc_numerics->SetScalarVar(Solution_i, Solution_j); -// visc_numerics->SetScalarVarGradient(node[iPoint]->GetGradient(), node[iPoint]->GetGradient()); -// -// /*--- Compute residual, and Jacobians ---*/ -// -// auto residual = visc_numerics->ComputeResidual(config); -// -// /*--- Subtract residual, and update Jacobians ---*/ -// -// LinSysRes.SubtractBlock(iPoint, residual); -// Jacobian.SubtractBlock2Diag(iPoint, residual.jacobian_i); + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - } + ghostSkip[iVertex] = false; } END_SU2_OMP_FOR + /*--- The diffusive term causes serious convergence problems, so it stays off, as it did before. ---*/ + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; + + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else if (config->GetNEMOProblem()) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } } void CTurbSASolver::BC_ActDisk_Inlet(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, @@ -1038,24 +893,24 @@ void CTurbSASolver::BC_ActDisk_Outlet(CGeometry *geometry, CSolver **solver_cont } void CTurbSASolver::BC_ActDisk(CGeometry *geometry, CSolver **solver_container, - CNumerics *conv_numerics, CNumerics *visc_numerics, + CNumerics*, CNumerics*, CConfig *config, unsigned short val_marker, bool val_inlet_surface) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + EnsureGhostFlowContainers(solver_container, config); - /*--- Loop over all the vertices on this boundary marker ---*/ + auto* flowSolver = solver_container[FLOW_SOL]; SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); - const auto GlobalIndex_donor = solver_container[FLOW_SOL]->GetDonorGlobalIndex(val_marker, iVertex); + const auto GlobalIndex_donor = flowSolver->GetDonorGlobalIndex(val_marker, iVertex); const auto GlobalIndex = geometry->nodes->GetGlobalIndex(iPoint); - /*--- Check if the node belongs to the domain (i.e., not a halo node) ---*/ - - if (!geometry->nodes->GetDomain(iPoint) || (GlobalIndex == GlobalIndex_donor)) { + /*--- No flux at a vertex whose donor is the point itself. ---*/ + if (GlobalIndex == GlobalIndex_donor) { + ghostSkip[iVertex] = true; continue; } @@ -1064,7 +919,6 @@ void CTurbSASolver::BC_ActDisk(CGeometry *geometry, CSolver **solver_container, su2double Normal[MAXNDIM] = {0.0}; for (auto iDim = 0u; iDim < nDim; iDim++) Normal[iDim] = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); su2double Area = GeometryToolbox::Norm(nDim, Normal); @@ -1072,9 +926,7 @@ void CTurbSASolver::BC_ActDisk(CGeometry *geometry, CSolver **solver_container, for (auto iDim = 0u; iDim < nDim; iDim++) UnitNormal[iDim] = Normal[iDim]/Area; - /*--- Retrieve solution at the farfield boundary node ---*/ - - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); + const auto* V_domain = flowSolver->GetNodes()->GetPrimitive(iPoint); /*--- Check the flow direction. Project the flow into the normal to the inlet face ---*/ @@ -1084,111 +936,69 @@ void CTurbSASolver::BC_ActDisk(CGeometry *geometry, CSolver **solver_container, if ((val_inlet_surface) && (Vn < 0.0)) { ReverseFlow = true; } if ((!val_inlet_surface) && (Vn > 0.0)) { ReverseFlow = true; } - /*--- Do not anything if there is a - reverse flow, Euler b.c. for the direct problem ---*/ + /*--- No flux at all if there is a reverse flow, Euler b.c. for the direct problem. ---*/ - if (ReverseFlow) continue; + if (ReverseFlow) { + ghostSkip[iVertex] = true; + continue; + } - /*--- Allocate the value at the infinity ---*/ + SetGhostPrimitives(iVertex, flowSolver->GetCharacPrimVar(val_marker, iVertex)); - if (val_inlet_surface) { - auto V_inlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, iVertex); - conv_numerics->SetPrimitive(V_domain, V_inlet); - } - else { - auto V_outlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, iVertex); - conv_numerics->SetPrimitive(V_domain, V_outlet); - } + /*--- Inflow analysis (interior extrapolation, a Neumann BC) or outflow analysis + * (prescribed for an inflow), depending on which side val_marker is. ReverseFlow is always + * false here (the other case returned above), so this reduces to val_inlet_surface. ---*/ - /*--- Set the turb. variable solution - set the turbulent variables. Here we use a Neumann BC such - that the turbulent variable is copied from the interior of the - domain to the outlet before computing the residual. - or set the turbulent variable states (prescribed for an inflow) ----*/ - - // if (val_inlet_surface) Solution_j[0] = 0.5*(nodes->GetSolution(iPoint,0)+V_outlet [nDim+9]); - // else Solution_j[0] = 0.5*(nodes->GetSolution(iPoint,0)+V_inlet [nDim+9]); - - // /*--- Inflow analysis (interior extrapolation) ---*/ - // if (((val_inlet_surface) && (!ReverseFlow)) || ((!val_inlet_surface) && (ReverseFlow))) { - // Solution_j[0] = 2.0*node[iPoint]->GetSolution(0) - node[iPoint_Normal]->GetSolution(0); - // } - - // /*--- Outflow analysis ---*/ - // else { - // if (val_inlet_surface) Solution_j[0] = Factor_nu_ActDisk*V_outlet [nDim+9]; - // else { Solution_j[0] = Factor_nu_ActDisk*V_inlet [nDim+9]; } - // } - - if (((val_inlet_surface) && (!ReverseFlow)) || ((!val_inlet_surface) && (ReverseFlow))) { - /*--- Inflow analysis (interior extrapolation) ---*/ - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), nodes->GetSolution(iPoint)); - } - else { - /*--- Outflow analysis ---*/ - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), nu_tilde_ActDisk); + if (val_inlet_surface) { + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, nodes->GetSolution(iPoint, iVar)); + } else { + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, nu_tilde_ActDisk[iVar]); } - /*--- Grid Movement ---*/ - - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), geometry->nodes->GetGridVel(iPoint)); - - /*--- Compute the residual using an upwind scheme ---*/ - - auto residual = conv_numerics->ComputeResidual(config); - LinSysRes.AddBlock(iPoint, residual); - - /*--- Jacobian contribution for implicit integration ---*/ - - if (implicit) Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i); - -// /*--- Viscous contribution, commented out because serious convergence problems ---*/ -// -// visc_numerics->SetNormal(Normal); -// su2double Coord_Reflected[MAXNDIM]; -// GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), -// geometry->nodes->GetCoord(iPoint), Coord_Reflected); -// visc_numerics->SetCoord(geometry->nodes->GetCoord(iPoint), Coord_Reflected); -// -// /*--- Conservative variables w/o reconstruction ---*/ -// -// if (val_inlet_surface) visc_numerics->SetPrimitive(V_domain, V_inlet); -// else visc_numerics->SetPrimitive(V_domain, V_outlet); -// -// /*--- Turbulent variables w/o reconstruction, and its gradients ---*/ -// -// visc_numerics->SetScalarVar(Solution_i, Solution_j); -// -// visc_numerics->SetScalarVarGradient(node[iPoint]->GetGradient(), node[iPoint]->GetGradient()); -// -// /*--- Compute residual, and Jacobians ---*/ -// -// auto residual = visc_numerics->ComputeResidual(config); -// -// /*--- Subtract residual, and update Jacobians ---*/ -// -// LinSysRes.SubtractBlock(iPoint, residual); -// Jacobian.SubtractBlock2Diag(iPoint, residual.jacobian_i); + for (auto iDim = 0u; iDim < nDim; iDim++) ghostNormal(iVertex, iDim) = Normal[iDim]; + ghostSkip[iVertex] = false; } END_SU2_OMP_FOR + /*--- The diffusive term causes serious convergence problems, so it stays off, as it did before. ---*/ + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; + + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else if (config->GetNEMOProblem()) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } } -void CTurbSASolver::BC_Inlet_MixingPlane(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, - CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { +void CTurbSASolver::BC_Inlet_MixingPlane(CGeometry *geometry, CSolver **solver_container, CNumerics*, + CNumerics*, CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + EnsureGhostFlowContainers(solver_container, config); + + auto* flowSolver = solver_container[FLOW_SOL]; const auto nSpanWiseSections = config->GetnSpanWiseSections(); - /*--- Loop over all the vertices on this boundary marker ---*/ + /*--- The span loop below reaches a vertex of val_marker through GetOldVertex, which need not + * cover every one of them, so every vertex starts skipped and only the ones actually filled + * are cleared. ---*/ + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) ghostSkip[iVertex] = true; + END_SU2_OMP_FOR + for (auto iSpan = 0u; iSpan < nSpanWiseSections; iSpan++){ su2double extAverageNu[MAXNVAR] = {0.0}; - extAverageNu[0] = solver_container[FLOW_SOL]->GetMixingState(val_marker, iSpan, 5); - - /*--- Loop over all the vertices on this boundary marker ---*/ + extAverageNu[0] = flowSolver->GetMixingState(val_marker, iSpan, 5); SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->GetnVertexSpan(val_marker,iSpan); iVertex++) { @@ -1202,92 +1012,73 @@ void CTurbSASolver::BC_Inlet_MixingPlane(CGeometry *geometry, CSolver **solver_c /*--- Index of the closest interior node ---*/ const auto Point_Normal = geometry->vertex[val_marker][oldVertex]->GetNormal_Neighbor(); - /*--- Normal vector for this vertex (negate for outward convention) ---*/ - - su2double Normal[MAXNDIM] = {0.0}; - for (auto iDim = 0u; iDim < nDim; iDim++) - Normal[iDim] = -geometry->vertex[val_marker][oldVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); - - /*--- Allocate the value at the inlet ---*/ - auto V_inlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, oldVertex); - - /*--- Retrieve solution at the farfield boundary node ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(oldVertex, iVar, extAverageNu[iVar]); - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); + SetGhostPrimitives(oldVertex, flowSolver->GetCharacPrimVar(val_marker, oldVertex)); - /*--- Set various quantities in the solver class ---*/ - - conv_numerics->SetPrimitive(V_domain, V_inlet); - - /*--- Set the turbulent variable states (prescribed for an inflow) ---*/ - - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), extAverageNu); - - /*--- Set various other quantities in the conv_numerics class ---*/ - - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), - geometry->nodes->GetGridVel(iPoint)); - - /*--- Compute the residual using an upwind scheme ---*/ - - auto conv_residual = conv_numerics->ComputeResidual(config); - - /*--- Jacobian contribution for implicit integration ---*/ - - LinSysRes.AddBlock(iPoint, conv_residual); - if (implicit) Jacobian.AddBlock2Diag(iPoint, conv_residual.jacobian_i); + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(oldVertex, iDim) = -geometry->vertex[val_marker][oldVertex]->GetNormal(iDim); - /*--- Viscous contribution ---*/ + /*--- Reflected coordinate, read by the diffusion term's gradient projection. ---*/ su2double Coord_Reflected[MAXNDIM]; GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), geometry->nodes->GetCoord(iPoint), Coord_Reflected); - visc_numerics->SetCoord(geometry->nodes->GetCoord(iPoint), Coord_Reflected); - visc_numerics->SetNormal(Normal); - - /*--- Conservative variables w/o reconstruction ---*/ - - visc_numerics->SetPrimitive(V_domain, V_inlet); - - /*--- Turbulent variables w/o reconstruction, and its gradients ---*/ - - visc_numerics->SetScalarVar(nodes->GetSolution(iPoint), extAverageNu); - - visc_numerics->SetScalarVarGradient(nodes->GetGradient(iPoint), - nodes->GetGradient(iPoint)); - - /*--- Compute residual, and Jacobians ---*/ - - auto visc_residual = visc_numerics->ComputeResidual(config); + for (auto iDim = 0u; iDim < nDim; iDim++) ghostCoord(oldVertex, iDim) = Coord_Reflected[iDim]; - /*--- Subtract residual, and update Jacobians ---*/ - - LinSysRes.SubtractBlock(iPoint, visc_residual); - if (implicit) Jacobian.SubtractBlock2Diag(iPoint, visc_residual.jacobian_i); + /*--- The diffusion term reads the ghost gradient; mirror the interior one, as before. ---*/ + auto ghostGrad = ghostNodes->GetGradient(oldVertex); + const auto interiorGrad = nodes->GetGradient(iPoint); + for (auto iVar = 0u; iVar < nVar; iVar++) + for (auto iDim = 0u; iDim < nDim; iDim++) ghostGrad(iVar, iDim) = interiorGrad(iVar, iDim); + ghostSkip[oldVertex] = false; } END_SU2_OMP_FOR } + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Turb(), true /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; + + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else if (config->GetNEMOProblem()) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } } -void CTurbSASolver::BC_Inlet_Turbo(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, - CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { +void CTurbSASolver::BC_Inlet_Turbo(CGeometry *geometry, CSolver **solver_container, CNumerics*, + CNumerics*, CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + EnsureGhostFlowContainers(solver_container, config); + + auto* flowSolver = solver_container[FLOW_SOL]; const auto nSpanWiseSections = config->GetnSpanWiseSections(); - CFluidModel *FluidModel = solver_container[FLOW_SOL]->GetFluidModel(); + CFluidModel *FluidModel = flowSolver->GetFluidModel(); su2double Factor_nu_Inf = config->GetNuFactor_FreeStream(); + /*--- The span loop below reaches a vertex of val_marker through GetOldVertex, which need not + * cover every one of them, so every vertex starts skipped and only the ones actually filled + * are cleared. ---*/ + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) ghostSkip[iVertex] = true; + END_SU2_OMP_FOR + /*--- Loop over all the spans on this boundary marker ---*/ for (auto iSpan = 0; iSpan < nSpanWiseSections; iSpan++) { - su2double rho = solver_container[FLOW_SOL]->GetAverageDensity(val_marker, iSpan); - su2double pressure = solver_container[FLOW_SOL]->GetAveragePressure(val_marker, iSpan); + su2double rho = flowSolver->GetAverageDensity(val_marker, iSpan); + su2double pressure = flowSolver->GetAveragePressure(val_marker, iSpan); FluidModel->SetTDState_Prho(pressure, rho); su2double muLam = FluidModel->GetLaminarViscosity(); @@ -1295,8 +1086,6 @@ void CTurbSASolver::BC_Inlet_Turbo(CGeometry *geometry, CSolver **solver_contain su2double nu_tilde[MAXNVAR] = {0.0}; nu_tilde[0] = Factor_nu_Inf*muLam/rho; - /*--- Loop over all the vertices on this boundary marker ---*/ - SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->GetnVertexSpan(val_marker,iSpan); iVertex++) { @@ -1309,74 +1098,178 @@ void CTurbSASolver::BC_Inlet_Turbo(CGeometry *geometry, CSolver **solver_contain /*--- Index of the closest interior node ---*/ const auto Point_Normal = geometry->vertex[val_marker][oldVertex]->GetNormal_Neighbor(); - /*--- Normal vector for this vertex (negate for outward convention) ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(oldVertex, iVar, nu_tilde[iVar]); + + SetGhostPrimitives(oldVertex, flowSolver->GetCharacPrimVar(val_marker, oldVertex)); - su2double Normal[MAXNDIM] = {0.0}; for (auto iDim = 0u; iDim < nDim; iDim++) - Normal[iDim] = -geometry->vertex[val_marker][oldVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); + ghostNormal(oldVertex, iDim) = -geometry->vertex[val_marker][oldVertex]->GetNormal(iDim); + + /*--- Reflected coordinate, read by the diffusion term's gradient projection. ---*/ + su2double Coord_Reflected[MAXNDIM]; + GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), + geometry->nodes->GetCoord(iPoint), Coord_Reflected); + for (auto iDim = 0u; iDim < nDim; iDim++) ghostCoord(oldVertex, iDim) = Coord_Reflected[iDim]; + + /*--- The diffusion term reads the ghost gradient; mirror the interior one, as before. ---*/ + auto ghostGrad = ghostNodes->GetGradient(oldVertex); + const auto interiorGrad = nodes->GetGradient(iPoint); + for (auto iVar = 0u; iVar < nVar; iVar++) + for (auto iDim = 0u; iDim < nDim; iDim++) ghostGrad(iVar, iDim) = interiorGrad(iVar, iDim); + + ghostSkip[oldVertex] = false; + } + END_SU2_OMP_FOR + } + + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Turb(), true /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; + + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else if (config->GetNEMOProblem()) { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } else { + RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, + implicit); + } +} - /*--- Allocate the value at the inlet ---*/ - auto V_inlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, oldVertex); +void CTurbSASolver::BC_Fluid_Interface(CGeometry *geometry, CSolver **solver_container, CNumerics*, + CNumerics*, CConfig *config) { + SU2_ZONE_SCOPED - /*--- Retrieve solution at the farfield boundary node ---*/ + if (solver_container[FLOW_SOL] == nullptr) return; - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); + EnsureGhostFlowContainers(solver_container, config); - /*--- Set various quantities in the solver class ---*/ + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions optConv{ + dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl*/, + }; + const ScalarFluxOptions optVisc{ + dynamic_grid, false /*boundedScalar, the mass-flux correction only applies with the convective term*/, + true /*correctGradient*/, false /*accurateJacobians*/, + false /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl*/, + }; - conv_numerics->SetPrimitive(V_domain, V_inlet); + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + RunSA_FluidInterface>(geometry, solver_container, config, optConv, + optVisc, implicit); + } else if (config->GetNEMOProblem()) { + RunSA_FluidInterface>(geometry, solver_container, config, optConv, + optVisc, implicit); + } else { + RunSA_FluidInterface>(geometry, solver_container, config, optConv, + optVisc, implicit); + } +} - /*--- Set the turbulent variable states (prescribed for an inflow) ---*/ +template +void CTurbSASolver::RunSA_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, + bool implicit) { + if (nDim == 2) RunSA_FluidInterface(geometry, solver_container, config, optConv, optVisc, implicit); + else RunSA_FluidInterface(geometry, solver_container, config, optConv, optVisc, implicit); +} - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), nu_tilde); +template +void CTurbSASolver::RunSA_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, + bool implicit) { + if (nVar == 1) RunSA_FluidInterface(geometry, solver_container, config, optConv, optVisc, implicit); + else RunSA_FluidInterface(geometry, solver_container, config, optConv, optVisc, implicit); +} - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), - geometry->nodes->GetGridVel(iPoint)); +/*! + * \brief The convective term is a per-donor weighted average, computed in the same pass that + * fills the ghost row of each donor; the diffusive term is computed once per vertex, after + * the donor loop, from the ghost state the last donor left behind -- the discretization the + * solver had before this migration. This does not fit the fill-pass-then-BoundaryFluxResidual + * shape the other boundaries use, so it drives the CScalarFlux_SA kernel directly. + */ +template +void CTurbSASolver::RunSA_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, + bool implicit) { + using Scheme = CScalarFlux_SA; + const Scheme flux(*config); - /*--- Compute the residual using an upwind scheme ---*/ + auto* flowSolver = solver_container[FLOW_SOL]; + auto* flowNodes = su2staticcast_p(flowSolver->GetNodes()); + const auto nPrimVar = flowSolver->GetnPrimVar(); - auto conv_residual = conv_numerics->ComputeResidual(config); + const EdgeSide side_i{*nodes, flowNodes, CMatrixView(geometry->nodes->GetCoord()), + dynamic_grid ? CMatrixView(geometry->nodes->GetGridVel()) + : CMatrixView()}; + const EdgeSide side_j{*ghostNodes, ghostFlowNodes.get(), CMatrixView(ghostCoord), + side_i.gridVel}; - /*--- Jacobian contribution for implicit integration ---*/ + su2activevector PrimVar_j(nPrimVar); - LinSysRes.AddBlock(iPoint, conv_residual); - if (implicit) Jacobian.AddBlock2Diag(iPoint, conv_residual.jacobian_i); + for (auto iMarker = 0u; iMarker < config->GetnMarker_All(); iMarker++) { + if (config->GetMarker_All_KindBC(iMarker) != FLUID_INTERFACE) continue; - /*--- Viscous contribution ---*/ + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (unsigned long iVertex = 0; iVertex < geometry->nVertex[iMarker]; iVertex++) { + const auto iPoint = geometry->vertex[iMarker][iVertex]->GetNode(); + if (!geometry->nodes->GetDomain(iPoint)) continue; - su2double Coord_Reflected[MAXNDIM]; - GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), - geometry->nodes->GetCoord(iPoint), Coord_Reflected); - visc_numerics->SetCoord(geometry->nodes->GetCoord(iPoint), Coord_Reflected); + const auto Point_Normal = geometry->vertex[iMarker][iVertex]->GetNormal_Neighbor(); + const auto nDonorVertex = GetnSlidingStates(iMarker, iVertex); + + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[iMarker][iVertex]->GetNormal(iDim); + const auto normal = gatherVariables(iVertex, ghostNormal); - visc_numerics->SetNormal(Normal); + /*--- Loop over the donors and accumulate the weighted-average convective residual. ---*/ + for (auto jVertex = 0; jVertex < nDonorVertex; jVertex++) { - /*--- Conservative variables w/o reconstruction ---*/ + for (auto iVar = 0u; iVar < nPrimVar; iVar++) + PrimVar_j[iVar] = flowSolver->GetSlidingState(iMarker, iVertex, iVar, jVertex); - visc_numerics->SetPrimitive(V_domain, V_inlet); + const su2double weight = flowSolver->GetSlidingState(iMarker, iVertex, nPrimVar, jVertex); - /*--- Turbulent variables w/o reconstruction, and its gradients ---*/ + for (auto iVar = 0u; iVar < nVarSA; iVar++) + ghostNodes->SetSolution(iVertex, iVar, GetSlidingState(iMarker, iVertex, iVar, jVertex)); - visc_numerics->SetScalarVar(nodes->GetSolution(iPoint), nu_tilde); + SetGhostPrimitives(iVertex, PrimVar_j.data()); - visc_numerics->SetScalarVarGradient(nodes->GetGradient(iPoint), - nodes->GetGradient(iPoint)); + su2double massFlux = 0.0; + if (optConv.boundedScalar) { + massFlux = BoundedScalarBCFlux(iPoint, true, flowNodes->GetDensity(iPoint), + &PrimVar_j[prim_idx.Velocity()], normal.data()); + } - /*--- Compute residual, and Jacobians ---*/ + const auto res = flux.ComputeFlux(optConv, iPoint, side_i, iVertex, side_j, normal, massFlux); - auto visc_residual = visc_numerics->ComputeResidual(config); + for (auto iVar = 0ul; iVar < res.nVar; ++iVar) LinSysRes(iPoint, iVar) += weight * res.flux_i(iVar); + if (implicit) Jacobian.AddBlock2Diag(iPoint, res.jac_ii, weight); + } - /*--- Subtract residual, and update Jacobians ---*/ + /*--- Diffusive term, computed once from the ghost state the last donor left behind. ---*/ + su2double Coord_Reflected[MAXNDIM]; + GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), + geometry->nodes->GetCoord(iPoint), Coord_Reflected); + for (auto iDim = 0u; iDim < nDim; iDim++) ghostCoord(iVertex, iDim) = Coord_Reflected[iDim]; - LinSysRes.SubtractBlock(iPoint, visc_residual); - if (implicit) Jacobian.SubtractBlock2Diag(iPoint, visc_residual.jacobian_i); + auto ghostGrad = ghostNodes->GetGradient(iVertex); + const auto interiorGrad = nodes->GetGradient(iPoint); + for (auto iVar = 0u; iVar < nVarSA; iVar++) + for (auto iDim = 0u; iDim < nDim; iDim++) ghostGrad(iVar, iDim) = interiorGrad(iVar, iDim); + const auto res = flux.ComputeFlux(optVisc, iPoint, side_i, iVertex, side_j, normal, su2double(0.0)); + for (auto iVar = 0ul; iVar < res.nVar; ++iVar) LinSysRes(iPoint, iVar) += res.flux_i(iVar); + if (implicit) Jacobian.AddBlock2Diag(iPoint, res.jac_ii); } END_SU2_OMP_FOR } - } void CTurbSASolver::SetTurbVars_WF(CGeometry *geometry, CSolver **solver_container, From 87462918dac6407d6d7d295a9e86058f55487743 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Fri, 28 Aug 2026 22:07:33 -0700 Subject: [PATCH 08/20] Common dispatcher for the regime/NEMO -> FlowIndices boundary dispatch The same three-way branch (incompressible -> CIncEulerVariable::CIndices, NEMO -> CNEMOEulerVariable::CIndices, else -> CEulerVariable::CIndices) was repeated at all ten RunSA/RunSA_Boundary/RunSA_FluidInterface call sites. CIndicesTag plus DispatchRegime collapse it to one branch, called with a generic lambda that recovers the type via decltype(tag)::type -- standing in for a C++20 template lambda, since this project's baseline is C++17. Pure refactor, no behavior change: bit-identical to the pre-refactor binary on turb_ONERAM6 and the compressible flatplate wallfunctions case. Co-Authored-By: Claude Sonnet 5 --- SU2_CFD/include/solvers/CTurbSASolver.hpp | 21 ++++ SU2_CFD/src/solvers/CTurbSASolver.cpp | 139 +++++++--------------- 2 files changed, 63 insertions(+), 97 deletions(-) diff --git a/SU2_CFD/include/solvers/CTurbSASolver.hpp b/SU2_CFD/include/solvers/CTurbSASolver.hpp index e24052e7047..3d77c3be761 100644 --- a/SU2_CFD/include/solvers/CTurbSASolver.hpp +++ b/SU2_CFD/include/solvers/CTurbSASolver.hpp @@ -30,6 +30,17 @@ #include "CTurbSolver.hpp" +/*! + * \brief Carries a type through a value, so a runtime branch can hand a compile-time type to a + * generic lambda (its parameter deduces as CIndicesTag, and the lambda recovers T as + * decltype(tag)::type). Standing in for a C++20 template lambda, which this project's + * C++17 baseline does not have. + */ +template +struct CIndicesTag { + using type = T; +}; + /*! * \class CTurbSASolver * \brief Main class for defining the turbulence model solver. @@ -95,6 +106,16 @@ class CTurbSASolver final : public CTurbSolver { */ void ComputeUnderRelaxationFactor(CSolver** solver_container, const CConfig *config) final; + /*! + * \brief Resolve the compile-time flow indices from the regime/NEMO flags of config, and call f + * with a CIndicesTag of the result: f is a generic lambda, `[&](auto tag){ using Indices + * = typename decltype(tag)::type; ... }`, so one dispatcher serves RunSA, RunSA_Boundary + * and RunSA_FluidInterface alike despite their differing trailing arguments, instead of + * every boundary site repeating this same three-way branch. + */ + template + static void DispatchRegime(const CConfig* config, F&& f); + /*! * \brief Resolve the compile-time flow indices, dimension and backscatter equation count, and * run the interior edge loop with the matching CScalarFlux_SA instantiation. Whether the diff --git a/SU2_CFD/src/solvers/CTurbSASolver.cpp b/SU2_CFD/src/solvers/CTurbSASolver.cpp index 3500916b41b..ff80930f4f1 100644 --- a/SU2_CFD/src/solvers/CTurbSASolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSASolver.cpp @@ -351,6 +351,17 @@ void CTurbSASolver::Postprocessing(CGeometry *geometry, CSolver **solver_contain AD::EndNoSharedReading(); } +template +void CTurbSASolver::DispatchRegime(const CConfig* config, F&& f) { + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + f(CIndicesTag>{}); + } else if (config->GetNEMOProblem()) { + f(CIndicesTag>{}); + } else { + f(CIndicesTag>{}); + } +} + void CTurbSASolver::Upwind_Residual(CGeometry* geometry, CSolver** solver_container, CNumerics** numerics_container, CConfig* config, unsigned short iMesh) { SU2_ZONE_SCOPED @@ -366,13 +377,9 @@ void CTurbSASolver::Upwind_Residual(CGeometry* geometry, CSolver** solver_contai config->GetMUSCL(), /*--- muscl ---*/ }; - if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { - RunSA>(geometry, solver_container, config, opt); - } else if (config->GetNEMOProblem()) { - RunSA>(geometry, solver_container, config, opt); - } else { - RunSA>(geometry, solver_container, config, opt); - } + DispatchRegime(config, [&](auto tag) { + RunSA(geometry, solver_container, config, opt); + }); } template @@ -426,16 +433,9 @@ void CTurbSASolver::BC_Far_Field(CGeometry *geometry, CSolver **solver_container true /*oneSided, the ghost point has no row*/, false /*muscl, a boundary never reconstructs*/, }; - if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else if (config->GetNEMOProblem()) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } + DispatchRegime(config, [&](auto tag) { + RunSA_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } template @@ -739,16 +739,9 @@ void CTurbSASolver::BC_Inlet(CGeometry *geometry, CSolver **solver_container, CN true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; - if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else if (config->GetNEMOProblem()) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } + DispatchRegime(config, [&](auto tag) { + RunSA_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } void CTurbSASolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, @@ -782,16 +775,9 @@ void CTurbSASolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, C true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; - if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else if (config->GetNEMOProblem()) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } + DispatchRegime(config, [&](auto tag) { + RunSA_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } void CTurbSASolver::BC_Engine_Inflow(CGeometry *geometry, CSolver **solver_container, CNumerics*, @@ -825,16 +811,9 @@ void CTurbSASolver::BC_Engine_Inflow(CGeometry *geometry, CSolver **solver_conta true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; - if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else if (config->GetNEMOProblem()) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } + DispatchRegime(config, [&](auto tag) { + RunSA_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } void CTurbSASolver::BC_Engine_Exhaust(CGeometry *geometry, CSolver **solver_container, CNumerics*, @@ -866,16 +845,9 @@ void CTurbSASolver::BC_Engine_Exhaust(CGeometry *geometry, CSolver **solver_cont true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; - if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else if (config->GetNEMOProblem()) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } + DispatchRegime(config, [&](auto tag) { + RunSA_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } void CTurbSASolver::BC_ActDisk_Inlet(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, @@ -968,16 +940,9 @@ void CTurbSASolver::BC_ActDisk(CGeometry *geometry, CSolver **solver_container, true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; - if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else if (config->GetNEMOProblem()) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } + DispatchRegime(config, [&](auto tag) { + RunSA_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } void CTurbSASolver::BC_Inlet_MixingPlane(CGeometry *geometry, CSolver **solver_container, CNumerics*, @@ -1042,16 +1007,9 @@ void CTurbSASolver::BC_Inlet_MixingPlane(CGeometry *geometry, CSolver **solver_c true /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; - if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else if (config->GetNEMOProblem()) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } + DispatchRegime(config, [&](auto tag) { + RunSA_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } void CTurbSASolver::BC_Inlet_Turbo(CGeometry *geometry, CSolver **solver_container, CNumerics*, @@ -1128,16 +1086,9 @@ void CTurbSASolver::BC_Inlet_Turbo(CGeometry *geometry, CSolver **solver_contain true /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; - if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else if (config->GetNEMOProblem()) { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } else { - RunSA_Boundary>(geometry, solver_container, config, opt, val_marker, - implicit); - } + DispatchRegime(config, [&](auto tag) { + RunSA_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } void CTurbSASolver::BC_Fluid_Interface(CGeometry *geometry, CSolver **solver_container, CNumerics*, @@ -1159,16 +1110,10 @@ void CTurbSASolver::BC_Fluid_Interface(CGeometry *geometry, CSolver **solver_con false /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl*/, }; - if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { - RunSA_FluidInterface>(geometry, solver_container, config, optConv, - optVisc, implicit); - } else if (config->GetNEMOProblem()) { - RunSA_FluidInterface>(geometry, solver_container, config, optConv, - optVisc, implicit); - } else { - RunSA_FluidInterface>(geometry, solver_container, config, optConv, - optVisc, implicit); - } + DispatchRegime(config, [&](auto tag) { + RunSA_FluidInterface(geometry, solver_container, config, optConv, optVisc, + implicit); + }); } template From 070b1e8d845b3631739362917ff13765f3095dbd Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Fri, 28 Aug 2026 22:40:02 -0700 Subject: [PATCH 09/20] Fix: BC_Inlet_MixingPlane/BC_Inlet_Turbo never applied the bounded-scalar mass-flux correction Both carried opt.boundedScalar = config->GetBounded_Turb(), copied from the other boundaries during the step-8 migration, but the code they replaced never called BoundedScalarBCFlux for these two -- unlike BC_Inlet and BC_Outlet, which do. Under CONV_NUM_METHOD_TURB= BOUNDED_SCALAR this added a spurious residual/Jacobian correction at every mixing-plane and turbo-inlet vertex. Found while cross-checking the equivalent SST boundary during the SST migration (SU2_CFD/src/solvers/CTurbSSTSolver.cpp), where the same omission in the pre-migration code made the discrepancy obvious. No local test case exercises turbomachinery + BOUNDED_SCALAR turbulence convection together, so this could not be verified bit-identical against a real run; it is correct by inspection against the pre-migration code read during step 8 (BC_Inlet_MixingPlane's old body: convective AddBlock/ AddBlock2Diag with no SetMassFlux/BoundedScalarBCFlux call, unlike BC_Inlet's and BC_Outlet's). The already-verified turb_ONERAM6 regression remains bit-identical (it doesn't reach this code path). Co-Authored-By: Claude Sonnet 5 --- SU2_CFD/src/solvers/CTurbSASolver.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/SU2_CFD/src/solvers/CTurbSASolver.cpp b/SU2_CFD/src/solvers/CTurbSASolver.cpp index ff80930f4f1..3efc47eb9f6 100644 --- a/SU2_CFD/src/solvers/CTurbSASolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSASolver.cpp @@ -1003,7 +1003,8 @@ void CTurbSASolver::BC_Inlet_MixingPlane(CGeometry *geometry, CSolver **solver_c const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ - dynamic_grid, config->GetBounded_Turb(), true /*correctGradient*/, false /*accurateJacobians*/, + dynamic_grid, false /*boundedScalar, this site never applied the mass-flux correction*/, + true /*correctGradient*/, false /*accurateJacobians*/, true /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; @@ -1082,7 +1083,8 @@ void CTurbSASolver::BC_Inlet_Turbo(CGeometry *geometry, CSolver **solver_contain const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ - dynamic_grid, config->GetBounded_Turb(), true /*correctGradient*/, false /*accurateJacobians*/, + dynamic_grid, false /*boundedScalar, this site never applied the mass-flux correction*/, + true /*correctGradient*/, false /*accurateJacobians*/, true /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; From c81c926a3a0d378176a0f45a1fdfa43cba1b2f80 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Fri, 28 Aug 2026 22:42:17 -0700 Subject: [PATCH 10/20] Promote CIndicesTag/DispatchRegime to CTurbSolver, shared by SA and (soon) SST Both models need the identical regime/NEMO -> FlowIndices dispatch; keeping it on CTurbSASolver alone would mean either duplicating it on CTurbSSTSolver or reaching across sibling classes. Header-defined (not just declared) on the shared CTurbSolver base, since it is a template over a deduced, unnameable lambda type and needs to be usable from more than one translation unit (CTurbSASolver.cpp and, next, CTurbSSTSolver.cpp). Pure refactor: bit-identical to the pre-refactor binary on turb_ONERAM6. Co-Authored-By: Claude Sonnet 5 --- SU2_CFD/include/solvers/CTurbSASolver.hpp | 21 -------------- SU2_CFD/include/solvers/CTurbSolver.hpp | 34 +++++++++++++++++++++++ SU2_CFD/src/solvers/CTurbSASolver.cpp | 14 ---------- 3 files changed, 34 insertions(+), 35 deletions(-) diff --git a/SU2_CFD/include/solvers/CTurbSASolver.hpp b/SU2_CFD/include/solvers/CTurbSASolver.hpp index 3d77c3be761..e24052e7047 100644 --- a/SU2_CFD/include/solvers/CTurbSASolver.hpp +++ b/SU2_CFD/include/solvers/CTurbSASolver.hpp @@ -30,17 +30,6 @@ #include "CTurbSolver.hpp" -/*! - * \brief Carries a type through a value, so a runtime branch can hand a compile-time type to a - * generic lambda (its parameter deduces as CIndicesTag, and the lambda recovers T as - * decltype(tag)::type). Standing in for a C++20 template lambda, which this project's - * C++17 baseline does not have. - */ -template -struct CIndicesTag { - using type = T; -}; - /*! * \class CTurbSASolver * \brief Main class for defining the turbulence model solver. @@ -106,16 +95,6 @@ class CTurbSASolver final : public CTurbSolver { */ void ComputeUnderRelaxationFactor(CSolver** solver_container, const CConfig *config) final; - /*! - * \brief Resolve the compile-time flow indices from the regime/NEMO flags of config, and call f - * with a CIndicesTag of the result: f is a generic lambda, `[&](auto tag){ using Indices - * = typename decltype(tag)::type; ... }`, so one dispatcher serves RunSA, RunSA_Boundary - * and RunSA_FluidInterface alike despite their differing trailing arguments, instead of - * every boundary site repeating this same three-way branch. - */ - template - static void DispatchRegime(const CConfig* config, F&& f); - /*! * \brief Resolve the compile-time flow indices, dimension and backscatter equation count, and * run the interior edge loop with the matching CScalarFlux_SA instantiation. Whether the diff --git a/SU2_CFD/include/solvers/CTurbSolver.hpp b/SU2_CFD/include/solvers/CTurbSolver.hpp index c5fce372e45..078ec2f8366 100644 --- a/SU2_CFD/include/solvers/CTurbSolver.hpp +++ b/SU2_CFD/include/solvers/CTurbSolver.hpp @@ -29,8 +29,22 @@ #include "CScalarSolver.hpp" #include "../variables/CTurbVariable.hpp" +#include "../variables/CEulerVariable.hpp" +#include "../variables/CIncEulerVariable.hpp" +#include "../variables/CNEMOEulerVariable.hpp" #include "../../../Common/include/parallelization/omp_structure.hpp" +/*! + * \brief Carries a type through a value, so a runtime branch can hand a compile-time type to a + * generic lambda (its parameter deduces as CIndicesTag, and the lambda recovers T as + * decltype(tag)::type). Standing in for a C++20 template lambda, which this project's + * C++17 baseline does not have. + */ +template +struct CIndicesTag { + using type = T; +}; + /*! * \class CTurbSolver * \brief Main class for defining the turbulence model solver. @@ -42,6 +56,26 @@ class CTurbSolver : public CScalarSolver { vector Inlet_TurbVars; /*!< \brief Turbulence variables at inlet profiles */ + /*! + * \brief Resolve the compile-time flow indices from the regime/NEMO flags of config, and call f + * with a CIndicesTag of the result: f is a generic lambda, `[&](auto tag){ using Indices + * = typename decltype(tag)::type; ... }`. Shared by every turbulence model's boundary + * dispatch (RunSA/RunSA_Boundary/RunSA_FluidInterface and their SST counterparts), which + * would otherwise each repeat this same three-way branch. Header-defined (not just + * declared) because it is a template with a deduced, unnameable lambda type, called from + * more than one translation unit (CTurbSASolver.cpp, CTurbSSTSolver.cpp). + */ + template + static void DispatchRegime(const CConfig* config, F&& f) { + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + f(CIndicesTag>{}); + } else if (config->GetNEMOProblem()) { + f(CIndicesTag>{}); + } else { + f(CIndicesTag>{}); + } + } + public: /*! * \brief Destructor of the class. diff --git a/SU2_CFD/src/solvers/CTurbSASolver.cpp b/SU2_CFD/src/solvers/CTurbSASolver.cpp index 3efc47eb9f6..6543ed2c287 100644 --- a/SU2_CFD/src/solvers/CTurbSASolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSASolver.cpp @@ -30,9 +30,6 @@ #include "../../include/variables/CTurbSAVariable.hpp" #include "../../include/variables/CFlowVariable.hpp" #include "../../include/numerics/turbulent/turb_sa_edge_flux.hpp" -#include "../../include/variables/CEulerVariable.hpp" -#include "../../include/variables/CIncEulerVariable.hpp" -#include "../../include/variables/CNEMOEulerVariable.hpp" #include "../../../Common/include/parallelization/omp_structure.hpp" #include "../../../Common/include/toolboxes/geometry_toolbox.hpp" #include "../../../Common/include/toolboxes/random_toolbox.hpp" @@ -351,17 +348,6 @@ void CTurbSASolver::Postprocessing(CGeometry *geometry, CSolver **solver_contain AD::EndNoSharedReading(); } -template -void CTurbSASolver::DispatchRegime(const CConfig* config, F&& f) { - if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { - f(CIndicesTag>{}); - } else if (config->GetNEMOProblem()) { - f(CIndicesTag>{}); - } else { - f(CIndicesTag>{}); - } -} - void CTurbSASolver::Upwind_Residual(CGeometry* geometry, CSolver** solver_container, CNumerics** numerics_container, CConfig* config, unsigned short iMesh) { SU2_ZONE_SCOPED From c051b6dc526d2efe51db1505330b359eadc36a8a Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sat, 29 Aug 2026 08:32:05 -0700 Subject: [PATCH 11/20] SST as a third-layer model, driving both the interior loop and boundaries CScalarFlux_SST is conservative (weight = density) with a coupled, non- diagonal 2x2 diffusion matrix, unlike SA's diagonal one. Its convective term is exactly the inherited CUpwScalarFlux default; only coefficients() and coefficientJacobians() are SST-specific. The old CAvgGrad_TurbSST evaluated the diffusion numerics twice per edge, once with i first and once with j first (CScalarSolver::Viscous_Residual_NonCons), because its cross term (a Langevin-style coupling between the k and omega equations) reads the transported omega of whichever point was passed first. Returning two different matrices from one coefficients() call -- D.i built with omega at i, D.j with omega at j -- reproduces that exactly: the same mechanism SA's coefficients() already uses for its own asymmetric term, derived independently here by hand-tracing the old twice-per-edge call against the new framework's single-pass Jacobian block assembly. coefficientJacobians() needed a framework change: SA's version is a per-edge constant (cb2/sigma only) and never needed the edge's point/side context, but SST's accurate-Jacobian correction depends on the transported omega at both endpoints. diffusionTerms now hands every model's coefficientJacobians the same (idx, iPoint, side_i, jPoint, side_j) context extraDiffusionTerms already gets; SA's override picks up the unused parameters and ignores them. F1 blending needed a new setter: CVariable::SetF1blending (default no-op), overridden on CTurbSSTVariable, so a boundary's fill pass can mirror the interior point's F1 into the ghost row the way it already mirrors the gradient -- there was previously only a getter, since the old code always read it through CNumerics::SetF1blending(F1_i, F1_j) rather than writing it anywhere. BC_HeatFlux_Wall/BC_Isothermal_Wall and SetTurbVars_WF (wall functions) are unchanged: purely algebraic Dirichlet conditions with no flux evaluation, outside the scope of this migration, same as SA's. CDriver::InstantiateTurbulentNumerics no longer builds conv_term/visc_term/ conv_bound_term/visc_bound_term for either model. CAvgGrad_TurbSST and CUpwSca_TurbSST are not deleted yet: CUpwSca_TransLM is a type alias of CUpwSca_TurbSST (transition/trans_convection.hpp), so it stays live until LM migrates too, per the design doc's ordering (SST, then LM, then species, then SA_Neg, before the numerics classes come out). Verification: built full build-green (SU2_CFD, SU2_DEF, SU2_DOT, SU2_GEO, SU2_SOL, UnitTests). turb_ONERAM6 (SA) remains bit-identical to the pre-SST-migration binary, confirming the coefficientJacobians signature change is behavior-preserving for SA. The compressible and incompressible SST flatplate wallfunctions cases are NOT bit-identical to the pre-migration binary, unlike every step so far -- differences appear from iteration 1 (~1e-7 relative) and grow through the run. Traced this as far as confirming it survives a deliberate fix (matching the old code's exact operation order for the diffusion cross term) with no change, meaning it is not a source-level reassociation the way earlier steps' differences were; it is a compiler-level difference (likely reciprocal-math under -ffast-math treating the shared-subexpression single-pass computation of D.i/D.j differently from the old code's two independent function calls), not something addressable by rewriting the formula. What was verified instead: a 300-iteration run converges to the same steady state -- CD and CL agree to 6+ significant figures at convergence -- confirming the physics is correct and the discrepancy is a numerically-benign difference in convergence path, not a formula error. This is a real, documented exception to this session's bit-identical verification bar, not a silent gap. Co-Authored-By: Claude Sonnet 5 --- .../numerics/scalar/scalar_edge_flux.hpp | 8 +- .../numerics/turbulent/turb_sa_edge_flux.hpp | 8 +- .../numerics/turbulent/turb_sst_edge_flux.hpp | 151 +++++ SU2_CFD/include/solvers/CTurbSSTSolver.hpp | 67 +- .../include/variables/CTurbSSTVariable.hpp | 5 + SU2_CFD/include/variables/CVariable.hpp | 7 + SU2_CFD/src/drivers/CDriver.cpp | 38 +- SU2_CFD/src/solvers/CTurbSSTSolver.cpp | 629 +++++++++--------- 8 files changed, 539 insertions(+), 374 deletions(-) create mode 100644 SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp diff --git a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp index 012f92230d3..23d0f5c3c15 100644 --- a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp +++ b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp @@ -157,8 +157,12 @@ class CAvgGradScalarBase { } if (opt.accurateJacobians) { - /*--- Coefficients that depend on the transported variables contribute here. ---*/ - self->coefficientJacobians(projGrad, res); + /*--- Coefficients that depend on the transported variables contribute here. A model whose + * correction is a per-edge constant (e.g. SA's) can ignore the side/point arguments; one + * whose correction depends on point values (e.g. SST's, on the transported variable at + * either endpoint) needs them, so every model is handed the same full context diffusionTerms + * itself has, matching extraDiffusionTerms's signature below. ---*/ + self->coefficientJacobians(idx, iPoint, side_i, jPoint, side_j, projGrad, res); } self->extraDiffusionTerms(idx, iPoint, side_i, jPoint, side_j, normal, vector_ij, res); diff --git a/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp b/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp index 2bbca14ff38..528787efc6c 100644 --- a/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp +++ b/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp @@ -118,9 +118,13 @@ class CScalarFlux_SA /*! * \brief Extra Jacobian terms from the dependence of the diffusion coefficient on nu_tilde. + * \note The two derivatives below are per-edge constants (cb2/sigma only), so the point/side + * context diffusionTerms hands every model's coefficientJacobians is unused here. */ - template - FORCEINLINE void coefficientJacobians(const Vector& projGrad, EdgeResidual& res) const { + template + FORCEINLINE void coefficientJacobians(const FlowIndices&, Int, const EdgeSide&, Int, + const EdgeSide&, const Vector& projGrad, + EdgeResidual& res) const { /*--- d(diffusion coefficient of i)/d(nu_tilde_i), and its counterpart w.r.t. nu_tilde_j; * the coefficient of j is the same expression with i and j swapped, so the same two * derivatives apply to both orientations. ---*/ diff --git a/SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp b/SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp new file mode 100644 index 00000000000..a16abda5337 --- /dev/null +++ b/SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp @@ -0,0 +1,151 @@ +/*! + * \file turb_sst_edge_flux.hpp + * \brief Menter SST model as a third-layer scalar flux, see numerics/scalar/scalar_edge_flux.hpp. + * \author P. Gomes + * \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 . + */ + +#pragma once + +#include "../scalar/scalar_edge_flux.hpp" + +/*! + * \class CScalarFlux_SST + * \brief Convection and diffusion of the Menter SST model, conservative with a coupled (but + * neither symmetric nor diagonal) 2x2 diffusion matrix. + * \note SST writes no finalizeFlux of its own: the inherited CUpwScalarFlux one is exactly + * flux(iVar) = a0*rho_i*phi_i(iVar) + a1*rho_j*phi_j(iVar), Conservative weighting by + * density, which is the whole of the model's old convective term. + */ +template +class CScalarFlux_SST + : public CUpwScalarBase, FlowIndices, nDim, nVar> { + public: + static constexpr bool Conservative = true; + static constexpr bool DiagonalDiffusion = false; + + using Base = CUpwScalarBase; + using Int = typename Base::Int; + using Base::Base; + + private: + /*--- Fixed regardless of SST_OPTIONS::version: only the production-limiter and source-term + * constants (alfa/gamma) differ by version, not these. ---*/ + static constexpr passivedouble sigma_k1 = 0.85; + static constexpr passivedouble sigma_k2 = 1.0; + static constexpr passivedouble sigma_om1 = 0.5; + static constexpr passivedouble sigma_om2 = 0.856; + + public: + /*! + * \brief Diffusion coefficients of both orientations of the edge, see CAvgGrad_TurbSST. + * \note The old discretization evaluates the diffusion numerics twice per edge, once with i + * first and once with j first (CScalarSolver::Viscous_Residual_NonCons), because the + * cross term below reads the transported omega of whichever point was passed first. That + * is exactly what returning two different matrices here reproduces: D.i, read by i's row, + * uses omega at i; D.j, read by j's row, uses omega at j. Every other entry is symmetric + * (an i/j average), so it is the same in both matrices. + */ + template + FORCEINLINE CPair> coefficients(const FlowIndices& idx, Int iPoint, + const EdgeSide& side_i, Int jPoint, + const EdgeSide& side_j) const { + const Double rho_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); + const Double rho_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); + const Double mu_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.LaminarViscosity()); + const Double mu_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.LaminarViscosity()); + const Double muT_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.EddyViscosity()); + const Double muT_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.EddyViscosity()); + + const Double F1_i = side_i.scalarNodes.GetF1blending(iPoint); + const Double F1_j = side_j.scalarNodes.GetF1blending(jPoint); + const Double omega_i = gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), 1); + const Double omega_j = gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), 1); + + const Double sigma_kine_i = F1_i * sigma_k1 + (1.0 - F1_i) * sigma_k2; + const Double sigma_kine_j = F1_j * sigma_k1 + (1.0 - F1_j) * sigma_k2; + const Double sigma_omega_i = F1_i * sigma_om1 + (1.0 - F1_i) * sigma_om2; + const Double sigma_omega_j = F1_j * sigma_om1 + (1.0 - F1_j) * sigma_om2; + + const Double diff_kine = 0.5 * ((mu_i + sigma_kine_i * muT_i) + (mu_j + sigma_kine_j * muT_j)); + const Double diff_omega = 0.5 * ((mu_i + sigma_omega_i * muT_i) + (mu_j + sigma_omega_j * muT_j)); + + const Double lambda_i = 2.0 * (1.0 - F1_i) * rho_i * sigma_omega_i; + const Double lambda_j = 2.0 * (1.0 - F1_j) * rho_j * sigma_omega_j; + const Double lambda_ij = 0.5 * (lambda_i + lambda_j); + const Double w_ij = 0.5 * (omega_i + omega_j); + + /*--- Same two terms as CAvgGrad_TurbSST's old diff_omega_T2/diff_omega_T3, kept as the exact + * same two additions (not algebraically simplified) so this rounds identically. ---*/ + const Double diff_omega_T2 = lambda_ij; + const Double diff_omega_T3_i = -omega_i * lambda_ij / w_ij; + const Double diff_omega_T3_j = -omega_j * lambda_ij / w_ij; + + Matrix D_i = Double(0.0), D_j = Double(0.0); + D_i(0, 0) = diff_kine; + D_i(1, 1) = diff_omega; + D_i(1, 0) = diff_omega_T2 + diff_omega_T3_i; + + D_j(0, 0) = diff_kine; + D_j(1, 1) = diff_omega; + D_j(1, 0) = diff_omega_T2 + diff_omega_T3_j; + + return {D_i, D_j}; + } + + /*! + * \brief Extra Jacobian terms from the dependence of the cross-diffusion coefficient on omega. + * \note Unlike SA's, this correction is not a per-edge constant: it comes out of the same + * twice-per-edge evaluation coefficients() reproduces, so it lands on all four Jacobian + * blocks rather than mirroring the D.i/D.j split (jac_ii and jac_ji share one term, jac_ij + * and jac_jj the other) -- worked out by hand against the old i->j / j->i pair of + * Viscous_Residual_NonCons calls, not read off the single-evaluation shape most other + * models have. + */ + template + FORCEINLINE void coefficientJacobians(const FlowIndices& idx, Int iPoint, const EdgeSide& side_i, + Int jPoint, const EdgeSide& side_j, + const Vector& projGrad, EdgeResidual& res) const { + const Double rho_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); + const Double rho_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); + const Double F1_i = side_i.scalarNodes.GetF1blending(iPoint); + const Double F1_j = side_j.scalarNodes.GetF1blending(jPoint); + const Double omega_i = gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), 1); + const Double omega_j = gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), 1); + + const Double sigma_omega_i = F1_i * sigma_om1 + (1.0 - F1_i) * sigma_om2; + const Double sigma_omega_j = F1_j * sigma_om1 + (1.0 - F1_j) * sigma_om2; + const Double lambda_i = 2.0 * (1.0 - F1_i) * rho_i * sigma_omega_i; + const Double lambda_j = 2.0 * (1.0 - F1_j) * rho_j * sigma_omega_j; + const Double lambda_ij = 0.5 * (lambda_i + lambda_j); + + const Double denom = pow(omega_i + omega_j, 2.0); + const Double E_i = 2.0 * lambda_ij * omega_i / denom * projGrad(0); + const Double E_j = 2.0 * lambda_ij * omega_j / denom * projGrad(0); + + res.jac_ii(1, 1) += E_j; + res.jac_ij(1, 1) -= E_i; + res.jac_ji(1, 1) += E_j; + res.jac_jj(1, 1) -= E_i; + } +}; diff --git a/SU2_CFD/include/solvers/CTurbSSTSolver.hpp b/SU2_CFD/include/solvers/CTurbSSTSolver.hpp index e7afce0cacd..f8d0cf6f07c 100644 --- a/SU2_CFD/include/solvers/CTurbSSTSolver.hpp +++ b/SU2_CFD/include/solvers/CTurbSSTSolver.hpp @@ -61,6 +61,39 @@ class CTurbSSTSolver final : public CTurbSolver { */ void ComputeUnderRelaxationFactor(CSolver** solver_container, const CConfig *config) override; + /*! + * \brief Resolve the compile-time flow indices and dimension, and run the interior edge loop + * with the matching CScalarFlux_SST instantiation. nVar is fixed at 2 (k and omega), so + * unlike SA's RunSA this dispatch has one axis fewer to resolve. + */ + template + void RunSST(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt); + + template + void RunSST(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt); + + /*! + * \brief Same dispatch as RunSST, for a boundary's call into BoundaryFluxResidual. + */ + template + void RunSST_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit); + + template + void RunSST_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit); + + /*! + * \brief Same dispatch as RunSST, for BC_Fluid_Interface's combined fill-and-flux donor loop. + */ + template + void RunSST_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, bool implicit); + + template + void RunSST_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, bool implicit); + public: /*! * \brief Constructor. @@ -106,16 +139,16 @@ class CTurbSSTSolver final : public CTurbSolver { unsigned short iMesh) override; /*! - * \brief Compute the viscous flux for the turbulent equation at a particular edge. - * \param[in] iEdge - Edge for which we want to compute the flux + * \brief Compute the spatial integration using the CScalarFlux_SST edge kernel, which computes + * and writes both the convective and the diffusive term of every edge. * \param[in] geometry - Geometrical definition of the problem. * \param[in] solver_container - Container vector with all the solutions. - * \param[in] numerics - Description of the numerical method. + * \param[in] numerics_container - Unused, kept only for the boundary conditions. * \param[in] config - Definition of the particular problem. - * \note Calls a generic implementation after defining a SolverSpecificNumerics object. + * \param[in] iMesh - Index of the mesh in multigrid computations. */ - void Viscous_Residual(const unsigned long iEdge, const CGeometry* geometry, CSolver** solver_container, - CNumerics* numerics, const CConfig* config) override; + void Upwind_Residual(CGeometry* geometry, CSolver** solver_container, CNumerics** numerics_container, + CConfig* config, unsigned short iMesh) override; /*! * \brief Source term computation. @@ -240,24 +273,18 @@ class CTurbSSTSolver final : public CTurbSolver { unsigned short val_marker) override; /*! - * \brief Impose the fluid interface boundary condition using tranfer data. + * \brief Impose the fluid interface (sliding mesh) boundary condition, via the CScalarFlux_SST + * edge kernel. The convective term is a per-donor weighted average, computed in the same + * pass that fills the ghost row of each donor; the diffusive term is computed once per + * vertex, after the donor loop, from the ghost state the last donor left behind. * \param[in] geometry - Geometrical definition of the problem. * \param[in] solver_container - Container vector with all the solutions. - * \param[in] conv_numerics - Description of the numerical method. - * \param[in] visc_numerics - Description of the numerical method. + * \param[in] conv_numerics - Unused, kept only for the boundary condition dispatch. + * \param[in] visc_numerics - Unused, kept only for the boundary condition dispatch. * \param[in] config - Definition of the particular problem. */ - void BC_Fluid_Interface(CGeometry *geometry, - CSolver **solver_container, - CNumerics *conv_numerics, - CNumerics *visc_numerics, - CConfig *config) final { - BC_Fluid_Interface_impl( - [&](unsigned long iPoint) { - visc_numerics->SetF1blending(nodes->GetF1blending(iPoint), nodes->GetF1blending(iPoint)); - }, - geometry, solver_container, conv_numerics, visc_numerics, config); - } + void BC_Fluid_Interface(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, + CNumerics *visc_numerics, CConfig *config) override; /*! * \brief Get the constants for the SST model. diff --git a/SU2_CFD/include/variables/CTurbSSTVariable.hpp b/SU2_CFD/include/variables/CTurbSSTVariable.hpp index 1fc1dbee89f..45c6559f9aa 100644 --- a/SU2_CFD/include/variables/CTurbSSTVariable.hpp +++ b/SU2_CFD/include/variables/CTurbSSTVariable.hpp @@ -78,6 +78,11 @@ class CTurbSSTVariable final : public CTurbVariable { */ inline su2double GetF1blending(unsigned long iPoint) const override { return F1(iPoint); } + /*! + * \brief Write the first blending function directly, for a ghost row (see CVariable's note). + */ + inline void SetF1blending(unsigned long iPoint, su2double val) override { F1(iPoint) = val; } + /*! * \brief Get the second blending function. */ diff --git a/SU2_CFD/include/variables/CVariable.hpp b/SU2_CFD/include/variables/CVariable.hpp index cfacf76af96..649fc581b80 100644 --- a/SU2_CFD/include/variables/CVariable.hpp +++ b/SU2_CFD/include/variables/CVariable.hpp @@ -1710,6 +1710,13 @@ class CVariable { */ inline virtual su2double GetF1blending(unsigned long iPoint) const { return 0.0; } + /*! + * \brief Write the first blending function of the SST model, for a ghost row: SetBlendingFunc + * derives F1 from the wall distance and viscous state, neither of which a ghost point + * has, so its ghost row is written directly with the interior point's own F1 instead. + */ + inline virtual void SetF1blending(unsigned long iPoint, su2double val) {} + /*! * \brief Get the second blending function of the SST model. */ diff --git a/SU2_CFD/src/drivers/CDriver.cpp b/SU2_CFD/src/drivers/CDriver.cpp index 550e4b98f63..1a4f0e8923a 100644 --- a/SU2_CFD/src/drivers/CDriver.cpp +++ b/SU2_CFD/src/drivers/CDriver.cpp @@ -76,8 +76,6 @@ #include "../../include/numerics/scalar/scalar_convection.hpp" #include "../../include/numerics/scalar/scalar_diffusion.hpp" #include "../../include/numerics/scalar/scalar_sources.hpp" -#include "../../include/numerics/turbulent/turb_convection.hpp" -#include "../../include/numerics/turbulent/turb_diffusion.hpp" #include "../../include/numerics/turbulent/turb_sources.hpp" #include "../../include/numerics/turbulent/transition/trans_convection.hpp" #include "../../include/numerics/turbulent/transition/trans_diffusion.hpp" @@ -1193,15 +1191,9 @@ void CDriver::FinalizeIntegration(CIntegration ***integration, CGeometry **geome template void CDriver::InstantiateTurbulentNumerics(unsigned short nVar_Turb, int offset, const CConfig *config, const CSolver* turb_solver, CNumerics ****&numerics) const { - const int conv_term = CONV_TERM + offset; - const int visc_term = VISC_TERM + offset; - const int source_first_term = SOURCE_FIRST_TERM + offset; const int source_second_term = SOURCE_SECOND_TERM + offset; - const int conv_bound_term = CONV_BOUND_TERM + offset; - const int visc_bound_term = VISC_BOUND_TERM + offset; - /*--- Assign turbulence model booleans ---*/ bool spalart_allmaras = false, menter_sst = false; @@ -1230,30 +1222,22 @@ void CDriver::InstantiateTurbulentNumerics(unsigned short nVar_Turb, int offset, omega_Inf = turb_solver->GetOmega_Inf(); } - /*--- Definition of the convective scheme for each equation and mesh level ---*/ + /*--- Definition of the convective scheme for each equation and mesh level. Both SA and SST now + * drive their interior loop through their own CScalarFlux_* edge kernel (see CTurbSASolver, + * CTurbSSTSolver), so conv_term is never set here; the switch stays only for the + * NO_CONVECTIVE error check. ---*/ switch (config->GetKind_ConvNumScheme_Turb()) { case NO_CONVECTIVE: SU2_MPI::Error("Config file is missing the CONV_NUM_METHOD_TURB option.", CURRENT_FUNCTION); break; case SPACE_UPWIND : - for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { - if (menter_sst) - numerics[iMGlevel][TURB_SOL][conv_term] = new CUpwSca_TurbSST(nDim, nVar_Turb, config); - } break; default: SU2_MPI::Error("Invalid convective scheme for the turbulence equations.", CURRENT_FUNCTION); break; } - /*--- Definition of the viscous scheme for each equation and mesh level ---*/ - - for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { - if (menter_sst) - numerics[iMGlevel][TURB_SOL][visc_term] = new CAvgGrad_TurbSST(nDim, nVar_Turb, constants, true, config); - } - /*--- Definition of the source term integration scheme for each equation and mesh level ---*/ for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { @@ -1268,17 +1252,9 @@ void CDriver::InstantiateTurbulentNumerics(unsigned short nVar_Turb, int offset, numerics[iMGlevel][TURB_SOL][source_second_term] = new CSourceNothing(nDim, nVar_Turb, config); } - /*--- Definition of the boundary condition method. SA drives its own boundaries through the - * CScalarFlux_SA edge kernel (see CTurbSASolver), so it needs no conv_bound_term/visc_bound_term - * here; menter_sst is unchanged, still on the CNumerics path. ---*/ - - for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { - if (menter_sst) { - numerics[iMGlevel][TURB_SOL][conv_bound_term] = new CUpwSca_TurbSST(nDim, nVar_Turb, config); - numerics[iMGlevel][TURB_SOL][visc_bound_term] = new CAvgGrad_TurbSST(nDim, nVar_Turb, constants, true, - config); - } - } + /*--- Definition of the boundary condition method. Both SA and SST drive their boundaries + * through their own CScalarFlux_* edge kernel, so neither needs conv_bound_term/visc_bound_term + * here. ---*/ } /*--- Explicit instantiation of the template above, needed because it is defined in a cpp file, instead of hpp. * NEMO has no explicit instantiation: NEMO with a turbulence model is rejected at configuration. ---*/ diff --git a/SU2_CFD/src/solvers/CTurbSSTSolver.cpp b/SU2_CFD/src/solvers/CTurbSSTSolver.cpp index 6bc32c814f7..d4971785a7d 100644 --- a/SU2_CFD/src/solvers/CTurbSSTSolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSSTSolver.cpp @@ -26,8 +26,10 @@ */ #include "../../include/solvers/CTurbSSTSolver.hpp" +#include "../../include/solvers/CScalarSolver.inl" #include "../../include/variables/CTurbSSTVariable.hpp" #include "../../include/variables/CFlowVariable.hpp" +#include "../../include/numerics/turbulent/turb_sst_edge_flux.hpp" #include "../../../Common/include/parallelization/omp_structure.hpp" #include "../../../Common/include/toolboxes/geometry_toolbox.hpp" @@ -152,6 +154,13 @@ CTurbSSTSolver::CTurbSSTSolver(CGeometry *geometry, CConfig *config, unsigned sh nodes = new CTurbSSTVariable(kine_Inf, omega_Inf, muT_Inf, nPoint, nDim, nVar, constants, config); SetBaseClassPointerToNodes(); + /*--- Ghost states for boundary conditions, sized to the largest marker (see BoundaryFluxResidual). ---*/ + unsigned long maxMarkerVertices = 0; + for (unsigned long iMarker = 0; iMarker < nMarker; iMarker++) + maxMarkerVertices = max(maxMarkerVertices, nVertex[iMarker]); + ghostNodes = make_unique(kine_Inf, omega_Inf, muT_Inf, maxMarkerVertices, nDim, nVar, constants, + config); + /*--- MPI solution ---*/ InitiateComms(geometry, config, MPI_QUANTITIES::SOLUTION_EDDY); @@ -203,6 +212,8 @@ void CTurbSSTSolver::Preprocessing(CGeometry *geometry, CSolver **solver_contain /*--- Upwind second order reconstruction and gradients ---*/ CommonPreprocessing(geometry, config, Output); + + EnsureGhostFlowContainers(solver_container, config); } void CTurbSSTSolver::Postprocessing(CGeometry *geometry, CSolver **solver_container, @@ -290,18 +301,37 @@ void CTurbSSTSolver::Postprocessing(CGeometry *geometry, CSolver **solver_contai AD::EndNoSharedReading(); } -void CTurbSSTSolver::Viscous_Residual(const unsigned long iEdge, const CGeometry* geometry, CSolver** solver_container, - CNumerics* numerics, const CConfig* config) { +void CTurbSSTSolver::Upwind_Residual(CGeometry* geometry, CSolver** solver_container, CNumerics** numerics_container, + CConfig* config, unsigned short iMesh) { + SU2_ZONE_SCOPED - /*--- Define an object to set solver specific numerics contribution. ---*/ - auto SolverSpecificNumerics = [&](unsigned long iPoint, unsigned long jPoint) { - /*--- Menter's first blending function (only SST)---*/ - numerics->SetF1blending(nodes->GetF1blending(iPoint), nodes->GetF1blending(jPoint)); + const ScalarFluxOptions opt{ + dynamic_grid, /*--- dynamicGrid ---*/ + config->GetBounded_Turb(), /*--- boundedScalar ---*/ + true, /*--- correctGradient ---*/ + config->GetUse_Accurate_Turb_Jacobians(), /*--- accurateJacobians ---*/ + true, /*--- convective ---*/ + true, /*--- viscous ---*/ + false, /*--- oneSided, this is the interior loop ---*/ + config->GetMUSCL(), /*--- muscl ---*/ }; - /*--- Now instantiate the generic non-conservative implementation with the functor above. ---*/ - Viscous_Residual_NonCons(iEdge, geometry, solver_container, numerics, config, SolverSpecificNumerics); + DispatchRegime(config, [&](auto tag) { + RunSST(geometry, solver_container, config, opt); + }); +} + +template +void CTurbSSTSolver::RunSST(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt) { + if (nDim == 2) RunSST(geometry, solver_container, config, opt); + else RunSST(geometry, solver_container, config, opt); +} +template +void CTurbSSTSolver::RunSST(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt) { + EdgeFluxResidual>(geometry, solver_container, config, opt); } void CTurbSSTSolver::Source_Residual(CGeometry *geometry, CSolver **solver_container, @@ -589,255 +619,146 @@ void CTurbSSTSolver::BC_Isothermal_Wall(CGeometry *geometry, CSolver **solver_co } -void CTurbSSTSolver::BC_Inlet(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, - CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { +void CTurbSSTSolver::BC_Inlet(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, + CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + EnsureGhostFlowContainers(solver_container, config); - /*--- Loop over all the vertices on this boundary marker ---*/ + auto* flowSolver = solver_container[FLOW_SOL]; SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { - - const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); - - /*--- Check if the node belongs to the domain (i.e., not a halo node) ---*/ - - if (geometry->nodes->GetDomain(iPoint)) { - - /*--- Normal vector for this vertex (negate for outward convention) ---*/ - - su2double Normal[MAXNDIM] = {0.0}; - for (auto iDim = 0u; iDim < nDim; iDim++) - Normal[iDim] = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); - - /*--- Allocate the value at the inlet ---*/ - - auto V_inlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, iVertex); - - /*--- Retrieve solution at the farfield boundary node ---*/ - - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); - - /*--- Set various quantities in the solver class ---*/ - - conv_numerics->SetPrimitive(V_domain, V_inlet); - - su2double Inlet_Vars[MAXNVAR]; - if (config->GetInlet_Profile_From_File()) { - /*--- Non-dimensionalize Inlet_TurbVars if Inlet-Files are used. ---*/ - Inlet_Vars[0] = Inlet_TurbVars[val_marker][iVertex][0] / pow(config->GetVelocity_Ref(), 2); - Inlet_Vars[1] = Inlet_TurbVars[val_marker][iVertex][1] * config->GetViscosity_Ref() / - (config->GetDensity_Ref() * pow(config->GetVelocity_Ref(), 2)); + const auto* V_inlet = flowSolver->GetCharacPrimVar(val_marker, iVertex); + + su2double Inlet_Vars[MAXNVAR]; + if (config->GetInlet_Profile_From_File()) { + /*--- Non-dimensionalize Inlet_TurbVars if Inlet-Files are used. ---*/ + Inlet_Vars[0] = Inlet_TurbVars[val_marker][iVertex][0] / pow(config->GetVelocity_Ref(), 2); + Inlet_Vars[1] = Inlet_TurbVars[val_marker][iVertex][1] * config->GetViscosity_Ref() / + (config->GetDensity_Ref() * pow(config->GetVelocity_Ref(), 2)); + } else { + /*--- Obtain fluid model for computing the kine and omega to impose at the inlet boundary. ---*/ + CFluidModel* FluidModel = flowSolver->GetFluidModel(); + + /*--- Obtain flow velocity vector at inlet boundary node ---*/ + + const su2double* Velocity_Inlet = &V_inlet[prim_idx.Velocity()]; + su2double Density_Inlet; + if (config->GetKind_Regime() == ENUM_REGIME::COMPRESSIBLE) { + Density_Inlet = V_inlet[prim_idx.Density()]; + FluidModel->SetTDState_Prho(V_inlet[prim_idx.Pressure()], Density_Inlet); } else { - /*--- Obtain fluid model for computing the kine and omega to impose at the inlet boundary. ---*/ - CFluidModel* FluidModel = solver_container[FLOW_SOL]->GetFluidModel(); - - /*--- Obtain flow velocity vector at inlet boundary node ---*/ - - const su2double* Velocity_Inlet = &V_inlet[prim_idx.Velocity()]; - su2double Density_Inlet; - if (config->GetKind_Regime() == ENUM_REGIME::COMPRESSIBLE) { - Density_Inlet = V_inlet[prim_idx.Density()]; - FluidModel->SetTDState_Prho(V_inlet[prim_idx.Pressure()], Density_Inlet); - } else { - const su2double* Scalar_Inlet = nullptr; - if (config->GetKind_Species_Model() != SPECIES_MODEL::NONE) { - Scalar_Inlet = config->GetInlet_SpeciesVal(config->GetMarker_All_TagBound(val_marker)); - } - FluidModel->SetTDState_T(V_inlet[prim_idx.Temperature()], Scalar_Inlet); - Density_Inlet = FluidModel->GetDensity(); + const su2double* Scalar_Inlet = nullptr; + if (config->GetKind_Species_Model() != SPECIES_MODEL::NONE) { + Scalar_Inlet = config->GetInlet_SpeciesVal(config->GetMarker_All_TagBound(val_marker)); } - const su2double Laminar_Viscosity_Inlet = FluidModel->GetLaminarViscosity(); - const su2double* Turb_Properties = config->GetInlet_TurbVal(config->GetMarker_All_TagBound(val_marker)); - const su2double Intensity = Turb_Properties[0]; - const su2double viscRatio = Turb_Properties[1]; - const su2double VelMag2 = GeometryToolbox::SquaredNorm(nDim, Velocity_Inlet); - - Inlet_Vars[0] = 3.0 / 2.0 * (VelMag2 * pow(Intensity, 2)); - Inlet_Vars[1] = Density_Inlet * Inlet_Vars[0] / (Laminar_Viscosity_Inlet * viscRatio); - } - - /*--- Set the turbulent variable states. Use free-stream SST - values for the turbulent state at the inflow. ---*/ - /*--- Load the inlet turbulence variables (uniform by default). ---*/ - - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), Inlet_Vars); - - /*--- Set various other quantities in the solver class ---*/ - - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), - geometry->nodes->GetGridVel(iPoint)); - - if (conv_numerics->GetBoundedScalar()) { - const su2double* velocity = &V_inlet[prim_idx.Velocity()]; - const su2double density = solver_container[FLOW_SOL]->GetNodes()->GetDensity(iPoint); - conv_numerics->SetMassFlux(BoundedScalarBCFlux(iPoint, implicit, density, velocity, Normal)); + FluidModel->SetTDState_T(V_inlet[prim_idx.Temperature()], Scalar_Inlet); + Density_Inlet = FluidModel->GetDensity(); } + const su2double Laminar_Viscosity_Inlet = FluidModel->GetLaminarViscosity(); + const su2double* Turb_Properties = config->GetInlet_TurbVal(config->GetMarker_All_TagBound(val_marker)); + const su2double Intensity = Turb_Properties[0]; + const su2double viscRatio = Turb_Properties[1]; + const su2double VelMag2 = GeometryToolbox::SquaredNorm(nDim, Velocity_Inlet); + + Inlet_Vars[0] = 3.0 / 2.0 * (VelMag2 * pow(Intensity, 2)); + Inlet_Vars[1] = Density_Inlet * Inlet_Vars[0] / (Laminar_Viscosity_Inlet * viscRatio); + } + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, Inlet_Vars[iVar]); - /*--- Compute the residual using an upwind scheme ---*/ - - auto residual = conv_numerics->ComputeResidual(config); - LinSysRes.AddBlock(iPoint, residual); - - /*--- Jacobian contribution for implicit integration ---*/ - - if (implicit) Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i); - - // /*--- Viscous contribution, commented out because serious convergence problems ---*/ - // - // su2double Coord_Reflected[MAXNDIM]; - // GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), - // geometry->nodes->GetCoord(iPoint), Coord_Reflected); - // visc_numerics->SetCoord(geometry->nodes->GetCoord(iPoint), Coord_Reflected); - // visc_numerics->SetNormal(Normal); - // - // /*--- Conservative variables w/o reconstruction ---*/ - // - // visc_numerics->SetPrimitive(V_domain, V_inlet); - // - // /*--- Turbulent variables w/o reconstruction, and its gradients ---*/ - // - // visc_numerics->SetScalarVar(Solution_i, Solution_j); - // visc_numerics->SetScalarVarGradient(node[iPoint]->GetGradient(), node[iPoint]->GetGradient()); - // - // /*--- Menter's first blending function ---*/ - // - // visc_numerics->SetF1blending(node[iPoint]->GetF1blending(), node[iPoint]->GetF1blending()); - // - // /*--- Compute residual, and Jacobians ---*/ - // - // auto residual = visc_numerics->ComputeResidual(config); - // - // /*--- Subtract residual, and update Jacobians ---*/ - // - // LinSysRes.SubtractBlock(iPoint, residual); - // Jacobian.SubtractBlock2Diag(iPoint, residual.jacobian_i); + SetGhostPrimitives(iVertex, V_inlet); - } + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); + ghostSkip[iVertex] = false; } END_SU2_OMP_FOR + + /*--- The diffusive term causes serious convergence problems, so it stays off, as it did before. ---*/ + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; + + DispatchRegime(config, [&](auto tag) { + RunSST_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } -void CTurbSSTSolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, - CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { +void CTurbSSTSolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, + CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + EnsureGhostFlowContainers(solver_container, config); - /*--- Loop over all the vertices on this boundary marker ---*/ + auto* flowSolver = solver_container[FLOW_SOL]; SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { - const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); - /*--- Check if the node belongs to the domain (i.e., not a halo node) ---*/ - - if (geometry->nodes->GetDomain(iPoint)) { - - /*--- Allocate the value at the outlet ---*/ - - auto V_outlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, iVertex); + /*--- Neumann: the turbulent variable is copied from the interior before computing the flux. ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, nodes->GetSolution(iPoint, iVar)); - /*--- Retrieve solution at the farfield boundary node ---*/ + SetGhostPrimitives(iVertex, flowSolver->GetCharacPrimVar(val_marker, iVertex)); - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - /*--- Set various quantities in the solver class ---*/ - - conv_numerics->SetPrimitive(V_domain, V_outlet); - - /*--- Set the turbulent variables. Here we use a Neumann BC such - that the turbulent variable is copied from the interior of the - domain to the outlet before computing the residual. ---*/ - - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), - nodes->GetSolution(iPoint)); - - /*--- Set Normal (negate for outward convention) ---*/ - - su2double Normal[MAXNDIM] = {0.0}; - for (auto iDim = 0u; iDim < nDim; iDim++) - Normal[iDim] = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); + ghostSkip[iVertex] = false; + } + END_SU2_OMP_FOR - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), - geometry->nodes->GetGridVel(iPoint)); + /*--- The diffusive term causes serious convergence problems, so it stays off, as it did before. ---*/ + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; - if (conv_numerics->GetBoundedScalar()) { - const su2double* velocity = &V_outlet[prim_idx.Velocity()]; - const su2double density = solver_container[FLOW_SOL]->GetNodes()->GetDensity(iPoint); - conv_numerics->SetMassFlux(BoundedScalarBCFlux(iPoint, implicit, density, velocity, Normal)); - } + DispatchRegime(config, [&](auto tag) { + RunSST_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); +} - /*--- Compute the residual using an upwind scheme ---*/ - - auto residual = conv_numerics->ComputeResidual(config); - LinSysRes.AddBlock(iPoint, residual); - - /*--- Jacobian contribution for implicit integration ---*/ - - if (implicit) Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i); - -// /*--- Viscous contribution, commented out because serious convergence problems ---*/ -// -// su2double Coord_Reflected[MAXNDIM]; -// GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), -// geometry->nodes->GetCoord(iPoint), Coord_Reflected); -// visc_numerics->SetCoord(geometry->nodes->GetCoord(iPoint), Coord_Reflected); -// visc_numerics->SetNormal(Normal); -// -// /*--- Conservative variables w/o reconstruction ---*/ -// -// visc_numerics->SetPrimitive(V_domain, V_outlet); -// -// /*--- Turbulent variables w/o reconstruction, and its gradients ---*/ -// -// visc_numerics->SetScalarVar(Solution_i, Solution_j); -// visc_numerics->SetScalarVarGradient(node[iPoint]->GetGradient(), node[iPoint]->GetGradient()); -// -// /*--- Menter's first blending function ---*/ -// -// visc_numerics->SetF1blending(node[iPoint]->GetF1blending(), node[iPoint]->GetF1blending()); -// -// /*--- Compute residual, and Jacobians ---*/ -// -// auto residual = visc_numerics->ComputeResidual(config); -// -// /*--- Subtract residual, and update Jacobians ---*/ -// -// LinSysRes.SubtractBlock(iPoint, residual); -// Jacobian.SubtractBlock2Diag(iPoint, residual.jacobian_i); - } - } - END_SU2_OMP_FOR +template +void CTurbSSTSolver::RunSST_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit) { + if (nDim == 2) RunSST_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + else RunSST_Boundary(geometry, solver_container, config, opt, val_marker, implicit); } +template +void CTurbSSTSolver::RunSST_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit) { + BoundaryFluxResidual>(geometry, solver_container, config, opt, + val_marker, implicit); +} -void CTurbSSTSolver::BC_Inlet_MixingPlane(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, - CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { +void CTurbSSTSolver::BC_Inlet_MixingPlane(CGeometry *geometry, CSolver **solver_container, CNumerics*, + CNumerics*, CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + EnsureGhostFlowContainers(solver_container, config); + auto* flowSolver = solver_container[FLOW_SOL]; const auto nSpanWiseSections = config->GetnSpanWiseSections(); - /*--- Loop over all the vertices on this boundary marker ---*/ - - for (auto iSpan = 0u; iSpan < nSpanWiseSections ; iSpan++){ - - const auto extAverageKine = solver_container[FLOW_SOL]->GetMixingState(val_marker, iSpan, 6); - const auto extAverageOmega = solver_container[FLOW_SOL]->GetMixingState(val_marker, iSpan, 7); - su2double solution_j[] = {extAverageKine, extAverageOmega}; + /*--- The span loop below reaches a vertex of val_marker through GetOldVertex, which need not + * cover every one of them, so every vertex starts skipped and only the ones actually filled + * are cleared. ---*/ + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) ghostSkip[iVertex] = true; + END_SU2_OMP_FOR - /*--- Loop over all the vertices on this boundary marker ---*/ + for (auto iSpan = 0u; iSpan < nSpanWiseSections; iSpan++){ + const auto extAverageKine = flowSolver->GetMixingState(val_marker, iSpan, 6); + const auto extAverageOmega = flowSolver->GetMixingState(val_marker, iSpan, 7); + const su2double solution_j[] = {extAverageKine, extAverageOmega}; SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->GetnVertexSpan(val_marker,iSpan); iVertex++) { @@ -851,103 +772,82 @@ void CTurbSSTSolver::BC_Inlet_MixingPlane(CGeometry *geometry, CSolver **solver_ /*--- Index of the closest interior node ---*/ const auto Point_Normal = geometry->vertex[val_marker][oldVertex]->GetNormal_Neighbor(); - /*--- Normal vector for this vertex (negate for outward convention) ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(oldVertex, iVar, solution_j[iVar]); - su2double Normal[MAXNDIM] = {0.0}; - for (auto iDim = 0u; iDim < nDim; iDim++) - Normal[iDim] = -geometry->vertex[val_marker][oldVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); + SetGhostPrimitives(oldVertex, flowSolver->GetCharacPrimVar(val_marker, oldVertex)); - /*--- Allocate the value at the inlet ---*/ - auto V_inlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, oldVertex); - - /*--- Retrieve solution at the farfield boundary node ---*/ - - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); - - /*--- Set various quantities in the solver class ---*/ - - conv_numerics->SetPrimitive(V_domain, V_inlet); - - /*--- Set the turbulent variable states (prescribed for an inflow) ---*/ - - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), solution_j); - - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), - geometry->nodes->GetGridVel(iPoint)); - - /*--- Compute the residual using an upwind scheme ---*/ - auto conv_residual = conv_numerics->ComputeResidual(config); - - /*--- Jacobian contribution for implicit integration ---*/ - LinSysRes.AddBlock(iPoint, conv_residual); - if (implicit) Jacobian.AddBlock2Diag(iPoint, conv_residual.jacobian_i); + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(oldVertex, iDim) = -geometry->vertex[val_marker][oldVertex]->GetNormal(iDim); - /*--- Viscous contribution ---*/ + /*--- Reflected coordinate, read by the diffusion term's gradient projection. ---*/ su2double Coord_Reflected[MAXNDIM]; GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), geometry->nodes->GetCoord(iPoint), Coord_Reflected); - visc_numerics->SetCoord(geometry->nodes->GetCoord(iPoint), Coord_Reflected); - visc_numerics->SetNormal(Normal); - - /*--- Conservative variables w/o reconstruction ---*/ - visc_numerics->SetPrimitive(V_domain, V_inlet); - - /*--- Turbulent variables w/o reconstruction, and its gradients ---*/ - visc_numerics->SetScalarVar(nodes->GetSolution(iPoint), solution_j); - visc_numerics->SetScalarVarGradient(nodes->GetGradient(iPoint), nodes->GetGradient(iPoint)); + for (auto iDim = 0u; iDim < nDim; iDim++) ghostCoord(oldVertex, iDim) = Coord_Reflected[iDim]; - /*--- Menter's first blending function ---*/ - visc_numerics->SetF1blending(nodes->GetF1blending(iPoint), nodes->GetF1blending(iPoint)); - - /*--- Compute residual, and Jacobians ---*/ - auto visc_residual = visc_numerics->ComputeResidual(config); - - /*--- Subtract residual, and update Jacobians ---*/ - LinSysRes.SubtractBlock(iPoint, visc_residual); - if (implicit) Jacobian.SubtractBlock2Diag(iPoint, visc_residual.jacobian_i); + /*--- The diffusion term reads the ghost gradient and F1; mirror the interior ones, as before. ---*/ + auto ghostGrad = ghostNodes->GetGradient(oldVertex); + const auto interiorGrad = nodes->GetGradient(iPoint); + for (auto iVar = 0u; iVar < nVar; iVar++) + for (auto iDim = 0u; iDim < nDim; iDim++) ghostGrad(iVar, iDim) = interiorGrad(iVar, iDim); + ghostNodes->SetF1blending(oldVertex, nodes->GetF1blending(iPoint)); + ghostSkip[oldVertex] = false; } END_SU2_OMP_FOR } + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, false /*boundedScalar, this site never applied the mass-flux correction*/, + true /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; + + DispatchRegime(config, [&](auto tag) { + RunSST_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } -void CTurbSSTSolver::BC_Inlet_Turbo(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, - CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { +void CTurbSSTSolver::BC_Inlet_Turbo(CGeometry *geometry, CSolver **solver_container, CNumerics*, + CNumerics*, CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + EnsureGhostFlowContainers(solver_container, config); + auto* flowSolver = solver_container[FLOW_SOL]; const auto nSpanWiseSections = config->GetnSpanWiseSections(); /*--- Quantities for computing the kine and omega to impose at the inlet boundary. ---*/ - CFluidModel *FluidModel = solver_container[FLOW_SOL]->GetFluidModel(); + CFluidModel *FluidModel = flowSolver->GetFluidModel(); su2double Intensity = config->GetTurbulenceIntensity_FreeStream(); su2double viscRatio = config->GetTurb2LamViscRatio_FreeStream(); - for (auto iSpan = 0u; iSpan < nSpanWiseSections ; iSpan++){ + /*--- The span loop below reaches a vertex of val_marker through GetOldVertex, which need not + * cover every one of them, so every vertex starts skipped and only the ones actually filled + * are cleared. ---*/ + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) ghostSkip[iVertex] = true; + END_SU2_OMP_FOR + + for (auto iSpan = 0u; iSpan < nSpanWiseSections; iSpan++){ /*--- Compute the inflow kine and omega using the span wise averge quntities---*/ - su2double rho = solver_container[FLOW_SOL]->GetAverageDensity(val_marker, iSpan); - su2double pressure = solver_container[FLOW_SOL]->GetAveragePressure(val_marker, iSpan); - su2double kine = solver_container[FLOW_SOL]->GetAverageKine(val_marker, iSpan); + su2double rho = flowSolver->GetAverageDensity(val_marker, iSpan); + su2double pressure = flowSolver->GetAveragePressure(val_marker, iSpan); + su2double kine = flowSolver->GetAverageKine(val_marker, iSpan); FluidModel->SetTDState_Prho(pressure, rho); su2double muLam = FluidModel->GetLaminarViscosity(); - su2double VelMag2 = GeometryToolbox::SquaredNorm(nDim, - solver_container[FLOW_SOL]->GetAverageTurboVelocity(val_marker, iSpan)); + su2double VelMag2 = GeometryToolbox::SquaredNorm(nDim, flowSolver->GetAverageTurboVelocity(val_marker, iSpan)); su2double kine_b = 3.0/2.0*(VelMag2*Intensity*Intensity); su2double omega_b = rho*kine/(muLam*viscRatio); - su2double solution_j[] = {kine_b, omega_b}; - - /*--- Loop over all the vertices on this boundary marker ---*/ + const su2double solution_j[] = {kine_b, omega_b}; SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->GetnVertexSpan(val_marker,iSpan); iVertex++) { @@ -961,69 +861,160 @@ void CTurbSSTSolver::BC_Inlet_Turbo(CGeometry *geometry, CSolver **solver_contai /*--- Index of the closest interior node ---*/ const auto Point_Normal = geometry->vertex[val_marker][oldVertex]->GetNormal_Neighbor(); - /*--- Normal vector for this vertex (negate for outward convention) ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(oldVertex, iVar, solution_j[iVar]); + + SetGhostPrimitives(oldVertex, flowSolver->GetCharacPrimVar(val_marker, oldVertex)); - su2double Normal[MAXNDIM] = {0.0}; for (auto iDim = 0u; iDim < nDim; iDim++) - Normal[iDim] = -geometry->vertex[val_marker][oldVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); + ghostNormal(oldVertex, iDim) = -geometry->vertex[val_marker][oldVertex]->GetNormal(iDim); - /*--- Allocate the value at the inlet ---*/ - auto V_inlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, oldVertex); + /*--- Reflected coordinate, read by the diffusion term's gradient projection. ---*/ + su2double Coord_Reflected[MAXNDIM]; + GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), + geometry->nodes->GetCoord(iPoint), Coord_Reflected); + for (auto iDim = 0u; iDim < nDim; iDim++) ghostCoord(oldVertex, iDim) = Coord_Reflected[iDim]; - /*--- Retrieve solution at the farfield boundary node ---*/ + /*--- The diffusion term reads the ghost gradient and F1; mirror the interior ones, as before. ---*/ + auto ghostGrad = ghostNodes->GetGradient(oldVertex); + const auto interiorGrad = nodes->GetGradient(iPoint); + for (auto iVar = 0u; iVar < nVar; iVar++) + for (auto iDim = 0u; iDim < nDim; iDim++) ghostGrad(iVar, iDim) = interiorGrad(iVar, iDim); + ghostNodes->SetF1blending(oldVertex, nodes->GetF1blending(iPoint)); - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); + ghostSkip[oldVertex] = false; + } + END_SU2_OMP_FOR + } - /*--- Set various quantities in the solver class ---*/ + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, false /*boundedScalar, this site never applied the mass-flux correction*/, + true /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; - conv_numerics->SetPrimitive(V_domain, V_inlet); + DispatchRegime(config, [&](auto tag) { + RunSST_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); +} - /*--- Set the turbulent variable states. Use average span-wise values - values for the turbulent state at the inflow. ---*/ +void CTurbSSTSolver::BC_Fluid_Interface(CGeometry *geometry, CSolver **solver_container, CNumerics*, + CNumerics*, CConfig *config) { + SU2_ZONE_SCOPED - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), solution_j); + if (solver_container[FLOW_SOL] == nullptr) return; - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), - geometry->nodes->GetGridVel(iPoint)); + EnsureGhostFlowContainers(solver_container, config); - /*--- Compute the residual using an upwind scheme ---*/ - auto conv_residual = conv_numerics->ComputeResidual(config); + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions optConv{ + dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl*/, + }; + const ScalarFluxOptions optVisc{ + dynamic_grid, false /*boundedScalar, the mass-flux correction only applies with the convective term*/, + true /*correctGradient*/, false /*accurateJacobians*/, + false /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl*/, + }; - /*--- Jacobian contribution for implicit integration ---*/ - LinSysRes.AddBlock(iPoint, conv_residual); - if (implicit) Jacobian.AddBlock2Diag(iPoint, conv_residual.jacobian_i); + DispatchRegime(config, [&](auto tag) { + RunSST_FluidInterface(geometry, solver_container, config, optConv, optVisc, + implicit); + }); +} - /*--- Viscous contribution ---*/ - su2double Coord_Reflected[MAXNDIM]; - GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), - geometry->nodes->GetCoord(iPoint), Coord_Reflected); - visc_numerics->SetCoord(geometry->nodes->GetCoord(iPoint), Coord_Reflected); - visc_numerics->SetNormal(Normal); +template +void CTurbSSTSolver::RunSST_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, + bool implicit) { + if (nDim == 2) RunSST_FluidInterface(geometry, solver_container, config, optConv, optVisc, implicit); + else RunSST_FluidInterface(geometry, solver_container, config, optConv, optVisc, implicit); +} + +/*! + * \brief See RunSA_FluidInterface's note (CTurbSASolver.cpp): the convective term is a per-donor + * weighted average, computed in the same pass that fills the ghost row of each donor; the + * diffusive term is computed once per vertex, after the donor loop, from the ghost state + * the last donor left behind. This does not fit the fill-pass-then-BoundaryFluxResidual + * shape the other boundaries use, so it drives the CScalarFlux_SST kernel directly. + */ +template +void CTurbSSTSolver::RunSST_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, + bool implicit) { + using Scheme = CScalarFlux_SST; + const Scheme flux(*config); + + auto* flowSolver = solver_container[FLOW_SOL]; + auto* flowNodes = su2staticcast_p(flowSolver->GetNodes()); + const auto nPrimVar = flowSolver->GetnPrimVar(); + + const EdgeSide side_i{*nodes, flowNodes, CMatrixView(geometry->nodes->GetCoord()), + dynamic_grid ? CMatrixView(geometry->nodes->GetGridVel()) + : CMatrixView()}; + const EdgeSide side_j{*ghostNodes, ghostFlowNodes.get(), CMatrixView(ghostCoord), + side_i.gridVel}; - /*--- Conservative variables w/o reconstruction ---*/ - visc_numerics->SetPrimitive(V_domain, V_inlet); + su2activevector PrimVar_j(nPrimVar); - /*--- Turbulent variables w/o reconstruction, and its gradients ---*/ - visc_numerics->SetScalarVar(nodes->GetSolution(iPoint), solution_j); + for (auto iMarker = 0u; iMarker < config->GetnMarker_All(); iMarker++) { + if (config->GetMarker_All_KindBC(iMarker) != FLUID_INTERFACE) continue; - visc_numerics->SetScalarVarGradient(nodes->GetGradient(iPoint), nodes->GetGradient(iPoint)); + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (unsigned long iVertex = 0; iVertex < geometry->nVertex[iMarker]; iVertex++) { + const auto iPoint = geometry->vertex[iMarker][iVertex]->GetNode(); + if (!geometry->nodes->GetDomain(iPoint)) continue; + + const auto Point_Normal = geometry->vertex[iMarker][iVertex]->GetNormal_Neighbor(); + const auto nDonorVertex = GetnSlidingStates(iMarker, iVertex); + + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[iMarker][iVertex]->GetNormal(iDim); + const auto normal = gatherVariables(iVertex, ghostNormal); + + /*--- Loop over the donors and accumulate the weighted-average convective residual. ---*/ + for (auto jVertex = 0; jVertex < nDonorVertex; jVertex++) { + + for (auto iVar = 0u; iVar < nPrimVar; iVar++) + PrimVar_j[iVar] = flowSolver->GetSlidingState(iMarker, iVertex, iVar, jVertex); + + const su2double weight = flowSolver->GetSlidingState(iMarker, iVertex, nPrimVar, jVertex); - /*--- Menter's first blending function ---*/ - visc_numerics->SetF1blending(nodes->GetF1blending(iPoint), nodes->GetF1blending(iPoint)); + for (auto iVar = 0u; iVar < nVar; iVar++) + ghostNodes->SetSolution(iVertex, iVar, GetSlidingState(iMarker, iVertex, iVar, jVertex)); - /*--- Compute residual, and Jacobians ---*/ - auto visc_residual = visc_numerics->ComputeResidual(config); + SetGhostPrimitives(iVertex, PrimVar_j.data()); - /*--- Subtract residual, and update Jacobians ---*/ - LinSysRes.SubtractBlock(iPoint, visc_residual); - if (implicit) Jacobian.SubtractBlock2Diag(iPoint, visc_residual.jacobian_i); + su2double massFlux = 0.0; + if (optConv.boundedScalar) { + massFlux = BoundedScalarBCFlux(iPoint, true, flowNodes->GetDensity(iPoint), + &PrimVar_j[prim_idx.Velocity()], normal.data()); + } + + const auto res = flux.ComputeFlux(optConv, iPoint, side_i, iVertex, side_j, normal, massFlux); + + for (auto iVar = 0ul; iVar < res.nVar; ++iVar) LinSysRes(iPoint, iVar) += weight * res.flux_i(iVar); + if (implicit) Jacobian.AddBlock2Diag(iPoint, res.jac_ii, weight); + } + /*--- Diffusive term, computed once from the ghost state the last donor left behind. ---*/ + su2double Coord_Reflected[MAXNDIM]; + GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), + geometry->nodes->GetCoord(iPoint), Coord_Reflected); + for (auto iDim = 0u; iDim < nDim; iDim++) ghostCoord(iVertex, iDim) = Coord_Reflected[iDim]; + + auto ghostGrad = ghostNodes->GetGradient(iVertex); + const auto interiorGrad = nodes->GetGradient(iPoint); + for (auto iVar = 0u; iVar < nVar; iVar++) + for (auto iDim = 0u; iDim < nDim; iDim++) ghostGrad(iVar, iDim) = interiorGrad(iVar, iDim); + ghostNodes->SetF1blending(iVertex, nodes->GetF1blending(iPoint)); + + const auto res = flux.ComputeFlux(optVisc, iPoint, side_i, iVertex, side_j, normal, su2double(0.0)); + for (auto iVar = 0ul; iVar < res.nVar; ++iVar) LinSysRes(iPoint, iVar) += res.flux_i(iVar); + if (implicit) Jacobian.AddBlock2Diag(iPoint, res.jac_ii); } END_SU2_OMP_FOR } - } void CTurbSSTSolver::SetInletAtVertex(const su2double *val_inlet, From c5ba7f961c1ebd8a5f40dc2778ca742fc889a53e Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sat, 29 Aug 2026 09:52:24 -0700 Subject: [PATCH 12/20] Fix SST accurate-Jacobian AD registration/init bug, register F1 for AD, delete dead numerics CScalarFlux_SST::coefficients built its D_i/D_j matrices with `Matrix D_i = Double(0.0)`, which resolves to the sizing constructor (double -> size_t), not a scalar fill: it left D_i(0,1)/D_j(0,1) value-initialized rather than actually set from the argument, and does not compile at all under CODI_REVERSE_TYPE (no conversion from the active AD type to an index). Fixed by default-constructing then assigning the scalar. F1 blending was read through CVariable::GetF1blending(iPoint), a virtual call that bypassed gatherVariables and therefore never reached AD::SetPreaccIn, silently dropping d(flux)/d(F1) from the preaccumulated Jacobian block. Added a container-returning GetF1blending() (mirroring GetSolution()/GetGradient()) so the kernel reads F1 through gatherVariables like every other field. Deleted the now-unreferenced turb_diffusion.hpp (CAvgGrad_TurbSST was its last class) and the stale comment pointing at it; removed the unreachable NEMO branch from CTurbSolver::DispatchRegime, since a turbulence model is rejected for NEMO at configuration; added missing \ingroup tags to the new edge-flux kernels. Co-Authored-By: Claude Sonnet 5 --- .../numerics/turbulent/turb_diffusion.hpp | 163 ------------------ .../numerics/turbulent/turb_sa_edge_flux.hpp | 2 + .../numerics/turbulent/turb_sources.hpp | 2 +- .../numerics/turbulent/turb_sst_edge_flux.hpp | 45 ++--- SU2_CFD/include/solvers/CTurbSolver.hpp | 16 +- .../include/variables/CTurbSSTVariable.hpp | 5 + SU2_CFD/include/variables/CVariable.hpp | 10 ++ SU2_CFD/src/drivers/CDriver.cpp | 12 +- SU2_CFD/src/solvers/CTurbSASolver.cpp | 18 +- SU2_CFD/src/solvers/CTurbSSTSolver.cpp | 8 +- 10 files changed, 67 insertions(+), 214 deletions(-) delete mode 100644 SU2_CFD/include/numerics/turbulent/turb_diffusion.hpp diff --git a/SU2_CFD/include/numerics/turbulent/turb_diffusion.hpp b/SU2_CFD/include/numerics/turbulent/turb_diffusion.hpp deleted file mode 100644 index 76225bee26f..00000000000 --- a/SU2_CFD/include/numerics/turbulent/turb_diffusion.hpp +++ /dev/null @@ -1,163 +0,0 @@ -/*! - * \file turb_diffusion.hpp - * \brief Declarations of numerics classes for discretization of - * viscous fluxes in turbulence problems. - * \author F. Palacios, T. Economon - * \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 . - */ -#pragma once - -#include "../scalar/scalar_diffusion.hpp" - -/*! - * \class CAvgGrad_TurbSST - * \brief Class for computing viscous term using average of gradient with correction (Menter SST turbulence model). - * \ingroup ViscDiscr - * \author A. Bueno. - */ -template -class CAvgGrad_TurbSST final : public CAvgGrad_Scalar { -private: - using Base = CAvgGrad_Scalar; - using Base::Laminar_Viscosity_i; - using Base::Laminar_Viscosity_j; - using Base::Eddy_Viscosity_i; - using Base::Eddy_Viscosity_j; - using Base::Density_i; - using Base::Density_j; - using Base::ScalarVar_i; - using Base::ScalarVar_j; - using Base::Proj_Mean_GradScalarVar; - using Base::proj_vector_ij; - using Base::Flux; - using Base::Jacobian_i; - using Base::Jacobian_j; - - const su2double sigma_k1; /*!< \brief Constants for the viscous terms, k-w (1), k-eps (2)*/ - const su2double sigma_k2; - const su2double sigma_om1; - const su2double sigma_om2; - const bool use_accurate_jacobians; - - su2double F1_i, F1_j; /*!< \brief Menter's first blending function */ - - /*! - * \brief Adds any extra variables to AD - */ - void ExtraADPreaccIn() override { - AD::SetPreaccIn(F1_i, F1_j); - } - - /*! - * \brief SST specific steps in the ComputeResidual method - * \param[in] config - Definition of the particular problem. - */ - void FinishResidualCalc(const CConfig* config) override { - const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; - - /*--- Compute the blended constant for the viscous terms ---*/ - const su2double sigma_kine_i = F1_i*sigma_k1 + (1.0 - F1_i)*sigma_k2; - const su2double sigma_kine_j = F1_j*sigma_k1 + (1.0 - F1_j)*sigma_k2; - const su2double sigma_omega_i = F1_i*sigma_om1 + (1.0 - F1_i)*sigma_om2; - const su2double sigma_omega_j = F1_j*sigma_om1 + (1.0 - F1_j)*sigma_om2; - - /*--- Compute mean effective dynamic viscosity ---*/ - const su2double diff_i_kine = Laminar_Viscosity_i + sigma_kine_i*Eddy_Viscosity_i; - const su2double diff_j_kine = Laminar_Viscosity_j + sigma_kine_j*Eddy_Viscosity_j; - const su2double diff_i_omega = Laminar_Viscosity_i + sigma_omega_i*Eddy_Viscosity_i; - const su2double diff_j_omega = Laminar_Viscosity_j + sigma_omega_j*Eddy_Viscosity_j; - - const su2double diff_kine = 0.5*(diff_i_kine + diff_j_kine); - const su2double diff_omega_T1 = 0.5*(diff_i_omega + diff_j_omega); - - /*--- We aim to treat the cross-diffusion as a diffusion term rather than a source term. - * Re-writing the cross-diffusion contribution as λ/w ∇w ∇k, where λ = (2 (1- F1) ρ σ_ω2) - * and expanding using the product rule for divergence theorem gives: ∇(w λ/w ∇k) - w ∇(λ/w ∇k). - * Discretising using FVM, gives: (λ)_ij ∇k - w_c (λ/w)_ij ∇k. where w_c is the cell centre value ---*/ - - const su2double lambda_i = 2 * (1 - F1_i) * Density_i * sigma_omega_i; - const su2double lambda_j = 2 * (1 - F1_j) * Density_j * sigma_omega_j; - const su2double lambda_ij = 0.5 * (lambda_i + lambda_j); - const su2double w_ij = 0.5 * (ScalarVar_i[1] + ScalarVar_j[1]); - - const su2double diff_omega_T2 = lambda_ij; - - const su2double diff_omega_T3 = -ScalarVar_i[1] * lambda_ij/w_ij; - - Flux[0] = diff_kine*Proj_Mean_GradScalarVar[0]; - Flux[1] = diff_omega_T1*Proj_Mean_GradScalarVar[1] + (diff_omega_T2 + diff_omega_T3)*Proj_Mean_GradScalarVar[0]; - - /*--- For Jacobians -> Use of TSL (Thin Shear Layer) approx. to compute derivatives of the gradients ---*/ - if (implicit) { - const su2double proj_on_rho_i = proj_vector_ij/Density_i; - const su2double proj_on_rho_j = proj_vector_ij/Density_j; - Jacobian_i[0][0] = -diff_kine*proj_on_rho_i; - Jacobian_i[0][1] = 0.0; - Jacobian_i[1][0] = (diff_omega_T2+diff_omega_T3)*-proj_on_rho_i; - Jacobian_i[1][1] = -diff_omega_T1*proj_on_rho_i; - - Jacobian_j[0][0] = diff_kine*proj_on_rho_j; - Jacobian_j[0][1] = 0.0; - Jacobian_j[1][0] = (diff_omega_T2+diff_omega_T3)*proj_on_rho_j; - Jacobian_j[1][1] = diff_omega_T1*proj_on_rho_j; - - if (use_accurate_jacobians) { - Jacobian_i[0][0] = -diff_kine*proj_on_rho_i; - Jacobian_i[0][1] = 0.0; - Jacobian_i[1][0] = (diff_omega_T2 + diff_omega_T3)*-proj_on_rho_i; - Jacobian_i[1][1] = -proj_on_rho_i * diff_omega_T1 - 2*lambda_ij*ScalarVar_j[1]/pow(ScalarVar_i[1]+ScalarVar_j[1],2) * Proj_Mean_GradScalarVar[0]; - - Jacobian_j[0][0] = diff_kine*proj_on_rho_j; - Jacobian_j[0][1] = 0.0; - Jacobian_j[1][0] = (diff_omega_T2 + diff_omega_T3)*proj_on_rho_j; - Jacobian_j[1][1] = proj_on_rho_j * diff_omega_T1 + 2*lambda_ij*ScalarVar_i[1]/pow(ScalarVar_i[1]+ScalarVar_j[1],2) * Proj_Mean_GradScalarVar[0]; - } - } - } - -public: - /*! - * \brief Constructor of the class. - * \param[in] val_nDim - Number of dimensions of the problem. - * \param[in] val_nVar - Number of variables of the problem. - * \param[in] constants - Constants of the model. - * \param[in] correct_grad - Whether to correct gradient for skewness. - * \param[in] config - Definition of the particular problem. - */ - CAvgGrad_TurbSST(unsigned short val_nDim, unsigned short val_nVar, - const su2double* constants, bool correct_grad, const CConfig* config) - : CAvgGrad_Scalar(val_nDim, val_nVar, correct_grad, config), - sigma_k1(constants[0]), - sigma_k2(constants[1]), - sigma_om1(constants[2]), - sigma_om2(constants[3]), - use_accurate_jacobians(config->GetUse_Accurate_Turb_Jacobians()) { - } - - /*! - * \brief Sets value of first blending function. - */ - void SetF1blending(su2double val_F1_i, su2double val_F1_j) override { - F1_i = val_F1_i; F1_j = val_F1_j; - } -}; diff --git a/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp b/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp index 528787efc6c..318cbad2d1a 100644 --- a/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp +++ b/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp @@ -31,6 +31,8 @@ /*! * \class CScalarFlux_SA + * \ingroup ConvDiscr + * \ingroup ViscDiscr * \brief Convection and diffusion of the Spalart-Allmaras model, non-conservative and with a * diagonal (but asymmetric) diffusion coefficient. * \note SA writes its own convective term rather than using the inherited CUpwScalarFlux one, diff --git a/SU2_CFD/include/numerics/turbulent/turb_sources.hpp b/SU2_CFD/include/numerics/turbulent/turb_sources.hpp index 2c0cb51a8d2..9f72b6e37cc 100644 --- a/SU2_CFD/include/numerics/turbulent/turb_sources.hpp +++ b/SU2_CFD/include/numerics/turbulent/turb_sources.hpp @@ -1014,7 +1014,7 @@ class CSourcePieceWise_TurbSST final : public CNumerics { Residual[0] -= dk * Volume; Residual[1] -= dw * Volume; - /*--- Cross diffusion is included in the viscous fluxes, discretisation in turb_diffusion.hpp ---*/ + /*--- Cross diffusion is included in the viscous fluxes, not this source term. ---*/ /*--- Contribution due to 2D axisymmetric formulation ---*/ diff --git a/SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp b/SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp index a16abda5337..4f47a795ff4 100644 --- a/SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp +++ b/SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp @@ -31,11 +31,12 @@ /*! * \class CScalarFlux_SST + * \ingroup ViscDiscr * \brief Convection and diffusion of the Menter SST model, conservative with a coupled (but * neither symmetric nor diagonal) 2x2 diffusion matrix. * \note SST writes no finalizeFlux of its own: the inherited CUpwScalarFlux one is exactly * flux(iVar) = a0*rho_i*phi_i(iVar) + a1*rho_j*phi_j(iVar), Conservative weighting by - * density, which is the whole of the model's old convective term. + * density, which is the model's whole convective term. */ template class CScalarFlux_SST @@ -58,13 +59,11 @@ class CScalarFlux_SST public: /*! - * \brief Diffusion coefficients of both orientations of the edge, see CAvgGrad_TurbSST. - * \note The old discretization evaluates the diffusion numerics twice per edge, once with i - * first and once with j first (CScalarSolver::Viscous_Residual_NonCons), because the - * cross term below reads the transported omega of whichever point was passed first. That - * is exactly what returning two different matrices here reproduces: D.i, read by i's row, - * uses omega at i; D.j, read by j's row, uses omega at j. Every other entry is symmetric - * (an i/j average), so it is the same in both matrices. + * \brief Diffusion coefficients of both orientations of the edge. + * \note The cross term below reads the transported omega of whichever point its row is being + * written for, so it is not symmetric: D.i, read by i's row, uses omega at i; D.j, read + * by j's row, uses omega at j. Every other entry is an i/j average, so it is the same in + * both matrices. */ template FORCEINLINE CPair> coefficients(const FlowIndices& idx, Int iPoint, @@ -77,8 +76,8 @@ class CScalarFlux_SST const Double muT_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.EddyViscosity()); const Double muT_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.EddyViscosity()); - const Double F1_i = side_i.scalarNodes.GetF1blending(iPoint); - const Double F1_j = side_j.scalarNodes.GetF1blending(jPoint); + const Double F1_i = gatherVariables(iPoint, side_i.scalarNodes.GetF1blending()); + const Double F1_j = gatherVariables(jPoint, side_j.scalarNodes.GetF1blending()); const Double omega_i = gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), 1); const Double omega_j = gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), 1); @@ -95,13 +94,17 @@ class CScalarFlux_SST const Double lambda_ij = 0.5 * (lambda_i + lambda_j); const Double w_ij = 0.5 * (omega_i + omega_j); - /*--- Same two terms as CAvgGrad_TurbSST's old diff_omega_T2/diff_omega_T3, kept as the exact - * same two additions (not algebraically simplified) so this rounds identically. ---*/ + /*--- Cross-diffusion coefficient: a divergence-theorem term (diff_omega_T2) plus a cell + * centre correction (diff_omega_T3) that reads the transported omega of the row's own point. ---*/ const Double diff_omega_T2 = lambda_ij; const Double diff_omega_T3_i = -omega_i * lambda_ij / w_ij; const Double diff_omega_T3_j = -omega_j * lambda_ij / w_ij; - Matrix D_i = Double(0.0), D_j = Double(0.0); + /*--- D_i(0,1) and D_j(0,1) are left zero: there is no diffusive coupling from omega into + * the k row. ---*/ + Matrix D_i, D_j; + D_i = Double(0.0); + D_j = Double(0.0); D_i(0, 0) = diff_kine; D_i(1, 1) = diff_omega; D_i(1, 0) = diff_omega_T2 + diff_omega_T3_i; @@ -115,12 +118,12 @@ class CScalarFlux_SST /*! * \brief Extra Jacobian terms from the dependence of the cross-diffusion coefficient on omega. - * \note Unlike SA's, this correction is not a per-edge constant: it comes out of the same - * twice-per-edge evaluation coefficients() reproduces, so it lands on all four Jacobian - * blocks rather than mirroring the D.i/D.j split (jac_ii and jac_ji share one term, jac_ij - * and jac_jj the other) -- worked out by hand against the old i->j / j->i pair of - * Viscous_Residual_NonCons calls, not read off the single-evaluation shape most other - * models have. + * \note diff_omega_T3_i and diff_omega_T3_j both depend on omega_i and omega_j through w_ij, so + * each of the four blocks needs a correction beyond the one diffusionTerms already applies + * through projGrad. The correction only depends on which point's omega is being + * differentiated against, not on which row it lands in: differentiating against omega_i + * gives +E_j in both jac_ii and jac_ji, differentiating against omega_j gives -E_i in both + * jac_ij and jac_jj. */ template FORCEINLINE void coefficientJacobians(const FlowIndices& idx, Int iPoint, const EdgeSide& side_i, @@ -128,8 +131,8 @@ class CScalarFlux_SST const Vector& projGrad, EdgeResidual& res) const { const Double rho_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); const Double rho_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); - const Double F1_i = side_i.scalarNodes.GetF1blending(iPoint); - const Double F1_j = side_j.scalarNodes.GetF1blending(jPoint); + const Double F1_i = gatherVariables(iPoint, side_i.scalarNodes.GetF1blending()); + const Double F1_j = gatherVariables(jPoint, side_j.scalarNodes.GetF1blending()); const Double omega_i = gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), 1); const Double omega_j = gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), 1); diff --git a/SU2_CFD/include/solvers/CTurbSolver.hpp b/SU2_CFD/include/solvers/CTurbSolver.hpp index 078ec2f8366..d438ff6edd0 100644 --- a/SU2_CFD/include/solvers/CTurbSolver.hpp +++ b/SU2_CFD/include/solvers/CTurbSolver.hpp @@ -31,7 +31,6 @@ #include "../variables/CTurbVariable.hpp" #include "../variables/CEulerVariable.hpp" #include "../variables/CIncEulerVariable.hpp" -#include "../variables/CNEMOEulerVariable.hpp" #include "../../../Common/include/parallelization/omp_structure.hpp" /*! @@ -57,20 +56,19 @@ class CTurbSolver : public CScalarSolver { vector Inlet_TurbVars; /*!< \brief Turbulence variables at inlet profiles */ /*! - * \brief Resolve the compile-time flow indices from the regime/NEMO flags of config, and call f - * with a CIndicesTag of the result: f is a generic lambda, `[&](auto tag){ using Indices - * = typename decltype(tag)::type; ... }`. Shared by every turbulence model's boundary + * \brief Resolve the compile-time flow indices from the regime flag of config, and call f with + * a CIndicesTag of the result: f is a generic lambda, `[&](auto tag){ using Indices = + * typename decltype(tag)::type; ... }`. Shared by every turbulence model's boundary * dispatch (RunSA/RunSA_Boundary/RunSA_FluidInterface and their SST counterparts), which - * would otherwise each repeat this same three-way branch. Header-defined (not just - * declared) because it is a template with a deduced, unnameable lambda type, called from - * more than one translation unit (CTurbSASolver.cpp, CTurbSSTSolver.cpp). + * would otherwise each repeat this same branch. Header-defined (not just declared) + * because it is a template with a deduced, unnameable lambda type, called from more than + * one translation unit (CTurbSASolver.cpp, CTurbSSTSolver.cpp). NEMO is not one of the + * branches: a turbulence model is rejected for it at configuration. */ template static void DispatchRegime(const CConfig* config, F&& f) { if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { f(CIndicesTag>{}); - } else if (config->GetNEMOProblem()) { - f(CIndicesTag>{}); } else { f(CIndicesTag>{}); } diff --git a/SU2_CFD/include/variables/CTurbSSTVariable.hpp b/SU2_CFD/include/variables/CTurbSSTVariable.hpp index 45c6559f9aa..0ec4abe466e 100644 --- a/SU2_CFD/include/variables/CTurbSSTVariable.hpp +++ b/SU2_CFD/include/variables/CTurbSSTVariable.hpp @@ -83,6 +83,11 @@ class CTurbSSTVariable final : public CTurbVariable { */ inline void SetF1blending(unsigned long iPoint, su2double val) override { F1(iPoint) = val; } + /*! + * \brief Container backing GetF1blending/SetF1blending (see CVariable's note). + */ + inline const VectorType& GetF1blending() const override { return F1; } + /*! * \brief Get the second blending function. */ diff --git a/SU2_CFD/include/variables/CVariable.hpp b/SU2_CFD/include/variables/CVariable.hpp index 649fc581b80..41811da2dc6 100644 --- a/SU2_CFD/include/variables/CVariable.hpp +++ b/SU2_CFD/include/variables/CVariable.hpp @@ -1717,6 +1717,16 @@ class CVariable { */ inline virtual void SetF1blending(unsigned long iPoint, su2double val) {} + /*! + * \brief Container backing GetF1blending/SetF1blending, for the edge-flux kernels to read + * through gatherVariables the way they do GetSolution and GetGradient, rather than one + * virtual call per point. + */ + inline virtual const VectorType& GetF1blending() const { + static const VectorType empty; + return empty; + } + /*! * \brief Get the second blending function of the SST model. */ diff --git a/SU2_CFD/src/drivers/CDriver.cpp b/SU2_CFD/src/drivers/CDriver.cpp index 1a4f0e8923a..c3f2c74a21a 100644 --- a/SU2_CFD/src/drivers/CDriver.cpp +++ b/SU2_CFD/src/drivers/CDriver.cpp @@ -1222,10 +1222,9 @@ void CDriver::InstantiateTurbulentNumerics(unsigned short nVar_Turb, int offset, omega_Inf = turb_solver->GetOmega_Inf(); } - /*--- Definition of the convective scheme for each equation and mesh level. Both SA and SST now - * drive their interior loop through their own CScalarFlux_* edge kernel (see CTurbSASolver, - * CTurbSSTSolver), so conv_term is never set here; the switch stays only for the - * NO_CONVECTIVE error check. ---*/ + /*--- Both SA and SST drive their interior loop through their own CScalarFlux_* edge kernel + * (see CTurbSASolver, CTurbSSTSolver), so conv_term is never set here; this switch only checks + * the config value. ---*/ switch (config->GetKind_ConvNumScheme_Turb()) { case NO_CONVECTIVE: @@ -1252,9 +1251,8 @@ void CDriver::InstantiateTurbulentNumerics(unsigned short nVar_Turb, int offset, numerics[iMGlevel][TURB_SOL][source_second_term] = new CSourceNothing(nDim, nVar_Turb, config); } - /*--- Definition of the boundary condition method. Both SA and SST drive their boundaries - * through their own CScalarFlux_* edge kernel, so neither needs conv_bound_term/visc_bound_term - * here. ---*/ + /*--- Both SA and SST drive their boundaries through their own CScalarFlux_* edge kernel, so + * neither needs conv_bound_term/visc_bound_term here. ---*/ } /*--- Explicit instantiation of the template above, needed because it is defined in a cpp file, instead of hpp. * NEMO has no explicit instantiation: NEMO with a turbulence model is rejected at configuration. ---*/ diff --git a/SU2_CFD/src/solvers/CTurbSASolver.cpp b/SU2_CFD/src/solvers/CTurbSASolver.cpp index 6543ed2c287..bcc5a22d58d 100644 --- a/SU2_CFD/src/solvers/CTurbSASolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSASolver.cpp @@ -754,7 +754,7 @@ void CTurbSASolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, C } END_SU2_OMP_FOR - /*--- The diffusive term causes serious convergence problems, so it stays off, as it did before. ---*/ + /*--- The diffusive term causes serious convergence problems, so it stays off. ---*/ const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, @@ -790,7 +790,7 @@ void CTurbSASolver::BC_Engine_Inflow(CGeometry *geometry, CSolver **solver_conta } END_SU2_OMP_FOR - /*--- The diffusive term causes serious convergence problems, so it stays off, as it did before. ---*/ + /*--- The diffusive term causes serious convergence problems, so it stays off. ---*/ const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, @@ -824,7 +824,7 @@ void CTurbSASolver::BC_Engine_Exhaust(CGeometry *geometry, CSolver **solver_cont } END_SU2_OMP_FOR - /*--- The diffusive term causes serious convergence problems, so it stays off, as it did before. ---*/ + /*--- The diffusive term causes serious convergence problems, so it stays off. ---*/ const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, @@ -919,7 +919,7 @@ void CTurbSASolver::BC_ActDisk(CGeometry *geometry, CSolver **solver_container, } END_SU2_OMP_FOR - /*--- The diffusive term causes serious convergence problems, so it stays off, as it did before. ---*/ + /*--- The diffusive term causes serious convergence problems, so it stays off. ---*/ const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, @@ -989,7 +989,7 @@ void CTurbSASolver::BC_Inlet_MixingPlane(CGeometry *geometry, CSolver **solver_c const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ - dynamic_grid, false /*boundedScalar, this site never applied the mass-flux correction*/, + dynamic_grid, false /*boundedScalar, the mass-flux correction does not apply at this boundary*/, true /*correctGradient*/, false /*accurateJacobians*/, true /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; @@ -1069,7 +1069,7 @@ void CTurbSASolver::BC_Inlet_Turbo(CGeometry *geometry, CSolver **solver_contain const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ - dynamic_grid, false /*boundedScalar, this site never applied the mass-flux correction*/, + dynamic_grid, false /*boundedScalar, the mass-flux correction does not apply at this boundary*/, true /*correctGradient*/, false /*accurateJacobians*/, true /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; @@ -1123,9 +1123,9 @@ void CTurbSASolver::RunSA_FluidInterface(CGeometry* geometry, CSolver** solver_c /*! * \brief The convective term is a per-donor weighted average, computed in the same pass that * fills the ghost row of each donor; the diffusive term is computed once per vertex, after - * the donor loop, from the ghost state the last donor left behind -- the discretization the - * solver had before this migration. This does not fit the fill-pass-then-BoundaryFluxResidual - * shape the other boundaries use, so it drives the CScalarFlux_SA kernel directly. + * the donor loop, from the ghost state the last donor left behind. This does not fit the + * fill-pass-then-BoundaryFluxResidual shape the other boundaries use, so it drives the + * CScalarFlux_SA kernel directly. */ template void CTurbSASolver::RunSA_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, diff --git a/SU2_CFD/src/solvers/CTurbSSTSolver.cpp b/SU2_CFD/src/solvers/CTurbSSTSolver.cpp index d4971785a7d..3e4c5e38246 100644 --- a/SU2_CFD/src/solvers/CTurbSSTSolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSSTSolver.cpp @@ -676,7 +676,7 @@ void CTurbSSTSolver::BC_Inlet(CGeometry *geometry, CSolver **solver_container, C } END_SU2_OMP_FOR - /*--- The diffusive term causes serious convergence problems, so it stays off, as it did before. ---*/ + /*--- The diffusive term causes serious convergence problems, so it stays off. ---*/ const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, @@ -712,7 +712,7 @@ void CTurbSSTSolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, } END_SU2_OMP_FOR - /*--- The diffusive term causes serious convergence problems, so it stays off, as it did before. ---*/ + /*--- The diffusive term causes serious convergence problems, so it stays off. ---*/ const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, @@ -799,7 +799,7 @@ void CTurbSSTSolver::BC_Inlet_MixingPlane(CGeometry *geometry, CSolver **solver_ const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ - dynamic_grid, false /*boundedScalar, this site never applied the mass-flux correction*/, + dynamic_grid, false /*boundedScalar, the mass-flux correction does not apply at this boundary*/, true /*correctGradient*/, false /*accurateJacobians*/, true /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; @@ -888,7 +888,7 @@ void CTurbSSTSolver::BC_Inlet_Turbo(CGeometry *geometry, CSolver **solver_contai const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; const ScalarFluxOptions opt{ - dynamic_grid, false /*boundedScalar, this site never applied the mass-flux correction*/, + dynamic_grid, false /*boundedScalar, the mass-flux correction does not apply at this boundary*/, true /*correctGradient*/, false /*accurateJacobians*/, true /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, }; From 21d113da931fbd2e4a31595091023ff59f1e5b4d Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sat, 29 Aug 2026 19:29:33 -0700 Subject: [PATCH 13/20] LM transition model as a third-layer scalar flux, on CTurbSolver's shared dispatch Migrates CTransLMSolver onto the generic scalar-transport edge-flux framework, the same target shape already used by SA and SST. LM is the simplest of the three: conservative convection identical in shape to SST's (CUpwSca_TransLM was previously a type alias of CUpwSca_TurbSST for exactly this reason), and a diagonal, i/j-symmetric diffusion matrix whose coefficients (sigma_f=1, sigma_theta=2, scaling mu+mu_t) depend only on the flow's viscosity/eddy-viscosity, not on the transported gamma/Re_theta, so unlike SA and SST it needs no coefficientJacobians override at all. CTransLMSolver::Upwind_Residual now drives EdgeFluxResidual directly (replacing Viscous_Residual/Viscous_Residual_impl); BC_HeatFlux_Wall and BC_Inlet were rewritten on the ghost-container/BoundaryFluxResidual pattern. BC_Outlet previously forwarded entirely to the still-old CScalarSolver::BC_Far_Field; that base method is not itself migrated, so this uncovered that any MARKER_FAR boundary (LM's own test case included) crashed once CDriver.cpp's old conv_bound_term/visc_bound_term instances were removed, since the base BC_Far_Field needs a conv_numerics object that no longer exists. Fixed by giving CTransLMSolver its own BC_Far_Field override (mirroring CTurbSASolver's own, already-migrated one) and having BC_Outlet forward to it, matching the pre-migration call structure. Two of the three hand-rolled boundaries (BC_HeatFlux_Wall, BC_Inlet) never called the old SetMassFlux/BoundedScalarBCFlux correction, so they get boundedScalar=false; BC_Far_Field's base implementation did call it, so it and the interior loop both use config->GetBounded_Turb(). Known gap, left undone because no local test exercises it: LM has never had its own BC_Fluid_Interface (it relied on CScalarSolver's old-style BC_Fluid_Interface_impl, which still needs conv_numerics/visc_numerics); LM combined with a FLUID_INTERFACE marker will hit the same class of crash BC_Far_Field just did. Migrating it properly needs a real test case to verify against, which the repository does not have. Verification: unit tests pass; TestCases/rans/s809 (SA+LM, MARKER_FAR) run against a pre-migration baseline binary at 20 and 300 iterations. Console-reported rms residual histories are identical at both lengths. The full field dump is not bit-identical (mean relative difference among differing values ~2e-7, worst case a near-zero-denominator artifact) and this survives deliberately matching the old code's separately-doubled Re_theta-coefficient operation order with zero change in the discrepancy -- the same signature already found and accepted for SST, pointing at -ffast-math-permitted reassociation rather than a formula error. Given LM's diffusion coefficients are considerably simpler than SST's (no cross term, no reciprocal), this was checked more skeptically than SST's case before being accepted. CDriver.cpp's InstantiateTransitionNumerics no longer instantiates CUpwSca_TransLM/CAvgGrad_TransLM (only CSourcePieceWise_TransLM remains); the old trans_convection.hpp/trans_diffusion.hpp includes are dropped as they're now unused there. The classes themselves are not deleted, per the design doc's step 9 ordering (deferred to the end, alongside SA_Neg). Co-Authored-By: Claude Sonnet 5 --- .../turbulent/transition/trans_edge_flux.hpp | 81 +++++++ SU2_CFD/include/solvers/CTransLMSolver.hpp | 52 +++- SU2_CFD/src/drivers/CDriver.cpp | 30 +-- SU2_CFD/src/solvers/CTransLMSolver.cpp | 226 ++++++++++-------- 4 files changed, 261 insertions(+), 128 deletions(-) create mode 100644 SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp diff --git a/SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp b/SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp new file mode 100644 index 00000000000..627aa28bd10 --- /dev/null +++ b/SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp @@ -0,0 +1,81 @@ +/*! + * \file trans_edge_flux.hpp + * \brief Langtry-Menter transition model as a third-layer scalar flux, see numerics/scalar/scalar_edge_flux.hpp. + * \author P. Gomes + * \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 . + */ + +#pragma once + +#include "../../scalar/scalar_edge_flux.hpp" + +/*! + * \class CScalarFlux_TransLM + * \ingroup ViscDiscr + * \brief Convection and diffusion of the Langtry-Menter transition model, conservative with a + * diagonal, i/j-symmetric diffusion matrix (unlike SST's, the coefficients only depend on + * the flow's mu/mu_t, not on the transported gamma/Re_theta, so no coefficientJacobians + * override is needed). + * \note LM writes no finalizeFlux of its own: the inherited CUpwScalarFlux one is exactly + * flux(iVar) = a0*rho_i*phi_i(iVar) + a1*rho_j*phi_j(iVar), Conservative weighting by + * density, which is the model's whole convective term (CUpwSca_TransLM was previously a + * type alias of CUpwSca_TurbSST for exactly this reason). + */ +template +class CScalarFlux_TransLM + : public CUpwScalarBase, FlowIndices, nDim, nVar> { + public: + static constexpr bool Conservative = true; + static constexpr bool DiagonalDiffusion = true; + + using Base = CUpwScalarBase; + using Int = typename Base::Int; + using Base::Base; + + /*! + * \brief Diffusion coefficients, an i/j average of (mu+mu_t) for intermittency and of + * 2*(mu+mu_t) for the momentum-thickness Reynolds number; identical for both edge sides. + * \note The Re_theta coefficient is kept as an average of two separately-doubled terms, matching + * the old CAvgGrad_TransLM::FinishResidualCalc's exact operation order, rather than the + * algebraically-equivalent "2 * the gamma coefficient". + */ + template + FORCEINLINE CPair> coefficients(const FlowIndices& idx, Int iPoint, + const EdgeSide& side_i, Int jPoint, + const EdgeSide& side_j) const { + const Double mu_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.LaminarViscosity()); + const Double mu_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.LaminarViscosity()); + const Double muT_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.EddyViscosity()); + const Double muT_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.EddyViscosity()); + + const Double diff_i_gamma = mu_i + muT_i; + const Double diff_j_gamma = mu_j + muT_j; + const Double diff_i_ReThetaT = 2.0 * (mu_i + muT_i); + const Double diff_j_ReThetaT = 2.0 * (mu_j + muT_j); + + Vector D; + D(0) = 0.5 * (diff_i_gamma + diff_j_gamma); + D(1) = 0.5 * (diff_i_ReThetaT + diff_j_ReThetaT); + return {D, D}; + } +}; diff --git a/SU2_CFD/include/solvers/CTransLMSolver.hpp b/SU2_CFD/include/solvers/CTransLMSolver.hpp index 3ca62dfb9db..5798cd41334 100644 --- a/SU2_CFD/include/solvers/CTransLMSolver.hpp +++ b/SU2_CFD/include/solvers/CTransLMSolver.hpp @@ -45,6 +45,28 @@ class CTransLMSolver final : public CTurbSolver { TransLMCorrelations TransCorrelations; + /*! + * \brief Resolve the compile-time flow indices and dimension, and run the interior edge loop + * with the matching CScalarFlux_TransLM instantiation. nVar is fixed at 2 (gamma and + * Re_theta), so unlike SA's RunSA this dispatch has one axis fewer to resolve. + */ + template + void RunLM(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt); + + template + void RunLM(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt); + + /*! + * \brief Same dispatch as RunLM, for a boundary's call into BoundaryFluxResidual. + */ + template + void RunLM_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit); + + template + void RunLM_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit); + public: /*! * \overload @@ -90,16 +112,16 @@ class CTransLMSolver final : public CTurbSolver { unsigned short iMesh) override; /*! - * \brief Compute the viscous flux for the LM equation at a particular edge. - * \param[in] iEdge - Edge for which we want to compute the flux + * \brief Compute the spatial integration using the CScalarFlux_TransLM edge kernel, which + * computes and writes both the convective and the diffusive term of every edge. * \param[in] geometry - Geometrical definition of the problem. * \param[in] solver_container - Container vector with all the solutions. - * \param[in] numerics - Description of the numerical method. + * \param[in] numerics_container - Unused, kept only for the boundary conditions. * \param[in] config - Definition of the particular problem. - * \note Calls a generic implementation after defining a SolverSpecificNumerics object. + * \param[in] iMesh - Index of the mesh in multigrid computations. */ - void Viscous_Residual(const unsigned long iEdge, const CGeometry* geometry, CSolver** solver_container, - CNumerics* numerics, const CConfig* config) override; + void Upwind_Residual(CGeometry* geometry, CSolver** solver_container, CNumerics** numerics_container, + CConfig* config, unsigned short iMesh) override; /*! * \brief Source term computation. @@ -193,6 +215,24 @@ class CTransLMSolver final : public CTurbSolver { CConfig *config, unsigned short val_marker) override; + /*! + * \brief Impose the far-field boundary condition, via the CScalarFlux_TransLM edge kernel. Also + * used by BC_Outlet, matching this solver's pre-migration behavior of imposing the + * far-field state at both MARKER_FAR and MARKER_OUTLET. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] solver_container - Container vector with all the solutions. + * \param[in] conv_numerics - Unused, kept only for the boundary condition dispatch. + * \param[in] visc_numerics - Unused, kept only for the boundary condition dispatch. + * \param[in] config - Definition of the particular problem. + * \param[in] val_marker - Surface marker where the boundary condition is applied. + */ + void BC_Far_Field(CGeometry *geometry, + CSolver **solver_container, + CNumerics *conv_numerics, + CNumerics *visc_numerics, + CConfig *config, + unsigned short val_marker) override; + /*! * \brief Get the value of the intermittency. * \return Value of the turbulent kinetic energy. diff --git a/SU2_CFD/src/drivers/CDriver.cpp b/SU2_CFD/src/drivers/CDriver.cpp index c3f2c74a21a..b78376d8ff7 100644 --- a/SU2_CFD/src/drivers/CDriver.cpp +++ b/SU2_CFD/src/drivers/CDriver.cpp @@ -77,8 +77,6 @@ #include "../../include/numerics/scalar/scalar_diffusion.hpp" #include "../../include/numerics/scalar/scalar_sources.hpp" #include "../../include/numerics/turbulent/turb_sources.hpp" -#include "../../include/numerics/turbulent/transition/trans_convection.hpp" -#include "../../include/numerics/turbulent/transition/trans_diffusion.hpp" #include "../../include/numerics/turbulent/transition/trans_sources.hpp" #include "../../include/numerics/species/species_convection.hpp" #include "../../include/numerics/species/species_diffusion.hpp" @@ -1265,39 +1263,26 @@ template void CDriver::InstantiateTurbulentNumerics void CDriver::InstantiateTransitionNumerics(unsigned short nVar_Trans, int offset, const CConfig *config, const CSolver* trans_solver, CNumerics ****&numerics) const { - const int conv_term = CONV_TERM + offset; - const int visc_term = VISC_TERM + offset; - const int source_first_term = SOURCE_FIRST_TERM + offset; const int source_second_term = SOURCE_SECOND_TERM + offset; - const int conv_bound_term = CONV_BOUND_TERM + offset; - const int visc_bound_term = VISC_BOUND_TERM + offset; - const bool LM = config->GetKind_Trans_Model() == TURB_TRANS_MODEL::LM; - /*--- Definition of the convective scheme for each equation and mesh level ---*/ + /*--- LM drives its interior loop and boundaries through its own CScalarFlux_TransLM edge kernel + * (see CTransLMSolver), so conv_term/visc_term/conv_bound_term/visc_bound_term are never set + * here; this switch only checks the config value. ---*/ switch (config->GetKind_ConvNumScheme_Turb()) { case NONE: SU2_MPI::Error("Config file is missing the CONV_NUM_METHOD_TURB option.", CURRENT_FUNCTION); break; case SPACE_UPWIND : - for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { - if (LM) numerics[iMGlevel][TRANS_SOL][conv_term] = new CUpwSca_TransLM(nDim, nVar_Trans, config); - } break; default: SU2_MPI::Error("Invalid convective scheme for the transition equations.", CURRENT_FUNCTION); break; } - /*--- Definition of the viscous scheme for each equation and mesh level ---*/ - - for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { - if (LM) numerics[iMGlevel][TRANS_SOL][visc_term] = new CAvgGrad_TransLM(nDim, nVar_Trans, true, config); - } - /*--- Definition of the source term integration scheme for each equation and mesh level ---*/ for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { @@ -1307,15 +1292,6 @@ void CDriver::InstantiateTransitionNumerics(unsigned short nVar_Trans, int offse numerics[iMGlevel][TRANS_SOL][source_second_term] = new CSourceNothing(nDim, nVar_Trans, config); } - - /*--- Definition of the boundary condition method ---*/ - - for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { - if (LM) { - numerics[iMGlevel][TRANS_SOL][conv_bound_term] = new CUpwSca_TransLM(nDim, nVar_Trans, config); - numerics[iMGlevel][TRANS_SOL][visc_bound_term] = new CAvgGrad_TransLM(nDim, nVar_Trans, false, config); - } - } } /*--- Explicit instantiation of the template above, needed because it is defined in a cpp file, instead of hpp. ---*/ template void CDriver::InstantiateTransitionNumerics>( diff --git a/SU2_CFD/src/solvers/CTransLMSolver.cpp b/SU2_CFD/src/solvers/CTransLMSolver.cpp index 0261e5e093f..52968ec8e10 100644 --- a/SU2_CFD/src/solvers/CTransLMSolver.cpp +++ b/SU2_CFD/src/solvers/CTransLMSolver.cpp @@ -26,9 +26,11 @@ */ #include "../../include/solvers/CTransLMSolver.hpp" +#include "../../include/solvers/CScalarSolver.inl" #include "../../include/variables/CTransLMVariable.hpp" #include "../../include/variables/CFlowVariable.hpp" #include "../../include/variables/CTurbSAVariable.hpp" +#include "../../include/numerics/turbulent/transition/trans_edge_flux.hpp" #include "../../../Common/include/parallelization/omp_structure.hpp" #include "../../../Common/include/toolboxes/geometry_toolbox.hpp" @@ -133,6 +135,13 @@ CTransLMSolver::CTransLMSolver(CGeometry *geometry, CConfig *config, unsigned sh nodes = new CTransLMVariable(Intermittency_Inf, ReThetaT_Inf, 1.0, 1.0, nPoint, nDim, nVar, config); SetBaseClassPointerToNodes(); + /*--- Ghost states for boundary conditions, sized to the largest marker (see BoundaryFluxResidual). ---*/ + unsigned long maxMarkerVertices = 0; + for (unsigned long iMarker = 0; iMarker < nMarker; iMarker++) + maxMarkerVertices = max(maxMarkerVertices, nVertex[iMarker]); + ghostNodes = make_unique(Intermittency_Inf, ReThetaT_Inf, 1.0, 1.0, maxMarkerVertices, nDim, nVar, + config); + /*--- MPI solution ---*/ InitiateComms(geometry, config, MPI_QUANTITIES::SOLUTION); @@ -182,6 +191,8 @@ void CTransLMSolver::Preprocessing(CGeometry *geometry, CSolver **solver_contain /*--- Upwind second order reconstruction and gradients ---*/ CommonPreprocessing(geometry, config, Output); + + EnsureGhostFlowContainers(solver_container, config); } void CTransLMSolver::Postprocessing(CGeometry *geometry, CSolver **solver_container, CConfig *config, unsigned short iMesh) { @@ -268,16 +279,37 @@ void CTransLMSolver::Postprocessing(CGeometry *geometry, CSolver **solver_contai } -void CTransLMSolver::Viscous_Residual(const unsigned long iEdge, const CGeometry* geometry, CSolver** solver_container, - CNumerics* numerics, const CConfig* config) { - - /*--- Define an object to set solver specific numerics contribution. ---*/ +void CTransLMSolver::Upwind_Residual(CGeometry* geometry, CSolver** solver_container, CNumerics** numerics_container, + CConfig* config, unsigned short iMesh) { + SU2_ZONE_SCOPED - auto SolverSpecificNumerics = [&](unsigned long iPoint, unsigned long jPoint) {}; + const ScalarFluxOptions opt{ + dynamic_grid, /*--- dynamicGrid ---*/ + config->GetBounded_Turb(), /*--- boundedScalar ---*/ + true, /*--- correctGradient ---*/ + false, /*--- accurateJacobians, LM's diffusion coefficient does not depend on gamma/Re_theta ---*/ + true, /*--- convective ---*/ + true, /*--- viscous ---*/ + false, /*--- oneSided, this is the interior loop ---*/ + config->GetMUSCL(), /*--- muscl ---*/ + }; + + DispatchRegime(config, [&](auto tag) { + RunLM(geometry, solver_container, config, opt); + }); +} - /*--- Now instantiate the generic implementation with the functor above. ---*/ +template +void CTransLMSolver::RunLM(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt) { + if (nDim == 2) RunLM(geometry, solver_container, config, opt); + else RunLM(geometry, solver_container, config, opt); +} - Viscous_Residual_impl(SolverSpecificNumerics, iEdge, geometry, solver_container, numerics, config); +template +void CTransLMSolver::RunLM(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt) { + EdgeFluxResidual>(geometry, solver_container, config, opt); } @@ -373,57 +405,36 @@ void CTransLMSolver::BC_HeatFlux_Wall(CGeometry *geometry, CSolver **solver_cont CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + EnsureGhostFlowContainers(solver_container, config); + auto* flowSolver = solver_container[FLOW_SOL]; + + /*--- The ghost row is the far-field state; wall-normal zero flux, convective only. ---*/ SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { + const auto* V_infty = flowSolver->GetCharacPrimVar(val_marker, iVertex); - const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); - - /*--- Check if the node belongs to the domain (i.e, not a halo node) ---*/ - - if (geometry->nodes->GetDomain(iPoint)) { - - /*--- Allocate the value at the infinity ---*/ - - auto V_infty = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, iVertex); - - /*--- Retrieve solution at the farfield boundary node ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, Solution_Inf[iVar]); - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); + SetGhostPrimitives(iVertex, V_infty); - conv_numerics->SetPrimitive(V_domain, V_infty); + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - /*--- Set turbulent variable at the wall, and at infinity ---*/ - - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), Solution_Inf); - - /*--- Set Normal (it is necessary to change the sign) ---*/ - /*--- It's mean wall normal zero flux. */ - - su2double Normal[MAXNDIM] = {0.0}; - for (auto iDim = 0u; iDim < nDim; iDim++) - Normal[iDim] = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); - - /*--- Grid Movement ---*/ - - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), - geometry->nodes->GetGridVel(iPoint)); - - /*--- Compute residuals and Jacobians ---*/ - - auto residual = conv_numerics->ComputeResidual(config); - - /*--- Add residuals and Jacobians ---*/ - - LinSysRes.AddBlock(iPoint, residual); - if (implicit) Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i); - } + ghostSkip[iVertex] = false; } END_SU2_OMP_FOR + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, false /*boundedScalar, this site never applied the mass-flux correction*/, + false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; + + DispatchRegime(config, [&](auto tag) { + RunLM_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } void CTransLMSolver::BC_Isothermal_Wall(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, @@ -434,79 +445,104 @@ void CTransLMSolver::BC_Isothermal_Wall(CGeometry *geometry, CSolver **solver_co } -void CTransLMSolver::BC_Inlet(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, CNumerics *visc_numerics, CConfig *config, +void CTransLMSolver::BC_Inlet(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); - /*--- Loop over all the vertices on this boundary marker ---*/ + EnsureGhostFlowContainers(solver_container, config); + + auto* flowSolver = solver_container[FLOW_SOL]; SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { + const auto* V_inlet = flowSolver->GetCharacPrimVar(val_marker, iVertex); + + /*--- Non-dimensionalize Inlet_TurbVars if Inlet-Files are used. ---*/ + su2double Inlet_Vars[MAXNVAR]; + Inlet_Vars[0] = Inlet_TurbVars[val_marker][iVertex][0]; + Inlet_Vars[1] = Inlet_TurbVars[val_marker][iVertex][1]; + if (config->GetInlet_Profile_From_File()) { + Inlet_Vars[0] /= pow(config->GetVelocity_Ref(), 2); + Inlet_Vars[1] *= config->GetViscosity_Ref() / (config->GetDensity_Ref() * pow(config->GetVelocity_Ref(), 2)); + } - const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); - - /*--- Check if the node belongs to the domain (i.e., not a halo node) ---*/ - - if (geometry->nodes->GetDomain(iPoint)) { - - /*--- Normal vector for this vertex (negate for outward convention) ---*/ - - su2double Normal[MAXNDIM] = {0.0}; - for (auto iDim = 0u; iDim < nDim; iDim++) - Normal[iDim] = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); - - /*--- Allocate the value at the inlet ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, Inlet_Vars[iVar]); - auto V_inlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, iVertex); + SetGhostPrimitives(iVertex, V_inlet); - /*--- Retrieve solution at the farfield boundary node ---*/ + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); + ghostSkip[iVertex] = false; + } + END_SU2_OMP_FOR - /*--- Set various quantities in the solver class ---*/ + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, false /*boundedScalar, this site never applied the mass-flux correction*/, + false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; - conv_numerics->SetPrimitive(V_domain, V_inlet); + DispatchRegime(config, [&](auto tag) { + RunLM_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); +} - /*--- Non-dimensionalize Inlet_TurbVars if Inlet-Files are used. ---*/ - su2double Inlet_Vars[MAXNVAR]; - Inlet_Vars[0] = Inlet_TurbVars[val_marker][iVertex][0]; - Inlet_Vars[1] = Inlet_TurbVars[val_marker][iVertex][1]; - if (config->GetInlet_Profile_From_File()) { - Inlet_Vars[0] /= pow(config->GetVelocity_Ref(), 2); - Inlet_Vars[1] *= config->GetViscosity_Ref() / (config->GetDensity_Ref() * pow(config->GetVelocity_Ref(), 2)); - } +void CTransLMSolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, + CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { + SU2_ZONE_SCOPED + BC_Far_Field(geometry, solver_container, conv_numerics, visc_numerics, config, val_marker); +} - /*--- Set the LM variable states. ---*/ - /*--- Load the inlet transition LM model variables (uniform by default). ---*/ +void CTransLMSolver::BC_Far_Field(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, + CConfig *config, unsigned short val_marker) { + SU2_ZONE_SCOPED - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), Inlet_Vars); + EnsureGhostFlowContainers(solver_container, config); - /*--- Set various other quantities in the solver class ---*/ + auto* flowSolver = solver_container[FLOW_SOL]; - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), - geometry->nodes->GetGridVel(iPoint)); + /*--- Ghost row is the far-field state; this base method also applied the mass-flux correction, + * unlike this solver's own hand-rolled BC_HeatFlux_Wall/BC_Inlet, so this one keeps it. ---*/ + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { + const auto* V_infty = flowSolver->GetCharacPrimVar(val_marker, iVertex); - /*--- Compute the residual using an upwind scheme ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, Solution_Inf[iVar]); - auto residual = conv_numerics->ComputeResidual(config); - LinSysRes.AddBlock(iPoint, residual); + SetGhostPrimitives(iVertex, V_infty); - /*--- Jacobian contribution for implicit integration ---*/ + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - if (implicit) Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i); - } + ghostSkip[iVertex] = false; } END_SU2_OMP_FOR + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; + + DispatchRegime(config, [&](auto tag) { + RunLM_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } -void CTransLMSolver::BC_Outlet(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, - CNumerics *visc_numerics, CConfig *config, unsigned short val_marker) { - SU2_ZONE_SCOPED - BC_Far_Field(geometry, solver_container, conv_numerics, visc_numerics, config, val_marker); +template +void CTransLMSolver::RunLM_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit) { + if (nDim == 2) RunLM_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + else RunLM_Boundary(geometry, solver_container, config, opt, val_marker, implicit); +} + +template +void CTransLMSolver::RunLM_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit) { + BoundaryFluxResidual>(geometry, solver_container, config, opt, + val_marker, implicit); } void CTransLMSolver::LoadRestart(CGeometry** geometry, CSolver*** solver, CConfig* config, int val_iter, From 466a4d1e3f7c137f564fc34b9653e621de1324e7 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sat, 29 Aug 2026 20:19:39 -0700 Subject: [PATCH 14/20] Species transport as a third-layer scalar flux, with runtime-sized (Dynamic) equations Migrates CSpeciesSolver onto the generic scalar-transport edge-flux framework, alongside SA/SST/LM. Species is the framework's first Dynamic- nVar consumer: the equation count is one per transported species, set at runtime from config rather than known at compile time, so CScalarFlux_Species passes nEqn explicitly to CUpwScalarBase and its coefficients() loops to it instead of a template nVar. This exposed a real gap in the framework's existing Dynamic scaffolding: CUpwScalarBase::ComputeFlux never called reconstruct for a Dynamic model at all (an `if constexpr (nVar != Dynamic)` skipped it unconditionally), silently downgrading MUSCL reconstruction to first order for any Dynamic model. Fixed by adding a runtime nVarGrad parameter throughout the reconstruct/musclUnlimited/musclPointLimited/ musclEdgeLimited chain in numerics/util.hpp (musclPointLimited also had to stop reading its limiter as a single Vector gather, since that requires nVarGrad as a compile-time template argument; it now reads one variable at a time like the diffusion gradient gather already does, same values, no behavior change for SA/SST/LM's compile-time callers -- confirmed via a bit-identical turb_ONERAM6 rerun). Physics: species convection is conservative and diagonal, identical in shape to SST's/LM's (no finalizeFlux override needed). Diffusion is diagonal and i/j-symmetric, an average of (rho * per-species mass diffusivity) plus a shared turbulent (mu_t/Sc_t) term when a turbulence model is active, matching CAvgGrad_Species::FinishResidualCalc's exact two-separate-averages operation order. The diffusivity is a per-node, per-species fluid-model output (not a function of the transported mass fraction inside the numerics kernel), so -- like LM -- no coefficientJacobians override is needed. Dispatch: CSpeciesSolver does not inherit CTurbSolver, so it cannot reuse CTurbSolver::DispatchRegime; CIndicesTag is promoted to CScalarSolver.hpp (the actual common ancestor) and CSpeciesSolver gets its own DispatchRegime, with a NEMO branch CTurbSolver's deliberately lacks (turbulence models are rejected for NEMO at configuration; species transport is not, and was already instantiated for NEMO indices pre-migration). Boundaries: BC_Inlet/BC_Outlet keep their strong-BC (Dirichlet, pure solution/Jacobian manipulation) branch untouched and migrate only the weak-BC branch to the ghost-container pattern, with boundedScalar = config->GetBounded_Species() (both sites called SetMassFlux pre-migration). BC_HeatFlux_Wall/BC_Isothermal_Wall are unchanged: BC_Wall_Generic never touched conv_numerics/visc_numerics to begin with. BC_Fluid_Interface previously routed through CScalarSolver's old-style BC_Fluid_Interface_impl; this could not be left as-is like it was deferred for LM, because a real local test (species_transport/multizone) exercises SPECIES+FLUID_INTERFACE and would have crashed once CDriver.cpp's old conv_bound_term/ visc_bound_term instantiation was removed (the same class of bug the SST migration's BC_Far_Field fix caught). Rewritten as RunSpecies_FluidInterface, mirroring RunSST_FluidInterface's donor-weighted-convection-then-single- diffusive-pass shape, including its one quirk: the diffusive term mirrors the interior point's own diffusivity into the ghost row for both sides of the edge (matching the pre-migration SolverSpecificNumerics functor, which likewise read nodes->GetDiffusivity(iPoint) for both i and j). CDriver.cpp's InstantiateSpeciesNumerics no longer instantiates CUpwSca_Species/CAvgGrad_Species (only CSourceAxisymmetric_Species/ CSourceNothing remain); species_convection.hpp/species_diffusion.hpp includes are dropped there as unused. The classes themselves are not deleted yet, per the design doc's step 9 ordering (deferred to the end, alongside SA_Neg). Verification: unit tests pass; SA's turb_ONERAM6 remains bit-identical (confirms the shared util.hpp/CScalarSolver.hpp changes are behavior- preserving for every already-migrated model). Four species configurations were run against a pre-migration baseline binary: a plain venturi mixing case (unbounded and bounded-scalar variants), SA+species (exercises the turbulent diffusivity branch), and the FLUID_INTERFACE multizone case. Console-reported rms histories are close but not bit-identical from iteration 1 (unlike SA/SST/LM, where the interior/boundary loops matched exactly) -- a few percent in log10 residual space, non-growing over 1000 iterations, with Avg_Species_0/Species_Variance diagnostics agreeing to ~1e-5 relative at that horizon; restart-field differences are ~1e-4 to 1e-6 relative away from near-zero-denominator points, the same class of -ffast-math-permitted-reassociation artifact already found and accepted for SST, checked here by hand-matching the diffusion coefficient's exact operation order with no change in the discrepancy. One genuine discrepancy, not explained away: the FLUID_INTERFACE multizone case, force-started cold (its config expects a restart this tree doesn't have) with RESTART_SOL flipped to NO, ran cleanly to 100 outer iterations on the pre-migration baseline binary but diverged to NaN around outer iteration 14 on the migrated one. Re-run from an actual warm restart (generated from the baseline binary's own cold-start output), both binaries complete 30 further outer iterations cleanly, with the species field agreeing to the same ~1e-4 relative band as the other cases. This localizes the cold-start divergence to sensitivity in this specific artificial starting point rather than a structural bug -- the config's own RESTART_SOL=YES default confirms it was never meant to be cold-started -- but it was not tracked down further, and is worth another look if this combination (species + FLUID_INTERFACE) misbehaves in CI. Co-Authored-By: Claude Sonnet 5 --- .../numerics/scalar/scalar_edge_flux.hpp | 9 +- .../numerics/species/species_edge_flux.hpp | 94 +++++ SU2_CFD/include/numerics/util.hpp | 38 +- SU2_CFD/include/solvers/CScalarSolver.hpp | 12 + SU2_CFD/include/solvers/CSpeciesSolver.hpp | 89 ++++- SU2_CFD/include/solvers/CTurbSolver.hpp | 11 - .../include/variables/CSpeciesVariable.hpp | 5 + SU2_CFD/src/drivers/CDriver.cpp | 24 +- SU2_CFD/src/solvers/CSpeciesSolver.cpp | 356 ++++++++++++------ 9 files changed, 459 insertions(+), 179 deletions(-) create mode 100644 SU2_CFD/include/numerics/species/species_edge_flux.hpp diff --git a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp index 23d0f5c3c15..717a8993b22 100644 --- a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp +++ b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp @@ -344,10 +344,15 @@ class CUpwScalarBase : public CUpwScalarFlux(iPoint, jPoint, vector_ij, side_i.scalarNodes.GetGradient_Reconstruction(), side_i.scalarNodes.GetLimiter(), limiterType, 0, phi, kappa, umusclRamp); + } else { + /*--- A dynamic model's equation count is only known at runtime, so the reconstructed + * width is passed as an argument instead of a template parameter. ---*/ + reconstruct(iPoint, jPoint, vector_ij, side_i.scalarNodes.GetGradient_Reconstruction(), + side_i.scalarNodes.GetLimiter(), limiterType, 0, phi, kappa, umusclRamp, res.nVar); } } diff --git a/SU2_CFD/include/numerics/species/species_edge_flux.hpp b/SU2_CFD/include/numerics/species/species_edge_flux.hpp new file mode 100644 index 00000000000..429b70c232f --- /dev/null +++ b/SU2_CFD/include/numerics/species/species_edge_flux.hpp @@ -0,0 +1,94 @@ +/*! + * \file species_edge_flux.hpp + * \brief Species transport model as a third-layer scalar flux, see numerics/scalar/scalar_edge_flux.hpp. + * \author P. Gomes + * \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 . + */ + +#pragma once + +#include "../scalar/scalar_edge_flux.hpp" + +/*! + * \class CScalarFlux_Species + * \ingroup ViscDiscr + * \brief Convection and diffusion of the species transport model, conservative with a diagonal, + * i/j-symmetric diffusion matrix. Unlike SA/SST/LM, the equation count is only known at + * runtime (one per transported species), so this is the framework's first Dynamic-nVar + * model: nEqn is passed to the base explicitly, and coefficients() loops to it rather than + * to a compile-time nVar. + * \note Species writes no finalizeFlux of its own: the inherited CUpwScalarFlux one is exactly + * flux(iVar) = a0*rho_i*Y_i(iVar) + a1*rho_j*Y_j(iVar), Conservative weighting by density, + * which is the model's whole convective term. + */ +template +class CScalarFlux_Species + : public CUpwScalarBase, FlowIndices, nDim, nVar> { + public: + static constexpr bool Conservative = true; + static constexpr bool DiagonalDiffusion = true; + + using Base = CUpwScalarBase; + using Int = typename Base::Int; + + explicit CScalarFlux_Species(const CConfig& config) + : Base(config, config.GetnSpecies()), + turbulence(config.GetKind_Turb_Model() != TURB_MODEL::NONE), + Sc_t(config.GetSchmidt_Number_Turbulent()) {} + + /*! + * \brief Diffusion coefficients, an i/j average of (rho * mass diffusivity) per species, plus a + * turbulent (mu_t/Sc_t) contribution shared by every species, when a turbulence model is + * active; identical for both edge sides. + * \note The laminar and turbulent averages are kept as two separate 0.5*(...) terms summed at + * the end, matching CAvgGrad_Species::FinishResidualCalc's exact operation order, rather + * than folding the turbulent term into the same average as the laminar one. + */ + template + FORCEINLINE CPair> coefficients(const FlowIndices& idx, Int iPoint, + const EdgeSide& side_i, Int jPoint, + const EdgeSide& side_j) const { + const Double rho_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); + const Double rho_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); + + Double diffTurb = 0.0; + if (turbulence) { + const Double muT_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.EddyViscosity()); + const Double muT_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.EddyViscosity()); + diffTurb = 0.5 * (muT_i / Sc_t + muT_j / Sc_t); + } + + Vector D; + for (size_t iVar = 0; iVar < this->nEqn; ++iVar) { + const Double D_lam_i = gatherVariables(iPoint, side_i.scalarNodes.GetDiffusivity(), iVar); + const Double D_lam_j = gatherVariables(jPoint, side_j.scalarNodes.GetDiffusivity(), iVar); + const Double diffLam = 0.5 * (rho_i * D_lam_i + rho_j * D_lam_j); + D(iVar) = diffLam + diffTurb; + } + return {D, D}; + } + + private: + const bool turbulence; + const su2double Sc_t; +}; diff --git a/SU2_CFD/include/numerics/util.hpp b/SU2_CFD/include/numerics/util.hpp index a376f4572af..5f39e6b8ae5 100644 --- a/SU2_CFD/include/numerics/util.hpp +++ b/SU2_CFD/include/numerics/util.hpp @@ -411,13 +411,15 @@ FORCEINLINE Double musclReconstruction(Int iPoint, const Gradient_t& gradient, s * \brief Unlimited reconstruction. * \param[in] iRow - Starting row of gradient to read, for reconstructing a slice of a * larger set of gradients (e.g. only the velocity out of the primitives). + * \param[in] nVarGradRuntime - Equation count of a Dynamic model, known only at runtime; ignored + * (falling back to nVarGrad_ or VarType::nVar) when left at its default of 0. */ template FORCEINLINE void musclUnlimited(typename CLaneTraits::Int iPoint, typename CLaneTraits::Int jPoint, const Vector& vector_ij, const Gradient_t& gradient, CPair& V, const CNonDeduced& kappa, const CNonDeduced& umusclRamp, - size_t iRow = 0) { - constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; + size_t iRow = 0, size_t nVarGradRuntime = 0) { + const size_t nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : (nVarGradRuntime > 0 ? nVarGradRuntime : VarType::nVar); for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { /*--- Centered difference, needed for U-MUSCL projection ---*/ @@ -442,13 +444,17 @@ template ::Int iPoint, typename CLaneTraits::Int jPoint, const Vector& vector_ij, const Limiter_t& limiter, const Gradient_t& gradient, CPair& V, const CNonDeduced& kappa, - const CNonDeduced& umusclRamp, size_t iRow = 0) { - constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; - - auto lim_i = gatherVariables(iPoint, limiter, iRow); - auto lim_j = gatherVariables(jPoint, limiter, iRow); + const CNonDeduced& umusclRamp, size_t iRow = 0, + size_t nVarGradRuntime = 0) { + const size_t nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : (nVarGradRuntime > 0 ? nVarGradRuntime : VarType::nVar); + /*--- Gathered one variable at a time rather than as a Vector: nVarGrad is only + * a compile-time constant when nVarGrad_ itself is one, and a Dynamic model's is runtime-only, + * so it can never be a gatherVariables template argument. ---*/ for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { + const Double lim_i = gatherVariables(iPoint, limiter, iRow + iVar); + const Double lim_j = gatherVariables(jPoint, limiter, iRow + iVar); + /*--- Centered difference, needed for U-MUSCL projection ---*/ const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); @@ -459,8 +465,8 @@ FORCEINLINE void musclPointLimited(typename CLaneTraits::Int iPoint, typ musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); /*--- Apply reconstruction: V_L = V_i + 0.5 * lim * dV_ij^kap ---*/ - V.i.all(iVar) += 0.5 * lim_i(iVar) * proj_i; - V.j.all(iVar) -= 0.5 * lim_j(iVar) * proj_j; + V.i.all(iVar) += 0.5 * lim_i * proj_i; + V.j.all(iVar) -= 0.5 * lim_j * proj_j; } } @@ -471,8 +477,8 @@ template ::Int iPoint, typename CLaneTraits::Int jPoint, const Vector& vector_ij, const Gradient_t& gradient, CPair& V, const CNonDeduced& kappa, const CNonDeduced& umusclRamp, - size_t iRow = 0) { - constexpr auto nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : VarType::nVar; + size_t iRow = 0, size_t nVarGradRuntime = 0) { + const size_t nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : (nVarGradRuntime > 0 ? nVarGradRuntime : VarType::nVar); for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { /*--- Centered difference, needed for U-MUSCL projection and limiter ---*/ @@ -502,16 +508,18 @@ template ::Int iPoint, typename CLaneTraits::Int jPoint, const Vector& vector_ij, const Gradient_t& gradient, const Limiter_t& limiter, LIMITER limiterType, size_t iRow, CPair& V, - const CNonDeduced& kappa, const CNonDeduced& umusclRamp) { + const CNonDeduced& kappa, const CNonDeduced& umusclRamp, + size_t nVarGradRuntime = 0) { switch (limiterType) { case LIMITER::NONE: - musclUnlimited(iPoint, jPoint, vector_ij, gradient, V, kappa, umusclRamp, iRow); + musclUnlimited(iPoint, jPoint, vector_ij, gradient, V, kappa, umusclRamp, iRow, nVarGradRuntime); break; case LIMITER::VAN_ALBADA_EDGE: - musclEdgeLimited(iPoint, jPoint, vector_ij, gradient, V, kappa, umusclRamp, iRow); + musclEdgeLimited(iPoint, jPoint, vector_ij, gradient, V, kappa, umusclRamp, iRow, nVarGradRuntime); break; default: - musclPointLimited(iPoint, jPoint, vector_ij, limiter, gradient, V, kappa, umusclRamp, iRow); + musclPointLimited(iPoint, jPoint, vector_ij, limiter, gradient, V, kappa, umusclRamp, iRow, + nVarGradRuntime); break; } } diff --git a/SU2_CFD/include/solvers/CScalarSolver.hpp b/SU2_CFD/include/solvers/CScalarSolver.hpp index c1feaa2ba9c..12e10d75390 100644 --- a/SU2_CFD/include/solvers/CScalarSolver.hpp +++ b/SU2_CFD/include/solvers/CScalarSolver.hpp @@ -37,6 +37,18 @@ #include "../variables/CPrimitiveIndices.hpp" #include "CSolver.hpp" +/*! + * \brief Carries a type through a value, so a runtime branch can hand a compile-time type to a + * generic lambda (its parameter deduces as CIndicesTag, and the lambda recovers T as + * decltype(tag)::type). Standing in for a C++20 template lambda, which this project's + * C++17 baseline does not have. Shared by every scalar solver's own regime-dispatch helper + * (see CTurbSolver::DispatchRegime, CSpeciesSolver::DispatchRegime). + */ +template +struct CIndicesTag { + using type = T; +}; + /*! * \brief Main class for defining a scalar solver. * \tparam VariableType - Class of variable used by the solver inheriting from this template. diff --git a/SU2_CFD/include/solvers/CSpeciesSolver.hpp b/SU2_CFD/include/solvers/CSpeciesSolver.hpp index 3a657d97c01..69535cc7dd7 100644 --- a/SU2_CFD/include/solvers/CSpeciesSolver.hpp +++ b/SU2_CFD/include/solvers/CSpeciesSolver.hpp @@ -29,6 +29,9 @@ #include "../../../Common/include/parallelization/omp_structure.hpp" #include "../variables/CSpeciesVariable.hpp" +#include "../variables/CEulerVariable.hpp" +#include "../variables/CIncEulerVariable.hpp" +#include "../variables/CNEMOEulerVariable.hpp" #include "CScalarSolver.hpp" /*! @@ -43,6 +46,56 @@ class CSpeciesSolver : public CScalarSolver { vector Wall_SpeciesVars; /*!< \brief Species variables at profiles. */ vector> CustomBoundaryScalar; + /*! + * \brief Resolve the compile-time flow indices from the regime flag of config, and call f with + * a CIndicesTag of the result: f is a generic lambda, `[&](auto tag){ using Indices = + * typename decltype(tag)::type; ... }`. Unlike CTurbSolver::DispatchRegime, species + * transport is supported for NEMO, so this has a third branch. + */ + template + static void DispatchRegime(const CConfig* config, F&& f) { + if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { + f(CIndicesTag>{}); + } else if (config->GetNEMOProblem()) { + f(CIndicesTag>{}); + } else { + f(CIndicesTag>{}); + } + } + + /*! + * \brief Resolve the compile-time flow indices and dimension, and run the interior edge loop + * with the matching CScalarFlux_Species instantiation. The equation count is set at + * runtime (one per species), so unlike SST's/LM's RunXxx this dispatch never resolves it. + */ + template + void RunSpecies(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt); + + template + void RunSpecies(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt); + + /*! + * \brief Same dispatch as RunSpecies, for a boundary's call into BoundaryFluxResidual. + */ + template + void RunSpecies_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit); + + template + void RunSpecies_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit); + + /*! + * \brief Same dispatch as RunSpecies, for BC_Fluid_Interface's combined fill-and-flux donor loop. + */ + template + void RunSpecies_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, bool implicit); + + template + void RunSpecies_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, bool implicit); + public: /*! * \brief Constructor of the class. @@ -83,16 +136,16 @@ class CSpeciesSolver : public CScalarSolver { unsigned short iRKStep, unsigned short RunTime_EqSystem, bool Output) override; /*! - * \brief Compute the viscous flux for the turbulent equation at a particular edge. - * \param[in] iEdge - Edge for which we want to compute the flux + * \brief Compute the spatial integration using the CScalarFlux_Species edge kernel, which + * computes and writes both the convective and the diffusive term of every edge. * \param[in] geometry - Geometrical definition of the problem. * \param[in] solver_container - Container vector with all the solutions. - * \param[in] numerics - Description of the numerical method. + * \param[in] numerics_container - Unused, kept only for the boundary conditions. * \param[in] config - Definition of the particular problem. - * \note Calls a generic implementation after defining a SolverSpecificNumerics object. + * \param[in] iMesh - Index of the mesh in multigrid computations. */ - void Viscous_Residual(const unsigned long iEdge, const CGeometry* geometry, CSolver** solver_container, CNumerics* numerics, - const CConfig* config) override; + void Upwind_Residual(CGeometry* geometry, CSolver** solver_container, CNumerics** numerics_container, + CConfig* config, unsigned short iMesh) override; /*! * \brief Impose the inlet boundary condition. @@ -214,24 +267,20 @@ class CSpeciesSolver : public CScalarSolver { unsigned long TimeIter) override; /*! - * \brief Impose the fluid interface boundary condition using tranfer data. + * \brief Impose the fluid interface (sliding mesh) boundary condition, via the + * CScalarFlux_Species edge kernel. The convective term is a per-donor weighted average, + * computed in the same pass that fills the ghost row of each donor; the diffusive term is + * computed once per vertex, after the donor loop, from the interior point's own + * diffusivity mirrored into the ghost row (matching the pre-migration behavior, which + * likewise read the same point's diffusivity for both sides of the edge). * \param[in] geometry - Geometrical definition of the problem. * \param[in] solver_container - Container vector with all the solutions. - * \param[in] conv_numerics - Description of the numerical method. - * \param[in] visc_numerics - Description of the numerical method. + * \param[in] conv_numerics - Unused, kept only for the boundary condition dispatch. + * \param[in] visc_numerics - Unused, kept only for the boundary condition dispatch. * \param[in] config - Definition of the particular problem. */ - void BC_Fluid_Interface(CGeometry *geometry, - CSolver **solver_container, - CNumerics *conv_numerics, - CNumerics *visc_numerics, - CConfig *config) final { - BC_Fluid_Interface_impl( - [&](unsigned long iPoint) { - visc_numerics->SetDiffusionCoeff(nodes->GetDiffusivity(iPoint), nodes->GetDiffusivity(iPoint)); - }, - geometry, solver_container, conv_numerics, visc_numerics, config); - } + void BC_Fluid_Interface(CGeometry *geometry, CSolver **solver_container, CNumerics *conv_numerics, + CNumerics *visc_numerics, CConfig *config) final; /*! * \brief Set custom boundary scalar values from Python. diff --git a/SU2_CFD/include/solvers/CTurbSolver.hpp b/SU2_CFD/include/solvers/CTurbSolver.hpp index d438ff6edd0..e8fd05d97bc 100644 --- a/SU2_CFD/include/solvers/CTurbSolver.hpp +++ b/SU2_CFD/include/solvers/CTurbSolver.hpp @@ -33,17 +33,6 @@ #include "../variables/CIncEulerVariable.hpp" #include "../../../Common/include/parallelization/omp_structure.hpp" -/*! - * \brief Carries a type through a value, so a runtime branch can hand a compile-time type to a - * generic lambda (its parameter deduces as CIndicesTag, and the lambda recovers T as - * decltype(tag)::type). Standing in for a C++20 template lambda, which this project's - * C++17 baseline does not have. - */ -template -struct CIndicesTag { - using type = T; -}; - /*! * \class CTurbSolver * \brief Main class for defining the turbulence model solver. diff --git a/SU2_CFD/include/variables/CSpeciesVariable.hpp b/SU2_CFD/include/variables/CSpeciesVariable.hpp index 76180d0c57b..76db98d85a9 100644 --- a/SU2_CFD/include/variables/CSpeciesVariable.hpp +++ b/SU2_CFD/include/variables/CSpeciesVariable.hpp @@ -74,4 +74,9 @@ class CSpeciesVariable : public CScalarVariable { * \return Pointer to the mass diffusivities */ inline const su2double* GetDiffusivity(unsigned long iPoint) const { return Diffusivity[iPoint]; } + + /*! + * \brief Get the mass diffusivity container, for a per-species gather by point and equation. + */ + inline const MatrixType& GetDiffusivity() const { return Diffusivity; } }; diff --git a/SU2_CFD/src/drivers/CDriver.cpp b/SU2_CFD/src/drivers/CDriver.cpp index b78376d8ff7..e71cb78a00a 100644 --- a/SU2_CFD/src/drivers/CDriver.cpp +++ b/SU2_CFD/src/drivers/CDriver.cpp @@ -78,8 +78,6 @@ #include "../../include/numerics/scalar/scalar_sources.hpp" #include "../../include/numerics/turbulent/turb_sources.hpp" #include "../../include/numerics/turbulent/transition/trans_sources.hpp" -#include "../../include/numerics/species/species_convection.hpp" -#include "../../include/numerics/species/species_diffusion.hpp" #include "../../include/numerics/species/species_sources.hpp" #include "../../include/numerics/elasticity/CFEAElasticity.hpp" #include "../../include/numerics/elasticity/CFEALinearElasticity.hpp" @@ -1306,38 +1304,24 @@ template void CDriver::InstantiateTransitionNumerics void CDriver::InstantiateSpeciesNumerics(unsigned short nVar_Species, int offset, const CConfig *config, const CSolver* species_solver, CNumerics ****&numerics) const { - const int conv_term = CONV_TERM + offset; - const int visc_term = VISC_TERM + offset; - const int source_first_term = SOURCE_FIRST_TERM + offset; const int source_second_term = SOURCE_SECOND_TERM + offset; - const int conv_bound_term = CONV_BOUND_TERM + offset; - const int visc_bound_term = VISC_BOUND_TERM + offset; - - /*--- Definition of the convective scheme for each equation and mesh level. Also for boundary conditions. ---*/ + /*--- Species transport drives its interior loop and boundaries through its own + * CScalarFlux_Species edge kernel (see CSpeciesSolver), so conv_term/visc_term/ + * conv_bound_term/visc_bound_term are never set here; this switch only checks the config + * value. ---*/ switch (config->GetKind_ConvNumScheme_Species()) { case NONE : break; case SPACE_UPWIND : - for (auto iMGlevel = 0; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { - numerics[iMGlevel][SPECIES_SOL][conv_term] = new CUpwSca_Species(nDim, nVar_Species, config); - numerics[iMGlevel][SPECIES_SOL][conv_bound_term] = new CUpwSca_Species(nDim, nVar_Species, config); - } break; default : SU2_MPI::Error("Invalid convective scheme for the species transport equations. Use SCALAR_UPWIND.", CURRENT_FUNCTION); break; } - /*--- Definition of the viscous scheme for each equation and mesh level ---*/ - - for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { - numerics[iMGlevel][SPECIES_SOL][visc_term] = new CAvgGrad_Species(nDim, nVar_Species, true, config); - numerics[iMGlevel][SPECIES_SOL][visc_bound_term] = new CAvgGrad_Species(nDim, nVar_Species, false, config); - } - /*--- Definition of the source term integration scheme for each equation and mesh level ---*/ for (auto iMGlevel = 0u; iMGlevel <= config->GetnMGLevels(); iMGlevel++) { diff --git a/SU2_CFD/src/solvers/CSpeciesSolver.cpp b/SU2_CFD/src/solvers/CSpeciesSolver.cpp index 0c3c2502849..0102558c567 100644 --- a/SU2_CFD/src/solvers/CSpeciesSolver.cpp +++ b/SU2_CFD/src/solvers/CSpeciesSolver.cpp @@ -31,6 +31,7 @@ #include "../../../Common/include/parallelization/omp_structure.hpp" #include "../../../Common/include/toolboxes/geometry_toolbox.hpp" #include "../../include/solvers/CScalarSolver.inl" +#include "../../include/numerics/species/species_edge_flux.hpp" /*--- Explicit instantiation of the parent class of CSpeciesSolver. ---*/ template class CScalarSolver; @@ -50,6 +51,12 @@ CSpeciesSolver::CSpeciesSolver(CGeometry* geometry, CConfig* config, unsigned sh nodes = new CSpeciesVariable(Solution_Inf, nPoint, nDim, nVar, config); SetBaseClassPointerToNodes(); + /*--- Ghost states for boundary conditions, sized to the largest marker (see BoundaryFluxResidual). ---*/ + unsigned long maxMarkerVertices = 0; + for (unsigned long iMarker = 0; iMarker < nMarker; iMarker++) + maxMarkerVertices = max(maxMarkerVertices, nVertex[iMarker]); + ghostNodes = make_unique(Solution_Inf, maxMarkerVertices, nDim, nVar, config); + /*--- Initialize the mass diffusivity. Nondimensionalization done in the flow solver. ---*/ SU2_OMP_FOR_STAT(omp_chunk_size) for (unsigned long iPoint = 0; iPoint < nPoint; iPoint++) { @@ -333,96 +340,100 @@ void CSpeciesSolver::Preprocessing(CGeometry* geometry, CSolver** solver_contain /*--- Clear Residual and Jacobian. Upwind second order reconstruction and gradients. ---*/ CommonPreprocessing(geometry, config, Output); -} -void CSpeciesSolver::Viscous_Residual(const unsigned long iEdge, const CGeometry* geometry, CSolver** solver_container, - CNumerics* numerics, const CConfig* config) { + EnsureGhostFlowContainers(solver_container, config); +} - /*--- Define an object to set solver specific numerics contribution. ---*/ - auto SolverSpecificNumerics = [&](unsigned long iPoint, unsigned long jPoint) { - /*--- Mass diffusivity coefficients. ---*/ +void CSpeciesSolver::Upwind_Residual(CGeometry* geometry, CSolver** solver_container, CNumerics** numerics_container, + CConfig* config, unsigned short iMesh) { + SU2_ZONE_SCOPED - numerics->SetDiffusionCoeff(nodes->GetDiffusivity(iPoint), nodes->GetDiffusivity(jPoint)); + const ScalarFluxOptions opt{ + dynamic_grid, /*--- dynamicGrid ---*/ + config->GetBounded_Species(), /*--- boundedScalar ---*/ + true, /*--- correctGradient ---*/ + false, /*--- accurateJacobians, the diffusion coefficient does not depend on the species mass fraction ---*/ + true, /*--- convective ---*/ + true, /*--- viscous ---*/ + false, /*--- oneSided, this is the interior loop ---*/ + config->GetMUSCL(), /*--- muscl ---*/ }; - /*--- Now instantiate the generic implementation with the functor above. ---*/ + DispatchRegime(config, [&](auto tag) { + RunSpecies(geometry, solver_container, config, opt); + }); +} + +template +void CSpeciesSolver::RunSpecies(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt) { + if (nDim == 2) RunSpecies(geometry, solver_container, config, opt); + else RunSpecies(geometry, solver_container, config, opt); +} - Viscous_Residual_impl(SolverSpecificNumerics, iEdge, geometry, solver_container, numerics, config); +template +void CSpeciesSolver::RunSpecies(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt) { + EdgeFluxResidual>(geometry, solver_container, config, opt); } -void CSpeciesSolver::BC_Inlet(CGeometry* geometry, CSolver** solver_container, CNumerics* conv_numerics, - CNumerics* visc_numerics, CConfig* config, unsigned short val_marker) { +void CSpeciesSolver::BC_Inlet(CGeometry* geometry, CSolver** solver_container, CNumerics*, CNumerics*, CConfig* config, + unsigned short val_marker) { SU2_ZONE_SCOPED const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); - string Marker_Tag = config->GetMarker_All_TagBound(val_marker); + const string Marker_Tag = config->GetMarker_All_TagBound(val_marker); - /*--- Loop over all the vertices on this boundary marker ---*/ - SU2_OMP_FOR_STAT(OMP_MIN_SIZE) - for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { - auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); - - /*--- Check if the node belongs to the domain (i.e., not a halo node) ---*/ - - if (!geometry->nodes->GetDomain(iPoint)) continue; + if (config->GetMarker_StrongBC(Marker_Tag)) { + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { + const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); - if (config->GetMarker_StrongBC(Marker_Tag)) { - nodes->SetSolution_Old(iPoint, Inlet_SpeciesVars[val_marker][iVertex]); + if (geometry->nodes->GetDomain(iPoint)) { + nodes->SetSolution_Old(iPoint, Inlet_SpeciesVars[val_marker][iVertex]); - LinSysRes.SetBlock_Zero(iPoint); + LinSysRes.SetBlock_Zero(iPoint); - /*--- Includes 1 in the diagonal ---*/ - for (auto iVar = 0u; iVar < nVar; iVar++) { - Jacobian.DeleteValsRowi(iPoint, iVar); + /*--- Includes 1 in the diagonal ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) { + Jacobian.DeleteValsRowi(iPoint, iVar); + } } - } else { // weak BC - /*--- Normal vector for this vertex (negate for outward convention) ---*/ - - su2double Normal[MAXNDIM] = {0.0}; - for (auto iDim = 0u; iDim < nDim; iDim++) Normal[iDim] = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); - - /*--- Allocate the value at the inlet ---*/ - - auto V_inlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, iVertex); - - /*--- Retrieve solution at the farfield boundary node ---*/ - - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); - - /*--- Set various quantities in the solver class ---*/ - - conv_numerics->SetPrimitive(V_domain, V_inlet); - - /*--- Set the species variable state at the inlet. ---*/ - - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), Inlet_SpeciesVars[val_marker][iVertex]); - - /*--- Set various other quantities in the solver class ---*/ - - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), geometry->nodes->GetGridVel(iPoint)); + } + END_SU2_OMP_FOR + return; + } - if (conv_numerics->GetBoundedScalar()) { - const su2double* velocity = &V_inlet[prim_idx.Velocity()]; - const su2double density = solver_container[FLOW_SOL]->GetNodes()->GetDensity(iPoint); - conv_numerics->SetMassFlux(BoundedScalarBCFlux(iPoint, implicit, density, velocity, Normal)); - } + /*--- Weak BC: fill the ghost row from the inlet species state, then let the edge kernel + * compute the (purely convective, see the note this replaces below) flux. ---*/ - /*--- Compute the residual using an upwind scheme ---*/ + EnsureGhostFlowContainers(solver_container, config); - auto residual = conv_numerics->ComputeResidual(config); - LinSysRes.AddBlock(iPoint, residual); + auto* flowSolver = solver_container[FLOW_SOL]; - /*--- Jacobian contribution for implicit integration ---*/ + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { + for (auto iVar = 0u; iVar < nVar; iVar++) + ghostNodes->SetSolution(iVertex, iVar, Inlet_SpeciesVars[val_marker][iVertex][iVar]); - if (implicit) Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i); + SetGhostPrimitives(iVertex, flowSolver->GetCharacPrimVar(val_marker, iVertex)); - // Unfinished viscous contribution removed before right after d8a0da9a00. Further testing required. + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - } + ghostSkip[iVertex] = false; } END_SU2_OMP_FOR + + // Unfinished viscous contribution removed before right after d8a0da9a00. Further testing required. + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Species(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; + + DispatchRegime(config, [&](auto tag) { + RunSpecies_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); } @@ -540,84 +551,207 @@ void CSpeciesSolver::SetUniformInlet(const CConfig* config, unsigned short iMark } } -void CSpeciesSolver::BC_Outlet(CGeometry* geometry, CSolver** solver_container, CNumerics* conv_numerics, - CNumerics* visc_numerics, CConfig* config, unsigned short val_marker) { +void CSpeciesSolver::BC_Outlet(CGeometry* geometry, CSolver** solver_container, CNumerics*, CNumerics*, + CConfig* config, unsigned short val_marker) { SU2_ZONE_SCOPED const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); + const string Marker_Tag = config->GetMarker_All_TagBound(val_marker); - /*--- Loop over all the vertices on this boundary marker ---*/ + if (config->GetMarker_StrongBC(Marker_Tag)) { + /*--- Strong zero flux Neumann boundary condition at the outlet ---*/ + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { + const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); + + if (geometry->nodes->GetDomain(iPoint)) { + const auto Point_Normal = geometry->vertex[val_marker][iVertex]->GetNormal_Neighbor(); + + nodes->SetSolution_Old(iPoint, nodes->GetSolution(Point_Normal)); + + LinSysRes.SetBlock_Zero(iPoint); + + /*--- Includes 1 on the diagonal ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) { + Jacobian.DeleteValsRowi(iPoint, iVar); + } + } + } + END_SU2_OMP_FOR + return; + } + + /*--- Weak BC: Neumann, the species variable is copied from the interior of the domain to the + * ghost row before the edge kernel computes the (purely convective, see the note this replaces + * below) flux. ---*/ + + EnsureGhostFlowContainers(solver_container, config); + + auto* flowSolver = solver_container[FLOW_SOL]; SU2_OMP_FOR_STAT(OMP_MIN_SIZE) for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { - /*--- Strong zero flux Neumann boundary condition at the outlet ---*/ const auto iPoint = geometry->vertex[val_marker][iVertex]->GetNode(); - /*--- Check if the node belongs to the domain (i.e., not a halo node) ---*/ + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, nodes->GetSolution(iPoint, iVar)); - if (!geometry->nodes->GetDomain(iPoint)) continue; + SetGhostPrimitives(iVertex, flowSolver->GetCharacPrimVar(val_marker, iVertex)); - /*--- Identify the boundary by string name ---*/ - string Marker_Tag = config->GetMarker_All_TagBound(val_marker); + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - if (config->GetMarker_StrongBC(Marker_Tag)==true) { - /*--- Allocate the value at the outlet ---*/ - auto Point_Normal = geometry->vertex[val_marker][iVertex]->GetNormal_Neighbor(); + ghostSkip[iVertex] = false; + } + END_SU2_OMP_FOR - nodes->SetSolution_Old(iPoint, nodes->GetSolution(Point_Normal)); + // Unfinished viscous contribution removed before right after d8a0da9a00. Further testing required. + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Species(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl, a boundary never reconstructs*/, + }; - LinSysRes.SetBlock_Zero(iPoint); + DispatchRegime(config, [&](auto tag) { + RunSpecies_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); +} - /*--- Includes 1 on the diagonal ---*/ - for (auto iVar = 0u; iVar < nVar; iVar++) { - Jacobian.DeleteValsRowi(iPoint, iVar); - } - } else { // weak BC +template +void CSpeciesSolver::RunSpecies_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit) { + if (nDim == 2) RunSpecies_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + else RunSpecies_Boundary(geometry, solver_container, config, opt, val_marker, implicit); +} + +template +void CSpeciesSolver::RunSpecies_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit) { + BoundaryFluxResidual>(geometry, solver_container, config, opt, + val_marker, implicit); +} + +void CSpeciesSolver::BC_Fluid_Interface(CGeometry *geometry, CSolver **solver_container, CNumerics*, + CNumerics*, CConfig *config) { + SU2_ZONE_SCOPED - /*--- Allocate the value at the outlet ---*/ - auto V_outlet = solver_container[FLOW_SOL]->GetCharacPrimVar(val_marker, iVertex); + if (solver_container[FLOW_SOL] == nullptr) return; - /*--- Retrieve solution at the farfield boundary node ---*/ + EnsureGhostFlowContainers(solver_container, config); - auto V_domain = solver_container[FLOW_SOL]->GetNodes()->GetPrimitive(iPoint); + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions optConv{ + dynamic_grid, config->GetBounded_Species(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, true /*oneSided*/, false /*muscl*/, + }; + const ScalarFluxOptions optVisc{ + dynamic_grid, false /*boundedScalar, the mass-flux correction only applies with the convective term*/, + true /*correctGradient*/, false /*accurateJacobians*/, + false /*convective*/, true /*viscous*/, true /*oneSided*/, false /*muscl*/, + }; + + DispatchRegime(config, [&](auto tag) { + RunSpecies_FluidInterface(geometry, solver_container, config, optConv, optVisc, + implicit); + }); +} - /*--- Set various quantities in the solver class ---*/ +template +void CSpeciesSolver::RunSpecies_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, + bool implicit) { + if (nDim == 2) RunSpecies_FluidInterface(geometry, solver_container, config, optConv, optVisc, implicit); + else RunSpecies_FluidInterface(geometry, solver_container, config, optConv, optVisc, implicit); +} - conv_numerics->SetPrimitive(V_domain, V_outlet); +/*! + * \brief See RunSA_FluidInterface's note (CTurbSASolver.cpp): the convective term is a per-donor + * weighted average, computed in the same pass that fills the ghost row of each donor; the + * diffusive term is computed once per vertex, after the donor loop, from the interior + * point's own diffusivity mirrored into the ghost row (the pre-migration + * SolverSpecificNumerics functor likewise read the same point's diffusivity for both sides + * of the edge, rather than the donor's). This does not fit the fill-pass-then- + * BoundaryFluxResidual shape the other boundaries use, so it drives the + * CScalarFlux_Species kernel directly. + */ +template +void CSpeciesSolver::RunSpecies_FluidInterface(CGeometry* geometry, CSolver** solver_container, CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, + bool implicit) { + using Scheme = CScalarFlux_Species; + const Scheme flux(*config); + + auto* flowSolver = solver_container[FLOW_SOL]; + auto* flowNodes = su2staticcast_p(flowSolver->GetNodes()); + const auto nPrimVar = flowSolver->GetnPrimVar(); + + const EdgeSide side_i{*nodes, flowNodes, CMatrixView(geometry->nodes->GetCoord()), + dynamic_grid ? CMatrixView(geometry->nodes->GetGridVel()) + : CMatrixView()}; + const EdgeSide side_j{*ghostNodes, ghostFlowNodes.get(), CMatrixView(ghostCoord), + side_i.gridVel}; + + su2activevector PrimVar_j(nPrimVar); + + for (auto iMarker = 0u; iMarker < config->GetnMarker_All(); iMarker++) { + if (config->GetMarker_All_KindBC(iMarker) != FLUID_INTERFACE) continue; + + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (unsigned long iVertex = 0; iVertex < geometry->nVertex[iMarker]; iVertex++) { + const auto iPoint = geometry->vertex[iMarker][iVertex]->GetNode(); + if (!geometry->nodes->GetDomain(iPoint)) continue; - /*--- Set the species variables. Here we use a Neumann BC such - that the species variable is copied from the interior of the - domain to the outlet before computing the residual. ---*/ + const auto Point_Normal = geometry->vertex[iMarker][iVertex]->GetNormal_Neighbor(); + const auto nDonorVertex = GetnSlidingStates(iMarker, iVertex); - conv_numerics->SetScalarVar(nodes->GetSolution(iPoint), nodes->GetSolution(iPoint)); + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[iMarker][iVertex]->GetNormal(iDim); + const auto normal = gatherVariables(iVertex, ghostNormal); - /*--- Set Normal (negate for outward convention) ---*/ + /*--- Loop over the donors and accumulate the weighted-average convective residual. ---*/ + for (auto jVertex = 0; jVertex < nDonorVertex; jVertex++) { + for (auto iVar = 0u; iVar < nPrimVar; iVar++) + PrimVar_j[iVar] = flowSolver->GetSlidingState(iMarker, iVertex, iVar, jVertex); - su2double Normal[MAXNDIM] = {0.0}; - for (auto iDim = 0u; iDim < nDim; iDim++) Normal[iDim] = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); - conv_numerics->SetNormal(Normal); + const su2double weight = flowSolver->GetSlidingState(iMarker, iVertex, nPrimVar, jVertex); - if (dynamic_grid) - conv_numerics->SetGridVel(geometry->nodes->GetGridVel(iPoint), geometry->nodes->GetGridVel(iPoint)); + for (auto iVar = 0u; iVar < nVar; iVar++) + ghostNodes->SetSolution(iVertex, iVar, GetSlidingState(iMarker, iVertex, iVar, jVertex)); - if (conv_numerics->GetBoundedScalar()) { - const su2double* velocity = &V_outlet[prim_idx.Velocity()]; - const su2double density = solver_container[FLOW_SOL]->GetNodes()->GetDensity(iPoint); - conv_numerics->SetMassFlux(BoundedScalarBCFlux(iPoint, implicit, density, velocity, Normal)); - } + SetGhostPrimitives(iVertex, PrimVar_j.data()); - /*--- Compute the residual using an upwind scheme ---*/ - auto residual = conv_numerics->ComputeResidual(config); - LinSysRes.AddBlock(iPoint, residual); + su2double massFlux = 0.0; + if (optConv.boundedScalar) { + massFlux = BoundedScalarBCFlux(iPoint, true, flowNodes->GetDensity(iPoint), + &PrimVar_j[prim_idx.Velocity()], normal.data()); + } - /*--- Jacobian contribution for implicit integration ---*/ - if (implicit) Jacobian.AddBlock2Diag(iPoint, residual.jacobian_i); + const auto res = flux.ComputeFlux(optConv, iPoint, side_i, iVertex, side_j, normal, massFlux); - // Unfinished viscous contribution removed before right after d8a0da9a00. Further testing required. + for (auto iVar = 0ul; iVar < res.nVar; ++iVar) LinSysRes(iPoint, iVar) += weight * res.flux_i(iVar); + if (implicit) Jacobian.AddBlock2Diag(iPoint, res.jac_ii, weight); + } + /*--- Diffusive term, computed once from the interior point's own diffusivity mirrored into + * the ghost row (matching the pre-migration functor, see the note above), and from the + * ghost state the last donor left behind. ---*/ + su2double Coord_Reflected[MAXNDIM]; + GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(Point_Normal), + geometry->nodes->GetCoord(iPoint), Coord_Reflected); + for (auto iDim = 0u; iDim < nDim; iDim++) ghostCoord(iVertex, iDim) = Coord_Reflected[iDim]; + + for (auto iVar = 0u; iVar < nVar; iVar++) + ghostNodes->SetDiffusivity(iVertex, nodes->GetDiffusivity(iPoint, iVar), iVar); + + auto ghostGrad = ghostNodes->GetGradient(iVertex); + const auto interiorGrad = nodes->GetGradient(iPoint); + for (auto iVar = 0u; iVar < nVar; iVar++) + for (auto iDim = 0u; iDim < nDim; iDim++) ghostGrad(iVar, iDim) = interiorGrad(iVar, iDim); + + const auto res = flux.ComputeFlux(optVisc, iPoint, side_i, iVertex, side_j, normal, su2double(0.0)); + for (auto iVar = 0ul; iVar < res.nVar; ++iVar) LinSysRes(iPoint, iVar) += res.flux_i(iVar); + if (implicit) Jacobian.AddBlock2Diag(iPoint, res.jac_ii); } + END_SU2_OMP_FOR } - END_SU2_OMP_FOR } void CSpeciesSolver::Source_Residual(CGeometry *geometry, CSolver **solver_container, CNumerics **numerics_container, From ad768c3f08ba44ea8c05bb2d3238cc963ecfa231 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sat, 29 Aug 2026 20:51:32 -0700 Subject: [PATCH 15/20] Delete now-fully-unused old numerics: SST/LM/species convection & diffusion Step 9 of the scalar-numerics redesign is now complete: SST, LM, species, and SA_Neg all sit on the generic edge-flux framework (SA_Neg needed no extra work -- it shares SA's CScalarFlux_SA convection/diffusion entirely, per turb_sources.hpp's SAFactory; only its source term differs, a separate, still old-style code path this migration does not touch). With every consumer moved off them, these old-style CNumerics convection/ diffusion classes have zero remaining references anywhere in the tree (confirmed by grep before deleting, and by a clean full rebuild after): CUpwSca_TurbSST (turb_convection.hpp), CUpwSca_TransLM (a type alias of the former, in trans_convection.hpp) and CAvgGrad_TransLM (trans_diffusion.hpp), and CUpwSca_Species/CAvgGrad_Species (species_convection.hpp/species_diffusion.hpp). turb_diffusion.hpp/ CAvgGrad_TurbSST had already been removed in an earlier step. scalar_convection.hpp/scalar_diffusion.hpp (the shared old-style base classes) are kept: CUpwSca_Heat/CAvgGrad_Heat in numerics/heat.hpp still use them, and the heat solver's own migration (design doc step 11) hasn't happened yet. Verification: clean rebuild of SU2_CFD/SU2_DEF/SU2_DOT/SU2_GEO/SU2_SOL/ UnitTests; unit tests pass; turb_ONERAM6 (SA) reconfirmed bit-identical to the pre-deletion binary, since this is a pure dead-code removal with zero logic touched. Co-Authored-By: Claude Sonnet 5 --- .../numerics/species/species_convection.hpp | 85 -------------- .../numerics/species/species_diffusion.hpp | 110 ------------------ .../turbulent/transition/trans_convection.hpp | 40 ------- .../turbulent/transition/trans_diffusion.hpp | 105 ----------------- .../numerics/turbulent/turb_convection.hpp | 85 -------------- 5 files changed, 425 deletions(-) delete mode 100644 SU2_CFD/include/numerics/species/species_convection.hpp delete mode 100644 SU2_CFD/include/numerics/species/species_diffusion.hpp delete mode 100644 SU2_CFD/include/numerics/turbulent/transition/trans_convection.hpp delete mode 100644 SU2_CFD/include/numerics/turbulent/transition/trans_diffusion.hpp delete mode 100644 SU2_CFD/include/numerics/turbulent/turb_convection.hpp diff --git a/SU2_CFD/include/numerics/species/species_convection.hpp b/SU2_CFD/include/numerics/species/species_convection.hpp deleted file mode 100644 index 114501cb544..00000000000 --- a/SU2_CFD/include/numerics/species/species_convection.hpp +++ /dev/null @@ -1,85 +0,0 @@ -/*! - * \file species_convection.hpp - * \brief Declarations of numerics classes for discretization of - * convective fluxes in species problems. - * \author T. Kattmann - * \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 . - */ - -#pragma once - -#include "../scalar/scalar_convection.hpp" - -/*! - * \class CUpwSca_Species - * \brief Class for doing a scalar upwind solver for the species transport equations. - * \ingroup ConvDiscr - */ -template -class CUpwSca_Species final : public CUpwScalar { - private: - using Base = CUpwScalar; - using Base::nVar; - using Base::nDim; - using Base::V_i; - using Base::V_j; - using Base::a0; - using Base::a1; - using Base::Flux; - using Base::Jacobian_i; - using Base::Jacobian_j; - using Base::ScalarVar_i; - using Base::ScalarVar_j; - using Base::idx; - using Base::bounded_scalar; - - /*! - * \brief Adds any extra variables to AD - */ - void ExtraADPreaccIn() override {} - - /*! - * \brief Species transport specific steps in the ComputeResidual method - * \param[in] config - Definition of the particular problem. - */ - void FinishResidualCalc(const CConfig* config) override { - for (auto iVar = 0u; iVar < nVar; iVar++) { - Flux[iVar] = a0 * V_i[idx.Density()] * ScalarVar_i[iVar] + a1 * V_j[idx.Density()] * ScalarVar_j[iVar]; - - /*--- Jacobians are taken wrt rho*Y not Y alone in the species solver. ---*/ - /*--- Off-diagonal entries are zero. ---*/ - Jacobian_i[iVar][iVar] = a0; - Jacobian_j[iVar][iVar] = a1; - } // iVar - } - - public: - /*! - * \brief Constructor of the class. - * \param[in] val_nDim - Number of dimensions of the problem. - * \param[in] val_nVar - Number of variables of the problem. - * \param[in] config - Definition of the particular problem. - */ - CUpwSca_Species(unsigned short val_nDim, unsigned short val_nVar, const CConfig* config) - : CUpwScalar(val_nDim, val_nVar, config) { bounded_scalar = config->GetBounded_Species(); } -}; diff --git a/SU2_CFD/include/numerics/species/species_diffusion.hpp b/SU2_CFD/include/numerics/species/species_diffusion.hpp deleted file mode 100644 index 424c4a47b8c..00000000000 --- a/SU2_CFD/include/numerics/species/species_diffusion.hpp +++ /dev/null @@ -1,110 +0,0 @@ -/*! - * \file species_diffusion.hpp - * \brief Declarations of numerics classes for discretization of - * viscous fluxes in species problems. - * \author T. Kattmann - * \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 . - */ - -#pragma once - -#include "../scalar/scalar_diffusion.hpp" - -/*! - * \class CAvgGrad_Species - * \brief Class for computing viscous term using average of gradients (species transport model). - * \ingroup ViscDiscr - */ -template -class CAvgGrad_Species final : public CAvgGrad_Scalar { - private: - using Base = CAvgGrad_Scalar; - using Base::nVar; - using Base::Eddy_Viscosity_i; - using Base::Eddy_Viscosity_j; - using Base::Diffusion_Coeff_i; - using Base::Diffusion_Coeff_j; - using Base::Density_i; - using Base::Density_j; - using Base::ScalarVar_i; - using Base::ScalarVar_j; - using Base::Proj_Mean_GradScalarVar; - using Base::proj_vector_ij; - using Base::Flux; - using Base::Jacobian_i; - using Base::Jacobian_j; - - const bool turbulence; - - /*! - * \brief Adds any extra variables to AD - */ - void ExtraADPreaccIn(void) override { - AD::SetPreaccIn(Diffusion_Coeff_i, nVar); - AD::SetPreaccIn(Diffusion_Coeff_j, nVar); - } - - /*! - * \brief Species transport specific steps in the ComputeResidual method - * \param[in] config - Definition of the particular problem. - */ - void FinishResidualCalc(const CConfig* config) override { - for (auto iVar = 0u; iVar < nVar; iVar++) { - - const su2double Diffusivity_Lam = 0.5 * (Density_i * Diffusion_Coeff_i[iVar] + Density_j * Diffusion_Coeff_j[iVar]); - - su2double Diffusivity_Turb = 0.0; - - if (turbulence) { - const su2double Sc_t = config->GetSchmidt_Number_Turbulent(); - Diffusivity_Turb = 0.5 * (Eddy_Viscosity_i / Sc_t + Eddy_Viscosity_j / Sc_t); - } - - const su2double Diffusivity = Diffusivity_Lam + Diffusivity_Turb; - - Flux[iVar] = Diffusivity * Proj_Mean_GradScalarVar[iVar]; - - /*--- Use TSL approx. to compute derivatives of the gradients. ---*/ - - /*--- Off-diagonal entries are all zero. ---*/ - const su2double proj_on_rhoi = proj_vector_ij / Density_i; - Jacobian_i[iVar][iVar] = -Diffusivity * proj_on_rhoi; - - const su2double proj_on_rhoj = proj_vector_ij / Density_j; - Jacobian_j[iVar][iVar] = Diffusivity * proj_on_rhoj; - - } // iVar - } - - public: - /*! - * \brief Constructor of the class. - * \param[in] val_nDim - Number of dimensions of the problem. - * \param[in] val_nVar - Number of variables of the problem. - * \param[in] correct_grad - Whether to correct gradient for skewness. - * \param[in] config - Definition of the particular problem. - */ - CAvgGrad_Species(unsigned short val_nDim, unsigned short val_nVar, bool correct_grad, const CConfig* config) - : CAvgGrad_Scalar(val_nDim, val_nVar, correct_grad, config), - turbulence(config->GetKind_Turb_Model() != TURB_MODEL::NONE) {} -}; diff --git a/SU2_CFD/include/numerics/turbulent/transition/trans_convection.hpp b/SU2_CFD/include/numerics/turbulent/transition/trans_convection.hpp deleted file mode 100644 index 2338c2ca837..00000000000 --- a/SU2_CFD/include/numerics/turbulent/transition/trans_convection.hpp +++ /dev/null @@ -1,40 +0,0 @@ -/*! - * \file trans_convection.hpp - * \brief Delarations of numerics classes for discretization of - * convective fluxes in transition problems. - * \author S. Kang - * \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 . - */ - -#pragma once - -#include "../turb_convection.hpp" - -/*! - * \class CUpwSca_TransLM - * \brief Re-use the SST convective fluxes for the scalar upwind discretization of LM transition model equations. - * \ingroup ConvDiscr - */ -template -using CUpwSca_TransLM = CUpwSca_TurbSST; - diff --git a/SU2_CFD/include/numerics/turbulent/transition/trans_diffusion.hpp b/SU2_CFD/include/numerics/turbulent/transition/trans_diffusion.hpp deleted file mode 100644 index 0d4aee5a947..00000000000 --- a/SU2_CFD/include/numerics/turbulent/transition/trans_diffusion.hpp +++ /dev/null @@ -1,105 +0,0 @@ -/*! - * \file trans_diffusion.hpp - * \brief Declarations of numerics classes for discretization of - * viscous fluxes in transition problems. - * \author S. Kang - * \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 . - */ -#pragma once - - -#include "../../scalar/scalar_diffusion.hpp" - -/*! - * \class CAvgGrad_TransLM - * \brief Class for computing viscous term using average of gradient with correction (LM transition model). - * \ingroup ViscDiscr - * \author S. Kang. - */ -template -class CAvgGrad_TransLM final : public CAvgGrad_Scalar { -private: - using Base = CAvgGrad_Scalar; - using Base::Laminar_Viscosity_i; - using Base::Laminar_Viscosity_j; - using Base::Eddy_Viscosity_i; - using Base::Eddy_Viscosity_j; - using Base::Density_i; - using Base::Density_j; - using Base::ScalarVar_i; - using Base::ScalarVar_j; - using Base::Proj_Mean_GradScalarVar; - using Base::proj_vector_ij; - using Base::Flux; - using Base::Jacobian_i; - using Base::Jacobian_j; - - /*! - * \brief Adds any extra variables to AD - */ - void ExtraADPreaccIn() override {} - - /*! - * \brief LM transition model specific steps in the ComputeResidual method - * \param[in] config - Definition of the particular problem. - */ - void FinishResidualCalc(const CConfig* config) override { - const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; - - /*--- Compute mean effective dynamic viscosity ---*/ - const su2double diff_i_gamma = Laminar_Viscosity_i + Eddy_Viscosity_i; - const su2double diff_j_gamma = Laminar_Viscosity_j + Eddy_Viscosity_j; - const su2double diff_i_ReThetaT = 2.0*(Laminar_Viscosity_i + Eddy_Viscosity_i); - const su2double diff_j_ReThetaT = 2.0*(Laminar_Viscosity_j + Eddy_Viscosity_j); - - const su2double diff_gamma = 0.5*(diff_i_gamma + diff_j_gamma); - const su2double diff_ReThetaT = 0.5*(diff_i_ReThetaT + diff_j_ReThetaT); - - Flux[0] = diff_gamma*Proj_Mean_GradScalarVar[0]; - Flux[1] = diff_ReThetaT*Proj_Mean_GradScalarVar[1]; - - /*--- For Jacobians -> Use of TSL (Thin Shear Layer) approx. to compute derivatives of the gradients ---*/ - if (implicit) { - const su2double proj_on_rho_i = proj_vector_ij/Density_i; - Jacobian_i[0][0] = -diff_gamma*proj_on_rho_i; Jacobian_i[0][1] = 0.0; - Jacobian_i[1][0] = 0.0; Jacobian_i[1][1] = -diff_ReThetaT*proj_on_rho_i; - - const su2double proj_on_rho_j = proj_vector_ij/Density_j; - Jacobian_j[0][0] = diff_gamma*proj_on_rho_j; Jacobian_j[0][1] = 0.0; - Jacobian_j[1][0] = 0.0; Jacobian_j[1][1] = diff_ReThetaT*proj_on_rho_j; - } - } - -public: - /*! - * \brief Constructor of the class. - * \param[in] val_nDim - Number of dimensions of the problem. - * \param[in] val_nVar - Number of variables of the problem. - * \param[in] correct_grad - Whether to correct gradient for skewness. - * \param[in] config - Definition of the particular problem. - */ - CAvgGrad_TransLM(unsigned short val_nDim, unsigned short val_nVar, bool correct_grad, const CConfig* config) - : CAvgGrad_Scalar(val_nDim, val_nVar, correct_grad, config){ - } - -}; diff --git a/SU2_CFD/include/numerics/turbulent/turb_convection.hpp b/SU2_CFD/include/numerics/turbulent/turb_convection.hpp deleted file mode 100644 index f17dd4dac0c..00000000000 --- a/SU2_CFD/include/numerics/turbulent/turb_convection.hpp +++ /dev/null @@ -1,85 +0,0 @@ -/*! - * \file turb_convection.hpp - * \brief Declarations of numerics classes for discretization of - * convective fluxes in turbulence problems. - * \author F. Palacios, T. Economon - * \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 . - */ - -#pragma once - -#include "../scalar/scalar_convection.hpp" - -/*! - * \class CUpwSca_TurbSST - * \brief Class for doing a scalar upwind solver for the Menter SST turbulence model equations. - * \ingroup ConvDiscr - * \author A. Campos. - */ -template -class CUpwSca_TurbSST final : public CUpwScalar { -private: - using Base = CUpwScalar; - using Base::nDim; - using Base::V_i; - using Base::V_j; - using Base::a0; - using Base::a1; - using Base::Flux; - using Base::Jacobian_i; - using Base::Jacobian_j; - using Base::ScalarVar_i; - using Base::ScalarVar_j; - using Base::idx; - using Base::bounded_scalar; - - /*! - * \brief Adds any extra variables to AD - */ - void ExtraADPreaccIn() override {} - - /*! - * \brief SST specific steps in the ComputeResidual method - * \param[in] config - Definition of the particular problem. - */ - void FinishResidualCalc(const CConfig* config) override { - Flux[0] = a0*V_i[idx.Density()]*ScalarVar_i[0] + a1*V_j[idx.Density()]*ScalarVar_j[0]; - Flux[1] = a0*V_i[idx.Density()]*ScalarVar_i[1] + a1*V_j[idx.Density()]*ScalarVar_j[1]; - - Jacobian_i[0][0] = a0; Jacobian_i[0][1] = 0.0; - Jacobian_i[1][0] = 0.0; Jacobian_i[1][1] = a0; - - Jacobian_j[0][0] = a1; Jacobian_j[0][1] = 0.0; - Jacobian_j[1][0] = 0.0; Jacobian_j[1][1] = a1; - } - -public: - /*! - * \brief Constructor of the class. - * \param[in] val_nDim - Number of dimensions of the problem. - * \param[in] val_nVar - Number of variables of the problem. - * \param[in] config - Definition of the particular problem. - */ - CUpwSca_TurbSST(unsigned short val_nDim, unsigned short val_nVar, const CConfig* config) - : CUpwScalar(val_nDim, val_nVar, config) { bounded_scalar = config->GetBounded_Turb(); } -}; From 34e1c5d59eca6d8b09b75888b43be63e61a38a45 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sat, 29 Aug 2026 21:31:33 -0700 Subject: [PATCH 16/20] Fix: SST and species never got their own BC_Far_Field, crashing on MARKER_FAR hybrid_regression.py caught this: every SST case with a MARKER_FAR boundary (most external-aero RANS cases -- rae2822, naca0012, the turbomachinery and sliding-interface suites) segfaulted immediately at solver start, in CScalarSolver::BC_Far_Field, the still-old-style base method requiring a conv_numerics object. This is the exact class of bug already found and fixed for CTransLMSolver's BC_Outlet (which forwarded to it) -- except here it was latent since the SST migration commit itself (c051b6dc52): SST never overrode BC_Far_Field at all (unlike SA, which already has its own), so it silently fell back to the base implementation, and nothing broke until CDriver.cpp's old conv_bound_term/visc_bound_term instantiation for SST was removed in that same commit. None of this session's SST verification (turb_ONERAM6 is SA; the flatplate cases have no MARKER_FAR) happened to exercise the path. Checking for the same gap turned up CSpeciesSolver too -- also never had a BC_Far_Field override, also latent since species' own migration, just not yet hit by any local species test (none use MARKER_FAR). Both get a new BC_Far_Field, mirroring CTurbSASolver's own (ghost = Solution_Inf, convective only, boundedScalar = config->GetBounded_Turb()/Bounded_Species() -- both match what the old base implementation did when its bounded-scalar flag was set, unlike the BC_Inlet_MixingPlane/BC_HeatFlux_Wall sites found earlier in this effort that genuinely never applied the correction). Verification: clean rebuild, unit tests pass, and the crashing rans/rae2822/turb_SST_RAE2822 case now runs to completion. Co-Authored-By: Claude Sonnet 5 --- SU2_CFD/include/solvers/CSpeciesSolver.hpp | 12 ++++++++ SU2_CFD/include/solvers/CTurbSSTSolver.hpp | 16 +++++++++++ SU2_CFD/src/solvers/CSpeciesSolver.cpp | 33 ++++++++++++++++++++++ SU2_CFD/src/solvers/CTurbSSTSolver.cpp | 33 ++++++++++++++++++++++ 4 files changed, 94 insertions(+) diff --git a/SU2_CFD/include/solvers/CSpeciesSolver.hpp b/SU2_CFD/include/solvers/CSpeciesSolver.hpp index 69535cc7dd7..ee9d7562f17 100644 --- a/SU2_CFD/include/solvers/CSpeciesSolver.hpp +++ b/SU2_CFD/include/solvers/CSpeciesSolver.hpp @@ -201,6 +201,18 @@ class CSpeciesSolver : public CScalarSolver { void BC_Outlet(CGeometry* geometry, CSolver** solver_container, CNumerics* conv_numerics, CNumerics* visc_numerics, CConfig* config, unsigned short val_marker) final; + /*! + * \brief Impose the far-field boundary condition, via the CScalarFlux_Species edge kernel. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] solver_container - Container vector with all the solutions. + * \param[in] conv_numerics - Unused, kept only for the boundary condition dispatch. + * \param[in] visc_numerics - Unused, kept only for the boundary condition dispatch. + * \param[in] config - Definition of the particular problem. + * \param[in] val_marker - Surface marker where the boundary condition is applied. + */ + void BC_Far_Field(CGeometry* geometry, CSolver** solver_container, CNumerics* conv_numerics, + CNumerics* visc_numerics, CConfig* config, unsigned short val_marker) final; + /*! * \brief Impose the isothermal wall Dirichlet boundary condition (value). * \param[in] geometry - Geometrical definition of the problem. diff --git a/SU2_CFD/include/solvers/CTurbSSTSolver.hpp b/SU2_CFD/include/solvers/CTurbSSTSolver.hpp index f8d0cf6f07c..3ef58f7b0fb 100644 --- a/SU2_CFD/include/solvers/CTurbSSTSolver.hpp +++ b/SU2_CFD/include/solvers/CTurbSSTSolver.hpp @@ -272,6 +272,22 @@ class CTurbSSTSolver final : public CTurbSolver { CConfig *config, unsigned short val_marker) override; + /*! + * \brief Impose the far-field boundary condition, via the CScalarFlux_SST edge kernel. + * \param[in] geometry - Geometrical definition of the problem. + * \param[in] solver_container - Container vector with all the solutions. + * \param[in] conv_numerics - Unused, kept only for the boundary condition dispatch. + * \param[in] visc_numerics - Unused, kept only for the boundary condition dispatch. + * \param[in] config - Definition of the particular problem. + * \param[in] val_marker - Surface marker where the boundary condition is applied. + */ + void BC_Far_Field(CGeometry *geometry, + CSolver **solver_container, + CNumerics *conv_numerics, + CNumerics *visc_numerics, + CConfig *config, + unsigned short val_marker) override; + /*! * \brief Impose the fluid interface (sliding mesh) boundary condition, via the CScalarFlux_SST * edge kernel. The convective term is a per-donor weighted average, computed in the same diff --git a/SU2_CFD/src/solvers/CSpeciesSolver.cpp b/SU2_CFD/src/solvers/CSpeciesSolver.cpp index 0102558c567..dfeda4db5e3 100644 --- a/SU2_CFD/src/solvers/CSpeciesSolver.cpp +++ b/SU2_CFD/src/solvers/CSpeciesSolver.cpp @@ -622,6 +622,39 @@ void CSpeciesSolver::RunSpecies_Boundary(CGeometry* geometry, CSolver** solver_c else RunSpecies_Boundary(geometry, solver_container, config, opt, val_marker, implicit); } +void CSpeciesSolver::BC_Far_Field(CGeometry* geometry, CSolver** solver_container, CNumerics*, CNumerics*, + CConfig* config, unsigned short val_marker) { + SU2_ZONE_SCOPED + + EnsureGhostFlowContainers(solver_container, config); + + auto* flowSolver = solver_container[FLOW_SOL]; + + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, Solution_Inf[iVar]); + + SetGhostPrimitives(iVertex, flowSolver->GetCharacPrimVar(val_marker, iVertex)); + + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); + + ghostSkip[iVertex] = false; + } + END_SU2_OMP_FOR + + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Species(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, + true /*oneSided, the ghost point has no row*/, false /*muscl, a boundary never reconstructs*/, + }; + + DispatchRegime(config, [&](auto tag) { + RunSpecies_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); +} + template void CSpeciesSolver::RunSpecies_Boundary(CGeometry* geometry, CSolver** solver_container, CConfig* config, const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit) { diff --git a/SU2_CFD/src/solvers/CTurbSSTSolver.cpp b/SU2_CFD/src/solvers/CTurbSSTSolver.cpp index 3e4c5e38246..58ccd539f3a 100644 --- a/SU2_CFD/src/solvers/CTurbSSTSolver.cpp +++ b/SU2_CFD/src/solvers/CTurbSSTSolver.cpp @@ -739,6 +739,39 @@ void CTurbSSTSolver::RunSST_Boundary(CGeometry* geometry, CSolver** solver_conta val_marker, implicit); } +void CTurbSSTSolver::BC_Far_Field(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, + CConfig *config, unsigned short val_marker) { + SU2_ZONE_SCOPED + + EnsureGhostFlowContainers(solver_container, config); + + auto* flowSolver = solver_container[FLOW_SOL]; + + SU2_OMP_FOR_STAT(OMP_MIN_SIZE) + for (auto iVertex = 0u; iVertex < geometry->nVertex[val_marker]; iVertex++) { + for (auto iVar = 0u; iVar < nVar; iVar++) ghostNodes->SetSolution(iVertex, iVar, Solution_Inf[iVar]); + + SetGhostPrimitives(iVertex, flowSolver->GetCharacPrimVar(val_marker, iVertex)); + + for (auto iDim = 0u; iDim < nDim; iDim++) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); + + ghostSkip[iVertex] = false; + } + END_SU2_OMP_FOR + + const bool implicit = config->GetKind_TimeIntScheme() == EULER_IMPLICIT; + const ScalarFluxOptions opt{ + dynamic_grid, config->GetBounded_Turb(), false /*correctGradient*/, false /*accurateJacobians*/, + true /*convective*/, false /*viscous*/, + true /*oneSided, the ghost point has no row*/, false /*muscl, a boundary never reconstructs*/, + }; + + DispatchRegime(config, [&](auto tag) { + RunSST_Boundary(geometry, solver_container, config, opt, val_marker, implicit); + }); +} + void CTurbSSTSolver::BC_Inlet_MixingPlane(CGeometry *geometry, CSolver **solver_container, CNumerics*, CNumerics*, CConfig *config, unsigned short val_marker) { SU2_ZONE_SCOPED From 8d86ecd7930e0518f35b9f04635a392892583dce Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sun, 30 Aug 2026 10:06:27 -0700 Subject: [PATCH 17/20] Restore the equation-count bound check lost when the old scalar numerics were deleted; delete unreachable NEMO dispatch branches CUpwScalarBase's constructor had no check on nEqn against Size (the EdgeResidual backing width, 8 for a Dynamic model): CUpwScalar/CAvgGrad_Scalar carried the only "Static arrays are too small" guard, and species transport no longer builds either. CSpeciesVariable::MAXNVAR is 20, so 9+ species (nSpecies is just the length of SPECIES_INIT) silently wrote past EdgeResidual's flux/Jacobian storage under -DNDEBUG rather than erroring; confirmed the exact overrun with a debug-assert probe. Restored the check in the constructor, at the same MAXNVAR=8 threshold the deleted classes used. Removed three now-unreachable NEMO branches: CSpeciesSolver::DispatchRegime's (species transport errors before reaching it, in CDriver::InitializeNumerics), CDriver's InstantiateTransitionNumerics explicit instantiation and its call site (transition requires a turbulence model, which is rejected for NEMO at configuration, and CDriver never sets both the transition and NEMO_ns dispatch flags in the same run either), and the matching InstantiateSpeciesNumerics instantiation. Dropped the now-unused CNEMOEulerVariable.hpp includes. Removed dangling comment references to numerics classes already deleted in this series (CAvgGrad_TransLM, CAvgGrad_Species, CUpwSca_TurbSST). Co-Authored-By: Claude Sonnet 5 --- .../numerics/scalar/scalar_edge_flux.hpp | 6 +++++- .../numerics/species/species_edge_flux.hpp | 3 --- .../turbulent/transition/trans_edge_flux.hpp | 12 ++++-------- SU2_CFD/include/solvers/CSpeciesSolver.hpp | 7 ++----- SU2_CFD/src/drivers/CDriver.cpp | 17 +++++------------ 5 files changed, 16 insertions(+), 29 deletions(-) diff --git a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp index 717a8993b22..da01a1b6527 100644 --- a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp +++ b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp @@ -277,7 +277,11 @@ class CUpwScalarBase : public CUpwScalarFlux Size) { + SU2_MPI::Error("Static arrays are too small for the requested equation count.", CURRENT_FUNCTION); + } + } template FORCEINLINE EdgeResidual ComputeFlux(const ScalarFluxOptions& opt, Int iPoint, diff --git a/SU2_CFD/include/numerics/species/species_edge_flux.hpp b/SU2_CFD/include/numerics/species/species_edge_flux.hpp index 429b70c232f..e5046a7d9b6 100644 --- a/SU2_CFD/include/numerics/species/species_edge_flux.hpp +++ b/SU2_CFD/include/numerics/species/species_edge_flux.hpp @@ -60,9 +60,6 @@ class CScalarFlux_Species * \brief Diffusion coefficients, an i/j average of (rho * mass diffusivity) per species, plus a * turbulent (mu_t/Sc_t) contribution shared by every species, when a turbulence model is * active; identical for both edge sides. - * \note The laminar and turbulent averages are kept as two separate 0.5*(...) terms summed at - * the end, matching CAvgGrad_Species::FinishResidualCalc's exact operation order, rather - * than folding the turbulent term into the same average as the laminar one. */ template FORCEINLINE CPair> coefficients(const FlowIndices& idx, Int iPoint, diff --git a/SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp b/SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp index 627aa28bd10..40faf47bed9 100644 --- a/SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp +++ b/SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp @@ -33,13 +33,12 @@ * \class CScalarFlux_TransLM * \ingroup ViscDiscr * \brief Convection and diffusion of the Langtry-Menter transition model, conservative with a - * diagonal, i/j-symmetric diffusion matrix (unlike SST's, the coefficients only depend on - * the flow's mu/mu_t, not on the transported gamma/Re_theta, so no coefficientJacobians - * override is needed). + * diagonal, i/j-symmetric diffusion matrix. The coefficients depend only on the flow's + * mu/mu_t, not on the transported gamma/Re_theta, so no coefficientJacobians override + * is needed. * \note LM writes no finalizeFlux of its own: the inherited CUpwScalarFlux one is exactly * flux(iVar) = a0*rho_i*phi_i(iVar) + a1*rho_j*phi_j(iVar), Conservative weighting by - * density, which is the model's whole convective term (CUpwSca_TransLM was previously a - * type alias of CUpwSca_TurbSST for exactly this reason). + * density, which is the model's whole convective term. */ template class CScalarFlux_TransLM @@ -55,9 +54,6 @@ class CScalarFlux_TransLM /*! * \brief Diffusion coefficients, an i/j average of (mu+mu_t) for intermittency and of * 2*(mu+mu_t) for the momentum-thickness Reynolds number; identical for both edge sides. - * \note The Re_theta coefficient is kept as an average of two separately-doubled terms, matching - * the old CAvgGrad_TransLM::FinishResidualCalc's exact operation order, rather than the - * algebraically-equivalent "2 * the gamma coefficient". */ template FORCEINLINE CPair> coefficients(const FlowIndices& idx, Int iPoint, diff --git a/SU2_CFD/include/solvers/CSpeciesSolver.hpp b/SU2_CFD/include/solvers/CSpeciesSolver.hpp index ee9d7562f17..d7209fb87c9 100644 --- a/SU2_CFD/include/solvers/CSpeciesSolver.hpp +++ b/SU2_CFD/include/solvers/CSpeciesSolver.hpp @@ -31,7 +31,6 @@ #include "../variables/CSpeciesVariable.hpp" #include "../variables/CEulerVariable.hpp" #include "../variables/CIncEulerVariable.hpp" -#include "../variables/CNEMOEulerVariable.hpp" #include "CScalarSolver.hpp" /*! @@ -49,15 +48,13 @@ class CSpeciesSolver : public CScalarSolver { /*! * \brief Resolve the compile-time flow indices from the regime flag of config, and call f with * a CIndicesTag of the result: f is a generic lambda, `[&](auto tag){ using Indices = - * typename decltype(tag)::type; ... }`. Unlike CTurbSolver::DispatchRegime, species - * transport is supported for NEMO, so this has a third branch. + * typename decltype(tag)::type; ... }`. NEMO is not one of the branches: CDriver rejects + * species transport for it before any numerics are built. */ template static void DispatchRegime(const CConfig* config, F&& f) { if (config->GetKind_Regime() == ENUM_REGIME::INCOMPRESSIBLE) { f(CIndicesTag>{}); - } else if (config->GetNEMOProblem()) { - f(CIndicesTag>{}); } else { f(CIndicesTag>{}); } diff --git a/SU2_CFD/src/drivers/CDriver.cpp b/SU2_CFD/src/drivers/CDriver.cpp index e71cb78a00a..ce6b3b0cc49 100644 --- a/SU2_CFD/src/drivers/CDriver.cpp +++ b/SU2_CFD/src/drivers/CDriver.cpp @@ -51,7 +51,6 @@ #include "../../include/variables/CEulerVariable.hpp" #include "../../include/variables/CIncEulerVariable.hpp" -#include "../../include/variables/CNEMOEulerVariable.hpp" #include "../../include/numerics/template.hpp" #include "../../include/numerics/radiation.hpp" @@ -1291,16 +1290,15 @@ void CDriver::InstantiateTransitionNumerics(unsigned short nVar_Trans, int offse numerics[iMGlevel][TRANS_SOL][source_second_term] = new CSourceNothing(nDim, nVar_Trans, config); } } -/*--- Explicit instantiation of the template above, needed because it is defined in a cpp file, instead of hpp. ---*/ +/*--- Explicit instantiation of the template above, needed because it is defined in a cpp file, instead of hpp. + * NEMO has no explicit instantiation: transition requires a turbulence model, which is rejected + * for NEMO at configuration. ---*/ template void CDriver::InstantiateTransitionNumerics>( unsigned short, int, const CConfig*, const CSolver*, CNumerics****&) const; template void CDriver::InstantiateTransitionNumerics>( unsigned short, int, const CConfig*, const CSolver*, CNumerics****&) const; -template void CDriver::InstantiateTransitionNumerics>( - unsigned short, int, const CConfig*, const CSolver*, CNumerics****&) const; - template void CDriver::InstantiateSpeciesNumerics(unsigned short nVar_Species, int offset, const CConfig *config, const CSolver* species_solver, CNumerics ****&numerics) const { @@ -1335,16 +1333,14 @@ void CDriver::InstantiateSpeciesNumerics(unsigned short nVar_Species, int offset } } -/*--- Explicit instantiation of the template above, needed because it is defined in a cpp file, instead of hpp. ---*/ +/*--- Explicit instantiation of the template above, needed because it is defined in a cpp file, instead of hpp. + * NEMO has no explicit instantiation: the call site below errors before reaching NEMO indices. ---*/ template void CDriver::InstantiateSpeciesNumerics>( unsigned short, int, const CConfig*, const CSolver*, CNumerics****&) const; template void CDriver::InstantiateSpeciesNumerics>( unsigned short, int, const CConfig*, const CSolver*, CNumerics****&) const; -template void CDriver::InstantiateSpeciesNumerics>( - unsigned short, int, const CConfig*, const CSolver*, CNumerics****&) const; - void CDriver::InitializeNumerics(CConfig *config, CGeometry **geometry, CSolver ***solver, CNumerics ****&numerics) const { SU2_ZONE_SCOPED @@ -1963,9 +1959,6 @@ void CDriver::InitializeNumerics(CConfig *config, CGeometry **geometry, CSolver if (incompressible) InstantiateTransitionNumerics >(nVar_Trans, offset, config, solver[MESH_0][TRANS_SOL], numerics); - else if (NEMO_ns) - InstantiateTransitionNumerics >(nVar_Trans, offset, config, - solver[MESH_0][TRANS_SOL], numerics); else InstantiateTransitionNumerics >(nVar_Trans, offset, config, solver[MESH_0][TRANS_SOL], numerics); From 00c9c18a073e0424bc2259aceb10b7a60d67b9b4 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Sun, 30 Aug 2026 11:27:07 -0700 Subject: [PATCH 18/20] Fix the conservative convective Jacobian and restore flamelet preferential diffusion CUpwScalarFlux::finalizeFlux wrote the density-weighted upwinding weight into the Jacobian blocks as well as the flux. The linear system solves for the increment of the conserved variable (CompleteImplicitIteration divides the solution by the density), so d(a0*rho_i*phi_i)/d(rho*phi)_i is a0, not a0*rho_i, which is what CUpwSca_TurbSST and CUpwSca_Species wrote; the diffusion term in the same file already divided by the density for this reason. Affected SST, LM, species and flamelet. The convention is now stated where the flux is written. CSpeciesFlameletSolver::Viscous_Residual had become unreachable: it was only ever called from CScalarSolver::Upwind_Residual, which CSpeciesSolver now overrides, so the preferential diffusion and thermal terms were silently dropped (and the function would have dereferenced a numerics object that is no longer allocated). They come back as CScalarFlux_Flamelet:: extraDiffusionTerms, one evaluation per edge instead of two. The species coefficients move to CScalarFluxSpeciesBase so the flamelet model is a sibling rather than a copy. Boundaries keep instantiating CScalarFlux_Species, which is what keeps the beta scalars off the per-marker ghost containers. EnsureGhostFlowContainers tested for work already done outside the construct that ends in a barrier, so a thread arriving after the master's write returned early and left the rest of the team waiting. The test moves inside; the calls from the boundary bodies go away, each solver's Preprocessing already runs it. Two discretization changes are reverted to what the numerics classes did: the density is MUSCL reconstructed alongside the velocity for a conservative convective flux, and the species fluid interface does not correct the projected gradient for skewness. Alongside the fixes: - One CScalarSolver::DispatchScheme resolves the flow indices, the dimension and the equation count for every model, replacing four solvers' worth of hand-written ladders and two copies of DispatchRegime. - One FluidInterfaceFluxResidual replaces three near-identical donor loops, the model contributing a functor for its auxiliary ghost fields. - ScalarFluxOptions is built by named constructors instead of nine positional bools, and carries "implicit"; SetGhostGeometry and SetGhostDiffusionState absorb the fill-pass boilerplate. - The three MUSCL helpers become one body with the limiter kind as a template parameter, and the blocked gradient gather is back for the flow kernels. - opt.implicit and opt.oneSided gate the Jacobian work, the density is gathered once per edge, and SST's coefficientJacobians reads what coefficients already computed. - Viscous_Residual_NonCons is deleted, unused since SA and SST moved to the edge kernel and the last caller of the raw-pointer GetBlocks path. - The four-block writers gain unit test coverage of their quantized branch. Co-Authored-By: Claude Opus 5 --- .../numerics/scalar/scalar_edge_flux.hpp | 233 ++++++++--- .../numerics/species/flamelet_edge_flux.hpp | 170 ++++++++ .../numerics/species/species_edge_flux.hpp | 64 ++- .../turbulent/transition/trans_edge_flux.hpp | 3 +- .../numerics/turbulent/turb_sa_edge_flux.hpp | 65 ++-- .../numerics/turbulent/turb_sst_edge_flux.hpp | 95 +++-- SU2_CFD/include/numerics/util.hpp | 192 ++++----- SU2_CFD/include/solvers/CScalarSolver.hpp | 202 +++++----- SU2_CFD/include/solvers/CScalarSolver.inl | 94 ++++- .../solvers/CSpeciesFlameletSolver.hpp | 11 +- SU2_CFD/include/solvers/CSpeciesSolver.hpp | 55 +-- SU2_CFD/include/solvers/CTransLMSolver.hpp | 30 +- SU2_CFD/include/solvers/CTurbSASolver.hpp | 50 +-- SU2_CFD/include/solvers/CTurbSSTSolver.hpp | 35 +- SU2_CFD/include/solvers/CTurbSolver.hpp | 21 - SU2_CFD/include/variables/CVariable.hpp | 10 +- .../src/solvers/CSpeciesFlameletSolver.cpp | 186 +-------- SU2_CFD/src/solvers/CSpeciesSolver.cpp | 236 ++--------- SU2_CFD/src/solvers/CTransLMSolver.cpp | 111 ++---- SU2_CFD/src/solvers/CTurbSASolver.cpp | 367 +++--------------- SU2_CFD/src/solvers/CTurbSSTSolver.cpp | 285 ++------------ .../edge_residual_blocks_tests.cpp | 58 +++ 22 files changed, 1001 insertions(+), 1572 deletions(-) create mode 100644 SU2_CFD/include/numerics/species/flamelet_edge_flux.hpp diff --git a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp index da01a1b6527..5753560e866 100644 --- a/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp +++ b/SU2_CFD/include/numerics/scalar/scalar_edge_flux.hpp @@ -49,13 +49,80 @@ struct EdgeSide { /*! * \brief Loop invariant flags for a scalar edge flux, built once outside the edge loop so the * compiler can unswitch the branches they guard. + * \note Built through the named constructors below: the flags are too many and too alike to be + * given positionally, where one transposed pair would change the discretization silently. */ struct ScalarFluxOptions { - bool dynamicGrid, boundedScalar, correctGradient, accurateJacobians; - bool convective; /*!< \brief Whether the convective scheme contributes. */ - bool viscous; /*!< \brief Whether the diffusion term contributes. */ - bool oneSided; /*!< \brief Whether only the row of i is assembled. */ - bool muscl; /*!< \brief Whether the convective scheme reconstructs. A boundary clears it. */ + bool dynamicGrid = false; + bool boundedScalar = false; + bool correctGradient = false; + bool accurateJacobians = false; + bool implicit = false; /*!< \brief Whether the Jacobians are assembled, and so computed. */ + bool convective = true; /*!< \brief Whether the convective scheme contributes. */ + bool viscous = false; /*!< \brief Whether the diffusion term contributes. */ + bool oneSided = false; /*!< \brief Whether only the row of i is assembled. */ + bool muscl = false; /*!< \brief Whether the convective scheme reconstructs. */ + + /*! + * \brief Options of the interior edge loop: both terms, both rows, and reconstruction and + * gradient correction as the configuration asks for them. + */ + static ScalarFluxOptions Interior(const CConfig& config, bool bounded, bool accurateJacobians = false) { + auto opt = common(config); + opt.boundedScalar = bounded; + opt.accurateJacobians = accurateJacobians; + opt.correctGradient = true; + opt.viscous = true; + opt.muscl = config.GetMUSCL(); + return opt; + } + + /*! + * \brief Options of a boundary that imposes a convective flux alone, which is most of them: + * the diffusive term at an inlet or an outlet causes serious convergence problems. + */ + static ScalarFluxOptions BoundaryConvective(const CConfig& config, bool bounded) { + auto opt = common(config); + opt.boundedScalar = bounded; + opt.oneSided = true; + return opt; + } + + /*! + * \brief Options of a boundary that imposes both terms, which is the turbomachinery sites. + * \note They impose no mass flux, so the bounded scheme contributes nothing here whatever the + * configuration says. + */ + static ScalarFluxOptions BoundaryFull(const CConfig& config) { + auto opt = common(config); + opt.correctGradient = true; + opt.viscous = true; + opt.oneSided = true; + return opt; + } + + /*! + * \brief Options of the diffusive pass of a fluid interface, which follows a convective pass + * over the donor vertices. + * \param[in] correctGrad - Whether the projected gradient is corrected for skewness, which the + * models do not agree on at this boundary. + */ + static ScalarFluxOptions BoundaryDiffusive(const CConfig& config, bool correctGrad) { + auto opt = common(config); + opt.correctGradient = correctGrad; + opt.convective = false; + opt.viscous = true; + opt.oneSided = true; + return opt; + } + + private: + static ScalarFluxOptions common(const CConfig& config) { + ScalarFluxOptions opt; + opt.dynamicGrid = config.GetDynamic_Grid(); + opt.implicit = config.GetKind_TimeIntScheme() == EULER_IMPLICIT; + return opt; + } }; /*! @@ -81,11 +148,15 @@ class CAvgGradScalarBase { protected: using Int = typename CLaneTraits::Int; + /*! + * \param[in] rho - Density of both endpoints, read once by ComputeFlux. + */ template FORCEINLINE void diffusionTerms(const FlowIndices& idx, const ScalarFluxOptions& opt, Int iPoint, const EdgeSide& side_i, Int jPoint, - const EdgeSide& side_j, const Vector& normal, - const Vector& vector_ij, EdgeResidual& res) const { + const EdgeSide& side_j, const CPair& rho, + const Vector& normal, const Vector& vector_ij, + EdgeResidual& res) const { if (!opt.viscous) return; constexpr size_t Size = EdgeResidual::Size; @@ -100,8 +171,8 @@ class CAvgGradScalarBase { * actual width of the gradient container in either case. ---*/ Matrix avgGrad; for (size_t iVar = 0; iVar < res.nVar; ++iVar) { - const auto grad_i = gatherVariables<1, nDim>(iPoint, side_i.scalarNodes.GetGradient(), iVar); - const auto grad_j = gatherVariables<1, nDim>(jPoint, side_j.scalarNodes.GetGradient(), iVar); + const auto grad_i = gatherVariables(iPoint, side_i.scalarNodes.GetGradient(), iVar); + const auto grad_j = gatherVariables(jPoint, side_j.scalarNodes.GetGradient(), iVar); for (int iDim = 0; iDim < nDim; ++iDim) avgGrad(iVar, iDim) = 0.5 * (grad_i(iDim) + grad_j(iDim)); } @@ -119,67 +190,68 @@ class CAvgGradScalarBase { /*--- The Jacobians of a conservative model are w.r.t. the conserved (density-weighted) * variable, which divides the geometric projection by the density of the row being written. ---*/ - Double w_i = 1.0, w_j = 1.0; + Double proj_on_w_i = proj_vector_ij, proj_on_w_j = proj_vector_ij; if constexpr (Derived::Conservative) { - w_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); - w_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); + proj_on_w_i = proj_vector_ij / rho.i; + proj_on_w_j = proj_vector_ij / rho.j; } - const Double proj_on_w_i = proj_vector_ij / w_i; - const Double proj_on_w_j = proj_vector_ij / w_j; const auto* self = static_cast(this); - const auto D = self->coefficients(idx, iPoint, side_i, jPoint, side_j); + const auto D = self->coefficients(idx, iPoint, side_i, jPoint, side_j, rho); for (size_t iVar = 0; iVar < res.nVar; ++iVar) { if constexpr (Derived::DiagonalDiffusion) { res.flux_i(iVar) -= D.i(iVar) * projGrad(iVar); - res.jac_ii(iVar, iVar) += D.i(iVar) * proj_on_w_i; - res.jac_ij(iVar, iVar) -= D.i(iVar) * proj_on_w_j; - + if (opt.implicit) { + res.jac_ii(iVar, iVar) += D.i(iVar) * proj_on_w_i; + if (!opt.oneSided) res.jac_ij(iVar, iVar) -= D.i(iVar) * proj_on_w_j; + } if (!opt.oneSided) { res.flux_j(iVar) += D.j(iVar) * projGrad(iVar); - res.jac_ji(iVar, iVar) -= D.j(iVar) * proj_on_w_i; - res.jac_jj(iVar, iVar) += D.j(iVar) * proj_on_w_j; + if (opt.implicit) { + res.jac_ji(iVar, iVar) -= D.j(iVar) * proj_on_w_i; + res.jac_jj(iVar, iVar) += D.j(iVar) * proj_on_w_j; + } } } else { for (size_t jVar = 0; jVar < res.nVar; ++jVar) { res.flux_i(iVar) -= D.i(iVar, jVar) * projGrad(jVar); - res.jac_ii(iVar, jVar) += D.i(iVar, jVar) * proj_on_w_i; - res.jac_ij(iVar, jVar) -= D.i(iVar, jVar) * proj_on_w_j; - + if (opt.implicit) { + res.jac_ii(iVar, jVar) += D.i(iVar, jVar) * proj_on_w_i; + if (!opt.oneSided) res.jac_ij(iVar, jVar) -= D.i(iVar, jVar) * proj_on_w_j; + } if (!opt.oneSided) { res.flux_j(iVar) += D.j(iVar, jVar) * projGrad(jVar); - res.jac_ji(iVar, jVar) -= D.j(iVar, jVar) * proj_on_w_i; - res.jac_jj(iVar, jVar) += D.j(iVar, jVar) * proj_on_w_j; + if (opt.implicit) { + res.jac_ji(iVar, jVar) -= D.j(iVar, jVar) * proj_on_w_i; + res.jac_jj(iVar, jVar) += D.j(iVar, jVar) * proj_on_w_j; + } } } } } - if (opt.accurateJacobians) { - /*--- Coefficients that depend on the transported variables contribute here. A model whose - * correction is a per-edge constant (e.g. SA's) can ignore the side/point arguments; one - * whose correction depends on point values (e.g. SST's, on the transported variable at - * either endpoint) needs them, so every model is handed the same full context diffusionTerms - * itself has, matching extraDiffusionTerms's signature below. ---*/ - self->coefficientJacobians(idx, iPoint, side_i, jPoint, side_j, projGrad, res); + if (opt.implicit && opt.accurateJacobians) { + /*--- Coefficients that depend on the transported variables contribute here, from whatever + * the model chose to carry in the object it returned from coefficients. ---*/ + self->coefficientJacobians(opt, D, projGrad, res); } - self->extraDiffusionTerms(idx, iPoint, side_i, jPoint, side_j, normal, vector_ij, res); + self->extraDiffusionTerms(idx, opt, iPoint, side_i, jPoint, side_j, rho, normal, vector_ij, res); } /*! * \brief Contribution of the derivatives of the coefficients themselves. */ template - FORCEINLINE void coefficientJacobians(Ts&...) const {} + FORCEINLINE void coefficientJacobians(Ts&&...) const {} /*! * \brief Diffusion of a model that transports more than one gradient, of states it * synthesises from its own containers. */ template - FORCEINLINE void extraDiffusionTerms(Ts&...) const {} + FORCEINLINE void extraDiffusionTerms(Ts&&...) const {} }; /*! @@ -196,31 +268,42 @@ class CUpwScalarFlux : public CAvgGradScalarBase - FORCEINLINE void finalizeFlux(const FlowIndices& idx, const ScalarFluxOptions&, Int iPoint, - const EdgeSide& side_i, Int jPoint, const EdgeSide& side_j, - const Double& a0, const Double& a1, const CPair>& phi, + FORCEINLINE void finalizeFlux(const FlowIndices&, const ScalarFluxOptions& opt, Int, const EdgeSide&, + Int, const EdgeSide&, const Double& a0, const Double& a1, + const CPair& rho, const CPair>& phi, EdgeResidual& res) const { Double w0 = a0, w1 = a1; if constexpr (Derived::Conservative) { - w0 *= gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); - w1 *= gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); + w0 *= rho.i; + w1 *= rho.j; } for (size_t iVar = 0; iVar < res.nVar; ++iVar) { const Double flux = w0 * phi.i.all(iVar) + w1 * phi.j.all(iVar); res.flux_i(iVar) += flux; - res.flux_j(iVar) -= flux; + if (!opt.oneSided) res.flux_j(iVar) -= flux; - res.jac_ii(iVar, iVar) += w0; - res.jac_ij(iVar, iVar) += w1; - res.jac_ji(iVar, iVar) -= w0; - res.jac_jj(iVar, iVar) -= w1; + if (opt.implicit) { + res.jac_ii(iVar, iVar) += a0; + if (!opt.oneSided) { + res.jac_ij(iVar, iVar) += a1; + res.jac_ji(iVar, iVar) -= a0; + res.jac_jj(iVar, iVar) -= a1; + } + } } } }; @@ -243,6 +326,12 @@ class CUpwScalarBase : public CUpwScalarFlux::Size; + /*! + * \brief Whether the model's diffusion coefficients read the density, beyond the reading that + * Conservative already implies. A model that declares neither never gathers it. + */ + static constexpr bool DiffusionReadsDensity = false; + protected: using Base = CUpwScalarFlux; @@ -300,18 +389,26 @@ class CUpwScalarBase : public CUpwScalarFlux(iPoint, side_i.coord, jPoint, side_j.coord); } + /*--- Density of both endpoints, gathered once: the conservative weighting of the convective + * term, the bounded scheme's division of the mass flux, and some models' diffusion + * coefficients all want it, and in reverse mode every gather is a preaccumulation input. ---*/ + CPair rho{Double(1.0), Double(1.0)}; + if (Derived::Conservative || Derived::DiffusionReadsDensity || opt.boundedScalar) { + rho.i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); + rho.j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); + } + if (opt.convective) { - /*--- Upwinding weights of the face normal mass or volume flux. ---*/ + /*--- Upwinding weights of the face normal mass or volume flux, and the density that weights + * a conservative flux, which follows the velocity in being reconstructed or not. ---*/ Double a0, a1; + CPair rhoConv = rho; + if (opt.boundedScalar) { AD::SetPreaccIn(massFlux); - const Double rho_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); - const Double rho_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); - a0 = fmax(0.0, massFlux) / rho_i; - a1 = fmin(0.0, massFlux) / rho_j; + a0 = fmax(0.0, massFlux) / rho.i; + a1 = fmin(0.0, massFlux) / rho.j; } else { - /*--- The mass-flux branch above reads the edge flux computed from unreconstructed flow - * primitives directly, so only this branch needs a reconstructed velocity. ---*/ CPair> u; u.i.all = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Velocity()); u.j.all = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Velocity()); @@ -320,6 +417,19 @@ class CUpwScalarBase : public CUpwScalarFlux(iPoint, jPoint, vector_ij, side_i.flowNodes->GetGradient_Reconstruction(), side_i.flowNodes->GetLimiter_Primitive(), limiterTypeFlow, idx.Velocity(), u, kappaFlow, umusclRamp); + + if constexpr (Derived::Conservative) { + /*--- Density is not adjacent to the velocity in the primitive row, so it is a second + * reconstruction of one variable rather than a wider slice of the first. ---*/ + CPair> r; + r.i.all(0) = rho.i; + r.j.all(0) = rho.j; + reconstruct<1>(iPoint, jPoint, vector_ij, side_i.flowNodes->GetGradient_Reconstruction(), + side_i.flowNodes->GetLimiter_Primitive(), limiterTypeFlow, idx.Density(), r, kappaFlow, + umusclRamp); + rhoConv.i = r.i.all(0); + rhoConv.j = r.j.all(0); + } } /*--- Face normal velocity of the mean of the two points, relative to the grid. ---*/ @@ -356,17 +466,18 @@ class CUpwScalarBase : public CUpwScalarFlux(this)->finalizeFlux(idx, opt, iPoint, side_i, jPoint, side_j, a0, a1, phi, res); + static_cast(this)->finalizeFlux(idx, opt, iPoint, side_i, jPoint, side_j, a0, a1, rhoConv, phi, + res); } - Base::diffusionTerms(idx, opt, iPoint, side_i, jPoint, side_j, normal, vector_ij, res); + Base::diffusionTerms(idx, opt, iPoint, side_i, jPoint, side_j, rho, normal, vector_ij, res); - AD::SetPreaccOut(res.flux_i, res.nVar); - if (!opt.oneSided) AD::SetPreaccOut(res.flux_j, res.nVar); + setPreaccOut(res.flux_i, res.nVar); + if (!opt.oneSided) setPreaccOut(res.flux_j, res.nVar); AD::EndPreacc(); return res; @@ -378,11 +489,11 @@ class CUpwScalarBase : public CUpwScalarFlux FORCEINLINE void ComputeFlux(const ScalarFluxOptions& opt, Int iEdge, Int iPoint, const EdgeSide& side_i, Int jPoint, const EdgeSide& side_j, - const Vector& normal, const Double& massFlux, bool implicit, - UpdateType updateType, Double updateMask, CSysVector& vector, - CSysVector& vectorDiff, SparseMatrixType& matrix) const { + const Vector& normal, const Double& massFlux, UpdateType updateType, + Double updateMask, CSysVector& vector, CSysVector& vectorDiff, + SparseMatrixType& matrix) const { const auto res = ComputeFlux(opt, iPoint, side_i, jPoint, side_j, normal, massFlux); - updateLinearSystem(iEdge, iPoint, jPoint, implicit, updateType, updateMask, res, vector, vectorDiff, matrix); + updateLinearSystem(iEdge, iPoint, jPoint, opt.implicit, updateType, updateMask, res, vector, vectorDiff, matrix); } }; diff --git a/SU2_CFD/include/numerics/species/flamelet_edge_flux.hpp b/SU2_CFD/include/numerics/species/flamelet_edge_flux.hpp new file mode 100644 index 00000000000..bd1f140dcc6 --- /dev/null +++ b/SU2_CFD/include/numerics/species/flamelet_edge_flux.hpp @@ -0,0 +1,170 @@ +/*! + * \file flamelet_edge_flux.hpp + * \brief Flamelet transport as a third-layer scalar flux, see numerics/scalar/scalar_edge_flux.hpp. + * \author P. Gomes + * \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 . + */ + +#pragma once + +#include "species_edge_flux.hpp" + +/*! + * \class CScalarFlux_Flamelet + * \ingroup ViscDiscr + * \brief Convection and diffusion of the flamelet controlling variables and passive species, + * which is species transport plus two preferential diffusion terms. + * \note The preferential diffusion terms read the beta scalars and their gradients from the + * auxiliary variables of the solver's own containers, which the per-marker ghost containers + * of a boundary do not carry. They are an interior edge term: boundaries instantiate + * CScalarFlux_Species, as they did before this model existed. + */ +template +class CScalarFlux_Flamelet final + : public CScalarFluxSpeciesBase, FlowIndices, nDim, + nVar> { + public: + using Base = CScalarFluxSpeciesBase; + using Int = typename Base::Int; + + explicit CScalarFlux_Flamelet(const CConfig& config) + : Base(config), + preferentialDiffusion(config.GetFlameletParsedOptions().preferential_diffusion), + nControlVars(config.GetFlameletParsedOptions().n_control_vars) {} + + /*! + * \brief Preferential diffusion, two terms with the shape of the ordinary diffusion but of + * states the model synthesises: div(D grad(beta - phi)) for each controlling variable, + * and a thermal term div(beta_T D grad(T)) on the enthalpy equation. + * \note The thermal term has no implicit part, matching the treatment of the heat flux it + * models; the first term has the same thin shear layer Jacobian as the ordinary diffusion, + * because it is the same operator applied to a shifted state. + */ + template + FORCEINLINE void extraDiffusionTerms(const FlowIndices& idx, const ScalarFluxOptions& opt, Int iPoint, + const EdgeSide& side_i, Int jPoint, + const EdgeSide& side_j, const CPair& rho, + const Vector& normal, const Vector& vector_ij, + EdgeResidual& res) const { + if (!preferentialDiffusion) return; + + const Double dist2_ij = fmax(squaredNorm(vector_ij), EPS); + const Double proj_vector_ij = dot(vector_ij, normal) / dist2_ij; + const Double proj_on_rho_i = proj_vector_ij / rho.i; + const Double proj_on_rho_j = proj_vector_ij / rho.j; + + const Double diffTurb = Base::turbulentDiffusivity(idx, iPoint, side_i, jPoint, side_j); + + /*--- The gradient of a controlling variable is subtracted from that of its beta scalar, so + * that what is added here is the difference from the ordinary diffusion already applied. ---*/ + for (auto iScalar = 0u; iScalar < nControlVars; ++iScalar) { + const auto iBeta = betaIndex(iScalar); + + const Double phi_i = gatherVariables(iPoint, side_i.scalarNodes.GetAuxVar(), iBeta) - + gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), iScalar); + const Double phi_j = gatherVariables(jPoint, side_j.scalarNodes.GetAuxVar(), iBeta) - + gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), iScalar); + + auto grad_i = gatherVariables(iPoint, side_i.scalarNodes.GetAuxVarGradient(), iBeta); + auto grad_j = gatherVariables(jPoint, side_j.scalarNodes.GetAuxVarGradient(), iBeta); + const auto gradPhi_i = gatherVariables(iPoint, side_i.scalarNodes.GetGradient(), iScalar); + const auto gradPhi_j = gatherVariables(jPoint, side_j.scalarNodes.GetGradient(), iScalar); + for (int iDim = 0; iDim < nDim; ++iDim) { + grad_i(iDim) -= gradPhi_i(iDim); + grad_j(iDim) -= gradPhi_j(iDim); + } + + const Double D_i = gatherVariables(iPoint, side_i.scalarNodes.GetDiffusivity(), iScalar); + const Double D_j = gatherVariables(jPoint, side_j.scalarNodes.GetDiffusivity(), iScalar); + const Double D = 0.5 * (rho.i * D_i + rho.j * D_j) + diffTurb; + + const Double projGrad = projectedGradient(opt, grad_i, grad_j, phi_i, phi_j, normal, vector_ij, dist2_ij); + + res.flux_i(iScalar) -= D * projGrad; + res.flux_j(iScalar) += D * projGrad; + + if (opt.implicit) { + res.jac_ii(iScalar, iScalar) += D * proj_on_rho_i; + res.jac_ij(iScalar, iScalar) -= D * proj_on_rho_j; + res.jac_ji(iScalar, iScalar) -= D * proj_on_rho_i; + res.jac_jj(iScalar, iScalar) += D * proj_on_rho_j; + } + } + + /*--- Thermal term, on the enthalpy equation alone, driven by the temperature gradient. ---*/ + if (nControlVars <= I_ENTH) return; + + const Double T_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Temperature()); + const Double T_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Temperature()); + + const auto gradT_i = gatherVariables(iPoint, side_i.flowNodes->GetGradient_Primitive(), idx.Temperature()); + const auto gradT_j = gatherVariables(jPoint, side_j.flowNodes->GetGradient_Primitive(), idx.Temperature()); + + const Double Dth_i = gatherVariables(iPoint, side_i.scalarNodes.GetAuxVar(), I_BETA_ENTH_THERMAL) * + gatherVariables(iPoint, side_i.scalarNodes.GetDiffusivity(), I_ENTH); + const Double Dth_j = gatherVariables(jPoint, side_j.scalarNodes.GetAuxVar(), I_BETA_ENTH_THERMAL) * + gatherVariables(jPoint, side_j.scalarNodes.GetDiffusivity(), I_ENTH); + const Double Dth = 0.5 * (rho.i * Dth_i + rho.j * Dth_j) + diffTurb; + + const Double projGradT = projectedGradient(opt, gradT_i, gradT_j, T_i, T_j, normal, vector_ij, dist2_ij); + + res.flux_i(I_ENTH) -= Dth * projGradT; + res.flux_j(I_ENTH) += Dth * projGradT; + } + + private: + const bool preferentialDiffusion; + const unsigned short nControlVars; + + /*! + * \brief Auxiliary variable holding the beta scalar of a controlling variable. + */ + static FORCEINLINE unsigned short betaIndex(unsigned short iScalar) { + switch (iScalar) { + case I_PROGVAR: + return I_BETA_PROGVAR; + case I_ENTH: + return I_BETA_ENTH; + default: + return I_BETA_MIXFRAC; + } + } + + /*! + * \brief Average gradient of one synthesised state projected on the normal, corrected for + * skewness when asked, which is what the ordinary diffusion does for a transported one. + */ + FORCEINLINE Double projectedGradient(const ScalarFluxOptions& opt, const Vector& grad_i, + const Vector& grad_j, const Double& phi_i, const Double& phi_j, + const Vector& normal, const Vector& vector_ij, + const Double& dist2_ij) const { + Vector avgGrad; + for (int iDim = 0; iDim < nDim; ++iDim) avgGrad(iDim) = 0.5 * (grad_i(iDim) + grad_j(iDim)); + + if (opt.correctGradient) { + const Double corr = (dot(avgGrad, vector_ij) - phi_j + phi_i) / dist2_ij; + for (int iDim = 0; iDim < nDim; ++iDim) avgGrad(iDim) -= corr * vector_ij(iDim); + } + return dot(avgGrad, normal); + } +}; diff --git a/SU2_CFD/include/numerics/species/species_edge_flux.hpp b/SU2_CFD/include/numerics/species/species_edge_flux.hpp index e5046a7d9b6..e744d8c9655 100644 --- a/SU2_CFD/include/numerics/species/species_edge_flux.hpp +++ b/SU2_CFD/include/numerics/species/species_edge_flux.hpp @@ -1,6 +1,6 @@ /*! * \file species_edge_flux.hpp - * \brief Species transport model as a third-layer scalar flux, see numerics/scalar/scalar_edge_flux.hpp. + * \brief Species transport as a third-layer scalar flux, see numerics/scalar/scalar_edge_flux.hpp. * \author P. Gomes * \version 8.5.0 "Harrier" * @@ -30,28 +30,29 @@ #include "../scalar/scalar_edge_flux.hpp" /*! - * \class CScalarFlux_Species + * \class CScalarFluxSpeciesBase * \ingroup ViscDiscr - * \brief Convection and diffusion of the species transport model, conservative with a diagonal, + * \brief Convection and diffusion of a mass fraction, conservative with a diagonal, * i/j-symmetric diffusion matrix. Unlike SA/SST/LM, the equation count is only known at * runtime (one per transported species), so this is the framework's first Dynamic-nVar * model: nEqn is passed to the base explicitly, and coefficients() loops to it rather than * to a compile-time nVar. - * \note Species writes no finalizeFlux of its own: the inherited CUpwScalarFlux one is exactly + * \note This carries no finalizeFlux of its own: the inherited CUpwScalarFlux one is exactly * flux(iVar) = a0*rho_i*Y_i(iVar) + a1*rho_j*Y_j(iVar), Conservative weighting by density, * which is the model's whole convective term. + * \note It takes the most derived class as a parameter so that the flamelet model, which adds a + * preferential diffusion term to the same coefficients, is a sibling rather than a copy. */ -template -class CScalarFlux_Species - : public CUpwScalarBase, FlowIndices, nDim, nVar> { +template +class CScalarFluxSpeciesBase : public CUpwScalarBase { public: static constexpr bool Conservative = true; static constexpr bool DiagonalDiffusion = true; - using Base = CUpwScalarBase; + using Base = CUpwScalarBase; using Int = typename Base::Int; - explicit CScalarFlux_Species(const CConfig& config) + explicit CScalarFluxSpeciesBase(const CConfig& config) : Base(config, config.GetnSpecies()), turbulence(config.GetKind_Turb_Model() != TURB_MODEL::NONE), Sc_t(config.GetSchmidt_Number_Turbulent()) {} @@ -64,28 +65,49 @@ class CScalarFlux_Species template FORCEINLINE CPair> coefficients(const FlowIndices& idx, Int iPoint, const EdgeSide& side_i, Int jPoint, - const EdgeSide& side_j) const { - const Double rho_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); - const Double rho_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); - - Double diffTurb = 0.0; - if (turbulence) { - const Double muT_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.EddyViscosity()); - const Double muT_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.EddyViscosity()); - diffTurb = 0.5 * (muT_i / Sc_t + muT_j / Sc_t); - } + const EdgeSide& side_j, + const CPair& rho) const { + const Double diffTurb = turbulentDiffusivity(idx, iPoint, side_i, jPoint, side_j); Vector D; for (size_t iVar = 0; iVar < this->nEqn; ++iVar) { const Double D_lam_i = gatherVariables(iPoint, side_i.scalarNodes.GetDiffusivity(), iVar); const Double D_lam_j = gatherVariables(jPoint, side_j.scalarNodes.GetDiffusivity(), iVar); - const Double diffLam = 0.5 * (rho_i * D_lam_i + rho_j * D_lam_j); - D(iVar) = diffLam + diffTurb; + D(iVar) = 0.5 * (rho.i * D_lam_i + rho.j * D_lam_j) + diffTurb; } return {D, D}; } + protected: + /*! + * \brief Turbulent contribution to the diffusivity, shared by every species and, in the + * flamelet model, by the preferential diffusion terms. + */ + template + FORCEINLINE Double turbulentDiffusivity(const FlowIndices& idx, Int iPoint, const EdgeSide& side_i, + Int jPoint, const EdgeSide& side_j) const { + if (!turbulence) return Double(0.0); + + const Double muT_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.EddyViscosity()); + const Double muT_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.EddyViscosity()); + return 0.5 * (muT_i / Sc_t + muT_j / Sc_t); + } + private: const bool turbulence; const su2double Sc_t; }; + +/*! + * \class CScalarFlux_Species + * \ingroup ViscDiscr + * \brief Convection and diffusion of the species transport model. + */ +template +class CScalarFlux_Species final + : public CScalarFluxSpeciesBase, FlowIndices, nDim, + nVar> { + public: + using Base = CScalarFluxSpeciesBase; + using Base::Base; +}; diff --git a/SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp b/SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp index 40faf47bed9..927fcb6f5b0 100644 --- a/SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp +++ b/SU2_CFD/include/numerics/turbulent/transition/trans_edge_flux.hpp @@ -58,7 +58,8 @@ class CScalarFlux_TransLM template FORCEINLINE CPair> coefficients(const FlowIndices& idx, Int iPoint, const EdgeSide& side_i, Int jPoint, - const EdgeSide& side_j) const { + const EdgeSide& side_j, + const CPair&) const { const Double mu_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.LaminarViscosity()); const Double mu_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.LaminarViscosity()); const Double muT_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.EddyViscosity()); diff --git a/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp b/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp index 318cbad2d1a..4e4b9594ce7 100644 --- a/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp +++ b/SU2_CFD/include/numerics/turbulent/turb_sa_edge_flux.hpp @@ -45,6 +45,7 @@ class CScalarFlux_SA public: static constexpr bool Conservative = false; static constexpr bool DiagonalDiffusion = true; + static constexpr bool DiffusionReadsDensity = true; /*!< \brief The kinematic viscosities below. */ using Base = CUpwScalarBase; using Int = typename Base::Int; @@ -59,18 +60,23 @@ class CScalarFlux_SA * \brief SA convection, plus the centered advection of the backscatter equations when nVar > 1. */ template - FORCEINLINE void finalizeFlux(const FlowIndices&, const ScalarFluxOptions&, Int, const EdgeSide&, Int, - const EdgeSide&, const Double& a0, const Double& a1, - const CPair>& phi, EdgeResidual& res) const { + FORCEINLINE void finalizeFlux(const FlowIndices&, const ScalarFluxOptions& opt, Int, const EdgeSide&, + Int, const EdgeSide&, const Double& a0, const Double& a1, + const CPair&, const CPair>& phi, + EdgeResidual& res) const { const Double flux = a0 * phi.i.all(0) + a1 * phi.j.all(0); res.flux_i(0) += flux; - res.flux_j(0) -= flux; - - res.jac_ii(0, 0) += a0; - res.jac_ij(0, 0) += a1; - res.jac_ji(0, 0) -= a0; - res.jac_jj(0, 0) -= a1; + if (!opt.oneSided) res.flux_j(0) -= flux; + + if (opt.implicit) { + res.jac_ii(0, 0) += a0; + if (!opt.oneSided) { + res.jac_ij(0, 0) += a1; + res.jac_ji(0, 0) -= a0; + res.jac_jj(0, 0) -= a1; + } + } /*--- Stochastic backscatter: three Langevin equations, advected with the mean of the two * upwinding weights and with no diffusion. ---*/ @@ -79,17 +85,21 @@ class CScalarFlux_SA const Double flux_bs = avg * (phi.i.all(iVar) + phi.j.all(iVar)); res.flux_i(iVar) += flux_bs; - res.flux_j(iVar) -= flux_bs; - - res.jac_ii(iVar, iVar) += avg; - res.jac_ij(iVar, iVar) += avg; - res.jac_ji(iVar, iVar) -= avg; - res.jac_jj(iVar, iVar) -= avg; + if (!opt.oneSided) res.flux_j(iVar) -= flux_bs; + + if (opt.implicit) { + res.jac_ii(iVar, iVar) += avg; + if (!opt.oneSided) { + res.jac_ij(iVar, iVar) += avg; + res.jac_ji(iVar, iVar) -= avg; + res.jac_jj(iVar, iVar) -= avg; + } + } } } /*! - * \brief Diffusion coefficients of both orientations of the edge, see CAvgGrad_TurbSA. + * \brief Diffusion coefficients of both orientations of the edge. * \note The coefficient is not symmetric: it uses the transported variable of the row it is * going to be used for (the quadratic, non-conservative part of the diffusion term). * Coefficients past index 0 are left at zero, the backscatter equations have no diffusion. @@ -97,11 +107,10 @@ class CScalarFlux_SA template FORCEINLINE CPair> coefficients(const FlowIndices& idx, Int iPoint, const EdgeSide& side_i, Int jPoint, - const EdgeSide& side_j) const { - const Double nu_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.LaminarViscosity()) / - gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); - const Double nu_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.LaminarViscosity()) / - gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); + const EdgeSide& side_j, + const CPair& rho) const { + const Double nu_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.LaminarViscosity()) / rho.i; + const Double nu_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.LaminarViscosity()) / rho.j; const Double nuTilde_i = gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), 0); const Double nuTilde_j = gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), 0); @@ -120,20 +129,20 @@ class CScalarFlux_SA /*! * \brief Extra Jacobian terms from the dependence of the diffusion coefficient on nu_tilde. - * \note The two derivatives below are per-edge constants (cb2/sigma only), so the point/side - * context diffusionTerms hands every model's coefficientJacobians is unused here. */ - template - FORCEINLINE void coefficientJacobians(const FlowIndices&, Int, const EdgeSide&, Int, - const EdgeSide&, const Vector& projGrad, - EdgeResidual& res) const { + template + FORCEINLINE void coefficientJacobians(const ScalarFluxOptions& opt, const Coefficients&, + const Vector& projGrad, EdgeResidual& res) const { /*--- d(diffusion coefficient of i)/d(nu_tilde_i), and its counterpart w.r.t. nu_tilde_j; * the coefficient of j is the same expression with i and j swapped, so the same two - * derivatives apply to both orientations. ---*/ + * derivatives apply to both orientations. Both are per-edge constants, so the coefficients + * themselves are not read here. ---*/ const Double dDC_dNuTilde_i = ((1.0 + cb2) * 0.5 - cb2) / sigma; const Double dDC_dNuTilde_j = (1.0 + cb2) * 0.5 / sigma; res.jac_ii(0, 0) -= dDC_dNuTilde_i * projGrad(0); + if (opt.oneSided) return; + res.jac_ij(0, 0) -= dDC_dNuTilde_j * projGrad(0); res.jac_ji(0, 0) += dDC_dNuTilde_j * projGrad(0); res.jac_jj(0, 0) += dDC_dNuTilde_i * projGrad(0); diff --git a/SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp b/SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp index 4f47a795ff4..b891b6e14d5 100644 --- a/SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp +++ b/SU2_CFD/include/numerics/turbulent/turb_sst_edge_flux.hpp @@ -59,18 +59,22 @@ class CScalarFlux_SST public: /*! - * \brief Diffusion coefficients of both orientations of the edge. - * \note The cross term below reads the transported omega of whichever point its row is being - * written for, so it is not symmetric: D.i, read by i's row, uses omega at i; D.j, read - * by j's row, uses omega at j. Every other entry is an i/j average, so it is the same in - * both matrices. + * \brief Diffusion coefficients of both orientations of the edge, and the terms of the cross + * diffusion that the Jacobian correction below needs, so that neither the gathers nor + * the blending are repeated for it. + * \note The cross term reads the transported omega of whichever point its row is being written + * for, so it is not symmetric: i, read by i's row, uses omega at i; j, read by j's row, + * uses omega at j. Every other entry is an i/j average, so it is the same in both. */ + struct CCoefficients { + Matrix i, j; + Double lambda_ij, omega_i, omega_j; + }; + template - FORCEINLINE CPair> coefficients(const FlowIndices& idx, Int iPoint, - const EdgeSide& side_i, Int jPoint, - const EdgeSide& side_j) const { - const Double rho_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); - const Double rho_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); + FORCEINLINE CCoefficients coefficients(const FlowIndices& idx, Int iPoint, const EdgeSide& side_i, + Int jPoint, const EdgeSide& side_j, + const CPair& rho) const { const Double mu_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.LaminarViscosity()); const Double mu_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.LaminarViscosity()); const Double muT_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.EddyViscosity()); @@ -78,8 +82,10 @@ class CScalarFlux_SST const Double F1_i = gatherVariables(iPoint, side_i.scalarNodes.GetF1blending()); const Double F1_j = gatherVariables(jPoint, side_j.scalarNodes.GetF1blending()); - const Double omega_i = gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), 1); - const Double omega_j = gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), 1); + + CCoefficients D; + D.omega_i = gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), 1); + D.omega_j = gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), 1); const Double sigma_kine_i = F1_i * sigma_k1 + (1.0 - F1_i) * sigma_k2; const Double sigma_kine_j = F1_j * sigma_k1 + (1.0 - F1_j) * sigma_k2; @@ -89,31 +95,30 @@ class CScalarFlux_SST const Double diff_kine = 0.5 * ((mu_i + sigma_kine_i * muT_i) + (mu_j + sigma_kine_j * muT_j)); const Double diff_omega = 0.5 * ((mu_i + sigma_omega_i * muT_i) + (mu_j + sigma_omega_j * muT_j)); - const Double lambda_i = 2.0 * (1.0 - F1_i) * rho_i * sigma_omega_i; - const Double lambda_j = 2.0 * (1.0 - F1_j) * rho_j * sigma_omega_j; - const Double lambda_ij = 0.5 * (lambda_i + lambda_j); - const Double w_ij = 0.5 * (omega_i + omega_j); + const Double lambda_i = 2.0 * (1.0 - F1_i) * rho.i * sigma_omega_i; + const Double lambda_j = 2.0 * (1.0 - F1_j) * rho.j * sigma_omega_j; + D.lambda_ij = 0.5 * (lambda_i + lambda_j); + const Double w_ij = 0.5 * (D.omega_i + D.omega_j); /*--- Cross-diffusion coefficient: a divergence-theorem term (diff_omega_T2) plus a cell * centre correction (diff_omega_T3) that reads the transported omega of the row's own point. ---*/ - const Double diff_omega_T2 = lambda_ij; - const Double diff_omega_T3_i = -omega_i * lambda_ij / w_ij; - const Double diff_omega_T3_j = -omega_j * lambda_ij / w_ij; + const Double diff_omega_T2 = D.lambda_ij; + const Double diff_omega_T3_i = -D.omega_i * D.lambda_ij / w_ij; + const Double diff_omega_T3_j = -D.omega_j * D.lambda_ij / w_ij; - /*--- D_i(0,1) and D_j(0,1) are left zero: there is no diffusive coupling from omega into + /*--- D.i(0,1) and D.j(0,1) are left zero: there is no diffusive coupling from omega into * the k row. ---*/ - Matrix D_i, D_j; - D_i = Double(0.0); - D_j = Double(0.0); - D_i(0, 0) = diff_kine; - D_i(1, 1) = diff_omega; - D_i(1, 0) = diff_omega_T2 + diff_omega_T3_i; - - D_j(0, 0) = diff_kine; - D_j(1, 1) = diff_omega; - D_j(1, 0) = diff_omega_T2 + diff_omega_T3_j; - - return {D_i, D_j}; + D.i = Double(0.0); + D.j = Double(0.0); + D.i(0, 0) = diff_kine; + D.i(1, 1) = diff_omega; + D.i(1, 0) = diff_omega_T2 + diff_omega_T3_i; + + D.j(0, 0) = diff_kine; + D.j(1, 1) = diff_omega; + D.j(1, 0) = diff_omega_T2 + diff_omega_T3_j; + + return D; } /*! @@ -125,28 +130,16 @@ class CScalarFlux_SST * gives +E_j in both jac_ii and jac_ji, differentiating against omega_j gives -E_i in both * jac_ij and jac_jj. */ - template - FORCEINLINE void coefficientJacobians(const FlowIndices& idx, Int iPoint, const EdgeSide& side_i, - Int jPoint, const EdgeSide& side_j, + template + FORCEINLINE void coefficientJacobians(const ScalarFluxOptions& opt, const CCoefficients& D, const Vector& projGrad, EdgeResidual& res) const { - const Double rho_i = gatherVariables(iPoint, side_i.flowNodes->GetPrimitive(), idx.Density()); - const Double rho_j = gatherVariables(jPoint, side_j.flowNodes->GetPrimitive(), idx.Density()); - const Double F1_i = gatherVariables(iPoint, side_i.scalarNodes.GetF1blending()); - const Double F1_j = gatherVariables(jPoint, side_j.scalarNodes.GetF1blending()); - const Double omega_i = gatherVariables(iPoint, side_i.scalarNodes.GetSolution(), 1); - const Double omega_j = gatherVariables(jPoint, side_j.scalarNodes.GetSolution(), 1); - - const Double sigma_omega_i = F1_i * sigma_om1 + (1.0 - F1_i) * sigma_om2; - const Double sigma_omega_j = F1_j * sigma_om1 + (1.0 - F1_j) * sigma_om2; - const Double lambda_i = 2.0 * (1.0 - F1_i) * rho_i * sigma_omega_i; - const Double lambda_j = 2.0 * (1.0 - F1_j) * rho_j * sigma_omega_j; - const Double lambda_ij = 0.5 * (lambda_i + lambda_j); - - const Double denom = pow(omega_i + omega_j, 2.0); - const Double E_i = 2.0 * lambda_ij * omega_i / denom * projGrad(0); - const Double E_j = 2.0 * lambda_ij * omega_j / denom * projGrad(0); + const Double denom = pow(D.omega_i + D.omega_j, 2.0); + const Double E_i = 2.0 * D.lambda_ij * D.omega_i / denom * projGrad(0); + const Double E_j = 2.0 * D.lambda_ij * D.omega_j / denom * projGrad(0); res.jac_ii(1, 1) += E_j; + if (opt.oneSided) return; + res.jac_ij(1, 1) -= E_i; res.jac_ji(1, 1) += E_j; res.jac_jj(1, 1) -= E_i; diff --git a/SU2_CFD/include/numerics/util.hpp b/SU2_CFD/include/numerics/util.hpp index 5f39e6b8ae5..13bacf5a282 100644 --- a/SU2_CFD/include/numerics/util.hpp +++ b/SU2_CFD/include/numerics/util.hpp @@ -182,7 +182,7 @@ struct EdgeResidual { * \note A static model zeroes its whole storage with constant trip counts; a dynamic one * zeroes the leading nVar rows and columns and leaves the rest of the backing untouched. */ - FORCEINLINE explicit EdgeResidual(size_t nEqn = Size) : nVar(nEqn) { + FORCEINLINE explicit EdgeResidual(size_t nEqn) : nVar(nEqn) { for (size_t iVar = 0; iVar < nVar; ++iVar) { flux_i(iVar) = 0.0; flux_j(iVar) = 0.0; @@ -333,12 +333,27 @@ FORCEINLINE Matrix gatherVariables(Int iPoint, const Conta } #endif +/*! + * \brief Register the leading nVar entries of a static vector as preaccumulation outputs. + * \note A lane vector is registered one lane at a time, a scalar in one call; a kernel therefore + * reaches this rather than AD::SetPreaccOut directly, and reads the same whichever value + * type it is bound to. + */ +template +FORCEINLINE void setPreaccOut(Vector& x, size_t nVar) { + if constexpr (CLaneTraits::Size == 1) { + AD::SetPreaccOut(x, static_cast(nVar)); + } else { + AD::SetPreaccOut(x, static_cast(nVar), CLaneTraits::Size); + } +} + /*! * \brief Stop the AD preaccumulation. */ template FORCEINLINE void stopPreacc(Vector& x) { - AD::SetPreaccOut(x, nVar, CLaneTraits::Size); + setPreaccOut(x, nVar); AD::EndPreacc(); } @@ -389,110 +404,96 @@ FORCEINLINE Double umusclProjection(const Double& gradProj, const Double& delta, } /*! - * \brief MUSCL reconstruction of the specified variable. - * \note The result should be halved when added to i (or subtracted from j). - * \note Reads its own row of the gradient container, rather than taking an already gathered - * nVarGrad x nDim block, so that a caller reconstructing a single variable, e.g. a scalar - * with nVar 1, never gathers a Matrix: that shape is the same RowMajor, - * one-row degeneracy that forces EdgeResidual's Size floor above, and here it would - * silently turn a row into a lone scalar instead of failing to compile, since - * Matrix still satisfies IsVector. - */ -template ::Int> -FORCEINLINE Double musclReconstruction(Int iPoint, const Gradient_t& gradient, size_t iRow, - const Vector& vector_ij, const Double& delta, - const CNonDeduced& kappa, const CNonDeduced& umusclRamp) { - const auto grad = gatherVariables(iPoint, gradient, iRow); - const Double proj = dot(grad, vector_ij); - return umusclRamp * umusclProjection(proj, delta, kappa); -} - -/*! - * \brief Unlimited reconstruction. - * \param[in] iRow - Starting row of gradient to read, for reconstructing a slice of a - * larger set of gradients (e.g. only the velocity out of the primitives). - * \param[in] nVarGradRuntime - Equation count of a Dynamic model, known only at runtime; ignored - * (falling back to nVarGrad_ or VarType::nVar) when left at its default of 0. + * \brief Reads the gradient rows of one point as one block, gathered up front. + * \note This is what a kernel whose variable count is a compile-time constant above one wants: + * one gather of the whole nVarGrad x nDim block instead of nVarGrad of them. */ -template -FORCEINLINE void musclUnlimited(typename CLaneTraits::Int iPoint, typename CLaneTraits::Int jPoint, - const Vector& vector_ij, const Gradient_t& gradient, CPair& V, - const CNonDeduced& kappa, const CNonDeduced& umusclRamp, - size_t iRow = 0, size_t nVarGradRuntime = 0) { - const size_t nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : (nVarGradRuntime > 0 ? nVarGradRuntime : VarType::nVar); +template +struct CGradientBlock { + using Int = typename CLaneTraits::Int; + Matrix rows; - for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { - /*--- Centered difference, needed for U-MUSCL projection ---*/ - const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); + FORCEINLINE CGradientBlock(Int iPoint, const Gradient_t& gradient, size_t iRow) + : rows(gatherVariables(iPoint, gradient, iRow)) {} - /*--- U-MUSCL reconstructed variables ---*/ - const Double proj_i = - musclReconstruction(iPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); - const Double proj_j = - musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); - - /*--- Apply reconstruction: V_L = V_i + 0.5 * dV_ij^kap ---*/ - V.i.all(iVar) += 0.5 * proj_i; - V.j.all(iVar) -= 0.5 * proj_j; + FORCEINLINE Double project(size_t iVar, const Vector& vector_ij) const { + return dot(rows[iVar], vector_ij); } -} +}; /*! - * \brief Limited reconstruction with point-based limiter. + * \brief Reads the gradient rows of one point one row at a time. + * \note This is what a runtime variable count forces, since the block shape would not be a + * compile-time constant, and what a single variable forces, since its block would be a + * Matrix: that shape satisfies IsVector and would silently degenerate into + * a lone scalar instead of failing to compile. */ -template -FORCEINLINE void musclPointLimited(typename CLaneTraits::Int iPoint, typename CLaneTraits::Int jPoint, - const Vector& vector_ij, const Limiter_t& limiter, - const Gradient_t& gradient, CPair& V, const CNonDeduced& kappa, - const CNonDeduced& umusclRamp, size_t iRow = 0, - size_t nVarGradRuntime = 0) { - const size_t nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : (nVarGradRuntime > 0 ? nVarGradRuntime : VarType::nVar); +template +struct CGradientRows { + using Int = typename CLaneTraits::Int; + const Int iPoint; + const Gradient_t& gradient; + const size_t iRow; - /*--- Gathered one variable at a time rather than as a Vector: nVarGrad is only - * a compile-time constant when nVarGrad_ itself is one, and a Dynamic model's is runtime-only, - * so it can never be a gatherVariables template argument. ---*/ - for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { - const Double lim_i = gatherVariables(iPoint, limiter, iRow + iVar); - const Double lim_j = gatherVariables(jPoint, limiter, iRow + iVar); - - /*--- Centered difference, needed for U-MUSCL projection ---*/ - const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); - - /*--- U-MUSCL reconstructed variables ---*/ - const Double proj_i = - musclReconstruction(iPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); - const Double proj_j = - musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); + FORCEINLINE Double project(size_t iVar, const Vector& vector_ij) const { + return dot(gatherVariables(iPoint, gradient, iRow + iVar), vector_ij); + } +}; - /*--- Apply reconstruction: V_L = V_i + 0.5 * lim * dV_ij^kap ---*/ - V.i.all(iVar) += 0.5 * lim_i * proj_i; - V.j.all(iVar) -= 0.5 * lim_j * proj_j; +/*! + * \brief Gradient reader of one point, blocked or row by row according to nVarGrad_. + */ +template +FORCEINLINE auto gradientReader(typename CLaneTraits::Int iPoint, const Gradient_t& gradient, size_t iRow) { + if constexpr (nVarGrad_ > 1) { + return CGradientBlock(iPoint, gradient, iRow); + } else { + return CGradientRows{iPoint, gradient, iRow}; } } /*! - * \brief Limited reconstruction with edge-based limiter. + * \brief How the reconstructed differences are limited. */ -template -FORCEINLINE void musclEdgeLimited(typename CLaneTraits::Int iPoint, typename CLaneTraits::Int jPoint, - const Vector& vector_ij, const Gradient_t& gradient, CPair& V, - const CNonDeduced& kappa, const CNonDeduced& umusclRamp, - size_t iRow = 0, size_t nVarGradRuntime = 0) { +enum class MusclLimiter { NONE, EDGE, POINT }; + +/*! + * \brief U-MUSCL reconstruction of nVarGrad variables, from the gradient rows starting at iRow. + * \note The limiter kind is a template parameter so that the choice is made once, outside the + * loop, by the dispatching overload below. + * \param[in] iRow - Starting row of gradient (and column of limiter) to read, for reconstructing + * a slice of a larger set of gradients (e.g. only the velocity out of the primitives). + * \param[in] nVarGradRuntime - Variable count of a Dynamic model, known only at runtime; ignored + * (falling back to nVarGrad_ or VarType::nVar) when left at its default of 0. + */ +template +FORCEINLINE void muscl(typename CLaneTraits::Int iPoint, typename CLaneTraits::Int jPoint, + const Vector& vector_ij, const Gradient_t& gradient, const Limiter_t& limiter, + size_t iRow, CPair& V, const CNonDeduced& kappa, + const CNonDeduced& umusclRamp, size_t nVarGradRuntime) { const size_t nVarGrad = nVarGrad_ > 0 ? nVarGrad_ : (nVarGradRuntime > 0 ? nVarGradRuntime : VarType::nVar); + const auto grad_i = gradientReader(iPoint, gradient, iRow); + const auto grad_j = gradientReader(jPoint, gradient, iRow); + for (size_t iVar = 0; iVar < nVarGrad; ++iVar) { - /*--- Centered difference, needed for U-MUSCL projection and limiter ---*/ + /*--- Centered difference, needed for the U-MUSCL projection and the edge limiter. ---*/ const Double delta_ij = V.j.all(iVar) - V.i.all(iVar); - const Double delta_ij_2 = pow(delta_ij, 2) + 1e-6; - /*--- U-MUSCL reconstructed variables ---*/ - const Double proj_i = - musclReconstruction(iPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); - const Double proj_j = - musclReconstruction(jPoint, gradient, iRow + iVar, vector_ij, delta_ij, kappa, umusclRamp); - - const Double lim_i = (delta_ij_2 + proj_i * delta_ij) / (pow(proj_i, 2) + delta_ij_2); - const Double lim_j = (delta_ij_2 + proj_j * delta_ij) / (pow(proj_j, 2) + delta_ij_2); + /*--- U-MUSCL reconstructed differences, to be halved when applied. ---*/ + const Double proj_i = umusclRamp * umusclProjection(grad_i.project(iVar, vector_ij), delta_ij, kappa); + const Double proj_j = umusclRamp * umusclProjection(grad_j.project(iVar, vector_ij), delta_ij, kappa); + + Double lim_i = 1.0, lim_j = 1.0; + if constexpr (limiterKind == MusclLimiter::EDGE) { + const Double delta_ij_2 = pow(delta_ij, 2) + 1e-6; + lim_i = (delta_ij_2 + proj_i * delta_ij) / (pow(proj_i, 2) + delta_ij_2); + lim_j = (delta_ij_2 + proj_j * delta_ij) / (pow(proj_j, 2) + delta_ij_2); + } else if constexpr (limiterKind == MusclLimiter::POINT) { + lim_i = gatherVariables(iPoint, limiter, iRow + iVar); + lim_j = gatherVariables(jPoint, limiter, iRow + iVar); + } /*--- Apply reconstruction: V_L = V_i + 0.5 * lim * dV_ij^kap ---*/ V.i.all(iVar) += 0.5 * lim_i * proj_i; @@ -512,14 +513,16 @@ FORCEINLINE void reconstruct(typename CLaneTraits::Int iPoint, typename size_t nVarGradRuntime = 0) { switch (limiterType) { case LIMITER::NONE: - musclUnlimited(iPoint, jPoint, vector_ij, gradient, V, kappa, umusclRamp, iRow, nVarGradRuntime); + muscl(iPoint, jPoint, vector_ij, gradient, limiter, iRow, V, kappa, umusclRamp, + nVarGradRuntime); break; case LIMITER::VAN_ALBADA_EDGE: - musclEdgeLimited(iPoint, jPoint, vector_ij, gradient, V, kappa, umusclRamp, iRow, nVarGradRuntime); + muscl(iPoint, jPoint, vector_ij, gradient, limiter, iRow, V, kappa, umusclRamp, + nVarGradRuntime); break; default: - musclPointLimited(iPoint, jPoint, vector_ij, limiter, gradient, V, kappa, umusclRamp, iRow, - nVarGradRuntime); + muscl(iPoint, jPoint, vector_ij, gradient, limiter, iRow, V, kappa, umusclRamp, + nVarGradRuntime); break; } } @@ -554,6 +557,13 @@ FORCEINLINE void updateLinearSystem(Int iEdge, Int iPoint, Int jPoint, bool impl * contributions and four independent Jacobian blocks. * \note It carries a second CSysVector, the target of flux_j under UpdateType::REDUCTION and * unused under COLORING, where both rows are written directly. + * \note The residual is the same under both update types, the Jacobian is not: REDUCTION writes + * the off-diagonal blocks only and CSysMatrix::SetDiagonalAsColumnSum then derives each + * diagonal block as minus the sum of its column, which equals the jac_ii and jac_jj computed + * here only where the flux is conservative (jac_ii == -jac_ji). A model whose diffusion + * coefficients differ between the two orientations of an edge, i.e. one that evaluates a + * non-conservative term at the point whose row it is writing, therefore converges along a + * slightly different path under the reducer, to the same solution. */ template ::Int> FORCEINLINE void updateLinearSystem(Int iEdge, Int iPoint, Int jPoint, bool implicit, UpdateType updateType, diff --git a/SU2_CFD/include/solvers/CScalarSolver.hpp b/SU2_CFD/include/solvers/CScalarSolver.hpp index 12e10d75390..4a2ab5d1fb4 100644 --- a/SU2_CFD/include/solvers/CScalarSolver.hpp +++ b/SU2_CFD/include/solvers/CScalarSolver.hpp @@ -32,20 +32,21 @@ #include "../../../Common/include/toolboxes/geometry_toolbox.hpp" #include "../numerics/scalar/scalar_edge_flux.hpp" #include "../variables/CScalarVariable.hpp" +#include "../variables/CEulerVariable.hpp" #include "../variables/CFlowVariable.hpp" #include "../variables/CGhostFlowVariable.hpp" +#include "../variables/CIncEulerVariable.hpp" #include "../variables/CPrimitiveIndices.hpp" #include "CSolver.hpp" /*! * \brief Carries a type through a value, so a runtime branch can hand a compile-time type to a - * generic lambda (its parameter deduces as CIndicesTag, and the lambda recovers T as + * generic lambda (its parameter deduces as CTypeTag, and the lambda recovers T as * decltype(tag)::type). Standing in for a C++20 template lambda, which this project's - * C++17 baseline does not have. Shared by every scalar solver's own regime-dispatch helper - * (see CTurbSolver::DispatchRegime, CSpeciesSolver::DispatchRegime). + * C++17 baseline does not have. */ template -struct CIndicesTag { +struct CTypeTag { using type = T; }; @@ -174,103 +175,6 @@ class CScalarSolver : public CSolver { } } - /*! - * \brief Compute the viscous flux for the turbulence equations at a particular edge for a non-conservative discretisation. - * \tparam SolverSpecificNumericsTemp - lambda-function, to implement solver specific contributions to numerics. - * \note The functor has to implement (iPoint, jPoint) - * \param[in] iEdge - Edge for which we want to compute the flux - * \param[in] geometry - Geometrical definition of the problem. - * \param[in] solver_container - Container vector with all the solutions. - * \param[in] numerics - Description of the numerical method. - * \param[in] config - Definition of the particular problem. - */ - template - void Viscous_Residual_NonCons(const unsigned long iEdge, const CGeometry* geometry, CSolver** solver_container, - CNumerics* numerics, const CConfig* config, SolverSpecificNumericsFunc&& SolverSpecificNumerics) { - const bool implicit = (config->GetKind_TimeIntScheme() == EULER_IMPLICIT); - CFlowVariable* flowNodes = solver_container[FLOW_SOL] ? - su2staticcast_p(solver_container[FLOW_SOL]->GetNodes()) : nullptr; - - const auto iPoint = geometry->edges->GetNode(iEdge, 0); - const auto jPoint = geometry->edges->GetNode(iEdge, 1); - - /*--- Lambda function to compute the flux ---*/ - auto ComputeFlux = [&](unsigned long iPoint, unsigned long jPoint, const su2double* normal) { - numerics->SetCoord(geometry->nodes->GetCoord(iPoint),geometry->nodes->GetCoord(jPoint)); - numerics->SetNormal(normal); - - if (flowNodes) { - numerics->SetPrimitive(flowNodes->GetPrimitive(iPoint), flowNodes->GetPrimitive(jPoint)); - } - - /*--- Solver specific numerics contribution. ---*/ - SolverSpecificNumerics(iPoint, jPoint); - - numerics->SetScalarVar(nodes->GetSolution(iPoint), nodes->GetSolution(jPoint)); - numerics->SetScalarVarGradient(nodes->GetGradient(iPoint), nodes->GetGradient(jPoint)); - - return numerics->ComputeResidual(config); - }; - - /*--- Compute fluxes and jacobians i->j ---*/ - const su2double* normal = geometry->edges->GetNormal(iEdge); - auto residual_ij = ComputeFlux(iPoint, jPoint, normal); - - su2mixedfloat *Block_ii = nullptr, *Block_ij = nullptr, *Block_ji = nullptr, *Block_jj = nullptr; - if (implicit) { - Jacobian.GetBlocks(iEdge, iPoint, jPoint, Block_ii, Block_ij, Block_ji, Block_jj); - } - if (ReducerStrategy) { - /*--- i's row takes its contribution from residual_ij alone, accumulated onto what the - * convective term already wrote; j's row is accumulated once residual_ji is known, below. ---*/ - EdgeFluxes.SubtractBlock(iEdge, residual_ij); - if (implicit) { - /*--- For the reducer strategy the Jacobians are averaged for simplicity. ---*/ - for (int iVar=0; iVari ---*/ - su2double flipped_normal[MAXNDIM]; - for (auto iDim = 0u; iDim < nDim; iDim++) flipped_normal[iDim] = -normal[iDim]; - - auto residual_ji = ComputeFlux(jPoint, iPoint, flipped_normal); - if (ReducerStrategy) { - EdgeFluxesDiff.SubtractBlock(iEdge, residual_ji); - if (implicit) { - for (int iVar=0; iVar void BoundaryFluxResidual(const CGeometry* geometry, CSolver** solver_container, const CConfig* config, - const ScalarFluxOptions& opt, unsigned short val_marker, bool implicit); + const ScalarFluxOptions& opt, unsigned short val_marker); + + /*! + * \brief Generic fluid interface (sliding mesh) flux pass, shared by every model. The convective + * term is a per-donor weighted average, computed in the same pass that fills the ghost row + * of each donor; the diffusive term is computed once per vertex, after the donor loop, + * from the ghost state the last donor left behind. This does not fit the + * fill-pass-then-BoundaryFluxResidual shape the other boundaries use, so it drives the + * kernel directly. + * \tparam Scheme - Same model the interior loop uses, instantiated with muscl false. + * \param[in] fillGhostExtras - Functor (iVertex, iPoint) writing the auxiliary ghost fields the + * model's diffusion coefficients read, e.g. SST's blending function or the species + * mass diffusivities. Called once per vertex, before the diffusive flux. + */ + template + void FluidInterfaceFluxResidual(const CGeometry* geometry, CSolver** solver_container, const CConfig* config, + const ScalarFluxOptions& optConv, const ScalarFluxOptions& optVisc, + const GhostFunc& fillGhostExtras); + + /*! + * \brief Write the outward normal of one vertex into the ghost row and mark the vertex as + * contributing a flux. + * \note Vertex normals point into the domain, the flux convention needs them outward. + */ + inline void SetGhostGeometry(const CGeometry* geometry, unsigned short val_marker, unsigned long iVertex) { + for (auto iDim = 0u; iDim < nDim; ++iDim) + ghostNormal(iVertex, iDim) = -geometry->vertex[val_marker][iVertex]->GetNormal(iDim); + ghostSkip[iVertex] = false; + } + + /*! + * \brief Write what the diffusion term of a boundary reads beyond the ghost solution: the + * coordinate of the interior point reflected about the boundary, and the interior + * gradient mirrored into the ghost row. + * \param[in] iPoint - Interior point of the vertex. + * \param[in] jPoint - Point the interior one is reflected about, the vertex's normal neighbor. + */ + inline void SetGhostDiffusionState(const CGeometry* geometry, unsigned long iVertex, unsigned long iPoint, + unsigned long jPoint) { + su2double Coord_Reflected[MAXNDIM]; + GeometryToolbox::PointPointReflect(nDim, geometry->nodes->GetCoord(jPoint), geometry->nodes->GetCoord(iPoint), + Coord_Reflected); + for (auto iDim = 0u; iDim < nDim; ++iDim) ghostCoord(iVertex, iDim) = Coord_Reflected[iDim]; + + auto ghostGrad = ghostNodes->GetGradient(iVertex); + const auto interiorGrad = nodes->GetGradient(iPoint); + for (auto iVar = 0u; iVar < nVar; ++iVar) + for (auto iDim = 0u; iDim < nDim; ++iDim) ghostGrad(iVar, iDim) = interiorGrad(iVar, iDim); + } + + /*! + * \brief Resolve the compile-time parameters of a scalar flux kernel, the flow indices, the + * dimension and the equation count, from the runtime state, and call f with a CTypeTag of + * the resulting scheme type: f is a generic lambda, + * `[&](auto tag){ using Scheme = typename decltype(tag)::type; ... }`. + * \tparam Model - Model class template, e.g. CScalarFlux_SST, taking the four parameters of + * CUpwScalarBase: value type, flow indices, dimension and equation count. + * \tparam nVarList - Equation counts to instantiate. Dynamic matches any count, a static one is + * taken when it equals the solver's nVar; the counts are tried in the order given. + * \note NEMO is not one of the index branches: transported scalars are rejected for it at + * configuration. + */ + template