From 3cf71e3f4d4b3000a2fc6517d41eb808359c4a4b Mon Sep 17 00:00:00 2001 From: DENEL Bertrand Date: Sat, 28 Mar 2026 17:44:13 -0500 Subject: [PATCH 1/5] Draft --- src/coreComponents/physicsSolvers/SolverStatistics.cpp | 5 +++-- .../physicsSolvers/multiphysics/CoupledSolver.hpp | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/coreComponents/physicsSolvers/SolverStatistics.cpp b/src/coreComponents/physicsSolvers/SolverStatistics.cpp index 4324bc684ef..d0fa7f3117d 100644 --- a/src/coreComponents/physicsSolvers/SolverStatistics.cpp +++ b/src/coreComponents/physicsSolvers/SolverStatistics.cpp @@ -20,6 +20,7 @@ #include "SolverStatistics.hpp" #include "fileIO/Outputs/OutputBase.hpp" +#include "common/MpiWrapper.hpp" namespace geos { @@ -142,7 +143,7 @@ void IterationsStatistics::updateTimeStepCut() void IterationsStatistics::writeIterationStatsToTable() { - if( m_numTimeSteps == 0 || !m_logOutputRequest ) + if( m_numTimeSteps == 0 || !m_logOutputRequest || MpiWrapper::commRank() != 0 ) return; m_iterationData.addRow( m_numTimeSteps, @@ -204,7 +205,7 @@ ConvergenceStatistics::ConvergenceStatistics(): void ConvergenceStatistics::writeConvergenceStatsToTable() { - if( !m_CSVOutputRequest ) + if( !m_CSVOutputRequest || MpiWrapper::commRank() != 0 ) return; stdVector< TableData::CellData > residualsNormCells; diff --git a/src/coreComponents/physicsSolvers/multiphysics/CoupledSolver.hpp b/src/coreComponents/physicsSolvers/multiphysics/CoupledSolver.hpp index c5cddf6acd1..c2cec6baf68 100644 --- a/src/coreComponents/physicsSolvers/multiphysics/CoupledSolver.hpp +++ b/src/coreComponents/physicsSolvers/multiphysics/CoupledSolver.hpp @@ -576,6 +576,7 @@ class CoupledSolver : public PhysicsSolverBase forEachArgInTuple( m_solvers, [&]( auto & solver, auto ) { solver->getIterationStats().iterateTimeStepStatistics(); + solver->getIterationStats().writeIterationStatsToTable(); } ); // get out of the time loop break; @@ -592,6 +593,7 @@ class CoupledSolver : public PhysicsSolverBase forEachArgInTuple( m_solvers, [&]( auto & solver, auto ) { solver->getIterationStats().updateTimeStepCut(); + solver->getIterationStats().writeIterationStatsToTable(); } ); } } From 109a70ec5a92b2b245c13e2ac6d4c1869058da1e Mon Sep 17 00:00:00 2001 From: DENEL Bertrand Date: Thu, 14 May 2026 15:45:34 -0500 Subject: [PATCH 2/5] Draft --- .../physicsSolvers/SolverStatistics.cpp | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/src/coreComponents/physicsSolvers/SolverStatistics.cpp b/src/coreComponents/physicsSolvers/SolverStatistics.cpp index d0fa7f3117d..5c4ca42e0b2 100644 --- a/src/coreComponents/physicsSolvers/SolverStatistics.cpp +++ b/src/coreComponents/physicsSolvers/SolverStatistics.cpp @@ -143,7 +143,7 @@ void IterationsStatistics::updateTimeStepCut() void IterationsStatistics::writeIterationStatsToTable() { - if( m_numTimeSteps == 0 || !m_logOutputRequest || MpiWrapper::commRank() != 0 ) + if( m_numTimeSteps == 0 || !m_logOutputRequest ) return; m_iterationData.addRow( m_numTimeSteps, @@ -157,18 +157,21 @@ void IterationsStatistics::writeIterationStatsToTable() m_setupTime, m_solveTime ); - if( !m_CSVOutputOpened ) + if( MpiWrapper::commRank() == 0 ) { - m_logStream.open( m_iterationsFilename ); - m_iterationCSVFormatter = std::make_unique< TableCSVFormatter >( m_iterationCSVLayout ); - m_logStream << m_iterationCSVFormatter->headerToString( ); - m_CSVOutputOpened = true; + if( !m_CSVOutputOpened ) + { + m_logStream.open( m_iterationsFilename ); + m_iterationCSVFormatter = std::make_unique< TableCSVFormatter >( m_iterationCSVLayout ); + m_logStream << m_iterationCSVFormatter->headerToString( ); + m_CSVOutputOpened = true; + } + + m_logStream << m_iterationCSVFormatter->dataToString( m_iterationData ); + m_logStream.flush(); } - m_logStream << m_iterationCSVFormatter->dataToString( m_iterationData ); - m_logStream.flush(); m_iterationData.clear(); - resetSolverLinearTime(); } @@ -205,7 +208,7 @@ ConvergenceStatistics::ConvergenceStatistics(): void ConvergenceStatistics::writeConvergenceStatsToTable() { - if( !m_CSVOutputRequest || MpiWrapper::commRank() != 0 ) + if( !m_CSVOutputRequest ) return; stdVector< TableData::CellData > residualsNormCells; @@ -229,23 +232,27 @@ void ConvergenceStatistics::writeConvergenceStatsToTable() m_convergenceData.addRow( residualsNormCells ); - if( !m_CSVOutputOpened ) + if( MpiWrapper::commRank() == 0 ) { - string_array header = {"Cycle number", "time_n (s)", "dt (s)", "iteration"}; - for( auto const & residual : m_residuals ) + if( !m_CSVOutputOpened ) { - header.emplace_back( residual.first ); + string_array header = {"Cycle number", "time_n (s)", "dt (s)", "iteration"}; + for( auto const & residual : m_residuals ) + { + header.emplace_back( residual.first ); + } + m_convergenceLayout.addColumns( header ); + + m_logStream.open( m_convergenceFilename ); + m_convergenceFormatter = std::make_unique< TableCSVFormatter >( m_convergenceLayout ); + m_logStream << m_convergenceFormatter->headerToString( ); + m_CSVOutputOpened = true; } - m_convergenceLayout.addColumns( header ); - m_logStream.open( m_convergenceFilename ); - m_convergenceFormatter = std::make_unique< TableCSVFormatter >( m_convergenceLayout ); - m_logStream << m_convergenceFormatter->headerToString( ); - m_CSVOutputOpened = true; + m_logStream << m_convergenceFormatter->dataToString( m_convergenceData ); + m_logStream.flush(); } - m_logStream << m_convergenceFormatter->dataToString( m_convergenceData ); - m_logStream.flush(); m_convergenceData.clear(); } From 8f038579fc8453dfc38622655f19a3993c5cded6 Mon Sep 17 00:00:00 2001 From: DENEL Bertrand Date: Wed, 10 Jun 2026 17:48:49 -0500 Subject: [PATCH 3/5] Added test --- .../physicsSolvers/SolverStatistics.cpp | 31 +-- .../multiphysics/CMakeLists.txt | 4 + .../multiphysics/unitTests/CMakeLists.txt | 19 ++ .../testCoupledSolverStatisticsCSV.cpp | 237 ++++++++++++++++++ 4 files changed, 277 insertions(+), 14 deletions(-) create mode 100644 src/coreComponents/physicsSolvers/multiphysics/unitTests/CMakeLists.txt create mode 100644 src/coreComponents/physicsSolvers/multiphysics/unitTests/testCoupledSolverStatisticsCSV.cpp diff --git a/src/coreComponents/physicsSolvers/SolverStatistics.cpp b/src/coreComponents/physicsSolvers/SolverStatistics.cpp index 5c4ca42e0b2..e3e44a560da 100644 --- a/src/coreComponents/physicsSolvers/SolverStatistics.cpp +++ b/src/coreComponents/physicsSolvers/SolverStatistics.cpp @@ -146,6 +146,13 @@ void IterationsStatistics::writeIterationStatsToTable() if( m_numTimeSteps == 0 || !m_logOutputRequest ) return; + real64 const setupTime = m_setupTime; + real64 const solveTime = m_solveTime; + resetSolverLinearTime(); + + if( !m_CSVOutputRequest || MpiWrapper::commRank() != 0 ) + return; + m_iterationData.addRow( m_numTimeSteps, m_numTimeStepCuts, m_numSuccessfulConfigIterations, @@ -154,27 +161,23 @@ void IterationsStatistics::writeIterationStatsToTable() m_numDiscardedConfigIterations, m_numDiscardedNonlinearIterations, m_numDiscardedLinearIterations, - m_setupTime, - m_solveTime ); + setupTime, + solveTime ); - if( MpiWrapper::commRank() == 0 ) + if( !m_CSVOutputOpened ) { - if( !m_CSVOutputOpened ) - { - m_logStream.open( m_iterationsFilename ); - m_iterationCSVFormatter = std::make_unique< TableCSVFormatter >( m_iterationCSVLayout ); - m_logStream << m_iterationCSVFormatter->headerToString( ); - m_CSVOutputOpened = true; - } - - m_logStream << m_iterationCSVFormatter->dataToString( m_iterationData ); - m_logStream.flush(); + m_logStream.open( m_iterationsFilename ); + m_iterationCSVFormatter = std::make_unique< TableCSVFormatter >( m_iterationCSVLayout ); + m_logStream << m_iterationCSVFormatter->headerToString( ); + m_CSVOutputOpened = true; } + m_logStream << m_iterationCSVFormatter->dataToString( m_iterationData ); + m_logStream.flush(); m_iterationData.clear(); - resetSolverLinearTime(); } + void IterationsStatistics::outputStatistics() const { // no statistics to output when no time steps have been recorded diff --git a/src/coreComponents/physicsSolvers/multiphysics/CMakeLists.txt b/src/coreComponents/physicsSolvers/multiphysics/CMakeLists.txt index 36e8a23c9c0..9cce971d562 100644 --- a/src/coreComponents/physicsSolvers/multiphysics/CMakeLists.txt +++ b/src/coreComponents/physicsSolvers/multiphysics/CMakeLists.txt @@ -129,3 +129,7 @@ install( TARGETS multiPhysicsSolvers LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX} if( externalComponentDeps ) target_include_directories( multiPhysicsSolvers PUBLIC ${CMAKE_SOURCE_DIR}/externalComponents ) endif() + +if( GEOS_ENABLE_TESTS ) + add_subdirectory( unitTests ) +endif() diff --git a/src/coreComponents/physicsSolvers/multiphysics/unitTests/CMakeLists.txt b/src/coreComponents/physicsSolvers/multiphysics/unitTests/CMakeLists.txt new file mode 100644 index 00000000000..979b2cd6452 --- /dev/null +++ b/src/coreComponents/physicsSolvers/multiphysics/unitTests/CMakeLists.txt @@ -0,0 +1,19 @@ +set( multiphysics_gtest_tests + testCoupledSolverStatisticsCSV.cpp ) + + set( dependencyList mainInterface testingUtilities gtest ${parallelDeps} ) + + blt_add_executable( NAME testCoupledSolverStatisticsCSV + SOURCES testCoupledSolverStatisticsCSV.cpp + OUTPUT_DIR ${TEST_OUTPUT_DIRECTORY} + DEPENDS_ON ${dependencyList} + FOLDER UnitTests ) + + geos_add_test( NAME testCoupledSolverStatisticsCSV + COMMAND testCoupledSolverStatisticsCSV ) + + if( ENABLE_MPI ) + geos_add_test( NAME testCoupledSolverStatisticsCSV_2ranks + COMMAND testCoupledSolverStatisticsCSV + NUM_MPI_TASKS 2 ) + endif() diff --git a/src/coreComponents/physicsSolvers/multiphysics/unitTests/testCoupledSolverStatisticsCSV.cpp b/src/coreComponents/physicsSolvers/multiphysics/unitTests/testCoupledSolverStatisticsCSV.cpp new file mode 100644 index 00000000000..4c37219ecc9 --- /dev/null +++ b/src/coreComponents/physicsSolvers/multiphysics/unitTests/testCoupledSolverStatisticsCSV.cpp @@ -0,0 +1,237 @@ +#include "integrationTests/fluidFlowTests/testCompFlowUtils.hpp" +#include "integrationTests/testingUtilities/TestingTasks.hpp" +#include "mainInterface/initialization.hpp" +#include "mainInterface/GeosxState.hpp" +#include "fileIO/Outputs/OutputBase.hpp" +#include "common/MpiWrapper.hpp" +#include "common/Path.hpp" + +#include +#include + +using namespace geos; +using namespace geos::dataRepository; +using namespace geos::testing; + +CommandLineOptions g_commandLineOptions; + +//////////////////////////////// Utilities //////////////////////////////// + +string replaceAll( string str, string_view token, string_view value ) +{ + for( size_t pos = 0; ( pos = str.find( token, pos ) ) != string::npos; pos += value.size() ) + str.replace( pos, token.size(), value ); + return str; +} + +struct CSVStats +{ + bool exists = false; + integer headerCount = 0; + integer dataRowCount = 0; +}; + +CSVStats inspectCSV( string const & path, string_view headerToken ) +{ + CSVStats stats; + std::ifstream file( path ); + if( !file.is_open() ) + return stats; + stats.exists = true; + string line; + while( std::getline( file, line ) ) + { + if( line.find( headerToken ) != string::npos ) + stats.headerCount++; + else if( !line.empty() ) + stats.dataRowCount++; + } + return stats; +} + +string statsFilePath( string_view solverName, string_view suffix ) +{ + return joinPath( OutputBase::getOutputDirectory(), + "convergence", + GEOS_FMT( "{}_{}.csv", solverName, suffix ) ); +} + +//////////////////////////////// Input deck //////////////////////////////// + +// Mandel-type consolidation problem (PoroElastic_Mandel_smoke), self-contained: +// initial-displacement table files removed, constant step load instead of loadFunction. +static char const * xmlTemplate = + R"xml( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + )xml"; + +//////////////////////////////// Tests //////////////////////////////// + +struct Params +{ + string couplingType; // "FullyImplicit" or "Sequential" + string csvMode; // "all", "none", ... + string suffix; // unique per variant so output files do not collide +}; + +// Run a single coupled-statistics scenario and verify its CSV output. +static void runScenario( Params const & p ) +{ + SCOPED_TRACE( GEOS_FMT( "{}_{}", p.couplingType, p.csvMode ) ); + + // Sequential coupling forbids line search; use "None" there. + string const lineSearchAction = ( p.couplingType == "Sequential" ) ? "None" : "Attempt"; + + string xmlInput = replaceAll( xmlTemplate, "@COUPLING_TYPE@", p.couplingType ); + xmlInput = replaceAll( xmlInput, "@CSV_MODE@", p.csvMode ); + xmlInput = replaceAll( xmlInput, "@SUFFIX@", p.suffix ); + xmlInput = replaceAll( xmlInput, "@LINE_SEARCH_ACTION@", lineSearchAction ); + + GeosxState state( std::make_unique< CommandLineOptions >( g_commandLineOptions ) ); + ProblemManager & problem = state.getProblemManager(); + setupProblemFromXML( problem, xmlInput.data() ); + + EXPECT_FALSE( problem.runSimulation() ) << "Simulation exited early."; + + MpiWrapper::barrier(); + if( MpiWrapper::commRank() != 0 ) + return; + + string const coupledCsv = statsFilePath( "poroSolve" + p.suffix, "iterations" ); + string const flowCsv = statsFilePath( "flowSolve" + p.suffix, "iterations" ); + string const mechCsv = statsFilePath( "lagsolve" + p.suffix, "iterations" ); + string_view const headerToken = "Number of time steps"; + + // 1) Coupled solver CSV: exists, exactly ONE header, and data rows. + CSVStats const coupled = inspectCSV( coupledCsv, headerToken ); + EXPECT_TRUE( coupled.exists ); + EXPECT_EQ( coupled.headerCount, 1 ); + EXPECT_GE( coupled.dataRowCount, 2 ); // one row per (sub)step, 2 forced steps + + // 2) Sub-solver CSVs: in Sequential mode the sub-solvers iterate on their own, + // so their iteration files must contain data. + if( p.couplingType == "Sequential" ) + { + for( string const & path : { flowCsv, mechCsv } ) + { + CSVStats const sub = inspectCSV( path, headerToken ); + EXPECT_TRUE( sub.exists ) << path; + EXPECT_EQ( sub.headerCount, 1 ) << path; + EXPECT_GE( sub.dataRowCount, 2 ) << path; + } + } + // In FullyImplicit mode sub-solvers take no independent time steps, + // so writeIterationStatsToTable() early-returns: no rows expected. +} + +TEST( CoupledSolverStatisticsCSVTest, checkCSVOutput ) +{ + stdVector< Params > const scenarios = { + { "FullyImplicit", "all", "FIM" }, + { "Sequential", "all", "Seq" }, + }; + + for( Params const & p : scenarios ) + { + runScenario( p ); + } +} + +int main( int argc, char * * argv ) +{ + ::testing::InitGoogleTest( &argc, argv ); + g_commandLineOptions = *geos::basicSetup( argc, argv ); + int const result = RUN_ALL_TESTS(); + geos::basicCleanup(); + return result; +} From b71b1a4a8e23f043c1b4e96b5ce8394e7799a236 Mon Sep 17 00:00:00 2001 From: bd713 <120652669+bd713@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:02:32 -0800 Subject: [PATCH 4/5] Tests --- .../solverStatisticsTests/CMakeLists.txt | 11 ++- .../testCoupledSolverStats.cpp} | 80 ++++++++++++++----- .../solverStatisticsTests/testSolverStats.cpp | 3 +- .../multiphysics/unitTests/CMakeLists.txt | 19 ----- 4 files changed, 74 insertions(+), 39 deletions(-) rename src/coreComponents/{physicsSolvers/multiphysics/unitTests/testCoupledSolverStatisticsCSV.cpp => integrationTests/solverStatisticsTests/testCoupledSolverStats.cpp} (71%) delete mode 100644 src/coreComponents/physicsSolvers/multiphysics/unitTests/CMakeLists.txt diff --git a/src/coreComponents/integrationTests/solverStatisticsTests/CMakeLists.txt b/src/coreComponents/integrationTests/solverStatisticsTests/CMakeLists.txt index 75a2b9e5aea..9cb6cbf273e 100644 --- a/src/coreComponents/integrationTests/solverStatisticsTests/CMakeLists.txt +++ b/src/coreComponents/integrationTests/solverStatisticsTests/CMakeLists.txt @@ -1,6 +1,7 @@ # Specify list of tests set( solverStatisticsTests_sources - testSolverStats.cpp ) + testSolverStats.cpp + testCoupledSolverStats.cpp ) set( dependencyList mainInterface gtest ${parallelDeps} ) @@ -16,3 +17,11 @@ foreach(test ${solverStatisticsTests_sources}) COMMAND ${test_name} ) endforeach() + +# Check that only one rank writes the statistics CSV files +if( ENABLE_MPI ) + set( nranks 2 ) + geos_add_test( NAME testCoupledSolverStats_mpi + COMMAND testCoupledSolverStats + NUM_MPI_TASKS ${nranks} ) +endif() diff --git a/src/coreComponents/physicsSolvers/multiphysics/unitTests/testCoupledSolverStatisticsCSV.cpp b/src/coreComponents/integrationTests/solverStatisticsTests/testCoupledSolverStats.cpp similarity index 71% rename from src/coreComponents/physicsSolvers/multiphysics/unitTests/testCoupledSolverStatisticsCSV.cpp rename to src/coreComponents/integrationTests/solverStatisticsTests/testCoupledSolverStats.cpp index 4c37219ecc9..a8d203e9c41 100644 --- a/src/coreComponents/physicsSolvers/multiphysics/unitTests/testCoupledSolverStatisticsCSV.cpp +++ b/src/coreComponents/integrationTests/solverStatisticsTests/testCoupledSolverStats.cpp @@ -1,5 +1,19 @@ +/* + * ------------------------------------------------------------------------------------------------------------ + * SPDX-License-Identifier: LGPL-2.1-only + * + * Copyright (c) 2016-2024 Lawrence Livermore National Security LLC + * Copyright (c) 2018-2024 TotalEnergies + * Copyright (c) 2018-2024 The Board of Trustees of the Leland Stanford Junior University + * Copyright (c) 2023-2024 Chevron + * Copyright (c) 2019- GEOS/GEOSX Contributors + * All rights reserved + * + * See top level LICENSE, COPYRIGHT, CONTRIBUTORS, NOTICE, and ACKNOWLEDGEMENTS files for details. + * ------------------------------------------------------------------------------------------------------------ + */ + #include "integrationTests/fluidFlowTests/testCompFlowUtils.hpp" -#include "integrationTests/testingUtilities/TestingTasks.hpp" #include "mainInterface/initialization.hpp" #include "mainInterface/GeosxState.hpp" #include "fileIO/Outputs/OutputBase.hpp" @@ -7,6 +21,7 @@ #include "common/Path.hpp" #include +#include #include using namespace geos; @@ -56,6 +71,14 @@ string statsFilePath( string_view solverName, string_view suffix ) GEOS_FMT( "{}_{}.csv", solverName, suffix ) ); } +void expectSingleHeaderCSV( string const & path, string_view headerToken, integer const minDataRows ) +{ + CSVStats const stats = inspectCSV( path, headerToken ); + EXPECT_TRUE( stats.exists ) << path; + EXPECT_EQ( stats.headerCount, 1 ) << path; + EXPECT_GE( stats.dataRowCount, minDataRows ) << path; +} + //////////////////////////////// Input deck //////////////////////////////// // Mandel-type consolidation problem (PoroElastic_Mandel_smoke), self-contained: @@ -169,12 +192,27 @@ static void runScenario( Params const & p ) { SCOPED_TRACE( GEOS_FMT( "{}_{}", p.couplingType, p.csvMode ) ); + // the serial and parallel variants of this test may run concurrently: keep their output files apart + string const suffix = GEOS_FMT( "{}_np{}", p.suffix, MpiWrapper::commSize() ); + stdVector< string > const solverNames = { "poroSolve" + suffix, "flowSolve" + suffix, "lagsolve" + suffix }; + + // remove files left over by a previous run, so that the checks below only see this run's output + if( MpiWrapper::commRank() == 0 ) + { + for( string const & solverName : solverNames ) + { + std::filesystem::remove( statsFilePath( solverName, "iterations" ) ); + std::filesystem::remove( statsFilePath( solverName, "convergence" ) ); + } + } + MpiWrapper::barrier(); + // Sequential coupling forbids line search; use "None" there. string const lineSearchAction = ( p.couplingType == "Sequential" ) ? "None" : "Attempt"; string xmlInput = replaceAll( xmlTemplate, "@COUPLING_TYPE@", p.couplingType ); xmlInput = replaceAll( xmlInput, "@CSV_MODE@", p.csvMode ); - xmlInput = replaceAll( xmlInput, "@SUFFIX@", p.suffix ); + xmlInput = replaceAll( xmlInput, "@SUFFIX@", suffix ); xmlInput = replaceAll( xmlInput, "@LINE_SEARCH_ACTION@", lineSearchAction ); GeosxState state( std::make_unique< CommandLineOptions >( g_commandLineOptions ) ); @@ -187,31 +225,37 @@ static void runScenario( Params const & p ) if( MpiWrapper::commRank() != 0 ) return; - string const coupledCsv = statsFilePath( "poroSolve" + p.suffix, "iterations" ); - string const flowCsv = statsFilePath( "flowSolve" + p.suffix, "iterations" ); - string const mechCsv = statsFilePath( "lagsolve" + p.suffix, "iterations" ); - string_view const headerToken = "Number of time steps"; + string_view const iterationHeaderToken = "Number of time steps"; + string_view const convergenceHeaderToken = "Cycle number"; + string const & coupledName = solverNames[0]; - // 1) Coupled solver CSV: exists, exactly ONE header, and data rows. - CSVStats const coupled = inspectCSV( coupledCsv, headerToken ); - EXPECT_TRUE( coupled.exists ); - EXPECT_EQ( coupled.headerCount, 1 ); - EXPECT_GE( coupled.dataRowCount, 2 ); // one row per (sub)step, 2 forced steps + // 1) Coupled solver CSVs: exist, exactly ONE header (a single rank writes), and data rows. + expectSingleHeaderCSV( statsFilePath( coupledName, "iterations" ), iterationHeaderToken, 2 ); // 2 forced steps + // Without subcycling, the Sequential scheme does a single pass and computes no outer-loop residual. + if( p.couplingType == "FullyImplicit" ) + { + expectSingleHeaderCSV( statsFilePath( coupledName, "convergence" ), convergenceHeaderToken, 2 ); + } // 2) Sub-solver CSVs: in Sequential mode the sub-solvers iterate on their own, - // so their iteration files must contain data. + // so their iteration and convergence files must contain data. if( p.couplingType == "Sequential" ) { - for( string const & path : { flowCsv, mechCsv } ) + for( string const & solverName : { solverNames[1], solverNames[2] } ) { - CSVStats const sub = inspectCSV( path, headerToken ); - EXPECT_TRUE( sub.exists ) << path; - EXPECT_EQ( sub.headerCount, 1 ) << path; - EXPECT_GE( sub.dataRowCount, 2 ) << path; + expectSingleHeaderCSV( statsFilePath( solverName, "iterations" ), iterationHeaderToken, 2 ); + expectSingleHeaderCSV( statsFilePath( solverName, "convergence" ), convergenceHeaderToken, 2 ); } } // In FullyImplicit mode sub-solvers take no independent time steps, - // so writeIterationStatsToTable() early-returns: no rows expected. + // so writeIterationStatsToTable() early-returns: no iteration file expected. + else + { + for( string const & solverName : { solverNames[1], solverNames[2] } ) + { + EXPECT_FALSE( inspectCSV( statsFilePath( solverName, "iterations" ), iterationHeaderToken ).exists ) << solverName; + } + } } TEST( CoupledSolverStatisticsCSVTest, checkCSVOutput ) diff --git a/src/coreComponents/integrationTests/solverStatisticsTests/testSolverStats.cpp b/src/coreComponents/integrationTests/solverStatisticsTests/testSolverStats.cpp index b781f669cfb..5920d78e679 100644 --- a/src/coreComponents/integrationTests/solverStatisticsTests/testSolverStats.cpp +++ b/src/coreComponents/integrationTests/solverStatisticsTests/testSolverStats.cpp @@ -79,7 +79,7 @@ static const string solverCSVOutput = name="SinglePhaseFlow" discretization="singlePhaseTPFA" targetRegions="{ Channel }" - writeStatistics="convergence" > + writeStatistics="all" > @@ -284,6 +284,7 @@ TEST( testSolverStats, testOutputFiles ) EXPECT_EQ( csvLines2[0], "Cycle number,time_n (s),dt (s),iteration,R,Rflow" ); + ASSERT_TRUE( std::remove( iterationStat.getFilename().c_str() ) == 0 ); ASSERT_TRUE( std::remove( convergenceStat.getFilename().c_str() ) == 0 ); } diff --git a/src/coreComponents/physicsSolvers/multiphysics/unitTests/CMakeLists.txt b/src/coreComponents/physicsSolvers/multiphysics/unitTests/CMakeLists.txt deleted file mode 100644 index 979b2cd6452..00000000000 --- a/src/coreComponents/physicsSolvers/multiphysics/unitTests/CMakeLists.txt +++ /dev/null @@ -1,19 +0,0 @@ -set( multiphysics_gtest_tests - testCoupledSolverStatisticsCSV.cpp ) - - set( dependencyList mainInterface testingUtilities gtest ${parallelDeps} ) - - blt_add_executable( NAME testCoupledSolverStatisticsCSV - SOURCES testCoupledSolverStatisticsCSV.cpp - OUTPUT_DIR ${TEST_OUTPUT_DIRECTORY} - DEPENDS_ON ${dependencyList} - FOLDER UnitTests ) - - geos_add_test( NAME testCoupledSolverStatisticsCSV - COMMAND testCoupledSolverStatisticsCSV ) - - if( ENABLE_MPI ) - geos_add_test( NAME testCoupledSolverStatisticsCSV_2ranks - COMMAND testCoupledSolverStatisticsCSV - NUM_MPI_TASKS 2 ) - endif() From ed79e4e17ba2451ff3945ed03244d7051bde7485 Mon Sep 17 00:00:00 2001 From: bd713 <120652669+bd713@users.noreply.github.com> Date: Wed, 4 Nov 2026 19:55:34 -0800 Subject: [PATCH 5/5] Reviews --- .../physicsSolvers/PhysicsSolverBase.cpp | 8 +++- .../physicsSolvers/SolverStatistics.cpp | 42 +++++++++---------- .../physicsSolvers/SolverStatistics.hpp | 8 ++-- .../multiphysics/CMakeLists.txt | 4 -- .../multiphysics/CoupledSolver.hpp | 1 + 5 files changed, 32 insertions(+), 31 deletions(-) diff --git a/src/coreComponents/physicsSolvers/PhysicsSolverBase.cpp b/src/coreComponents/physicsSolvers/PhysicsSolverBase.cpp index f5dc5cdb3b1..3712dd6d203 100644 --- a/src/coreComponents/physicsSolvers/PhysicsSolverBase.cpp +++ b/src/coreComponents/physicsSolvers/PhysicsSolverBase.cpp @@ -1598,9 +1598,13 @@ void PhysicsSolverBase::cleanup( real64 const GEOS_UNUSED_PARAM( time_n ), getIterationStats().outputStatistics(); } - bool wrongIterationCSVOutputRequest = getIterationStats().getCSVOutputRequest() && + // CSV files are only written by rank 0 + bool const isRankZero = MpiWrapper::commRank() == 0; + bool wrongIterationCSVOutputRequest = isRankZero && + getIterationStats().getCSVOutputRequest() && !getIterationStats().getCSVOutputOpened(); - bool wrongConvergenceCSVOutputRequest = getConvergenceStats().getCSVOutputRequest() && + bool wrongConvergenceCSVOutputRequest = isRankZero && + getConvergenceStats().getCSVOutputRequest() && !getConvergenceStats().getCSVOutputOpened(); GEOS_WARNING_IF( wrongIterationCSVOutputRequest, diff --git a/src/coreComponents/physicsSolvers/SolverStatistics.cpp b/src/coreComponents/physicsSolvers/SolverStatistics.cpp index e3e44a560da..4c1c6da0203 100644 --- a/src/coreComponents/physicsSolvers/SolverStatistics.cpp +++ b/src/coreComponents/physicsSolvers/SolverStatistics.cpp @@ -146,10 +146,13 @@ void IterationsStatistics::writeIterationStatsToTable() if( m_numTimeSteps == 0 || !m_logOutputRequest ) return; + // linear solver timings are reported per row, so they are reset on every rank, even when no CSV is written. + // Timings of attempts made before the first converged time step are accumulated into the first row. real64 const setupTime = m_setupTime; real64 const solveTime = m_solveTime; resetSolverLinearTime(); + // only rank 0 writes the CSV file if( !m_CSVOutputRequest || MpiWrapper::commRank() != 0 ) return; @@ -166,18 +169,17 @@ void IterationsStatistics::writeIterationStatsToTable() if( !m_CSVOutputOpened ) { - m_logStream.open( m_iterationsFilename ); + m_csvStream.open( m_iterationsFilename ); m_iterationCSVFormatter = std::make_unique< TableCSVFormatter >( m_iterationCSVLayout ); - m_logStream << m_iterationCSVFormatter->headerToString( ); + m_csvStream << m_iterationCSVFormatter->headerToString( ); m_CSVOutputOpened = true; } - m_logStream << m_iterationCSVFormatter->dataToString( m_iterationData ); - m_logStream.flush(); + m_csvStream << m_iterationCSVFormatter->dataToString( m_iterationData ); + m_csvStream.flush(); m_iterationData.clear(); } - void IterationsStatistics::outputStatistics() const { // no statistics to output when no time steps have been recorded @@ -211,8 +213,10 @@ ConvergenceStatistics::ConvergenceStatistics(): void ConvergenceStatistics::writeConvergenceStatsToTable() { - if( !m_CSVOutputRequest ) + // residual norms are global quantities: only rank 0 writes the CSV file + if( !m_CSVOutputRequest || MpiWrapper::commRank() != 0 ) return; + stdVector< TableData::CellData > residualsNormCells; residualsNormCells.emplace_back( TableData::CellData( {CellType::Value, @@ -235,27 +239,23 @@ void ConvergenceStatistics::writeConvergenceStatsToTable() m_convergenceData.addRow( residualsNormCells ); - if( MpiWrapper::commRank() == 0 ) + if( !m_CSVOutputOpened ) { - if( !m_CSVOutputOpened ) + string_array header = {"Cycle number", "time_n (s)", "dt (s)", "iteration"}; + for( auto const & residual : m_residuals ) { - string_array header = {"Cycle number", "time_n (s)", "dt (s)", "iteration"}; - for( auto const & residual : m_residuals ) - { - header.emplace_back( residual.first ); - } - m_convergenceLayout.addColumns( header ); - - m_logStream.open( m_convergenceFilename ); - m_convergenceFormatter = std::make_unique< TableCSVFormatter >( m_convergenceLayout ); - m_logStream << m_convergenceFormatter->headerToString( ); - m_CSVOutputOpened = true; + header.emplace_back( residual.first ); } + m_convergenceLayout.addColumns( header ); - m_logStream << m_convergenceFormatter->dataToString( m_convergenceData ); - m_logStream.flush(); + m_csvStream.open( m_convergenceFilename ); + m_convergenceFormatter = std::make_unique< TableCSVFormatter >( m_convergenceLayout ); + m_csvStream << m_convergenceFormatter->headerToString( ); + m_CSVOutputOpened = true; } + m_csvStream << m_convergenceFormatter->dataToString( m_convergenceData ); + m_csvStream.flush(); m_convergenceData.clear(); } diff --git a/src/coreComponents/physicsSolvers/SolverStatistics.hpp b/src/coreComponents/physicsSolvers/SolverStatistics.hpp index 0cef5c39d20..56ff32b9aa4 100644 --- a/src/coreComponents/physicsSolvers/SolverStatistics.hpp +++ b/src/coreComponents/physicsSolvers/SolverStatistics.hpp @@ -182,7 +182,7 @@ class IterationsStatistics : public dataRepository::Group * @brief Close the stream output file if a file was created */ void closeFile() - { if( m_CSVOutputOpened ) { m_logStream.close(); m_CSVOutputOpened = false; } } + { if( m_CSVOutputOpened ) { m_csvStream.close(); m_CSVOutputOpened = false; } } /** * @brief Get the number of time steps @@ -238,7 +238,7 @@ class IterationsStatistics : public dataRepository::Group /// A boolean indicating whether the iteration CSV file was opened or not bool m_CSVOutputOpened = false; /// Stream output for the iteration statistics - std::ofstream m_logStream; + std::ofstream m_csvStream; /// Table Layout contenaning header for both CSV and log TableLayout m_iterationCSVLayout; /// Contain the iteration data for both CSV and log output @@ -337,7 +337,7 @@ class ConvergenceStatistics * @brief Close the stream output file if a file was created */ void closeFile() - { if( m_CSVOutputOpened ) { m_logStream.close(); m_CSVOutputOpened = false; } } + { if( m_CSVOutputOpened ) { m_csvStream.close(); m_CSVOutputOpened = false; } } private: /// The time at the beginning of the step @@ -360,7 +360,7 @@ class ConvergenceStatistics /// A boolean indicating whether the CSV file was opened or not bool m_CSVOutputOpened = false; /// Stream output for the convergence statistics - std::ofstream m_logStream; + std::ofstream m_csvStream; /// Contain the layout for both the CSV and log output. /// For a solver, output all residuals residuals name available TableLayout m_convergenceLayout; diff --git a/src/coreComponents/physicsSolvers/multiphysics/CMakeLists.txt b/src/coreComponents/physicsSolvers/multiphysics/CMakeLists.txt index 9cce971d562..36e8a23c9c0 100644 --- a/src/coreComponents/physicsSolvers/multiphysics/CMakeLists.txt +++ b/src/coreComponents/physicsSolvers/multiphysics/CMakeLists.txt @@ -129,7 +129,3 @@ install( TARGETS multiPhysicsSolvers LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX} if( externalComponentDeps ) target_include_directories( multiPhysicsSolvers PUBLIC ${CMAKE_SOURCE_DIR}/externalComponents ) endif() - -if( GEOS_ENABLE_TESTS ) - add_subdirectory( unitTests ) -endif() diff --git a/src/coreComponents/physicsSolvers/multiphysics/CoupledSolver.hpp b/src/coreComponents/physicsSolvers/multiphysics/CoupledSolver.hpp index c2cec6baf68..79e628cec33 100644 --- a/src/coreComponents/physicsSolvers/multiphysics/CoupledSolver.hpp +++ b/src/coreComponents/physicsSolvers/multiphysics/CoupledSolver.hpp @@ -590,6 +590,7 @@ class CoupledSolver : public PhysicsSolverBase // notify the solver statistics counter that this is a time step cut getIterationStats().updateTimeStepCut(); + getIterationStats().writeIterationStatsToTable(); forEachArgInTuple( m_solvers, [&]( auto & solver, auto ) { solver->getIterationStats().updateTimeStepCut();