From 9abcce5486fc3730e1e61a47d5d9e1d49d6ff527 Mon Sep 17 00:00:00 2001 From: Jared Bellon Date: Thu, 3 Sep 2026 13:21:32 -0400 Subject: [PATCH 1/3] Compare NEMO freestream mass fractions against a roundoff tolerance CSU2TCLib rejected the freestream composition with an exact floating-point equality on an accumulated sum at four gas-model sites, so a physically exact input such as ( 0.999, 0.00025, 0.00025, 0.00025, 0.00025 ), which sums to 1 - 1.1e-16 in double precision, was refused as a typo. Compare against a 1e-10 roundoff tolerance instead, report the offending sum with 16 significant digits (to_string would print 1.000000 for a sum that fails the tolerance by less than 5e-7), and fix the "Intial" typo. Add a Catch2 unit test that constructs the model for the AIR-5 roundoff composition and for N2 ( 0.999, 0.001 ); on stock develop the driver aborts in the AIR-5 constructor. No flow numbers change. Co-Authored-By: Claude Fable 5.1 --- SU2_CFD/src/fluid/CSU2TCLib.cpp | 36 +++++-- .../fluid/CNEMOGas_composition_tests.cpp | 100 ++++++++++++++++++ UnitTests/meson.build | 1 + 3 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 UnitTests/SU2_CFD/fluid/CNEMOGas_composition_tests.cpp diff --git a/SU2_CFD/src/fluid/CSU2TCLib.cpp b/SU2_CFD/src/fluid/CSU2TCLib.cpp index 4f81220fac0..fefbd074ef4 100644 --- a/SU2_CFD/src/fluid/CSU2TCLib.cpp +++ b/SU2_CFD/src/fluid/CSU2TCLib.cpp @@ -28,6 +28,26 @@ #include "../../include/fluid/CSU2TCLib.hpp" #include "../../../Common/include/option_structure.hpp" +#include +#include + +/*--- Freestream mass fractions are supplied by the user as decimal literals and + accumulated in double precision, so a physically exact composition need not + sum to bit-exact unity (e.g. 0.999 + 4*0.00025 sums to 1 - 1.1e-16). Compare + against a roundoff tolerance instead: tight enough that any real typo is + still rejected, loose enough that valid input never is. ---*/ +constexpr passivedouble MASSFRAC_SUM_TOL = 1.0E-10; + +/*--- Report the offending sum with enough digits to tell it apart from unity; + the default six decimals of to_string would print 1.000000 for a sum that + fails the tolerance by less than 5e-7. ---*/ +static string MassFracSumErrorMessage(su2double mf) { + ostringstream msg; + msg << "CONFIG ERROR: Initial gas mass fractions do not sum to 1 (sum = " << setprecision(16) + << SU2_TYPE::GetValue(mf) << ")"; + return msg.str(); +} + CSU2TCLib::CSU2TCLib(const CConfig* config, unsigned short val_nDim, bool viscous): CNEMOGas(config, val_nDim){ unsigned short maxEl = 0; @@ -66,8 +86,8 @@ CSU2TCLib::CSU2TCLib(const CConfig* config, unsigned short val_nDim, bool viscou mf = 0.0; for (iSpecies = 0; iSpecies < nSpecies; iSpecies++) mf += MassFrac_Freestream[iSpecies]; - if (mf != 1.0) { - SU2_MPI::Error("CONFIG ERROR: Intial gas mass fractions do not sum to 1!", CURRENT_FUNCTION); + if (fabs(mf - 1.0) > MASSFRAC_SUM_TOL) { + SU2_MPI::Error(MassFracSumErrorMessage(mf), CURRENT_FUNCTION); } /*--- Define parameters of the gas model ---*/ @@ -133,8 +153,8 @@ CSU2TCLib::CSU2TCLib(const CConfig* config, unsigned short val_nDim, bool viscou mf = 0.0; for (iSpecies = 0; iSpecies < nSpecies; iSpecies++) mf += MassFrac_Freestream[iSpecies]; - if (mf != 1.0) { - SU2_MPI::Error("CONFIG ERROR: Intial gas mass fractions do not sum to 1!", CURRENT_FUNCTION); + if (fabs(mf - 1.0) > MASSFRAC_SUM_TOL) { + SU2_MPI::Error(MassFracSumErrorMessage(mf), CURRENT_FUNCTION); } /*--- Define parameters of the gas model ---*/ @@ -296,8 +316,8 @@ CSU2TCLib::CSU2TCLib(const CConfig* config, unsigned short val_nDim, bool viscou mf = 0.0; for (iSpecies = 0; iSpecies < nSpecies; iSpecies++) mf += MassFrac_Freestream[iSpecies]; - if (mf != 1.0) { - SU2_MPI::Error("CONFIG ERROR: Intial gas mass fractions do not sum to 1!", CURRENT_FUNCTION); + if (fabs(mf - 1.0) > MASSFRAC_SUM_TOL) { + SU2_MPI::Error(MassFracSumErrorMessage(mf), CURRENT_FUNCTION); } /*--- Define parameters of the gas model ---*/ @@ -663,8 +683,8 @@ CSU2TCLib::CSU2TCLib(const CConfig* config, unsigned short val_nDim, bool viscou mf = 0.0; for (iSpecies = 0; iSpecies < nSpecies; iSpecies++) mf += MassFrac_Freestream[iSpecies]; - if (mf != 1.0) { - SU2_MPI::Error("CONFIG ERROR: Intial gas mass fractions do not sum to 1!", CURRENT_FUNCTION); + if (fabs(mf - 1.0) > MASSFRAC_SUM_TOL) { + SU2_MPI::Error(MassFracSumErrorMessage(mf), CURRENT_FUNCTION); } /*--- Define parameters of the gas model ---*/ diff --git a/UnitTests/SU2_CFD/fluid/CNEMOGas_composition_tests.cpp b/UnitTests/SU2_CFD/fluid/CNEMOGas_composition_tests.cpp new file mode 100644 index 00000000000..2a5687c4fad --- /dev/null +++ b/UnitTests/SU2_CFD/fluid/CNEMOGas_composition_tests.cpp @@ -0,0 +1,100 @@ +/*! + * \file CNEMOGas_composition_tests.cpp + * \brief Unit tests for the freestream mass fraction validation of the + * SU2 two-temperature thermochemistry library. + * \author J. Bellon + * \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 "../../../Common/include/CConfig.hpp" +#include "../../../SU2_CFD/include/fluid/CSU2TCLib.hpp" + +namespace { + +/*--- Minimal dimensional NEMO configuration for a given gas model and + * freestream composition. ---*/ +std::stringstream NEMOConfigOptions(const std::string& gasModel, const std::string& composition) { + std::stringstream cfg; + cfg << "SOLVER= NEMO_EULER\n" + << "MATH_PROBLEM= DIRECT\n" + << "SYSTEM_MEASUREMENTS= SI\n" + << "GAS_MODEL= " << gasModel << "\n" + << "FLUID_MODEL= SU2_NONEQ\n" + << "GAS_COMPOSITION= " << composition << "\n" + << "FROZEN_MIXTURE= NO\n" + << "TRANSPORT_COEFF_MODEL= WILKE\n" + << "INIT_OPTION= TD_CONDITIONS\n" + << "FREESTREAM_OPTION= TEMPERATURE_FS\n" + << "REF_DIMENSIONALIZATION= DIMENSIONAL\n" + << "MACH_NUMBER= 5\n" + << "FREESTREAM_PRESSURE= 476.0\n" + << "FREESTREAM_TEMPERATURE= 300.0\n" + << "FREESTREAM_TEMPERATURE_VE= 300.0\n" + << "CONV_NUM_METHOD_FLOW= AUSM\n" + << "TIME_DISCRE_FLOW= EULER_IMPLICIT\n"; + return cfg; +} + +/*--- Accumulate the parsed composition the same way the constructor does. ---*/ +double AccumulatedMassFraction(const CConfig& config) { + double sum = 0.0; + const su2double* massFrac = config.GetGas_Composition(); + for (unsigned short iSpecies = 0; iSpecies < config.GetnSpecies(); ++iSpecies) + sum += SU2_TYPE::GetValue(massFrac[iSpecies]); + return sum; +} + +} // namespace + +TEST_CASE("NEMO freestream mass fractions that sum to unity within roundoff are accepted", "[NEMO][fluid]") { + SECTION("AIR-5 with four trace species, decimal literals sum to 1 minus one ulp") { + auto options = NEMOConfigOptions("AIR-5", "( 0.999, 0.00025, 0.00025, 0.00025, 0.00025 )"); + CConfig config(options, SU2_COMPONENT::SU2_CFD, false); + REQUIRE(config.GetnSpecies() == 5); + + /*--- The accumulated double precision sum is not bit-exact unity; an + * exact equality check would reject this physically exact input. ---*/ + const double sum = AccumulatedMassFraction(config); + CHECK(sum != 1.0); + CHECK(std::abs(sum - 1.0) <= 1.0e-10); + + /*--- The library constructor validates the composition and exits the + * process on rejection, so reaching the assertions is the test. ---*/ + CSU2TCLib model(&config, 3, false); + REQUIRE(model.GetSpeciesMolarMass().size() == 5); + } + + SECTION("N2 with a trace atomic species") { + auto options = NEMOConfigOptions("N2", "( 0.999, 0.001 )"); + CConfig config(options, SU2_COMPONENT::SU2_CFD, false); + REQUIRE(config.GetnSpecies() == 2); + CHECK(std::abs(AccumulatedMassFraction(config) - 1.0) <= 1.0e-10); + + CSU2TCLib model(&config, 3, false); + REQUIRE(model.GetSpeciesMolarMass().size() == 2); + } +} diff --git a/UnitTests/meson.build b/UnitTests/meson.build index f8c22511b5f..2e1274487e0 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/fluid/CNEMOGas_composition_tests.cpp', 'SU2_CFD/gradients.cpp', 'SU2_CFD/windowing.cpp', 'Common/toolboxes/random_toolbox_tests.cpp', From e0395aaf6e77795a81af6d8bcbafdea7e5755872 Mon Sep 17 00:00:00 2001 From: Jared Bellon Date: Sat, 5 Sep 2026 18:43:53 -0400 Subject: [PATCH 2/3] Address review: remove broad unit test and add author Remove the broad CNEMOGas constructor test at maintainer request and add Jared Bellon to AUTHORS.md. The solver-launch acceptance/rejection matrix remains the behavioral coverage. Co-Authored-By: Claude Fable 5.1 --- AUTHORS.md | 1 + .../fluid/CNEMOGas_composition_tests.cpp | 100 ------------------ UnitTests/meson.build | 1 - 3 files changed, 1 insertion(+), 101 deletions(-) delete mode 100644 UnitTests/SU2_CFD/fluid/CNEMOGas_composition_tests.cpp diff --git a/AUTHORS.md b/AUTHORS.md index 67ecbf541e5..5e4b86688ff 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -91,6 +91,7 @@ J. Sinsay JSmith36 Jacob Needels Jairo Paes Cavalcante Filho +Jared Bellon Jason Howison Jayant Mukhopadhaya Jeffrey van Oostrom diff --git a/UnitTests/SU2_CFD/fluid/CNEMOGas_composition_tests.cpp b/UnitTests/SU2_CFD/fluid/CNEMOGas_composition_tests.cpp deleted file mode 100644 index 2a5687c4fad..00000000000 --- a/UnitTests/SU2_CFD/fluid/CNEMOGas_composition_tests.cpp +++ /dev/null @@ -1,100 +0,0 @@ -/*! - * \file CNEMOGas_composition_tests.cpp - * \brief Unit tests for the freestream mass fraction validation of the - * SU2 two-temperature thermochemistry library. - * \author J. Bellon - * \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 "../../../Common/include/CConfig.hpp" -#include "../../../SU2_CFD/include/fluid/CSU2TCLib.hpp" - -namespace { - -/*--- Minimal dimensional NEMO configuration for a given gas model and - * freestream composition. ---*/ -std::stringstream NEMOConfigOptions(const std::string& gasModel, const std::string& composition) { - std::stringstream cfg; - cfg << "SOLVER= NEMO_EULER\n" - << "MATH_PROBLEM= DIRECT\n" - << "SYSTEM_MEASUREMENTS= SI\n" - << "GAS_MODEL= " << gasModel << "\n" - << "FLUID_MODEL= SU2_NONEQ\n" - << "GAS_COMPOSITION= " << composition << "\n" - << "FROZEN_MIXTURE= NO\n" - << "TRANSPORT_COEFF_MODEL= WILKE\n" - << "INIT_OPTION= TD_CONDITIONS\n" - << "FREESTREAM_OPTION= TEMPERATURE_FS\n" - << "REF_DIMENSIONALIZATION= DIMENSIONAL\n" - << "MACH_NUMBER= 5\n" - << "FREESTREAM_PRESSURE= 476.0\n" - << "FREESTREAM_TEMPERATURE= 300.0\n" - << "FREESTREAM_TEMPERATURE_VE= 300.0\n" - << "CONV_NUM_METHOD_FLOW= AUSM\n" - << "TIME_DISCRE_FLOW= EULER_IMPLICIT\n"; - return cfg; -} - -/*--- Accumulate the parsed composition the same way the constructor does. ---*/ -double AccumulatedMassFraction(const CConfig& config) { - double sum = 0.0; - const su2double* massFrac = config.GetGas_Composition(); - for (unsigned short iSpecies = 0; iSpecies < config.GetnSpecies(); ++iSpecies) - sum += SU2_TYPE::GetValue(massFrac[iSpecies]); - return sum; -} - -} // namespace - -TEST_CASE("NEMO freestream mass fractions that sum to unity within roundoff are accepted", "[NEMO][fluid]") { - SECTION("AIR-5 with four trace species, decimal literals sum to 1 minus one ulp") { - auto options = NEMOConfigOptions("AIR-5", "( 0.999, 0.00025, 0.00025, 0.00025, 0.00025 )"); - CConfig config(options, SU2_COMPONENT::SU2_CFD, false); - REQUIRE(config.GetnSpecies() == 5); - - /*--- The accumulated double precision sum is not bit-exact unity; an - * exact equality check would reject this physically exact input. ---*/ - const double sum = AccumulatedMassFraction(config); - CHECK(sum != 1.0); - CHECK(std::abs(sum - 1.0) <= 1.0e-10); - - /*--- The library constructor validates the composition and exits the - * process on rejection, so reaching the assertions is the test. ---*/ - CSU2TCLib model(&config, 3, false); - REQUIRE(model.GetSpeciesMolarMass().size() == 5); - } - - SECTION("N2 with a trace atomic species") { - auto options = NEMOConfigOptions("N2", "( 0.999, 0.001 )"); - CConfig config(options, SU2_COMPONENT::SU2_CFD, false); - REQUIRE(config.GetnSpecies() == 2); - CHECK(std::abs(AccumulatedMassFraction(config) - 1.0) <= 1.0e-10); - - CSU2TCLib model(&config, 3, false); - REQUIRE(model.GetSpeciesMolarMass().size() == 2); - } -} diff --git a/UnitTests/meson.build b/UnitTests/meson.build index 2e1274487e0..f8c22511b5f 100644 --- a/UnitTests/meson.build +++ b/UnitTests/meson.build @@ -15,7 +15,6 @@ 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/fluid/CNEMOGas_composition_tests.cpp', 'SU2_CFD/gradients.cpp', 'SU2_CFD/windowing.cpp', 'Common/toolboxes/random_toolbox_tests.cpp', From 54b46ba8239f5614ce0e24efee34aba3606b8d5a Mon Sep 17 00:00:00 2001 From: BlueChips RunPod Integration Date: Sat, 5 Sep 2026 19:07:22 -0400 Subject: [PATCH 3/3] Use BlueChips contributor attribution --- AUTHORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.md b/AUTHORS.md index 5e4b86688ff..d82ccb599fe 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -60,6 +60,7 @@ Arne Voß Ayush Kumar Beckett Y. Zhou Benjamin S. Kirk +BlueChips Brendan Tracey Brian Munguía Carsten Othmer @@ -91,7 +92,6 @@ J. Sinsay JSmith36 Jacob Needels Jairo Paes Cavalcante Filho -Jared Bellon Jason Howison Jayant Mukhopadhaya Jeffrey van Oostrom