diff --git a/SU2_CFD/src/output/CNEMOCompOutput.cpp b/SU2_CFD/src/output/CNEMOCompOutput.cpp
index c28736296be..790531185e2 100644
--- a/SU2_CFD/src/output/CNEMOCompOutput.cpp
+++ b/SU2_CFD/src/output/CNEMOCompOutput.cpp
@@ -124,30 +124,36 @@ void CNEMOCompOutput::SetHistoryOutputFields(CConfig *config){
/// END_GROUP
/// BEGIN_GROUP: MAX_RES, DESCRIPTION: The maximum residuals of the SOLUTION variables.
- /// DESCRIPTION: Maximum residual of the density.
- AddHistoryOutput("MAX_DENSITY", "max[Rho]", ScreenOutputFormat::FIXED, "MAX_RES", "Maximum square residual of the density.", HistoryFieldType::RESIDUAL);
+ /// DESCRIPTION: Maximum residual of the species densities.
+ for(iSpecies = 0; iSpecies < nSpecies; iSpecies++)
+ AddHistoryOutput("MAX_DENSITY_" + std::to_string(iSpecies), "max[Rho_" + std::to_string(iSpecies) + "]", ScreenOutputFormat::FIXED, "MAX_RES", "Maximum residual of the species density " + std::to_string(iSpecies) + ".", HistoryFieldType::RESIDUAL);
/// DESCRIPTION: Maximum residual of the momentum x-component.
- AddHistoryOutput("MAX_MOMENTUM-X", "max[RhoU]", ScreenOutputFormat::FIXED, "MAX_RES", "Maximum square residual of the momentum x-component.", HistoryFieldType::RESIDUAL);
+ AddHistoryOutput("MAX_MOMENTUM-X", "max[RhoU]", ScreenOutputFormat::FIXED, "MAX_RES", "Maximum residual of the momentum x-component.", HistoryFieldType::RESIDUAL);
/// DESCRIPTION: Maximum residual of the momentum y-component.
- AddHistoryOutput("MAX_MOMENTUM-Y", "max[RhoV]", ScreenOutputFormat::FIXED, "MAX_RES", "Maximum square residual of the momentum y-component.", HistoryFieldType::RESIDUAL);
+ AddHistoryOutput("MAX_MOMENTUM-Y", "max[RhoV]", ScreenOutputFormat::FIXED, "MAX_RES", "Maximum residual of the momentum y-component.", HistoryFieldType::RESIDUAL);
/// DESCRIPTION: Maximum residual of the momentum z-component.
- if (nDim == 3) AddHistoryOutput("MAX_MOMENTUM-Z", "max[RhoW]", ScreenOutputFormat::FIXED,"MAX_RES", "Maximum residual of the z-component.", HistoryFieldType::RESIDUAL);
+ if (nDim == 3) AddHistoryOutput("MAX_MOMENTUM-Z", "max[RhoW]", ScreenOutputFormat::FIXED,"MAX_RES", "Maximum residual of the momentum z-component.", HistoryFieldType::RESIDUAL);
/// DESCRIPTION: Maximum residual of the energy.
AddHistoryOutput("MAX_ENERGY", "max[RhoE]", ScreenOutputFormat::FIXED, "MAX_RES", "Maximum residual of the energy.", HistoryFieldType::RESIDUAL);
+ /// DESCRIPTION: Maximum residual of the vibrational-electronic energy.
+ AddHistoryOutput("MAX_ENERGY_VE", "max[RhoEve]", ScreenOutputFormat::FIXED, "MAX_RES", "Maximum residual of the vibrational-electronic energy.", HistoryFieldType::RESIDUAL);
AddHistoryOutputFields_ScalarMAX_RES(config);
/// END_GROUP
/// BEGIN_GROUP: BGS_RES, DESCRIPTION: The block Gauss Seidel residuals of the SOLUTION variables.
- /// DESCRIPTION: Maximum residual of the density.
- AddHistoryOutput("BGS_DENSITY", "bgs[Rho]", ScreenOutputFormat::FIXED, "BGS_RES", "BGS residual of the density.", HistoryFieldType::RESIDUAL);
- /// DESCRIPTION: Maximum residual of the momentum x-component.
+ /// DESCRIPTION: BGS residual of the species densities.
+ for(iSpecies = 0; iSpecies < nSpecies; iSpecies++)
+ AddHistoryOutput("BGS_DENSITY_" + std::to_string(iSpecies), "bgs[Rho_" + std::to_string(iSpecies) + "]", ScreenOutputFormat::FIXED, "BGS_RES", "BGS residual of the species density " + std::to_string(iSpecies) + ".", HistoryFieldType::RESIDUAL);
+ /// DESCRIPTION: BGS residual of the momentum x-component.
AddHistoryOutput("BGS_MOMENTUM-X", "bgs[RhoU]", ScreenOutputFormat::FIXED, "BGS_RES", "BGS residual of the momentum x-component.", HistoryFieldType::RESIDUAL);
- /// DESCRIPTION: Maximum residual of the momentum y-component.
+ /// DESCRIPTION: BGS residual of the momentum y-component.
AddHistoryOutput("BGS_MOMENTUM-Y", "bgs[RhoV]", ScreenOutputFormat::FIXED, "BGS_RES", "BGS residual of the momentum y-component.", HistoryFieldType::RESIDUAL);
- /// DESCRIPTION: Maximum residual of the momentum z-component.
- if (nDim == 3) AddHistoryOutput("BGS_MOMENTUM-Z", "bgs[RhoW]", ScreenOutputFormat::FIXED, "BGS_RES", "BGS residual of the z-component.", HistoryFieldType::RESIDUAL);
- /// DESCRIPTION: Maximum residual of the energy.
+ /// DESCRIPTION: BGS residual of the momentum z-component.
+ if (nDim == 3) AddHistoryOutput("BGS_MOMENTUM-Z", "bgs[RhoW]", ScreenOutputFormat::FIXED, "BGS_RES", "BGS residual of the momentum z-component.", HistoryFieldType::RESIDUAL);
+ /// DESCRIPTION: BGS residual of the energy.
AddHistoryOutput("BGS_ENERGY", "bgs[RhoE]", ScreenOutputFormat::FIXED, "BGS_RES", "BGS residual of the energy.", HistoryFieldType::RESIDUAL);
+ /// DESCRIPTION: BGS residual of the vibrational-electronic energy.
+ AddHistoryOutput("BGS_ENERGY_VE", "bgs[RhoEve]", ScreenOutputFormat::FIXED, "BGS_RES", "BGS residual of the vibrational-electronic energy.", HistoryFieldType::RESIDUAL);
AddHistoryOutputFields_ScalarBGS_RES(config);
/// END_GROUP
@@ -410,24 +416,32 @@ void CNEMOCompOutput::LoadHistoryData(CConfig *config, CGeometry *geometry, CSol
SetHistoryOutputValue("RMS_ENERGY", log10(NEMO_solver->GetRes_RMS(nSpecies+3)));
SetHistoryOutputValue("RMS_ENERGY_VE", log10(NEMO_solver->GetRes_RMS(nSpecies+4)));
}
- SetHistoryOutputValue("MAX_DENSITY", log10(NEMO_solver->GetRes_Max(0)));
- SetHistoryOutputValue("MAX_MOMENTUM-X", log10(NEMO_solver->GetRes_Max(1)));
- SetHistoryOutputValue("MAX_MOMENTUM-Y", log10(NEMO_solver->GetRes_Max(2)));
- if (nDim == 2)
- SetHistoryOutputValue("MAX_ENERGY", log10(NEMO_solver->GetRes_Max(3)));
- else {
- SetHistoryOutputValue("MAX_MOMENTUM-Z", log10(NEMO_solver->GetRes_Max(3)));
- SetHistoryOutputValue("MAX_ENERGY", log10(NEMO_solver->GetRes_Max(4)));
+ for(iSpecies = 0; iSpecies < nSpecies; iSpecies++)
+ SetHistoryOutputValue("MAX_DENSITY_" + std::to_string(iSpecies), log10(NEMO_solver->GetRes_Max(iSpecies)));
+
+ SetHistoryOutputValue("MAX_MOMENTUM-X", log10(NEMO_solver->GetRes_Max(nSpecies)));
+ SetHistoryOutputValue("MAX_MOMENTUM-Y", log10(NEMO_solver->GetRes_Max(nSpecies+1)));
+ if (nDim == 2){
+ SetHistoryOutputValue("MAX_ENERGY", log10(NEMO_solver->GetRes_Max(nSpecies+2)));
+ SetHistoryOutputValue("MAX_ENERGY_VE", log10(NEMO_solver->GetRes_Max(nSpecies+3)));
+ } else {
+ SetHistoryOutputValue("MAX_MOMENTUM-Z", log10(NEMO_solver->GetRes_Max(nSpecies+2)));
+ SetHistoryOutputValue("MAX_ENERGY", log10(NEMO_solver->GetRes_Max(nSpecies+3)));
+ SetHistoryOutputValue("MAX_ENERGY_VE", log10(NEMO_solver->GetRes_Max(nSpecies+4)));
}
if (multiZone){
- SetHistoryOutputValue("BGS_DENSITY", log10(NEMO_solver->GetRes_BGS(0)));
- SetHistoryOutputValue("BGS_MOMENTUM-X", log10(NEMO_solver->GetRes_BGS(1)));
- SetHistoryOutputValue("BGS_MOMENTUM-Y", log10(NEMO_solver->GetRes_BGS(2)));
- if (nDim == 2)
- SetHistoryOutputValue("BGS_ENERGY", log10(NEMO_solver->GetRes_BGS(3)));
- else {
- SetHistoryOutputValue("BGS_MOMENTUM-Z", log10(NEMO_solver->GetRes_BGS(3)));
- SetHistoryOutputValue("BGS_ENERGY", log10(NEMO_solver->GetRes_BGS(4)));
+ for(iSpecies = 0; iSpecies < nSpecies; iSpecies++)
+ SetHistoryOutputValue("BGS_DENSITY_" + std::to_string(iSpecies), log10(NEMO_solver->GetRes_BGS(iSpecies)));
+
+ SetHistoryOutputValue("BGS_MOMENTUM-X", log10(NEMO_solver->GetRes_BGS(nSpecies)));
+ SetHistoryOutputValue("BGS_MOMENTUM-Y", log10(NEMO_solver->GetRes_BGS(nSpecies+1)));
+ if (nDim == 2){
+ SetHistoryOutputValue("BGS_ENERGY", log10(NEMO_solver->GetRes_BGS(nSpecies+2)));
+ SetHistoryOutputValue("BGS_ENERGY_VE", log10(NEMO_solver->GetRes_BGS(nSpecies+3)));
+ } else {
+ SetHistoryOutputValue("BGS_MOMENTUM-Z", log10(NEMO_solver->GetRes_BGS(nSpecies+2)));
+ SetHistoryOutputValue("BGS_ENERGY", log10(NEMO_solver->GetRes_BGS(nSpecies+3)));
+ SetHistoryOutputValue("BGS_ENERGY_VE", log10(NEMO_solver->GetRes_BGS(nSpecies+4)));
}
}
diff --git a/SU2_CFD/src/solvers/CSolver.cpp b/SU2_CFD/src/solvers/CSolver.cpp
index 5662aa46ed4..78f53668463 100644
--- a/SU2_CFD/src/solvers/CSolver.cpp
+++ b/SU2_CFD/src/solvers/CSolver.cpp
@@ -2048,6 +2048,14 @@ void CSolver::SetResidual_RMS(const CGeometry *geometry, const CConfig *config,
}
}
+ /*--- Floor the maximum residual like the RMS residual above, so an
+ * exactly-zero residual (e.g. an inert species at a cold start)
+ * cannot become a nonfinite log10 in screen or history output. ---*/
+
+ for (unsigned short iVar = 0; iVar < nVar; iVar++) {
+ Residual_Max[iVar] = max(EPS*EPS, Residual_Max[iVar]);
+ }
+
}
END_SU2_OMP_SAFE_GLOBAL_ACCESS
}
diff --git a/UnitTests/SU2_CFD/output/CNEMOCompOutput_tests.cpp b/UnitTests/SU2_CFD/output/CNEMOCompOutput_tests.cpp
new file mode 100644
index 00000000000..465149b08be
--- /dev/null
+++ b/UnitTests/SU2_CFD/output/CNEMOCompOutput_tests.cpp
@@ -0,0 +1,214 @@
+/*!
+ * \file CNEMOCompOutput_tests.cpp
+ * \brief Unit tests for NEMO residual history output.
+ * \version 8.5.0 "Harrier"
+ *
+ * SU2 Project Website: https://su2code.github.io
+ *
+ * The SU2 Project is maintained by the SU2 Foundation
+ * (http://su2foundation.org)
+ *
+ * Copyright 2012-2026, SU2 Contributors (cf. AUTHORS.md)
+ *
+ * SU2 is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * SU2 is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with SU2. If not, see .
+ */
+
+#include "catch.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "../../../Common/include/CConfig.hpp"
+#include "../../../Common/include/geometry/CGeometry.hpp"
+#include "../../../SU2_CFD/include/output/CNEMOCompOutput.hpp"
+#include "../../../SU2_CFD/include/solvers/CSolver.hpp"
+#include "../../../SU2_CFD/include/variables/CVariable.hpp"
+
+namespace {
+
+std::unique_ptr MakeNEMOConfig(bool multizone) {
+ std::stringstream options;
+ options << "SOLVER= NEMO_EULER\n"
+ << "GAS_MODEL= AIR-5\n"
+ << "GAS_COMPOSITION= (0.77, 0.23, 0.0, 0.0, 0.0)\n"
+ << "FLUID_MODEL= SU2_NONEQ\n"
+ << "MATH_PROBLEM= DIRECT\n"
+ << "MACH_NUMBER= 5.0\n"
+ << "FREESTREAM_PRESSURE= 101325.0\n"
+ << "FREESTREAM_TEMPERATURE= 288.15\n"
+ << "FREESTREAM_TEMPERATURE_VE= 288.15\n"
+ << "COMM_LEVEL= MINIMAL\n";
+ auto config = std::make_unique(options, SU2_COMPONENT::SU2_CFD, false);
+ config->SetMultizone_Problem(multizone);
+ return config;
+}
+
+class CTestGeometry final : public CGeometry {
+ public:
+ explicit CTestGeometry(unsigned short dimension) {
+ nDim = dimension;
+ nPoint = nPointDomain = Global_nPoint = Global_nPointDomain = 1;
+ MGLevel = MESH_0;
+ }
+};
+
+class CTestSolver final : public CSolver {
+ private:
+ CVariable variables;
+
+ CVariable* GetBaseClassPointerToNodes() override { return &variables; }
+
+ public:
+ CTestSolver(const CConfig* config, unsigned short dimension)
+ : variables(0, dimension, config->GetnSpecies() + dimension + 2, config) {
+ nDim = dimension;
+ nVar = config->GetnSpecies() + dimension + 2;
+ nPoint = nPointDomain = 1;
+
+ Residual_RMS.resize(nVar);
+ Residual_Max.resize(nVar);
+ Residual_BGS.resize(nVar);
+ Residual_Max_BGS.resize(nVar);
+ Point_Max.resize(nVar);
+ Point_Max_BGS.resize(nVar);
+ Point_Max_Coord.resize(nVar, nDim);
+ Point_Max_Coord_BGS.resize(nVar, nDim);
+
+ SetBaseClassPointerToNodes();
+ SetCFL_Local_Stats(1.0);
+ SetResLinSolver(1.0);
+ }
+
+ void SeedResidualSums() {
+ for (unsigned short iVar = 0; iVar < nVar; ++iVar) {
+ const auto rmsExponent = static_cast(iVar) + 1;
+ const auto maxExponent = static_cast(iVar) + 22;
+ const auto bgsExponent = static_cast(iVar) + 12;
+ Residual_RMS[iVar] = std::pow(10.0, -2.0 * rmsExponent);
+ Residual_Max[iVar] = std::pow(10.0, -maxExponent);
+ Residual_BGS[iVar] = std::pow(10.0, -2.0 * bgsExponent);
+ }
+
+ /* An exactly-zero species maximum exercises the finite output floor. */
+ Residual_Max[2] = 0.0;
+ }
+};
+
+std::vector ExpectedResidualFields(const std::string& prefix, unsigned short dimension) {
+ std::vector fields;
+ for (unsigned short iSpecies = 0; iSpecies < 5; ++iSpecies) {
+ fields.push_back(prefix + "_DENSITY_" + std::to_string(iSpecies));
+ }
+ fields.push_back(prefix + "_MOMENTUM-X");
+ fields.push_back(prefix + "_MOMENTUM-Y");
+ if (dimension == 3) fields.push_back(prefix + "_MOMENTUM-Z");
+ fields.push_back(prefix + "_ENERGY");
+ fields.push_back(prefix + "_ENERGY_VE");
+ return fields;
+}
+
+std::vector ExpectedFieldNames(const std::string& prefix, unsigned short dimension) {
+ std::vector names;
+ for (unsigned short iSpecies = 0; iSpecies < 5; ++iSpecies) {
+ names.push_back(prefix + "[Rho_" + std::to_string(iSpecies) + "]");
+ }
+ names.push_back(prefix + "[RhoU]");
+ names.push_back(prefix + "[RhoV]");
+ if (dimension == 3) names.push_back(prefix + "[RhoW]");
+ names.push_back(prefix + "[RhoE]");
+ names.push_back(prefix + "[RhoEve]");
+ return names;
+}
+
+void CheckRegisteredGroup(CNEMOCompOutput& output, const std::string& group, const std::string& prefix,
+ unsigned short dimension) {
+ const auto expectedFields = ExpectedResidualFields(group == "MAX_RES" ? "MAX" : "BGS", dimension);
+ const auto expectedNames = ExpectedFieldNames(prefix, dimension);
+ const auto registered = output.GetHistoryGroup(group);
+
+ REQUIRE(registered.size() == expectedFields.size());
+ const auto& fields = output.GetHistoryFields();
+ for (std::size_t i = 0; i < expectedFields.size(); ++i) {
+ INFO("history field " << expectedFields[i]);
+ REQUIRE(fields.count(expectedFields[i]) == 1);
+ CHECK(fields.at(expectedFields[i]).fieldName == expectedNames[i]);
+ CHECK(registered[i].fieldName == expectedNames[i]);
+ }
+}
+
+void CheckLoadedResiduals(unsigned short dimension) {
+ auto config = MakeNEMOConfig(true);
+ CTestGeometry geometry(dimension);
+ CTestSolver flow(config.get(), dimension);
+ flow.SeedResidualSums();
+
+ /* Exercise the actual reductions used before MAX and multizone BGS output. */
+ flow.SetResidual_RMS(&geometry, config.get());
+ flow.SetResidual_BGS(&geometry, config.get());
+
+ CNEMOCompOutput output(config.get(), dimension);
+ output.SetHistoryOutputFields(config.get());
+ std::array solvers{};
+ solvers[FLOW_SOL] = &flow;
+ solvers[MESH_SOL] = &flow;
+ output.LoadHistoryData(config.get(), &geometry, solvers.data());
+
+ const unsigned short nVar = config->GetnSpecies() + dimension + 2;
+ const auto maxFields = ExpectedResidualFields("MAX", dimension);
+ const auto bgsFields = ExpectedResidualFields("BGS", dimension);
+ REQUIRE(maxFields.size() == nVar);
+ REQUIRE(bgsFields.size() == nVar);
+
+ for (unsigned short iVar = 0; iVar < nVar; ++iVar) {
+ const auto maxValue = SU2_TYPE::GetValue(output.GetHistoryFieldValue(maxFields[iVar]));
+ const auto bgsValue = SU2_TYPE::GetValue(output.GetHistoryFieldValue(bgsFields[iVar]));
+ INFO("residual variable " << iVar);
+ CHECK(std::isfinite(maxValue));
+ CHECK(maxValue == Approx(iVar == 2 ? -32.0 : -(static_cast(iVar) + 22.0)));
+ CHECK(bgsValue == Approx(-(static_cast(iVar) + 12.0)));
+ }
+}
+
+} // namespace
+
+TEST_CASE("NEMO MAX and BGS history fields use the species-first layout", "[NEMO][Output]") {
+ auto config = MakeNEMOConfig(false);
+
+ SECTION("2D") {
+ CNEMOCompOutput output(config.get(), 2);
+ output.SetHistoryOutputFields(config.get());
+ CheckRegisteredGroup(output, "MAX_RES", "max", 2);
+ CheckRegisteredGroup(output, "BGS_RES", "bgs", 2);
+ CHECK(output.GetHistoryFields().count("MAX_DENSITY") == 0);
+ CHECK(output.GetHistoryFields().count("BGS_DENSITY") == 0);
+ }
+
+ SECTION("3D") {
+ CNEMOCompOutput output(config.get(), 3);
+ output.SetHistoryOutputFields(config.get());
+ CheckRegisteredGroup(output, "MAX_RES", "max", 3);
+ CheckRegisteredGroup(output, "BGS_RES", "bgs", 3);
+ CHECK(output.GetHistoryFields().count("MAX_DENSITY") == 0);
+ CHECK(output.GetHistoryFields().count("BGS_DENSITY") == 0);
+ }
+}
+
+TEST_CASE("NEMO history loads finite MAX and multizone BGS residuals by species-first index", "[NEMO][Output]") {
+ SECTION("2D") { CheckLoadedResiduals(2); }
+ SECTION("3D") { CheckLoadedResiduals(3); }
+}
diff --git a/UnitTests/meson.build b/UnitTests/meson.build
index f8c22511b5f..d5094c59613 100644
--- a/UnitTests/meson.build
+++ b/UnitTests/meson.build
@@ -15,6 +15,7 @@ su2_cfd_tests = files(['Common/geometry/primal_grid/CPrimalGrid_tests.cpp',
'Common/toolboxes/multilayer_perceptron/CLookUp_ANN_tests.cpp',
'SU2_CFD/numerics/CNumerics_tests.cpp',
'SU2_CFD/fluid/CFluidModel_tests.cpp',
+ 'SU2_CFD/output/CNEMOCompOutput_tests.cpp',
'SU2_CFD/gradients.cpp',
'SU2_CFD/windowing.cpp',
'Common/toolboxes/random_toolbox_tests.cpp',