diff --git a/src/coreComponents/physicsSolvers/SolverStatistics.cpp b/src/coreComponents/physicsSolvers/SolverStatistics.cpp index 4324bc684ef..e3e44a560da 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 { @@ -145,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, @@ -153,24 +161,23 @@ void IterationsStatistics::writeIterationStatsToTable() m_numDiscardedConfigIterations, m_numDiscardedNonlinearIterations, m_numDiscardedLinearIterations, - m_setupTime, - m_solveTime ); + setupTime, + solveTime ); 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_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 @@ -228,23 +235,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(); } 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/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(); } ); } } 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; +}