diff --git a/CMakeLists_files.cmake b/CMakeLists_files.cmake index cda462e0fa5..4d8cd0c39da 100644 --- a/CMakeLists_files.cmake +++ b/CMakeLists_files.cmake @@ -1172,7 +1172,20 @@ if(dune-alugrid_FOUND) examples/fracture_discretefracture.cpp ) endif() - +if(USE_MPI) + list (APPEND MAIN_SOURCE_FILES + opm/simulators/flow/ReservoirCoupling.cpp + opm/simulators/flow/ReservoirCouplingMaster.cpp + opm/simulators/flow/ReservoirCouplingSlave.cpp + opm/simulators/flow/ReservoirCouplingSpawnSlaves.cpp + ) + list (APPEND PUBLIC_HEADER_FILES + opm/simulators/flow/ReservoirCoupling.hpp + opm/simulators/flow/ReservoirCouplingMaster.hpp + opm/simulators/flow/ReservoirCouplingSlave.hpp + opm/simulators/flow/ReservoirCouplingSpawnSlaves.hpp + ) +endif() if(HYPRE_FOUND) list(APPEND PUBLIC_HEADER_FILES opm/simulators/linalg/HyprePreconditioner.hpp diff --git a/opm/simulators/flow/FlowGenericVanguard.cpp b/opm/simulators/flow/FlowGenericVanguard.cpp index 1622411b98f..1f6e0af49e5 100644 --- a/opm/simulators/flow/FlowGenericVanguard.cpp +++ b/opm/simulators/flow/FlowGenericVanguard.cpp @@ -177,7 +177,7 @@ void FlowGenericVanguard::readDeck(const std::string& filename) modelParams_.actionState_, modelParams_.wtestState_, modelParams_.eclSummaryConfig_, - nullptr, "normal", "normal", "100", false, false, false, {}); + nullptr, "normal", "normal", "100", false, false, false, {}, /*slaveMode=*/false); modelParams_.setupTime_ = setupTimer.stop(); } diff --git a/opm/simulators/flow/FlowMain.hpp b/opm/simulators/flow/FlowMain.hpp index 24a888c637a..610b735e5fe 100644 --- a/opm/simulators/flow/FlowMain.hpp +++ b/opm/simulators/flow/FlowMain.hpp @@ -32,6 +32,9 @@ #include <opm/simulators/flow/FlowUtils.hpp> #include <opm/simulators/flow/SimulatorFullyImplicitBlackoil.hpp> +#if HAVE_MPI +#define RESERVOIR_COUPLING_ENABLED +#endif #if HAVE_DUNE_FEM #include <dune/fem/misc/mpimanager.hh> #else @@ -50,7 +53,6 @@ namespace Opm::Parameters { // Do not merge parallel output files or warn about them struct EnableLoggingFalloutWarning { static constexpr bool value = false; }; - struct OutputInterval { static constexpr int value = 1; }; } // namespace Opm::Parameters @@ -366,7 +368,11 @@ namespace Opm { // Callback that will be called from runSimulatorInitOrRun_(). int runSimulatorRunCallback_() { +#ifdef RESERVOIR_COUPLING_ENABLED + SimulatorReport report = simulator_->run(*simtimer_, this->argc_, this->argv_); +#else SimulatorReport report = simulator_->run(*simtimer_); +#endif runSimulatorAfterSim_(report); return report.success.exit_status; } @@ -374,7 +380,11 @@ namespace Opm { // Callback that will be called from runSimulatorInitOrRun_(). int runSimulatorInitCallback_() { +#ifdef RESERVOIR_COUPLING_ENABLED + simulator_->init(*simtimer_, this->argc_, this->argv_); +#else simulator_->init(*simtimer_); +#endif return EXIT_SUCCESS; } diff --git a/opm/simulators/flow/Main.cpp b/opm/simulators/flow/Main.cpp index 3abc4c07da7..9ce461cce0e 100644 --- a/opm/simulators/flow/Main.cpp +++ b/opm/simulators/flow/Main.cpp @@ -44,11 +44,21 @@ #include <amgx_c.h> #endif +#include <iostream> +// NOTE: There is no C++ header replacement for these C posix headers (as of C++17) +#include <fcntl.h> // for open() +#include <unistd.h> // for dup2(), close() + +#include <iostream> + namespace Opm { Main::Main(int argc, char** argv, bool ownMPI) : argc_(argc), argv_(argv), ownMPI_(ownMPI) { +#if HAVE_MPI + maybeSaveReservoirCouplingSlaveLogFilename_(); +#endif if (ownMPI_) { initMPI(); } @@ -132,6 +142,53 @@ Main::~Main() #endif } +#if HAVE_MPI +void Main::maybeSaveReservoirCouplingSlaveLogFilename_() +{ + // If first command line argument is "--slave-log-file=<filename>", + // then redirect stdout and stderr to the specified file. + if (this->argc_ >= 2) { + std::string_view arg = this->argv_[1]; + if (arg.substr(0, 17) == "--slave-log-file=") { + // For now, just save the basename of the filename and we will append the rank + // to the filename after having called MPI_Init() to avoid all ranks outputting + // to the same file. + this->reservoirCouplingSlaveOutputFilename_ = arg.substr(17); + this->argc_ -= 1; + char *program_name = this->argv_[0]; + this->argv_ += 1; + // We assume the "argv" array pointers remain valid (not freed) for the lifetime + // of this program, so the following assignment is safe. + // Overwrite the first argument with the program name, i.e. pretend the program + // was called with the same arguments, but without the "--slave-log-file" argument. + this->argv_[0] = program_name; + } + } +} +#endif +#if HAVE_MPI +void Main::maybeRedirectReservoirCouplingSlaveOutput_() { + if (!this->reservoirCouplingSlaveOutputFilename_.empty()) { + std::string filename = this->reservoirCouplingSlaveOutputFilename_ + + "." + std::to_string(FlowGenericVanguard::comm().rank()) + ".log"; + int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (fd == -1) { + std::string error_msg = "Slave: Failed to open stdout+stderr file" + filename; + perror(error_msg.c_str()); + MPI_Abort(MPI_COMM_WORLD, /*status=*/1); + } + // Redirect stdout and stderr to the file. + if (dup2(fd, fileno(stdout)) == -1 || dup2(fileno(stdout), fileno(stderr)) == -1) { + std::string error_msg = "Slave: Failed to redirect stdout+stderr to " + filename; + perror(error_msg.c_str()); + close(fd); + MPI_Abort(MPI_COMM_WORLD, /*status=*/1); + } + close(fd); + } +} +#endif + void Main::setArgvArgc_(const std::string& filename) { this->deckFilename_ = filename; @@ -166,6 +223,7 @@ void Main::initMPI() handleTestSplitCommunicatorCmdLine_(); #if HAVE_MPI + maybeRedirectReservoirCouplingSlaveOutput_(); if (test_split_comm_ && FlowGenericVanguard::comm().size() > 1) { int world_rank = FlowGenericVanguard::comm().rank(); int color = (world_rank == 0); @@ -230,6 +288,7 @@ void Main::readDeck(const std::string& deckFilename, const bool keepKeywords, const std::size_t numThreads, const int output_param, + const bool slaveMode, const std::string& parameters, std::string_view moduleVersion, std::string_view compileTimestamp) @@ -265,7 +324,8 @@ void Main::readDeck(const std::string& deckFilename, init_from_restart_file, outputCout_, keepKeywords, - outputInterval); + outputInterval, + slaveMode); verifyValidCellGeometry(FlowGenericVanguard::comm(), *this->eclipseState_); @@ -294,7 +354,7 @@ void Main::setupDamaris(const std::string& outputDir ) //const auto find_replace_map = Opm::DamarisOutput::DamarisKeywords<PreTypeTag>(EclGenericVanguard::comm(), outputDir); std::map<std::string, std::string> find_replace_map; find_replace_map = DamarisOutput::getDamarisKeywords(FlowGenericVanguard::comm(), outputDir); - + // By default EnableDamarisOutputCollective is true so all simulation results will // be written into one single file for each iteration using Parallel HDF5. // If set to false, FilePerCore mode is used in Damaris, then simulation results in each diff --git a/opm/simulators/flow/Main.hpp b/opm/simulators/flow/Main.hpp index 9a4e0f47c60..6a9e1a7b8e8 100644 --- a/opm/simulators/flow/Main.hpp +++ b/opm/simulators/flow/Main.hpp @@ -150,7 +150,8 @@ class Main ~Main(); void setArgvArgc_(const std::string& filename); - + void maybeSaveReservoirCouplingSlaveLogFilename_(); + void maybeRedirectReservoirCouplingSlaveOutput_(); void initMPI(); int runDynamic() @@ -413,6 +414,7 @@ class Main keepKeywords, getNumThreads(), Parameters::Get<Parameters::EclOutputInterval>(), + Parameters::Get<Parameters::Slave>(), cmdline_params, Opm::moduleVersion(), Opm::compileTimestamp()); @@ -697,6 +699,7 @@ class Main const bool keepKeywords, const std::size_t numThreads, const int output_param, + const bool slaveMode, const std::string& parameters, std::string_view moduleVersion, std::string_view compileTimestamp); @@ -740,6 +743,9 @@ class Main // To demonstrate run with non_world_comm bool test_split_comm_ = false; bool isSimulationRank_ = true; +#if HAVE_MPI + std::string reservoirCouplingSlaveOutputFilename_{}; +#endif #if HAVE_DAMARIS bool enableDamarisOutput_ = false; #endif diff --git a/opm/simulators/flow/ReservoirCoupling.cpp b/opm/simulators/flow/ReservoirCoupling.cpp new file mode 100644 index 00000000000..8c4499a87ba --- /dev/null +++ b/opm/simulators/flow/ReservoirCoupling.cpp @@ -0,0 +1,116 @@ +/* + Copyright 2024 Equinor ASA + + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <config.h> + +#include <opm/simulators/flow/ReservoirCoupling.hpp> +#include <opm/simulators/utils/ParallelCommunication.hpp> +#include <opm/common/OpmLog/OpmLog.hpp> + +#include <fmt/format.h> + +namespace Opm { +namespace ReservoirCoupling { + +void custom_error_handler_(MPI_Comm* comm, int* err, const std::string &msg) +{ + // It can be useful to have a custom error handler for debugging purposes. + // If not, MPI will use the default error handler which aborts the program, and + // it can be difficult to determine exactly where the error occurred. With a custom + // error handler you can at least set a breakpoint here to get a backtrace. + int rank; + MPI_Comm_rank(*comm, &rank); + char err_string[MPI_MAX_ERROR_STRING]; + int len; + MPI_Error_string(*err, err_string, &len); + const std::string error_msg = fmt::format( + "Reservoir coupling MPI error handler {} rank {}: {}", msg, rank, err_string); + // NOTE: The output to terminal vie stderr or stdout has been redirected to files for + // the slaves, see Main.cpp. So the following output will not be visible in the terminal + // if we are called from a slave process. + // std::cerr << error_msg << std::endl; + OpmLog::error(error_msg); // Output to log file, also shows the message on stdout for master. + MPI_Abort(*comm, *err); +} + +void custom_error_handler_slave_(MPI_Comm* comm, int* err, ...) +{ + custom_error_handler_(comm, err, "slave"); +} + +void custom_error_handler_master_(MPI_Comm* comm, int* err, ...) +{ + custom_error_handler_(comm, err, "master"); +} + +void setErrhandler(MPI_Comm comm, bool is_master) +{ + MPI_Errhandler errhandler; + // NOTE: Lambdas with captures cannot be used as C function pointers, also + // converting lambdas that use ellipsis "..." as last argument to a C function pointer + // is not currently possible, so we need to use static functions instead. + if (is_master) { + MPI_Comm_create_errhandler(custom_error_handler_master_, &errhandler); + } + else { + MPI_Comm_create_errhandler(custom_error_handler_slave_, &errhandler); + } + // NOTE: The errhandler is a handle (an integer) that is associated with the communicator + // that is why we pass this by value below. And it is safe to free the errhandler after it has + // been associated with the communicator. + MPI_Comm_set_errhandler(comm, errhandler); + // Mark the error handler for deletion. According to the documentation: "The error handler will + // be deallocated after all the objects associated with it (communicator, window, or file) have + // been deallocated." So the error handler will still be in effect until the communicator is + // deallocated. + MPI_Errhandler_free(&errhandler); +} + +bool Seconds::compare_eq(double a, double b) +{ + // Are a and b equal? + return std::abs(a - b) < std::max(abstol, reltol * std::max(std::abs(a), std::abs(b))); +} + +bool Seconds::compare_gt_or_eq(double a, double b) +{ + // Is a greater than or equal to b? + if (compare_eq(a, b)) { + return true; + } + return a > b; +} + +bool Seconds::compare_gt(double a, double b) +{ + // Is a greater than b? + return !compare_eq(a, b) && a > b; +} + +bool Seconds::compare_lt_or_eq(double a, double b) +{ + // Is a less than or equal to b? + if (compare_eq(a, b)) { + return true; + } + return a < b; +} + +} // namespace ReservoirCoupling +} // namespace Opm diff --git a/opm/simulators/flow/ReservoirCoupling.hpp b/opm/simulators/flow/ReservoirCoupling.hpp new file mode 100644 index 00000000000..2308d4ce454 --- /dev/null +++ b/opm/simulators/flow/ReservoirCoupling.hpp @@ -0,0 +1,113 @@ +/* + Copyright 2024 Equinor ASA + + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef OPM_RESERVOIR_COUPLING_HPP +#define OPM_RESERVOIR_COUPLING_HPP +#include <mpi.h> +#include <cmath> +#include <iostream> +#include <memory> + +namespace Opm { +namespace ReservoirCoupling { + +enum class MessageTag : int { + SlaveSimulationStartDate, + SlaveActivationDate, + SlaveProcessTermination, + SlaveNextReportDate, + SlaveNextTimeStep, + MasterGroupNames, + MasterGroupNamesSize, +}; + +// Helper functions +void custom_error_handler_(MPI_Comm* comm, int* err, const std::string &msg); +void setErrhandler(MPI_Comm comm, bool is_master); + +/// \brief Utility class for comparing double values representing epoch dates or elapsed time. +/// +/// This class is used to compare double values that represent: +/// - Epoch dates (seconds since the Unix epoch) +/// - Elapsed time (seconds since the start of the simulation) +/// +/// \note When comparing against the start of a report step or similar, it is important not to miss +/// these points due to numerical issues. Communication between master and slave processes is based +/// on these specific points in time. +/// +/// \note Epoch values in this century (2000-2100) fall within the range [1e9, 4e9]. A double variable +/// cannot represent such large values with high precision. For example, the date 01-01-2020 corresponds +/// to 1.5778368e9 seconds, and adding 1e-7 seconds to this value does not change it. Microseconds (1e-6) +/// are approximately the smallest time units that can be represented accurately for such numbers. +/// +/// \note Report steps appear to have a maximum resolution of whole seconds. See the `stepLength()` +/// function in `Schedule.cpp` in the `opm-common` module, which returns the step length in seconds. +struct Seconds { + /// \brief Absolute tolerance used for comparisons. + static constexpr double abstol = 1e-15; + + /// \brief Relative tolerance used for comparisons. + static constexpr double reltol = 1e-15; + + /// \brief Determines if two double values are equal within a specified tolerance. + /// + /// Two values \a a and \a b are considered equal if: + /// \f[ |a - b| \leq \text{tol} = \text{abstol} + \text{reltol} \times \max(|a|, |b|) \f] + /// + /// For example, if \a abstol = \a reltol = 1e-15: + /// - If \a |a| and \a |b| are below 1, the absolute tolerance applies. + /// - If \a a and \a b are above 1, the relative tolerance applies. + /// + /// \note For epoch dates between 01-01-2000 and 01-01-2100, epoch values are in the range [1e9, 4e9]. + /// Therefore, \f$ 10^{-15} \times 10^9 = 10^{-6} \f$, meaning differences smaller than one microsecond + /// will be considered equal. + /// + /// \note This approach is not accurate for numbers close to zero, but such comparisons are not expected. + /// + /// \param a First double value. + /// \param b Second double value. + /// \return True if \a a and \a b are considered equal within the tolerance. + static bool compare_eq(double a, double b); + + /// \brief Determines if \a a is greater than \a b within the specified tolerance. + /// + /// \param a First double value. + /// \param b Second double value. + /// \return True if \a a is greater than \a b. + static bool compare_gt(double a, double b); + + /// \brief Determines if \a a is greater than \a b within the specified tolerance. + /// + /// \param a First double value. + /// \param b Second double value. + /// \return True if \a a is greater than \a b. + static bool compare_gt_or_eq(double a, double b); + + /// \brief Determines if \a a is less than or equal to \a b within the specified tolerance. + /// + /// \param a First double value. + /// \param b Second double value. + /// \return True if \a a is less than or equal to \a b. + static bool compare_lt_or_eq(double a, double b); +}; + +} // namespace ReservoirCoupling +} // namespace Opm + +#endif // OPM_RESERVOIR_COUPLING_HPP diff --git a/opm/simulators/flow/ReservoirCouplingMaster.cpp b/opm/simulators/flow/ReservoirCouplingMaster.cpp new file mode 100644 index 00000000000..c0e82fe3279 --- /dev/null +++ b/opm/simulators/flow/ReservoirCouplingMaster.cpp @@ -0,0 +1,217 @@ +/* + Copyright 2024 Equinor ASA + + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <config.h> + +#include <opm/simulators/flow/ReservoirCouplingMaster.hpp> +#include <opm/simulators/flow/ReservoirCouplingSpawnSlaves.hpp> + +#include <opm/input/eclipse/Schedule/ResCoup/ReservoirCouplingInfo.hpp> +#include <opm/input/eclipse/Schedule/ResCoup/MasterGroup.hpp> +#include <opm/input/eclipse/Schedule/ResCoup/Slaves.hpp> +#include <opm/common/ErrorMacros.hpp> + +#include <opm/simulators/utils/ParallelCommunication.hpp> + +#include <opm/input/eclipse/Schedule/Schedule.hpp> + + +#include <filesystem> +#include <vector> + +#include <fmt/format.h> + +namespace Opm { + +ReservoirCouplingMaster:: +ReservoirCouplingMaster( + const Parallel::Communication &comm, + const Schedule &schedule, + int argc, char **argv +) : + comm_{comm}, + schedule_{schedule}, + argc_{argc}, + argv_{argv} +{ + this->activation_date_ = this->getMasterActivationDate_(); +} + +// ------------------ +// Public methods +// ------------------ + +void +ReservoirCouplingMaster:: +maybeSpawnSlaveProcesses(int report_step) +{ + if (this->numSlavesStarted() > 0) { // We have already spawned the slave processes + return; + } + const auto& rescoup = this->schedule_[report_step].rescoup(); + auto slave_count = rescoup.slaveCount(); + auto master_group_count = rescoup.masterGroupCount(); + if (slave_count > 0 && master_group_count > 0) { + ReservoirCouplingSpawnSlaves spawn_slaves{*this, rescoup, report_step}; + spawn_slaves.spawn(); + } +} + + +double +ReservoirCouplingMaster:: +maybeChopSubStep(double suggested_timestep_original, double elapsed_time) const +{ + // Check if the suggested timestep needs to be adjusted based on the slave processes' + // next report step, or if the slave process has not started yet: the start of a slave process. + // NOTE: getStartTime() returns a std::time_t value, which is typically a long integer. It should + // be possible to represent reasonable epoch values within a double. See comment for + // getMasterActivationDate_() for more information. + double start_date = this->schedule_.getStartTime(); + double step_start_date{start_date + elapsed_time}; + double step_end_date{step_start_date + suggested_timestep_original}; + double suggested_timestep{suggested_timestep_original}; + auto num_slaves = this->numSlavesStarted(); + // Determine the minimum step_end_date and the corresponding suggested_timestep such that no + // slave process will report or start during the timestep [step_start_date, step_end_date] + // where suggested_timestep = step_end_date - step_start_date + for (std::size_t i = 0; i < num_slaves; i++) { + double slave_start_date = this->slave_start_dates_[i]; + double slave_next_report_date{this->slave_next_report_time_offsets_[i] + slave_start_date}; + if (Seconds::compare_gt_or_eq(slave_start_date, step_end_date)) { + // The slave process has not started yet, and will not start during this timestep + continue; + } + double slave_elapsed_time; + if (Seconds::compare_lt_or_eq(slave_start_date,step_start_date)) { + // The slave process has already started, and will continue during this timestep + if (Seconds::compare_gt(slave_next_report_date, step_end_date)) { + // The slave process will not report during this timestep + continue; + } + // The slave process will report during this timestep + slave_elapsed_time = slave_next_report_date - step_start_date; + } + else { + // The slave process will start during the timestep, but not at the beginning + slave_elapsed_time = slave_start_date - step_start_date; + } + suggested_timestep = slave_elapsed_time; + step_end_date = step_start_date + suggested_timestep; + } + return suggested_timestep; +} + +void +ReservoirCouplingMaster:: +sendNextTimeStepToSlaves(double timestep) +{ + if (this->comm_.rank() == 0) { + for (unsigned int i = 0; i < this->master_slave_comm_.size(); i++) { + MPI_Send( + ×tep, + /*count=*/1, + /*datatype=*/MPI_DOUBLE, + /*dest_rank=*/0, + /*tag=*/static_cast<int>(MessageTag::SlaveNextTimeStep), + this->getSlaveComm(i) + ); + OpmLog::info(fmt::format( + "Sent next time step {} from master process rank 0 to slave process " + "rank 0 with name: {}", timestep, this->slave_names_[i]) + ); + } + } +} + + +void +ReservoirCouplingMaster:: +receiveNextReportDateFromSlaves() +{ + auto num_slaves = this->numSlavesStarted(); + OpmLog::info("Receiving next report dates from slave processes"); + if (this->comm_.rank() == 0) { + for (unsigned int i = 0; i < num_slaves; i++) { + double slave_next_report_time_offset; // Elapsed time from the beginning of the simulation + // NOTE: All slave-master communicators have set a custom error handler, which eventually + // will call MPI_Abort() so there is no need to check the return value of any MPI_Recv() + // or MPI_Send() calls. + MPI_Recv( + &slave_next_report_time_offset, + /*count=*/1, + /*datatype=*/MPI_DOUBLE, + /*source_rank=*/0, + /*tag=*/static_cast<int>(MessageTag::SlaveNextReportDate), + this->getSlaveComm(i), + MPI_STATUS_IGNORE + ); + this->slave_next_report_time_offsets_[i] = slave_next_report_time_offset; + OpmLog::info( + fmt::format( + "Received simulation slave next report date from slave process with name: {}. " + "Next report date: {}", this->slave_names_[i], slave_next_report_time_offset + ) + ); + } + } + this->comm_.broadcast( + this->slave_next_report_time_offsets_.data(), /*count=*/num_slaves, /*emitter_rank=*/0 + ); + OpmLog::info("Broadcasted slave next report dates to all ranks"); +} + + +std::size_t +ReservoirCouplingMaster:: +numSlavesStarted() const +{ + return this->slave_names_.size(); +} + +// ------------------ +// Private methods +// ------------------ + +double +ReservoirCouplingMaster:: +getMasterActivationDate_() const +{ + // Assume master mode is activated when the first SLAVES keyword is encountered in the schedule + // NOTE: getStartTime() returns a std::time_t value, which is typically a long integer representing + // the number of seconds since the epoch (1970-01-01 00:00:00 UTC) + // The maximum integer that can be represented by a double is 2^53 - 1, which is approximately + // 9e15. This corresponds to a date in the year 2.85e8 or 285 million years into the future. + // So we should be able to represent reasonable epoch values within a double. + double start_date = this->schedule_.getStartTime(); + for (std::size_t report_step = 0; report_step < this->schedule_.size(); ++report_step) { + auto rescoup = this->schedule_[report_step].rescoup(); + if (rescoup.slaveCount() > 0) { + return start_date + this->schedule_.seconds(report_step); + } + } + // NOTE: Consistency between SLAVES and GRUPMAST keywords has already been checked in + // init() in SimulatorFullyImplicitBlackoil.hpp + OPM_THROW(std::runtime_error, "Reservoir coupling: Failed to find master activation time: " + "No SLAVES keyword found in schedule"); +} + + +} // namespace Opm + diff --git a/opm/simulators/flow/ReservoirCouplingMaster.hpp b/opm/simulators/flow/ReservoirCouplingMaster.hpp new file mode 100644 index 00000000000..c8de9316ffc --- /dev/null +++ b/opm/simulators/flow/ReservoirCouplingMaster.hpp @@ -0,0 +1,91 @@ +/* + Copyright 2024 Equinor ASA + + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef OPM_RESERVOIR_COUPLING_MASTER_HPP +#define OPM_RESERVOIR_COUPLING_MASTER_HPP + +#include <opm/simulators/utils/ParallelCommunication.hpp> +#include <opm/simulators/flow/ReservoirCoupling.hpp> +#include <opm/input/eclipse/Schedule/Schedule.hpp> +#include <opm/common/OpmLog/OpmLog.hpp> + +#include <mpi.h> + +#include <filesystem> +#include <vector> + +namespace Opm { + +class ReservoirCouplingMaster { +public: + using MessageTag = ReservoirCoupling::MessageTag; + using Seconds = ReservoirCoupling::Seconds; + ReservoirCouplingMaster( + const Parallel::Communication &comm, + const Schedule &schedule, + int argc, char **argv + ); + + bool activated() { return this->numSlavesStarted() > 0; } + void addSlaveCommunicator(MPI_Comm comm) { + this->master_slave_comm_.push_back(comm); + } + void addSlaveName(const std::string &name) { this->slave_names_.push_back(name); } + void addSlaveStartDate(std::time_t date) { this->slave_start_dates_.push_back(date); } + double getActivationDate() const { return this->activation_date_; } + int getArgc() const { return this->argc_; } + char *getArgv(int index) const { return this->argv_[index]; } + char **getArgv() const { return this->argv_; } + const Parallel::Communication &getComm() const { return this->comm_; } + double getSimulationStartDate() const { return this->schedule_.getStartTime(); } + MPI_Comm getSlaveComm(int index) const { return this->master_slave_comm_[index]; } + const std::string &getSlaveName(int index) const { return this->slave_names_[index]; } + const double *getSlaveStartDates() { return this->slave_start_dates_.data(); } + double maybeChopSubStep(double suggested_timestep, double current_time) const; + void maybeSpawnSlaveProcesses(int report_step); + std::size_t numSlavesStarted() const; + void receiveNextReportDateFromSlaves(); + void resizeSlaveStartDates(int size) { this->slave_start_dates_.resize(size); } + void resizeNextReportDates(int size) { this->slave_next_report_time_offsets_.resize(size); } + void sendNextTimeStepToSlaves(double next_time_step); + +private: + double getMasterActivationDate_() const; + + const Parallel::Communication &comm_; + const Schedule& schedule_; + int argc_; + char **argv_; + // NOTE: MPI_Comm is just an integer handle, so we can just copy it into the vector + std::vector<MPI_Comm> master_slave_comm_; // MPI communicators for the slave processes + std::vector<std::string> slave_names_; + // The start dates are in whole seconds since the epoch. We use a double to store the value + // since both schedule_.getStartTime() and schedule_.stepLength(report_step) returns + // a double value representing whole seconds. + // However, note that schedule_[report_step].start_time() returns a time_point + // which can include milliseconds. The double values are also convenient when we need to + // to add fractions of seconds for sub steps to the start date. + std::vector<double> slave_start_dates_; + // Elapsed time from the beginning of the simulation + std::vector<double> slave_next_report_time_offsets_; + double activation_date_{0.0}; // The date when SLAVES is encountered in the schedule +}; + +} // namespace Opm +#endif // OPM_RESERVOIR_COUPLING_MASTER_HPP diff --git a/opm/simulators/flow/ReservoirCouplingSlave.cpp b/opm/simulators/flow/ReservoirCouplingSlave.cpp new file mode 100644 index 00000000000..eb16256af37 --- /dev/null +++ b/opm/simulators/flow/ReservoirCouplingSlave.cpp @@ -0,0 +1,276 @@ +/* + Copyright 2024 Equinor ASA + + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <config.h> +#include <opm/simulators/flow/ReservoirCoupling.hpp> +#include <opm/simulators/flow/ReservoirCouplingSlave.hpp> + +#include <opm/input/eclipse/Schedule/ResCoup/ReservoirCouplingInfo.hpp> +#include <opm/input/eclipse/Schedule/ResCoup/MasterGroup.hpp> +#include <opm/input/eclipse/Schedule/ResCoup/Slaves.hpp> +#include <opm/common/ErrorMacros.hpp> +#include <opm/simulators/utils/ParallelCommunication.hpp> + +#include <dune/common/parallel/mpitraits.hh> + +#include <vector> +#include <fmt/format.h> + +namespace Opm { + +ReservoirCouplingSlave:: +ReservoirCouplingSlave( + const Parallel::Communication &comm, + const Schedule &schedule, + const SimulatorTimer &timer +) : + comm_{comm}, + schedule_{schedule}, + timer_{timer} +{ + this->slave_master_comm_ = MPI_COMM_NULL; + MPI_Comm_get_parent(&this->slave_master_comm_); + if (this->slave_master_comm_ == MPI_COMM_NULL) { + OPM_THROW(std::runtime_error, "Slave process is not spawned by a master process"); + } + // NOTE: By installing a custom error handler for all slave-master communicators, which + // eventually will call MPI_Abort(), there is no need to check the return value of any + // MPI_Recv() or MPI_Send() calls as errors will be caught by the error handler. + ReservoirCoupling::setErrhandler(this->slave_master_comm_, /*is_master=*/false); +} + +double +ReservoirCouplingSlave:: +receiveNextTimeStepFromMaster() { + double timestep; + if (this->comm_.rank() == 0) { + // NOTE: All slave-master communicators have set a custom error handler, which eventually + // will call MPI_Abort() so there is no need to check the return value of any MPI_Recv() + // or MPI_Send() calls. + MPI_Recv( + ×tep, + /*count=*/1, + /*datatype=*/MPI_DOUBLE, + /*source_rank=*/0, + /*tag=*/static_cast<int>(MessageTag::SlaveNextTimeStep), + this->slave_master_comm_, + MPI_STATUS_IGNORE + ); + OpmLog::info( + fmt::format("Slave rank 0 received next timestep {} from master.", timestep) + ); + } + this->comm_.broadcast(×tep, /*count=*/1, /*emitter_rank=*/0); + OpmLog::info("Broadcasted slave next time step to all ranks"); + return timestep; +} + +void +ReservoirCouplingSlave:: +receiveMasterGroupNamesFromMasterProcess() { + std::size_t size; + std::vector<char> group_names; + if (this->comm_.rank() == 0) { + auto MPI_SIZE_T_TYPE = Dune::MPITraits<std::size_t>::getType(); + // NOTE: All slave-master communicators have set a custom error handler, which eventually + // will call MPI_Abort() so there is no need to check the return value of any MPI_Recv() + // or MPI_Send() calls. + MPI_Recv( + &size, + /*count=*/1, + /*datatype=*/MPI_SIZE_T_TYPE, + /*source_rank=*/0, + /*tag=*/static_cast<int>(MessageTag::MasterGroupNamesSize), + this->slave_master_comm_, + MPI_STATUS_IGNORE + ); + OpmLog::info("Received master group names size from master process rank 0"); + group_names.resize(size); + MPI_Recv( + group_names.data(), + /*count=*/size, + /*datatype=*/MPI_CHAR, + /*source_rank=*/0, + /*tag=*/static_cast<int>(MessageTag::MasterGroupNames), + this->slave_master_comm_, + MPI_STATUS_IGNORE + ); + OpmLog::info("Received master group names from master process rank 0"); + } + this->comm_.broadcast(&size, /*count=*/1, /*emitter_rank=*/0); + if (this->comm_.rank() != 0) { + group_names.resize(size); + } + this->comm_.broadcast(group_names.data(), /*count=*/size, /*emitter_rank=*/0); + this->saveMasterGroupNamesAsMap_(group_names); + this->checkGrupSlavGroupNames_(); +} + +void +ReservoirCouplingSlave:: +sendNextReportDateToMasterProcess() const +{ + if (this->comm_.rank() == 0) { + double elapsed_time = this->timer_.simulationTimeElapsed(); + double current_step_length = this->timer_.currentStepLength(); + // NOTE: This is an offset in seconds from the start date, so it will be 0 if the next report + // would be the start date. In general, it should be a positive number. + double next_report_time_offset = elapsed_time + current_step_length; + // NOTE: All slave-master communicators have set a custom error handler, which eventually + // will call MPI_Abort() so there is no need to check the return value of any MPI_Recv() + // or MPI_Send() calls. + MPI_Send( + &next_report_time_offset, + /*count=*/1, + /*datatype=*/MPI_DOUBLE, + /*dest_rank=*/0, + /*tag=*/static_cast<int>(MessageTag::SlaveNextReportDate), + this->slave_master_comm_ + ); + OpmLog::info("Sent next report date to master process from rank 0"); + } +} + +void +ReservoirCouplingSlave:: +sendActivationDateToMasterProcess() const +{ + if (this->comm_.rank() == 0) { + // NOTE: The master process needs the s + double activation_date = this->getGrupSlavActivationDate_(); + // NOTE: All slave-master communicators have set a custom error handler, which eventually + // will call MPI_Abort() so there is no need to check the return value of any MPI_Recv() + // or MPI_Send() calls. + MPI_Send( + &activation_date, + /*count=*/1, + /*datatype=*/MPI_DOUBLE, + /*dest_rank=*/0, + /*tag=*/static_cast<int>(MessageTag::SlaveActivationDate), + this->slave_master_comm_ + ); + OpmLog::info("Sent simulation activation date to master process from rank 0"); + } +} + +void +ReservoirCouplingSlave:: +sendSimulationStartDateToMasterProcess() const +{ + if (this->comm_.rank() == 0) { + // NOTE: The master process needs the s + double start_date = this->schedule_.getStartTime(); + // NOTE: All slave-master communicators have set a custom error handler, which eventually + // will call MPI_Abort() so there is no need to check the return value of any MPI_Recv() + // or MPI_Send() calls. + MPI_Send( + &start_date, + /*count=*/1, + /*datatype=*/MPI_DOUBLE, + /*dest_rank=*/0, + /*tag=*/static_cast<int>(MessageTag::SlaveSimulationStartDate), + this->slave_master_comm_ + ); + OpmLog::info("Sent simulation start date to master process from rank 0"); + } +} + +// ------------------ +// Private methods +// ------------------ + +void +ReservoirCouplingSlave:: +checkGrupSlavGroupNames_() +{ + // Validate that each slave group name has a corresponding master group name + bool grup_slav_found = false; + for (std::size_t report_step = 0; report_step < this->schedule_.size(); ++report_step) { + auto rescoup = this->schedule_[report_step].rescoup(); + if (rescoup.grupSlavCount() > 0) { + grup_slav_found = true; + auto grup_slavs = rescoup.grupSlavs(); + for (const auto& [slave_group_name, grup_slav] : grup_slavs) { + auto master_group_name_it = this->master_group_names_.find(slave_group_name); + if (master_group_name_it == this->master_group_names_.end()) { + OPM_THROW(std::runtime_error, + "Reservoir coupling: Failed to find master group name for slave group: " + + slave_group_name); + } + else { + auto master_group_name = master_group_name_it->second; + if (grup_slav.masterGroupName() != master_group_name) { + OPM_THROW(std::runtime_error, + "Reservoir coupling: Inconsistent master group name for slave group: " + + slave_group_name); + } + } + } + } + } + if (!grup_slav_found) { + OPM_THROW(std::runtime_error, "Reservoir coupling: Failed to find slave group names: " + "No GRUPSLAV keyword found in schedule"); + } +} + +double +ReservoirCouplingSlave:: +getGrupSlavActivationDate_() const +{ + double start_date = this->schedule_.getStartTime(); + for (std::size_t report_step = 0; report_step < this->schedule_.size(); ++report_step) { + auto rescoup = this->schedule_[report_step].rescoup(); + if (rescoup.grupSlavCount() > 0) { + return start_date + this->schedule_.seconds(report_step); + } + } + OPM_THROW(std::runtime_error, "Reservoir coupling: Failed to find slave activation time: " + "No GRUPSLAV keyword found in schedule"); +} + +void +ReservoirCouplingSlave:: +maybeActivate(int report_step) { + if (!this->activated()) { + auto rescoup = this->schedule_[report_step].rescoup(); + if (rescoup.grupSlavCount() > 0) { + this->activated_ = true; + } + } +} + +void +ReservoirCouplingSlave:: +saveMasterGroupNamesAsMap_(const std::vector<char>& group_names) { + // Deserialize the group names vector into a map of slavegroup names -> mastergroup names + auto total_size = group_names.size(); + std::size_t offset = 0; + while (offset < total_size) { + std::string master_group{group_names.data() + offset}; + offset += master_group.size() + 1; + assert(offset < total_size); + std::string slave_group{group_names.data() + offset}; + offset += slave_group.size() + 1; + this->master_group_names_[slave_group] = master_group; + } +} + + +} // namespace Opm diff --git a/opm/simulators/flow/ReservoirCouplingSlave.hpp b/opm/simulators/flow/ReservoirCouplingSlave.hpp new file mode 100644 index 00000000000..61a8d3c0cd8 --- /dev/null +++ b/opm/simulators/flow/ReservoirCouplingSlave.hpp @@ -0,0 +1,65 @@ +/* + Copyright 2024 Equinor ASA + + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef OPM_RESERVOIR_COUPLING_SLAVE_HPP +#define OPM_RESERVOIR_COUPLING_SLAVE_HPP + +#include <opm/simulators/flow/ReservoirCoupling.hpp> +#include <opm/input/eclipse/Schedule/Schedule.hpp> +#include <opm/simulators/utils/ParallelCommunication.hpp> +#include <opm/simulators/timestepping/SimulatorTimer.hpp> +#include <opm/common/OpmLog/OpmLog.hpp> + +#include <mpi.h> + +#include <vector> + +namespace Opm { + +class ReservoirCouplingSlave { +public: + using MessageTag = ReservoirCoupling::MessageTag; + + ReservoirCouplingSlave( + const Parallel::Communication &comm, const Schedule &schedule, const SimulatorTimer &timer + ); + bool activated() const { return activated_; } + void maybeActivate(int report_step); + void sendActivationDateToMasterProcess() const; + void sendNextReportDateToMasterProcess() const; + void sendSimulationStartDateToMasterProcess() const; + void receiveMasterGroupNamesFromMasterProcess(); + double receiveNextTimeStepFromMaster(); + +private: + void checkGrupSlavGroupNames_(); + double getGrupSlavActivationDate_() const; + void saveMasterGroupNamesAsMap_(const std::vector<char>& group_names); + + const Parallel::Communication &comm_; + const Schedule& schedule_; + const SimulatorTimer &timer_; + // MPI parent communicator for a slave process + MPI_Comm slave_master_comm_{MPI_COMM_NULL}; + std::map<std::string, std::string> master_group_names_; + bool activated_{false}; +}; + +} // namespace Opm +#endif // OPM_RESERVOIR_COUPLING_SLAVE_HPP diff --git a/opm/simulators/flow/ReservoirCouplingSpawnSlaves.cpp b/opm/simulators/flow/ReservoirCouplingSpawnSlaves.cpp new file mode 100644 index 00000000000..741bbb224aa --- /dev/null +++ b/opm/simulators/flow/ReservoirCouplingSpawnSlaves.cpp @@ -0,0 +1,335 @@ +/* + Copyright 2024 Equinor ASA + + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <config.h> + +#include <opm/simulators/flow/ReservoirCouplingSpawnSlaves.hpp> + +#include <opm/input/eclipse/Schedule/ResCoup/ReservoirCouplingInfo.hpp> +#include <opm/input/eclipse/Schedule/ResCoup/MasterGroup.hpp> +#include <opm/input/eclipse/Schedule/ResCoup/Slaves.hpp> +#include <opm/common/ErrorMacros.hpp> + +#include <opm/simulators/utils/ParallelCommunication.hpp> + +#include <opm/input/eclipse/Schedule/Schedule.hpp> + +#include <dune/common/parallel/mpitraits.hh> + +#include <filesystem> +#include <vector> + +#include <fmt/format.h> + +namespace Opm { + +// ------------------ +// Public methods +// ------------------ + +ReservoirCouplingSpawnSlaves:: +ReservoirCouplingSpawnSlaves( + ReservoirCouplingMaster &master, + const ReservoirCoupling::CouplingInfo &rescoup, + const int report_step +) : + master_{master}, + rescoup_{rescoup}, + report_step_{report_step}, + comm_{master.getComm()} +{ +} + +void +ReservoirCouplingSpawnSlaves:: +spawn() +{ + // spawn the slave processes and get the simulation start date from the slaves, + // and finally send the master group names to the slaves + this->spawnSlaveProcesses_(); + this->receiveActivationDateFromSlaves_(); + this->receiveSimulationStartDateFromSlaves_(); + this->sendMasterGroupNamesToSlaves_(); + this->prepareTimeStepping_(); + OpmLog::info("Reservoir coupling slave processes was spawned successfully"); +} + + +// ------------------ +// Private methods +// ------------------ + +std::vector<char *> ReservoirCouplingSpawnSlaves:: +getSlaveArgv_( + const std::filesystem::path &data_file, + const std::string &slave_name, + std::string &log_filename) const +{ + // Calculate the size of the slave_argv vector like this: + // - We will not use the first argument in argv, as it is the program name + // - Replace the data file name in argv with the data_file path + // - Insert as first argument --slave-log-file=<slave_name>.log + // - Also add the argument "--slave=true" to the argv + // - Add a nullptr at the end of the argv + // So the size of the slave_argv vector will be argc + 2 + // + // Assume: All parameters will be on the form --parameter=value (as a string without spaces) + // + // Important: The returned vector will have pointers to argv pointers, + // data_file string buffer, and slave_name string buffer. So the caller + // must ensure that these buffers are not deallocated before the slave_argv has + // been used. + auto argc = this->master_.getArgc(); + auto argv = this->master_.getArgv(); + std::vector<char *> slave_argv(argc + 2); + log_filename = "--slave-log-file=" + slave_name; // .log extension will be added by the slave process + slave_argv[0] = const_cast<char*>(log_filename.c_str()); + for (int i = 1; i < argc; i++) { + // Check if the argument starts with "--", if not, we will assume it is a positional argument + // and we will replace it with the data file path + if (std::string(argv[i]).substr(0, 2) == "--") { + slave_argv[i] = argv[i]; + } else { + slave_argv[i] = const_cast<char*>(data_file.c_str()); + } + } + slave_argv[argc] = const_cast<char *>("--slave=true"); + slave_argv[argc+1] = nullptr; + return slave_argv; +} + +std::pair<std::vector<char>, std::size_t> +ReservoirCouplingSpawnSlaves:: +getMasterGroupNamesForSlave_(const std::string &slave_name) const +{ + // For the given slave name, get all pairs of master group names and slave group names + // Serialize the data such that it can be sent over MPI in one chunk + auto master_groups = this->rescoup_.masterGroups(); + std::vector<std::string> data; + std::vector<std::string> master_group_names; + for (const auto& [group_name, master_group] : master_groups) { + if (master_group.slaveName() == slave_name) { + data.push_back(group_name); + data.push_back(master_group.slaveGroupName()); + } + } + assert(data.size() % 2 == 0); + assert(data.size() > 0); + return this->serializeStrings_(std::move(data)); +} + +void +ReservoirCouplingSpawnSlaves:: +prepareTimeStepping_() +{ + // Prepare the time stepping for the master process + // This is done after the slave processes have been spawned + // and the master group names have been sent to the slaves + auto num_slaves = this->master_.numSlavesStarted(); + this->master_.resizeNextReportDates(num_slaves); +} + +void +ReservoirCouplingSpawnSlaves:: +receiveActivationDateFromSlaves_() +{ + // Currently, we only use the activation date to check that no slave process + // starts before the master process. + auto num_slaves = this->master_.numSlavesStarted(); + if (this->comm_.rank() == 0) { + for (unsigned int i = 0; i < num_slaves; i++) { + double slave_activation_date; + // NOTE: All slave-master communicators have set a custom error handler, which eventually + // will call MPI_Abort() so there is no need to check the return value of any MPI_Recv() + // or MPI_Send() calls. + MPI_Recv( + &slave_activation_date, + /*count=*/1, + /*datatype=*/MPI_DOUBLE, + /*source_rank=*/0, + /*tag=*/static_cast<int>(MessageTag::SlaveActivationDate), + this->master_.getSlaveComm(i), + MPI_STATUS_IGNORE + ); + if (slave_activation_date < this->master_.getActivationDate()) { + OPM_THROW(std::runtime_error, "Slave process start date is earlier than " + "the master process' activation date"); + } + OpmLog::info( + fmt::format( + "Received activation date from slave process with name: {}. " + "Activation date: {}", this->master_.getSlaveName(i), slave_activation_date + ) + ); + } + } +} + +void +ReservoirCouplingSpawnSlaves:: +receiveSimulationStartDateFromSlaves_() +{ + auto num_slaves = this->master_.numSlavesStarted(); + if (this->comm_.rank() == 0) { + for (unsigned int i = 0; i < num_slaves; i++) { + double slave_start_date; + // NOTE: All slave-master communicators have set a custom error handler, which eventually + // will call MPI_Abort() so there is no need to check the return value of any MPI_Recv() + // or MPI_Send() calls. + MPI_Recv( + &slave_start_date, + /*count=*/1, + /*datatype=*/MPI_DOUBLE, + /*source_rank=*/0, + /*tag=*/static_cast<int>(MessageTag::SlaveSimulationStartDate), + this->master_.getSlaveComm(i), + MPI_STATUS_IGNORE + ); + this->master_.addSlaveStartDate(slave_start_date); + OpmLog::info( + fmt::format( + "Received start date from slave process with name: {}. " + "Start date: {}", this->master_.getSlaveName(i), slave_start_date + ) + ); + } + } + if (this->comm_.rank() != 0) { + // Ensure that all ranks have the same number of slave activation dates + this->master_.resizeSlaveStartDates(num_slaves); + } + const double* data = this->master_.getSlaveStartDates(); + this->comm_.broadcast(const_cast<double *>(data), /*count=*/num_slaves, /*emitter_rank=*/0); + OpmLog::info("Broadcasted slave start dates to all ranks"); +} + +void +ReservoirCouplingSpawnSlaves:: +sendMasterGroupNamesToSlaves_() +{ + if (this->comm_.rank() == 0) { + auto num_slaves = this->master_.numSlavesStarted(); + for (unsigned int i = 0; i < num_slaves; i++) { + auto slave_name = this->master_.getSlaveName(i); + auto [group_names, size] = this->getMasterGroupNamesForSlave_(slave_name); + // NOTE: All slave-master communicators have set a custom error handler, which eventually + // will call MPI_Abort() so there is no need to check the return value of any MPI_Recv() + // or MPI_Send() calls. + static_assert(std::is_same_v<decltype(size), std::size_t>, "size must be of type std::size_t"); + auto MPI_SIZE_T_TYPE = Dune::MPITraits<std::size_t>::getType(); + MPI_Send( + &size, + /*count=*/1, + /*datatype=*/MPI_SIZE_T_TYPE, + /*dest_rank=*/0, + /*tag=*/static_cast<int>(MessageTag::MasterGroupNamesSize), + this->master_.getSlaveComm(i) + ); + MPI_Send( + group_names.data(), + /*count=*/size, + /*datatype=*/MPI_CHAR, + /*dest_rank=*/0, + /*tag=*/static_cast<int>(MessageTag::MasterGroupNames), + this->master_.getSlaveComm(i) + ); + OpmLog::info(fmt::format( + "Sent master group names to slave process rank 0 with name: {}", slave_name) + ); + } + } +} + +std::pair<std::vector<char>, std::size_t> +ReservoirCouplingSpawnSlaves:: +serializeStrings_(std::vector<std::string> data) const +{ + std::size_t total_size = 0; + std::vector<char> serialized_data; + for (const auto& str: data) { + for (const auto& c: str) { + serialized_data.push_back(c); + } + serialized_data.push_back('\0'); + total_size += str.size() + 1; + } + return {serialized_data, total_size}; +} + +// NOTE: This functions is executed for all ranks, but only rank 0 will spawn +// the slave processes +void +ReservoirCouplingSpawnSlaves:: +spawnSlaveProcesses_() +{ + char *flow_program_name = this->master_.getArgv(0); + for (const auto& [slave_name, slave] : this->rescoup_.slaves()) { + MPI_Comm master_slave_comm = MPI_COMM_NULL; + const auto& data_file_name = slave.dataFilename(); + const auto& directory_path = slave.directoryPath(); + // Concatenate the directory path and the data file name to get the full path + std::filesystem::path dir_path{directory_path}; + std::filesystem::path data_file{data_file_name}; + std::filesystem::path full_path = dir_path / data_file; + std::string log_filename; // the getSlaveArgv() function will set this + std::vector<char *> slave_argv = this->getSlaveArgv_( + full_path, slave_name, log_filename + ); + auto num_procs = slave.numprocs(); + std::vector<int> errcodes(num_procs); + // TODO: We need to decide how to handle the output from the slave processes.. + // As far as I can tell, open MPI does not support redirecting the output + // to a file, so we might need to implement a custom solution for this + int spawn_result = MPI_Comm_spawn( + flow_program_name, + slave_argv.data(), + /*maxprocs=*/num_procs, + /*info=*/MPI_INFO_NULL, + /*root=*/0, // Rank 0 spawns the slave processes + /*comm=*/this->comm_, + /*intercomm=*/&master_slave_comm, + /*array_of_errcodes=*/errcodes.data() + ); + if (spawn_result != MPI_SUCCESS || (master_slave_comm == MPI_COMM_NULL)) { + for (unsigned int i = 0; i < num_procs; i++) { + if (errcodes[i] != MPI_SUCCESS) { + char error_string[MPI_MAX_ERROR_STRING]; + int length_of_error_string; + MPI_Error_string(errcodes[i], error_string, &length_of_error_string); + OpmLog::info(fmt::format("Error spawning process {}: {}", i, error_string)); + } + } + OPM_THROW(std::runtime_error, "Failed to spawn slave process"); + } + // NOTE: By installing a custom error handler for all slave-master communicators, which + // eventually will call MPI_Abort(), there is no need to check the return value of any + // MPI_Recv() or MPI_Send() calls as errors will be caught by the error handler. + ReservoirCoupling::setErrhandler(master_slave_comm, /*is_master=*/true); + OpmLog::info( + fmt::format( + "Spawned reservoir coupling slave simulation for slave with name: " + "{}. Standard output logfile name: {}.log", slave_name, slave_name + ) + ); + this->master_.addSlaveCommunicator(master_slave_comm); + this->master_.addSlaveName(slave_name); + } +} + +} // namespace Opm diff --git a/opm/simulators/flow/ReservoirCouplingSpawnSlaves.hpp b/opm/simulators/flow/ReservoirCouplingSpawnSlaves.hpp new file mode 100644 index 00000000000..054f9939b23 --- /dev/null +++ b/opm/simulators/flow/ReservoirCouplingSpawnSlaves.hpp @@ -0,0 +1,72 @@ +/* + Copyright 2024 Equinor ASA + + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef OPM_RESERVOIR_COUPLING_SPAWN_SLAVES_HPP +#define OPM_RESERVOIR_COUPLING_SPAWN_SLAVES_HPP + +#include <opm/simulators/utils/ParallelCommunication.hpp> +#include <opm/simulators/flow/ReservoirCoupling.hpp> +#include <opm/simulators/flow/ReservoirCouplingMaster.hpp> + +#include <opm/input/eclipse/Schedule/ResCoup/ReservoirCouplingInfo.hpp> +#include <opm/input/eclipse/Schedule/Schedule.hpp> +#include <opm/common/OpmLog/OpmLog.hpp> + +#include <mpi.h> + +#include <filesystem> +#include <vector> + +namespace Opm { + +class ReservoirCouplingSpawnSlaves { +public: + using MessageTag = ReservoirCoupling::MessageTag; + + ReservoirCouplingSpawnSlaves( + ReservoirCouplingMaster &master, + const ReservoirCoupling::CouplingInfo &rescoup, + const int report_step + ); + + void spawn(); + +private: + std::pair<std::vector<char>, std::size_t> + getMasterGroupNamesForSlave_(const std::string &slave_name) const; + std::vector<char *> getSlaveArgv_( + const std::filesystem::path &data_file, + const std::string &slave_name, + std::string &log_filename) const; + void prepareTimeStepping_(); + void receiveActivationDateFromSlaves_(); + void receiveSimulationStartDateFromSlaves_(); + void sendMasterGroupNamesToSlaves_(); + std::pair<std::vector<char>, std::size_t> + serializeStrings_(std::vector<std::string> data) const; + void spawnSlaveProcesses_(); + + ReservoirCouplingMaster &master_; + const ReservoirCoupling::CouplingInfo &rescoup_; + const int report_step_; + const Parallel::Communication &comm_; +}; + +} // namespace Opm +#endif // OPM_RESERVOIR_COUPLING_SPAWN_SLAVES_HPP diff --git a/opm/simulators/flow/SimulatorFullyImplicitBlackoil.cpp b/opm/simulators/flow/SimulatorFullyImplicitBlackoil.cpp index aac95efe23c..e4f2a8a796c 100644 --- a/opm/simulators/flow/SimulatorFullyImplicitBlackoil.cpp +++ b/opm/simulators/flow/SimulatorFullyImplicitBlackoil.cpp @@ -58,6 +58,10 @@ void registerSimulatorParameters() ("FileName for .OPMRST file used to load serialized state. " "If empty, CASENAME.OPMRST is used."); Parameters::Hide<Parameters::LoadFile>(); + Parameters::Register<Parameters::Slave> + ("Specify if the simulation is a slave simulation in a master-slave simulation"); + Parameters::Hide<Parameters::Slave>(); + } } // namespace Opm::detail diff --git a/opm/simulators/flow/SimulatorFullyImplicitBlackoil.hpp b/opm/simulators/flow/SimulatorFullyImplicitBlackoil.hpp index 42e18dd98c3..4198cc3777c 100644 --- a/opm/simulators/flow/SimulatorFullyImplicitBlackoil.hpp +++ b/opm/simulators/flow/SimulatorFullyImplicitBlackoil.hpp @@ -24,6 +24,18 @@ #include <opm/common/ErrorMacros.hpp> +#if HAVE_MPI +#define RESERVOIR_COUPLING_ENABLED +#endif +#ifdef RESERVOIR_COUPLING_ENABLED +#include <opm/input/eclipse/Schedule/ResCoup/ReservoirCouplingInfo.hpp> +#include <opm/input/eclipse/Schedule/ResCoup/MasterGroup.hpp> +#include <opm/input/eclipse/Schedule/ResCoup/Slaves.hpp> +#include <opm/simulators/flow/ReservoirCouplingMaster.hpp> +#include <opm/simulators/flow/ReservoirCouplingSlave.hpp> +#include <opm/common/Exceptions.hpp> +#endif + #include <opm/input/eclipse/Units/UnitSystem.hpp> #include <opm/grid/utility/StopWatch.hpp> @@ -63,6 +75,7 @@ struct SaveStep { static constexpr auto* value = ""; }; struct SaveFile { static constexpr auto* value = ""; }; struct LoadFile { static constexpr auto* value = ""; }; struct LoadStep { static constexpr int value = -1; }; +struct Slave { static constexpr bool value = false; }; } // namespace Opm::Parameters @@ -78,6 +91,8 @@ namespace Opm { template<class TypeTag> class SimulatorFullyImplicitBlackoil : private SerializableSim { +protected: + struct MPI_Comm_Deleter; public: using Simulator = GetPropType<TypeTag, Properties::Simulator>; using Grid = GetPropType<TypeTag, Properties::Grid>; @@ -171,9 +186,15 @@ class SimulatorFullyImplicitBlackoil : private SerializableSim /// \param[in,out] timer governs the requested reporting timesteps /// \param[in,out] state state of reservoir: pressure, fluxes /// \return simulation report, with timing data +#ifdef RESERVOIR_COUPLING_ENABLED + SimulatorReport run(SimulatorTimer& timer, int argc, char** argv) + { + init(timer, argc, argv); +#else SimulatorReport run(SimulatorTimer& timer) { init(timer); +#endif // Make cache up to date. No need for updating it in elementCtx. // NB! Need to be at the correct step in case of restart simulator_.setEpisodeIndex(timer.currentStepNum()); @@ -188,8 +209,69 @@ class SimulatorFullyImplicitBlackoil : private SerializableSim return finalize(); } +#ifdef RESERVOIR_COUPLING_ENABLED + // This method should only be called if slave mode (i.e. Parameters::Get<Parameters::Slave>()) + // is false. We try to determine if this is a normal flow simulation or a reservoir + // coupling master. It is a normal flow simulation if the schedule does not contain + // any SLAVES and GRUPMAST keywords. + bool checkRunningAsReservoirCouplingMaster() + { + for (std::size_t report_step = 0; report_step < this->schedule().size(); ++report_step) { + auto rescoup = this->schedule()[report_step].rescoup(); + auto slave_count = rescoup.slaveCount(); + auto master_group_count = rescoup.masterGroupCount(); + // - GRUPMAST and SLAVES keywords need to be specified at the same report step + // - They can only occur once in the schedule + if (slave_count > 0 && master_group_count > 0) { + return true; + } + else if (slave_count > 0 && master_group_count == 0) { + throw ReservoirCouplingError( + "Inconsistent reservoir coupling master schedule: " + "Slave count is greater than 0 but master group count is 0" + ); + } + else if (slave_count == 0 && master_group_count > 0) { + throw ReservoirCouplingError( + "Inconsistent reservoir coupling master schedule: " + "Master group count is greater than 0 but slave count is 0" + ); + } + } + return false; + } +#endif + +#ifdef RESERVOIR_COUPLING_ENABLED + // NOTE: The argc and argv will be used when launching a slave process + void init(SimulatorTimer &timer, int argc, char** argv) + { + auto slave_mode = Parameters::Get<Parameters::Slave>(); + if (slave_mode) { + this->reservoirCouplingSlave_ = + std::make_unique<ReservoirCouplingSlave>( + FlowGenericVanguard::comm(), + this->schedule(), timer + ); + this->reservoirCouplingSlave_->sendActivationDateToMasterProcess(); + this->reservoirCouplingSlave_->sendSimulationStartDateToMasterProcess(); + this->reservoirCouplingSlave_->receiveMasterGroupNamesFromMasterProcess(); + } + else { + auto master_mode = checkRunningAsReservoirCouplingMaster(); + if (master_mode) { + this->reservoirCouplingMaster_ = + std::make_unique<ReservoirCouplingMaster>( + FlowGenericVanguard::comm(), + this->schedule(), + argc, argv + ); + } + } +#else void init(SimulatorTimer &timer) { +#endif simulator_.setEpisodeIndex(-1); // Create timers and file for writing timing info. @@ -212,7 +294,14 @@ class SimulatorFullyImplicitBlackoil : private SerializableSim else { adaptiveTimeStepping_ = std::make_unique<TimeStepper>(unitSystem, max_next_tstep, terminalOutput_); } - +#ifdef RESERVOIR_COUPLING_ENABLED + if (this->reservoirCouplingSlave_) { + adaptiveTimeStepping_->setReservoirCouplingSlave(this->reservoirCouplingSlave_.get()); + } + else if (this->reservoirCouplingMaster_) { + adaptiveTimeStepping_->setReservoirCouplingMaster(this->reservoirCouplingMaster_.get()); + } +#endif if (isRestart()) { // For restarts the simulator may have gotten some information // about the next timestep size from the OPMEXTRA field @@ -358,6 +447,14 @@ class SimulatorFullyImplicitBlackoil : private SerializableSim tuningUpdater(timer.simulationTimeElapsed(), this->adaptiveTimeStepping_->suggestedNextStep(), 0); +#ifdef RESERVOIR_COUPLING_ENABLED + if (this->reservoirCouplingMaster_) { + this->reservoirCouplingMaster_->maybeSpawnSlaveProcesses(timer.currentStepNum()); + } + else if (this->reservoirCouplingSlave_) { + this->reservoirCouplingSlave_->maybeActivate(timer.currentStepNum()); + } +#endif const auto& events = schedule()[timer.currentStepNum()].events(); bool event = events.hasEvent(ScheduleEvents::NEW_WELL) || events.hasEvent(ScheduleEvents::INJECTION_TYPE_CHANGED) || @@ -555,6 +652,12 @@ class SimulatorFullyImplicitBlackoil : private SerializableSim SimulatorConvergenceOutput convergence_output_; +#ifdef RESERVOIR_COUPLING_ENABLED + bool slaveMode_{false}; + std::unique_ptr<ReservoirCouplingMaster> reservoirCouplingMaster_{nullptr}; + std::unique_ptr<ReservoirCouplingSlave> reservoirCouplingSlave_{nullptr}; +#endif + SimulatorSerializer serializer_; }; diff --git a/opm/simulators/timestepping/AdaptiveSimulatorTimer.cpp b/opm/simulators/timestepping/AdaptiveSimulatorTimer.cpp index 2c36efcf56d..4fe45e5ab35 100644 --- a/opm/simulators/timestepping/AdaptiveSimulatorTimer.cpp +++ b/opm/simulators/timestepping/AdaptiveSimulatorTimer.cpp @@ -36,25 +36,28 @@ namespace Opm { AdaptiveSimulatorTimer:: - AdaptiveSimulatorTimer( const SimulatorTimerInterface& timer, - const double lastStepTaken, - const double maxTimeStep ) - : start_date_time_( std::make_shared<boost::posix_time::ptime>(timer.startDateTime()) ) - , start_time_( timer.simulationTimeElapsed() ) - , total_time_( start_time_ + timer.currentStepLength() ) - , report_step_( timer.reportStepNum() ) - , max_time_step_( maxTimeStep ) - , current_time_( start_time_ ) - , dt_( 0.0 ) - , current_step_( 0 ) - , steps_() - , lastStepFailed_( false ) + AdaptiveSimulatorTimer( const boost::posix_time::ptime simulation_start_time, + const double step_length, + const double elapsed_time, + const double last_step_taken, + const int report_step, + const double max_time_step ) + : start_date_time_{ std::make_shared<boost::posix_time::ptime>(simulation_start_time) } + , start_time_{ elapsed_time } + , total_time_{ start_time_ + step_length } + , report_step_{ report_step } + , max_time_step_{ max_time_step } + , current_time_{ start_time_ } + , dt_{ 0.0 } + , current_step_{ 0 } + , steps_{} + , last_step_failed_{ false } { // reserve memory for sub steps steps_.reserve( 10 ); // set appropriate value for dt_ - provideTimeStepEstimate( lastStepTaken ); + provideTimeStepEstimate( last_step_taken ); } bool AdaptiveSimulatorTimer::initialStep () const diff --git a/opm/simulators/timestepping/AdaptiveSimulatorTimer.hpp b/opm/simulators/timestepping/AdaptiveSimulatorTimer.hpp index 3dffd49084d..31ae49f4ce8 100644 --- a/opm/simulators/timestepping/AdaptiveSimulatorTimer.hpp +++ b/opm/simulators/timestepping/AdaptiveSimulatorTimer.hpp @@ -44,9 +44,12 @@ namespace Opm /// \param timer in case of sub stepping this is the outer timer /// \param lastStepTaken last suggested time step /// \param maxTimeStep maximum time step allowed - AdaptiveSimulatorTimer( const SimulatorTimerInterface& timer, - const double lastStepTaken, - const double maxTimeStep = std::numeric_limits<double>::max() ); + AdaptiveSimulatorTimer( const boost::posix_time::ptime simulation_start_time, + const double step_length, + const double elapsed_time, + const double last_step_taken, + const int report_step, + const double max_time_step = std::numeric_limits<double>::max() ); /// \brief advance time by currentStepLength AdaptiveSimulatorTimer& operator++ (); @@ -101,10 +104,10 @@ namespace Opm boost::posix_time::ptime startDateTime() const; /// \brief Return true if last time step failed - bool lastStepFailed() const {return lastStepFailed_;} + bool lastStepFailed() const {return last_step_failed_;} /// \brief tell the timestepper whether timestep failed or not - void setLastStepFailed(bool lastStepFailed) {lastStepFailed_ = lastStepFailed;} + void setLastStepFailed(bool last_step_failed) {last_step_failed_ = last_step_failed;} /// return copy of object virtual std::unique_ptr< SimulatorTimerInterface > clone() const; @@ -121,7 +124,7 @@ namespace Opm int current_step_; std::vector< double > steps_; - bool lastStepFailed_; + bool last_step_failed_; }; diff --git a/opm/simulators/timestepping/AdaptiveTimeStepping.hpp b/opm/simulators/timestepping/AdaptiveTimeStepping.hpp index 822d5ee8a57..a90aefd8e70 100644 --- a/opm/simulators/timestepping/AdaptiveTimeStepping.hpp +++ b/opm/simulators/timestepping/AdaptiveTimeStepping.hpp @@ -19,12 +19,23 @@ #include <opm/simulators/timestepping/TimeStepControl.hpp> #include <opm/simulators/timestepping/TimeStepControlInterface.hpp> -#include <cmath> +#if HAVE_MPI +#define RESERVOIR_COUPLING_ENABLED +#endif +#ifdef RESERVOIR_COUPLING_ENABLED +#include <opm/simulators/flow/ReservoirCoupling.hpp> +#include <opm/simulators/flow/ReservoirCouplingMaster.hpp> +#include <opm/simulators/flow/ReservoirCouplingSlave.hpp> +#endif + #include <functional> #include <memory> #include <set> +#include <sstream> +#include <stdexcept> #include <string> #include <tuple> +#include <utility> #include <vector> namespace Opm::Parameters { @@ -55,148 +66,210 @@ class UnitSystem; struct StepReport; namespace detail { - -void logTimer(const AdaptiveSimulatorTimer& substepTimer); - -std::set<std::string> consistentlyFailingWells(const std::vector<StepReport>& sr); - -void registerAdaptiveParameters(); - -std::tuple<TimeStepControlType, - std::unique_ptr<TimeStepControlInterface>, - bool> -createController(const UnitSystem& unitSystem); - + void logTimer(const AdaptiveSimulatorTimer& substep_timer); + std::set<std::string> consistentlyFailingWells(const std::vector<StepReport>& sr); + void registerAdaptiveParameters(); + std::tuple<TimeStepControlType, std::unique_ptr<TimeStepControlInterface>, bool> + createController(const UnitSystem& unitSystem); } - // AdaptiveTimeStepping - //--------------------- - template<class TypeTag> - class AdaptiveTimeStepping +template<class TypeTag> +class AdaptiveTimeStepping +{ +private: + using Scalar = GetPropType<TypeTag, Properties::Scalar>; + template <class Solver> + class SolutionTimeErrorSolverWrapper : public RelativeChangeInterface { - using Scalar = GetPropType<TypeTag, Properties::Scalar>; - template <class Solver> - class SolutionTimeErrorSolverWrapper : public RelativeChangeInterface - { - const Solver& solver_; - public: - SolutionTimeErrorSolverWrapper(const Solver& solver) - : solver_(solver) - {} - - /// return || u^n+1 - u^n || / || u^n+1 || - double relativeChange() const - { return solver_.model().relativeChange(); } - }; - - template<class E> - void logException_(const E& exception, bool verbose) - { - if (verbose) { - std::string message; - message = "Caught Exception: "; - message += exception.what(); - OpmLog::debug(message); - } - } - public: - AdaptiveTimeStepping() = default; - - //! \brief contructor taking parameter object - explicit AdaptiveTimeStepping(const UnitSystem& unitSystem, - const double max_next_tstep = -1.0, - const bool terminalOutput = true); - - //! \brief contructor taking parameter object - //! \param tuning Pointer to ecl TUNING keyword - //! \param timeStep current report step - AdaptiveTimeStepping(double max_next_tstep, - const Tuning& tuning, - const UnitSystem& unitSystem, - const bool terminalOutput = true); - - static void registerParameters(); - - /** \brief step method that acts like the solver::step method - in a sub cycle of time steps - \param tuningUpdater Function used to update TUNING parameters before each - time step. ACTIONX might change tuning. - */ - template <class Solver> - SimulatorReport step(const SimulatorTimer& simulatorTimer, - Solver& solver, - const bool isEvent, - const std::function<bool(const double, const double, const int)> tuningUpdater); - - /** \brief Returns the simulator report for the failed substeps of the last - * report step. - */ - double suggestedNextStep() const - { return suggestedNextTimestep_; } - - void setSuggestedNextStep(const double x) - { suggestedNextTimestep_ = x; } + SolutionTimeErrorSolverWrapper(const Solver& solver); + double relativeChange() const; + private: + const Solver& solver_; + }; - void updateTUNING(double max_next_tstep, const Tuning& tuning); + // Forward declaration of SubStepIteration + // TODO: This forward declaration is not enough for GCC version 9.4.0 (released June 2021), + // but it works fine with GCC version 13.2.0 (released July 2023). + template <class Solver> class SubStepIteration; - void updateNEXTSTEP(double max_next_tstep); + template <class Solver> + class SubStepper { + public: + SubStepper( + AdaptiveTimeStepping<TypeTag>& adaptive_time_stepping, + const SimulatorTimer& simulator_timer, + Solver& solver, + const bool is_event, + const std::function<bool(const double, const double, const int)>& tuning_updater + ); + + AdaptiveTimeStepping<TypeTag>& getAdaptiveTimerStepper(); + SimulatorReport run(); + friend class AdaptiveTimeStepping<TypeTag>::template SubStepIteration<Solver>; - template<class Serializer> - void serializeOp(Serializer& serializer); + private: + bool isReservoirCouplingMaster_() const; + bool isReservoirCouplingSlave_() const; + void maybeModifySuggestedTimeStepAtBeginningOfReportStep_(const double originalTimeStep); + bool maybeUpdateTuning_(double elapsed, double dt, int sub_step_number) const; + double maxTimeStep_() const; + SimulatorReport runStepOriginal_(); +#ifdef RESERVOIR_COUPLING_ENABLED + ReservoirCouplingMaster& reservoirCouplingMaster_(); + ReservoirCouplingSlave& reservoirCouplingSlave_(); + SimulatorReport runStepReservoirCouplingMaster_(); + SimulatorReport runStepReservoirCouplingSlave_(); +#endif + double suggestedNextTimestep_() const; + + AdaptiveTimeStepping<TypeTag>& adaptive_time_stepping_; + const SimulatorTimer& simulator_timer_; + Solver& solver_; + const bool is_event_; + const std::function<bool(double elapsed, double dt, int sub_step_number)>& tuning_updater_; + }; - static AdaptiveTimeStepping<TypeTag> serializationTestObjectHardcoded(); - static AdaptiveTimeStepping<TypeTag> serializationTestObjectPID(); - static AdaptiveTimeStepping<TypeTag> serializationTestObjectPIDIt(); - static AdaptiveTimeStepping<TypeTag> serializationTestObjectSimple(); + template <class Solver> + class SubStepIteration { + public: + SubStepIteration( + SubStepper<Solver>& substepper, + AdaptiveSimulatorTimer& substep_timer, + const double original_time_step, + const bool final_step + ); - bool operator==(const AdaptiveTimeStepping<TypeTag>& rhs) const; + SimulatorReport run(); private: - template<class Controller> - static AdaptiveTimeStepping<TypeTag> serializationTestObject_(); - - template<class T, class Serializer> - void allocAndSerialize(Serializer& serializer) - { - if (!serializer.isSerializing()) { - timeStepControl_ = std::make_unique<T>(); - } - serializer(*static_cast<T*>(timeStepControl_.get())); - } - - template<class T> - bool castAndComp(const AdaptiveTimeStepping<TypeTag>& Rhs) const - { - const T* lhs = static_cast<const T*>(timeStepControl_.get()); - const T* rhs = static_cast<const T*>(Rhs.timeStepControl_.get()); - return *lhs == *rhs; - } - - protected: - void init_(const UnitSystem& unitSystem); - - using TimeStepController = std::unique_ptr<TimeStepControlInterface>; - - TimeStepControlType timeStepControlType_{TimeStepControlType::PIDAndIterationCount}; //!< type of time step control object - TimeStepController timeStepControl_{}; //!< time step control object - double restartFactor_{}; //!< factor to multiply time step with when solver fails to converge - double growthFactor_{}; //!< factor to multiply time step when solver recovered from failed convergence - double maxGrowth_{}; //!< factor that limits the maximum growth of a time step - double maxTimeStep_{}; //!< maximal allowed time step size in days - double minTimeStep_{}; //!< minimal allowed time step size before throwing - bool ignoreConvergenceFailure_{false}; //!< continue instead of stop when minimum time step is reached - int solverRestartMax_{}; //!< how many restart of solver are allowed - bool solverVerbose_{false}; //!< solver verbosity - bool timestepVerbose_{false}; //!< timestep verbosity - double suggestedNextTimestep_{}; //!< suggested size of next timestep - bool fullTimestepInitially_{false}; //!< beginning with the size of the time step from data file - double timestepAfterEvent_{}; //!< suggested size of timestep after an event - bool useNewtonIteration_{false}; //!< use newton iteration count for adaptive time step control - double minTimeStepBeforeShuttingProblematicWells_{}; //! < shut problematic wells when time step size in days are less than this + bool checkContinueOnUnconvergedSolution_(double dt) const; + void checkTimeStepMaxRestartLimit_(const int restarts) const; + void checkTimeStepMinLimit_(const int new_time_step) const; + void chopTimeStep_(const double new_time_step); + bool chopTimeStepOrCloseFailingWells_(const int new_time_step); + boost::posix_time::ptime currentDateTime_() const; + int getNumIterations_(const SimulatorReportSingle &substep_report) const; + double growthFactor_() const; + bool ignoreConvergenceFailure_() const; + void maybeReportSubStep_(SimulatorReportSingle substep_report) const; + double maybeRestrictTimeStepGrowth_( + const double dt, double dt_estimate, const int restarts) const; + void maybeUpdateTuningAndTimeStep_(); + double maxGrowth_() const; + double minTimeStepBeforeClosingWells_() const; + double minTimeStep_() const; + double restartFactor_() const; + SimulatorReportSingle runSubStep_(); + int solverRestartMax_() const; + double suggestedNextTimestep_() const; + void setSuggestedNextStep_(double step); + void setTimeStep_(double dt_estimate); + Solver& solver_() const; + bool solverVerbose_() const; + const SimulatorTimer& simulatorTimer_() const; + boost::posix_time::ptime startDateTime_() const; + double timeStepControlComputeEstimate_( + const double dt, const int iterations, double elapsed) const; + bool timeStepVerbose_() const; + void updateSuggestedNextStep_(); + bool useNewtonIteration_() const; + double writeOutput_() const; + + SubStepper<Solver>& substepper_; + AdaptiveSimulatorTimer& substep_timer_; + const double original_time_step_; + const bool final_step_; + std::string cause_of_failure_; + AdaptiveTimeStepping<TypeTag>& adaptive_time_stepping_; }; -} -#include <opm/simulators/timestepping/AdaptiveTimeStepping_impl.hpp> +public: + AdaptiveTimeStepping() = default; + + AdaptiveTimeStepping( + const UnitSystem& unitSystem, + const double max_next_tstep = -1.0, + const bool terminalOutput = true + ); + + AdaptiveTimeStepping( + double max_next_tstep, + const Tuning& tuning, + const UnitSystem& unitSystem, + const bool terminalOutput = true + ); + bool operator==(const AdaptiveTimeStepping<TypeTag>& rhs); + + static void registerParameters(); +#ifdef RESERVOIR_COUPLING_ENABLED + void setReservoirCouplingMaster(ReservoirCouplingMaster *reservoir_coupling_master); + void setReservoirCouplingSlave(ReservoirCouplingSlave *reservoir_coupling_slave); +#endif + void setSuggestedNextStep(const double x); + double suggestedNextStep() const; + + template <class Solver> + SimulatorReport step(const SimulatorTimer& simulator_timer, + Solver& solver, + const bool is_event, + const std::function<bool(const double, const double, const int)> + tuning_updater); + + void updateTUNING(double max_next_tstep, const Tuning& tuning); + void updateNEXTSTEP(double max_next_tstep); + + template<class Serializer> + void serializeOp(Serializer& serializer); + + static AdaptiveTimeStepping<TypeTag> serializationTestObjectHardcoded(); + static AdaptiveTimeStepping<TypeTag> serializationTestObjectPID(); + static AdaptiveTimeStepping<TypeTag> serializationTestObjectPIDIt(); + static AdaptiveTimeStepping<TypeTag> serializationTestObjectSimple(); + +private: + void maybeModifySuggestedTimeStepAtBeginningOfReportStep_(const double original_time_step, + const bool is_event); + + template<class Controller> + static AdaptiveTimeStepping<TypeTag> serializationTestObject_(); + + template<class T, class Serializer> + void allocAndSerialize(Serializer& serializer); + + template<class T> + bool castAndComp(const AdaptiveTimeStepping<TypeTag>& Rhs) const; + +protected: + void init_(const UnitSystem& unitSystem); + + using TimeStepController = std::unique_ptr<TimeStepControlInterface>; + + TimeStepControlType time_step_control_type_; //!< type of time step control object + TimeStepController time_step_control_; //!< time step control object + double restart_factor_; //!< factor to multiply time step with when solver fails to converge + double growth_factor_; //!< factor to multiply time step when solver recovered from failed convergence + double max_growth_; //!< factor that limits the maximum growth of a time step + double max_time_step_; //!< maximal allowed time step size in days + double min_time_step_; //!< minimal allowed time step size before throwing + bool ignore_convergence_failure_; //!< continue instead of stop when minimum time step is reached + int solver_restart_max_; //!< how many restart of solver are allowed + bool solver_verbose_; //!< solver verbosity + bool timestep_verbose_; //!< timestep verbosity + double suggested_next_timestep_; //!< suggested size of next timestep + bool full_timestep_initially_; //!< beginning with the size of the time step from data file + double timestep_after_event_; //!< suggested size of timestep after an event + bool use_newton_iteration_; //!< use newton iteration count for adaptive time step control + + //! < shut problematic wells when time step size in days are less than this + double min_time_step_before_shutting_problematic_wells_; +#ifdef RESERVOIR_COUPLING_ENABLED + ReservoirCouplingMaster *reservoir_coupling_master_ = nullptr; + ReservoirCouplingSlave *reservoir_coupling_slave_ = nullptr; +#endif +}; + +} // namespace Opm +#include <opm/simulators/timestepping/AdaptiveTimeStepping_impl.hpp> #endif // OPM_ADAPTIVE_TIME_STEPPING_HPP diff --git a/opm/simulators/timestepping/AdaptiveTimeStepping_impl.hpp b/opm/simulators/timestepping/AdaptiveTimeStepping_impl.hpp index 26343d47ff0..4e113fe5e93 100644 --- a/opm/simulators/timestepping/AdaptiveTimeStepping_impl.hpp +++ b/opm/simulators/timestepping/AdaptiveTimeStepping_impl.hpp @@ -1,5 +1,22 @@ /* + Copyright 2024 Equinor ASA. + + This file is part of the Open Porous Media project (OPM). + + OPM is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + OPM 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with OPM. If not, see <http://www.gnu.org/licenses/>. */ + #ifndef OPM_ADAPTIVE_TIME_STEPPING_IMPL_HPP #define OPM_ADAPTIVE_TIME_STEPPING_IMPL_HPP @@ -27,537 +44,1262 @@ #include <sstream> #include <stdexcept> +#include <boost/date_time/posix_time/posix_time.hpp> #include <fmt/format.h> #include <fmt/ranges.h> namespace Opm { +/********************************************* + * Public methods of AdaptiveTimeStepping + * ******************************************/ + + +//! \brief contructor taking parameter object +template<class TypeTag> +AdaptiveTimeStepping<TypeTag>::AdaptiveTimeStepping( + const UnitSystem& unit_system, + const double max_next_tstep, + const bool terminal_output +) + : time_step_control_{} + , restart_factor_{Parameters::Get<Parameters::SolverRestartFactor<Scalar>>()} // 0.33 + , growth_factor_{Parameters::Get<Parameters::SolverGrowthFactor<Scalar>>()} // 2.0 + , max_growth_{Parameters::Get<Parameters::SolverMaxGrowth<Scalar>>()} // 3.0 + , max_time_step_{ + Parameters::Get<Parameters::SolverMaxTimeStepInDays<Scalar>>() * 24 * 60 * 60} // 365.25 + , min_time_step_{ + unit_system.to_si(UnitSystem::measure::time, + Parameters::Get<Parameters::SolverMinTimeStep<Scalar>>())} // 1e-12; + , ignore_convergence_failure_{ + Parameters::Get<Parameters::SolverContinueOnConvergenceFailure>()} // false; + , solver_restart_max_{Parameters::Get<Parameters::SolverMaxRestarts>()} // 10 + , solver_verbose_{Parameters::Get<Parameters::SolverVerbosity>() > 0 && terminal_output} // 2 + , timestep_verbose_{Parameters::Get<Parameters::TimeStepVerbosity>() > 0 && terminal_output} // 2 + , suggested_next_timestep_{ + (max_next_tstep <= 0 ? Parameters::Get<Parameters::InitialTimeStepInDays>() + : max_next_tstep) * 24 * 60 * 60} // 1.0 + , full_timestep_initially_{Parameters::Get<Parameters::FullTimeStepInitially>()} // false + , timestep_after_event_{ + Parameters::Get<Parameters::TimeStepAfterEventInDays<Scalar>>() * 24 * 60 * 60} // 1e30 + , use_newton_iteration_{false} + , min_time_step_before_shutting_problematic_wells_{ + Parameters::Get<Parameters::MinTimeStepBeforeShuttingProblematicWellsInDays>() * unit::day} + +{ + init_(unit_system); +} + +//! \brief contructor +//! \param tuning Pointer to ecl TUNING keyword +template<class TypeTag> +AdaptiveTimeStepping<TypeTag>::AdaptiveTimeStepping(double max_next_tstep, + const Tuning& tuning, + const UnitSystem& unit_system, + const bool terminal_output +) + : time_step_control_{} + , restart_factor_{tuning.TSFCNV} + , growth_factor_{tuning.TFDIFF} + , max_growth_{tuning.TSFMAX} + , max_time_step_{tuning.TSMAXZ} // 365.25 + , min_time_step_{tuning.TSFMIN} // 0.1; + , ignore_convergence_failure_{true} + , solver_restart_max_{Parameters::Get<Parameters::SolverMaxRestarts>()} // 10 + , solver_verbose_{Parameters::Get<Parameters::SolverVerbosity>() > 0 && terminal_output} // 2 + , timestep_verbose_{Parameters::Get<Parameters::TimeStepVerbosity>() > 0 && terminal_output} // 2 + , suggested_next_timestep_{ + max_next_tstep <= 0 ? Parameters::Get<Parameters::InitialTimeStepInDays>() * 24 * 60 * 60 + : max_next_tstep} // 1.0 + , full_timestep_initially_{Parameters::Get<Parameters::FullTimeStepInitially>()} // false + , timestep_after_event_{tuning.TMAXWC} // 1e30 + , use_newton_iteration_{false} + , min_time_step_before_shutting_problematic_wells_{ + Parameters::Get<Parameters::MinTimeStepBeforeShuttingProblematicWellsInDays>() * unit::day} +{ + init_(unit_system); +} template<class TypeTag> +bool AdaptiveTimeStepping<TypeTag>:: -AdaptiveTimeStepping(const UnitSystem& unitSystem, - const double max_next_tstep, - const bool terminalOutput) - : timeStepControl_() - , restartFactor_(Parameters::Get<Parameters::SolverRestartFactor<Scalar>>()) // 0.33 - , growthFactor_(Parameters::Get<Parameters::SolverGrowthFactor<Scalar>>()) // 2.0 - , maxGrowth_(Parameters::Get<Parameters::SolverMaxGrowth<Scalar>>()) // 3.0 - , maxTimeStep_(Parameters::Get<Parameters::SolverMaxTimeStepInDays<Scalar>>() * 24 * 60 * 60) // 365.25 - , minTimeStep_(unitSystem.to_si(UnitSystem::measure::time, Parameters::Get<Parameters::SolverMinTimeStep<Scalar>>())) // 1e-12; - , ignoreConvergenceFailure_(Parameters::Get<Parameters::SolverContinueOnConvergenceFailure>()) // false; - , solverRestartMax_(Parameters::Get<Parameters::SolverMaxRestarts>()) // 10 - , solverVerbose_(Parameters::Get<Parameters::SolverVerbosity>() > 0 && terminalOutput) // 2 - , timestepVerbose_(Parameters::Get<Parameters::TimeStepVerbosity>() > 0 && terminalOutput) // 2 - , suggestedNextTimestep_((max_next_tstep <= 0 ? Parameters::Get<Parameters::InitialTimeStepInDays>() : max_next_tstep) * 24 * 60 * 60) // 1.0 - , fullTimestepInitially_(Parameters::Get<Parameters::FullTimeStepInitially>()) // false - , timestepAfterEvent_(Parameters::Get<Parameters::TimeStepAfterEventInDays<Scalar>>() * 24 * 60 * 60) // 1e30 - , useNewtonIteration_(false) - , minTimeStepBeforeShuttingProblematicWells_(Parameters::Get<Parameters::MinTimeStepBeforeShuttingProblematicWellsInDays>() * unit::day) - -{ - init_(unitSystem); +operator==(const AdaptiveTimeStepping<TypeTag>& rhs) +{ + if (this->time_step_control_type_ != rhs.time_step_control_type_ || + (this->time_step_control_ && !rhs.time_step_control_) || + (!this->time_step_control_ && rhs.time_step_control_)) { + return false; + } + + bool result = false; + switch (this->time_step_control_type_) { + case TimeStepControlType::HardCodedTimeStep: + result = castAndComp<HardcodedTimeStepControl>(rhs); + break; + case TimeStepControlType::PIDAndIterationCount: + result = castAndComp<PIDAndIterationCountTimeStepControl>(rhs); + break; + case TimeStepControlType::SimpleIterationCount: + result = castAndComp<SimpleIterationCountTimeStepControl>(rhs); + break; + case TimeStepControlType::PID: + result = castAndComp<PIDTimeStepControl>(rhs); + break; + } + + return result && + this->restart_factor_ == rhs.restart_factor_ && + this->growth_factor_ == rhs.growth_factor_ && + this->max_growth_ == rhs.max_growth_ && + this->max_time_step_ == rhs.max_time_step_ && + this->min_time_step_ == rhs.min_time_step_ && + this->ignore_convergence_failure_ == rhs.ignore_convergence_failure_ && + this->solver_restart_max_== rhs.solver_restart_max_ && + this->solver_verbose_ == rhs.solver_verbose_ && + this->full_timestep_initially_ == rhs.full_timestep_initially_ && + this->timestep_after_event_ == rhs.timestep_after_event_ && + this->use_newton_iteration_ == rhs.use_newton_iteration_ && + this->min_time_step_before_shutting_problematic_wells_ == + rhs.min_time_step_before_shutting_problematic_wells_; } template<class TypeTag> +void AdaptiveTimeStepping<TypeTag>:: -AdaptiveTimeStepping(double max_next_tstep, - const Tuning& tuning, - const UnitSystem& unitSystem, - const bool terminalOutput) - : timeStepControl_() - , restartFactor_(tuning.TSFCNV) - , growthFactor_(tuning.TFDIFF) - , maxGrowth_(tuning.TSFMAX) - , maxTimeStep_(tuning.TSMAXZ) // 365.25 - , minTimeStep_(tuning.TSFMIN) // 0.1; - , ignoreConvergenceFailure_(true) - , solverRestartMax_(Parameters::Get<Parameters::SolverMaxRestarts>()) // 10 - , solverVerbose_(Parameters::Get<Parameters::SolverVerbosity>() > 0 && terminalOutput) // 2 - , timestepVerbose_(Parameters::Get<Parameters::TimeStepVerbosity>() > 0 && terminalOutput) // 2 - , suggestedNextTimestep_(max_next_tstep <= 0 ? Parameters::Get<Parameters::InitialTimeStepInDays>() * 24 * 60 * 60 : max_next_tstep) // 1.0 - , fullTimestepInitially_(Parameters::Get<Parameters::FullTimeStepInitially>()) // false - , timestepAfterEvent_(tuning.TMAXWC) // 1e30 - , useNewtonIteration_(false) - , minTimeStepBeforeShuttingProblematicWells_(Parameters::Get<Parameters::MinTimeStepBeforeShuttingProblematicWellsInDays>() * unit::day) -{ - init_(unitSystem); -} - -template<class TypeTag> -void AdaptiveTimeStepping<TypeTag>::registerParameters() +registerParameters() { registerEclTimeSteppingParameters<Scalar>(); detail::registerAdaptiveParameters(); } +#ifdef RESERVOIR_COUPLING_ENABLED template<class TypeTag> -template<class Solver> +void +AdaptiveTimeStepping<TypeTag>:: +setReservoirCouplingMaster(ReservoirCouplingMaster *reservoir_coupling_master) +{ + this->reservoir_coupling_master_ = reservoir_coupling_master; +} + +template<class TypeTag> +void +AdaptiveTimeStepping<TypeTag>:: +setReservoirCouplingSlave(ReservoirCouplingSlave *reservoir_coupling_slave) +{ + this->reservoir_coupling_slave_ = reservoir_coupling_slave; +} +#endif + +/** \brief step method that acts like the solver::step method + in a sub cycle of time steps + \param tuningUpdater Function used to update TUNING parameters before each + time step. ACTIONX might change tuning. +*/ +template<class TypeTag> +template <class Solver> SimulatorReport AdaptiveTimeStepping<TypeTag>:: -step(const SimulatorTimer& simulatorTimer, +step(const SimulatorTimer& simulator_timer, Solver& solver, - const bool isEvent, - const std::function<bool(const double, const double, const int)> tuningUpdater) + const bool is_event, + const std::function<bool(const double /*current_time*/, + const double /*dt*/, + const int /*substep_number*/ + )> tuning_updater +) { - // Maybe update tuning - tuningUpdater(simulatorTimer.simulationTimeElapsed(), suggestedNextTimestep_, 0); - SimulatorReport report; - const double timestep = simulatorTimer.currentStepLength(); + SubStepper sub_stepper{ + *this, simulator_timer, solver, is_event, tuning_updater, + }; + return sub_stepper.run(); +} + +template<class TypeTag> +template<class Serializer> +void +AdaptiveTimeStepping<TypeTag>:: +serializeOp(Serializer& serializer) +{ + serializer(this->time_step_control_type_); + switch (this->time_step_control_type_) { + case TimeStepControlType::HardCodedTimeStep: + allocAndSerialize<HardcodedTimeStepControl>(serializer); + break; + case TimeStepControlType::PIDAndIterationCount: + allocAndSerialize<PIDAndIterationCountTimeStepControl>(serializer); + break; + case TimeStepControlType::SimpleIterationCount: + allocAndSerialize<SimpleIterationCountTimeStepControl>(serializer); + break; + case TimeStepControlType::PID: + allocAndSerialize<PIDTimeStepControl>(serializer); + break; + } + serializer(this->restart_factor_); + serializer(this->growth_factor_); + serializer(this->max_growth_); + serializer(this->max_time_step_); + serializer(this->min_time_step_); + serializer(this->ignore_convergence_failure_); + serializer(this->solver_restart_max_); + serializer(this->solver_verbose_); + serializer(this->timestep_verbose_); + serializer(this->suggested_next_timestep_); + serializer(this->full_timestep_initially_); + serializer(this->timestep_after_event_); + serializer(this->use_newton_iteration_); + serializer(this->min_time_step_before_shutting_problematic_wells_); +} + +template<class TypeTag> +AdaptiveTimeStepping<TypeTag> +AdaptiveTimeStepping<TypeTag>:: +serializationTestObjectHardcoded() +{ + return serializationTestObject_<HardcodedTimeStepControl>(); +} + +template<class TypeTag> +AdaptiveTimeStepping<TypeTag> +AdaptiveTimeStepping<TypeTag>:: +serializationTestObjectPID() +{ + return serializationTestObject_<PIDTimeStepControl>(); +} + +template<class TypeTag> +AdaptiveTimeStepping<TypeTag> +AdaptiveTimeStepping<TypeTag>:: +serializationTestObjectPIDIt() +{ + return serializationTestObject_<PIDAndIterationCountTimeStepControl>(); +} + +template<class TypeTag> +AdaptiveTimeStepping<TypeTag> +AdaptiveTimeStepping<TypeTag>:: +serializationTestObjectSimple() +{ + return serializationTestObject_<SimpleIterationCountTimeStepControl>(); +} + + +template<class TypeTag> +void +AdaptiveTimeStepping<TypeTag>:: +setSuggestedNextStep(const double x) +{ + this->suggested_next_timestep_ = x; +} + +template<class TypeTag> +double +AdaptiveTimeStepping<TypeTag>:: +suggestedNextStep() const +{ + return this->suggested_next_timestep_; +} + + +template<class TypeTag> +void +AdaptiveTimeStepping<TypeTag>:: +updateNEXTSTEP(double max_next_tstep) +{ + // \Note Only update next suggested step if TSINIT was explicitly + // set in TUNING or NEXTSTEP is active. + if (max_next_tstep > 0) { + this->suggested_next_timestep_ = max_next_tstep; + } +} +template<class TypeTag> +void +AdaptiveTimeStepping<TypeTag>:: +updateTUNING(double max_next_tstep, const Tuning& tuning) +{ + this->restart_factor_ = tuning.TSFCNV; + this->growth_factor_ = tuning.TFDIFF; + this->max_growth_ = tuning.TSFMAX; + this->max_time_step_ = tuning.TSMAXZ; + updateNEXTSTEP(max_next_tstep); + this->timestep_after_event_ = tuning.TMAXWC; +} + +/********************************************* + * Private methods of AdaptiveTimeStepping + * ******************************************/ + +template<class TypeTag> +template<class T, class Serializer> +void +AdaptiveTimeStepping<TypeTag>:: +allocAndSerialize(Serializer& serializer) +{ + if (!serializer.isSerializing()) { + this->time_step_control_ = std::make_unique<T>(); + } + serializer(*static_cast<T*>(this->time_step_control_.get())); +} + +template<class TypeTag> +template<class T> +bool +AdaptiveTimeStepping<TypeTag>:: +castAndComp(const AdaptiveTimeStepping<TypeTag>& Rhs) const +{ + const T* lhs = static_cast<const T*>(this->time_step_control_.get()); + const T* rhs = static_cast<const T*>(Rhs.time_step_control_.get()); + return *lhs == *rhs; +} + +template<class TypeTag> +void +AdaptiveTimeStepping<TypeTag>:: +maybeModifySuggestedTimeStepAtBeginningOfReportStep_(const double original_time_step, + bool is_event) +{ // init last time step as a fraction of the given time step - if (suggestedNextTimestep_ < 0) { - suggestedNextTimestep_ = restartFactor_ * timestep; + if (this->suggested_next_timestep_ < 0) { + this->suggested_next_timestep_ = this->restart_factor_ * original_time_step; } - if (fullTimestepInitially_) { - suggestedNextTimestep_ = timestep; + if (this->full_timestep_initially_) { + this->suggested_next_timestep_ = original_time_step; } // use seperate time step after event - if (isEvent && timestepAfterEvent_ > 0) { - suggestedNextTimestep_ = timestepAfterEvent_; + if (is_event && this->timestep_after_event_ > 0) { + this->suggested_next_timestep_ = this->timestep_after_event_; } +} - auto& simulator = solver.model().simulator(); - auto& problem = simulator.problem(); +template<class TypeTag> +template<class Controller> +AdaptiveTimeStepping<TypeTag> +AdaptiveTimeStepping<TypeTag>:: +serializationTestObject_() +{ + AdaptiveTimeStepping<TypeTag> result; - // create adaptive step timer with previously used sub step size - AdaptiveSimulatorTimer substepTimer(simulatorTimer, suggestedNextTimestep_, maxTimeStep_); + result.restart_factor_ = 1.0; + result.growth_factor_ = 2.0; + result.max_growth_ = 3.0; + result.max_time_step_ = 4.0; + result.min_time_step_ = 5.0; + result.ignore_convergence_failure_ = true; + result.solver_restart_max_ = 6; + result.solver_verbose_ = true; + result.timestep_verbose_ = true; + result.suggested_next_timestep_ = 7.0; + result.full_timestep_initially_ = true; + result.use_newton_iteration_ = true; + result.min_time_step_before_shutting_problematic_wells_ = 9.0; + result.time_step_control_type_ = Controller::Type; + result.time_step_control_ = + std::make_unique<Controller>(Controller::serializationTestObject()); - // counter for solver restarts - int restarts = 0; + return result; +} - // sub step time loop - while (!substepTimer.done()) { - // Maybe update tuning - // get current delta t - auto oldValue = suggestedNextTimestep_; - if (tuningUpdater(substepTimer.simulationTimeElapsed(), - substepTimer.currentStepLength(), - substepTimer.currentStepNum())) { - // Use provideTimeStepEstimate to make we sure don't simulate longer than the report step is. - substepTimer.provideTimeStepEstimate(suggestedNextTimestep_); - suggestedNextTimestep_ = oldValue; - } - const double dt = substepTimer.currentStepLength(); - if (timestepVerbose_) { - detail::logTimer(substepTimer); - } +/********************************************* + * Protected methods of AdaptiveTimeStepping + * ******************************************/ + +template<class TypeTag> +void AdaptiveTimeStepping<TypeTag>:: +init_(const UnitSystem& unitSystem) +{ + std::tie(time_step_control_type_, + time_step_control_, + use_newton_iteration_) = detail::createController(unitSystem); + // make sure growth factor is something reasonable + if (this->growth_factor_ < 1.0) { + OPM_THROW(std::runtime_error, + "Growth factor cannot be less than 1."); + } +} - SimulatorReportSingle substepReport; - std::string causeOfFailure; - try { - substepReport = solver.step(substepTimer); - if (solverVerbose_) { - // report number of linear iterations - OpmLog::debug("Overall linear iterations used: " + - std::to_string(substepReport.total_linear_iterations)); - } - } - catch (const TooManyIterations& e) { - substepReport = solver.failureReport(); - causeOfFailure = "Solver convergence failure - Iteration limit reached"; - logException_(e, solverVerbose_); - // since linearIterations is < 0 this will restart the solver +/************************************************ + * Private class SubStepper public methods + ************************************************/ + +template<class TypeTag> +template<class Solver> +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +SubStepper( + AdaptiveTimeStepping<TypeTag>& adaptive_time_stepping, + const SimulatorTimer& simulator_timer, + Solver& solver, + const bool is_event, + const std::function<bool(const double /*current_time*/, + const double /*dt*/, + const int /*substep_number*/ + )>& tuning_updater + +) + : adaptive_time_stepping_{adaptive_time_stepping} + , simulator_timer_{simulator_timer} + , solver_{solver} + , is_event_{is_event} + , tuning_updater_{tuning_updater} +{ +} + +template<class TypeTag> +template<class Solver> +AdaptiveTimeStepping<TypeTag>& +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +getAdaptiveTimerStepper() +{ + return adaptive_time_stepping_; +} + +template<class TypeTag> +template<class Solver> +SimulatorReport +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +run() +{ +#ifdef RESERVOIR_COUPLING_ENABLED + if (isReservoirCouplingSlave_() && reservoirCouplingSlave_().activated()) { + return runStepReservoirCouplingSlave_(); + } + else if (isReservoirCouplingMaster_() && reservoirCouplingMaster_().activated()) { + return runStepReservoirCouplingMaster_(); + } + else { + return runStepOriginal_(); + } +#else + return runStepOriginal_(); +#endif +} + +/************************************************ + * Private class SubStepper private methods + ************************************************/ + + +template<class TypeTag> +template<class Solver> +bool +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +isReservoirCouplingMaster_() const +{ + return this->adaptive_time_stepping_.reservoir_coupling_master_ != nullptr; +} + +template<class TypeTag> +template<class Solver> +bool +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +isReservoirCouplingSlave_() const +{ + return this->adaptive_time_stepping_.reservoir_coupling_slave_ != nullptr; +} + +template<class TypeTag> +template<class Solver> +void +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +maybeModifySuggestedTimeStepAtBeginningOfReportStep_(const double original_time_step) +{ + this->adaptive_time_stepping_.maybeModifySuggestedTimeStepAtBeginningOfReportStep_( + original_time_step, this->is_event_ + ); +} + +// The maybeUpdateTuning_() lambda callback is defined in SimulatorFullyImplicitBlackoil::runStep() +// It has to be called for each substep since TUNING might have been changed for next sub step due +// to ACTIONX (via NEXTSTEP) or WCYCLE keywords. +template<class TypeTag> +template<class Solver> +bool +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +maybeUpdateTuning_(double elapsed, double dt, int sub_step_number) const +{ + return this->tuning_updater_(elapsed, dt, sub_step_number); +} + +template<class TypeTag> +template<class Solver> +double +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +maxTimeStep_() const +{ + return this->adaptive_time_stepping_.max_time_step_; +} + +template <class TypeTag> +template <class Solver> +SimulatorReport +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +runStepOriginal_() +{ + auto elapsed = this->simulator_timer_.simulationTimeElapsed(); + auto original_time_step = this->simulator_timer_.currentStepLength(); + auto report_step = this->simulator_timer_.reportStepNum(); + maybeUpdateTuning_(elapsed, original_time_step, report_step); + maybeModifySuggestedTimeStepAtBeginningOfReportStep_(original_time_step); + + AdaptiveSimulatorTimer substep_timer{ + this->simulator_timer_.startDateTime(), + original_time_step, + elapsed, + suggestedNextTimestep_(), + report_step, + maxTimeStep_() + }; + SubStepIteration substepIteration{*this, substep_timer, original_time_step, /*final_step=*/true}; + return substepIteration.run(); +} + +#ifdef RESERVOIR_COUPLING_ENABLED +template <class TypeTag> +template <class Solver> +ReservoirCouplingMaster& +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +reservoirCouplingMaster_() +{ + return *adaptive_time_stepping_.reservoir_coupling_master_; +} +#endif + +#ifdef RESERVOIR_COUPLING_ENABLED +template <class TypeTag> +template <class Solver> +ReservoirCouplingSlave& +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +reservoirCouplingSlave_() +{ + return *this->adaptive_time_stepping_.reservoir_coupling_slave_; +} +#endif + +#ifdef RESERVOIR_COUPLING_ENABLED +// Description of the reservoir coupling master and slave substep loop +// ------------------------------------------------------------------- +// The master and slave processes attempts to reach the end of the report step using a series of substeps +// (also called timesteps). Each substep have an upper limit that is roughly determined by a combination +// of the keywords TUNING (through the TSINIT, TSMAXZ values), NEXSTEP, WCYCLE, and the start of the +// next report step (the last substep needs to coincide with this time). Note that NEXTSTEP can be +// updated from an ACTIONX keyword. Although, this comment focuses on the maximum substep limit, +// note that there is also a lower limit on the substep length. And the substep sizes will be adjusted +// automatically (or retried) based on the convergence behavior of the solver and other criteria. +// +// When using reservoir coupling, the upper limit on each substep is further limited to: a) not overshoot +// next report date of a slave reservoir, and b) to keep the flow rate of the slave groups within +// certain limits. To determine this potential further limitation on the substep, the following procedure +// is used at the beginning of each master substep: +// - First, the slaves sends the master the date of their next report step +// - The master receives the date of the next report step from the slaves +// - If necessary, the master computes a modified substep length based on the received dates +// TODO: explain how the substep is limited to keep the flow rate of the slave groups within certain +// limits. Since this is not implemented yet, this part of the procedure is not explained here. +// +// Then, after the master has determined the substep length using the above procedure, it sends it +// to the slaves. The slaves will now use the end of this substep as a fixed point (like a mini report +// step), that they will try to reach using a single substep or multiple substeps. The end of the +// substep received from the master will either conincide with the end of its current report step, or +// it will end before (it cannot not end after since the master has already adjusted the step to not +// exceed any report time in a slave). If the end of this substep is earlier in time than its next report +// date, the slave will enter a loop and wait for the master to send it a new substep. +// The loop is finished when the end of the received time step conincides with the end of its current +// report step. + +template <class TypeTag> +template <class Solver> +SimulatorReport +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +runStepReservoirCouplingMaster_() +{ + bool substep_done = false; + int iteration = 0; + const double original_time_step = this->simulator_timer_.currentStepLength(); + double current_time{this->simulator_timer_.simulationTimeElapsed()}; + double step_end_time = current_time + original_time_step; + auto current_step_length = original_time_step; + SimulatorReport report; + while(!substep_done) { + reservoirCouplingMaster_().receiveNextReportDateFromSlaves(); + if (iteration == 0) { + maybeUpdateTuning_(current_time, current_step_length, /*substep=*/0); } - catch (const ConvergenceMonitorFailure& e) { - substepReport = solver.failureReport(); - causeOfFailure = "Convergence monitor failure"; + current_step_length = reservoirCouplingMaster_().maybeChopSubStep( + current_step_length, current_time); + reservoirCouplingMaster_().sendNextTimeStepToSlaves(current_step_length); + if (iteration == 0) { + maybeModifySuggestedTimeStepAtBeginningOfReportStep_(current_step_length); } - catch (const LinearSolverProblem& e) { - substepReport = solver.failureReport(); - causeOfFailure = "Linear solver convergence failure"; - - logException_(e, solverVerbose_); - // since linearIterations is < 0 this will restart the solver + AdaptiveSimulatorTimer substep_timer{ + this->simulator_timer_.startDateTime(), + /*stepLength=*/current_step_length, + /*elapsedTime=*/current_time, + /*timeStepEstimate=*/suggestedNextTimestep_(), + /*reportStep=*/this->simulator_timer_.reportStepNum(), + maxTimeStep_() + }; + bool final_step = ReservoirCoupling::Seconds::compare_gt_or_eq( + current_time + current_step_length, step_end_time + ); + SubStepIteration substepIteration{*this, substep_timer, current_step_length, final_step}; + auto sub_steps_report = substepIteration.run(); + report += sub_steps_report; + current_time += current_step_length; + if (final_step) { + break; } - catch (const NumericalProblem& e) { - substepReport = solver.failureReport(); - causeOfFailure = "Solver convergence failure - Numerical problem encountered"; + iteration++; + } + return report; +} +#endif - logException_(e, solverVerbose_); - // since linearIterations is < 0 this will restart the solver +#ifdef RESERVOIR_COUPLING_ENABLED +template <class TypeTag> +template <class Solver> +SimulatorReport +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +runStepReservoirCouplingSlave_() +{ + bool substep_done = false; + int iteration = 0; + const double original_time_step = this->simulator_timer_.currentStepLength(); + double current_time{this->simulator_timer_.simulationTimeElapsed()}; + double step_end_time = current_time + original_time_step; + SimulatorReport report; + while(!substep_done) { + reservoirCouplingSlave_().sendNextReportDateToMasterProcess(); + auto timestep = reservoirCouplingSlave_().receiveNextTimeStepFromMaster(); + if (iteration == 0) { + maybeUpdateTuning_(current_time, original_time_step, /*substep=*/0); + maybeModifySuggestedTimeStepAtBeginningOfReportStep_(timestep); } - catch (const std::runtime_error& e) { - substepReport = solver.failureReport(); - - logException_(e, solverVerbose_); - // also catch linear solver not converged + AdaptiveSimulatorTimer substep_timer{ + this->simulator_timer_.startDateTime(), + /*step_length=*/timestep, + /*elapsed_time=*/current_time, + /*time_step_estimate=*/suggestedNextTimestep_(), + this->simulator_timer_.reportStepNum(), + maxTimeStep_() + }; + bool final_step = ReservoirCoupling::Seconds::compare_gt_or_eq( + current_time + timestep, step_end_time + ); + SubStepIteration substepIteration{*this, substep_timer, timestep, final_step}; + auto sub_steps_report = substepIteration.run(); + report += sub_steps_report; + current_time += timestep; + if (final_step) { + substep_done = true; + break; } - catch (const Dune::ISTLError& e) { - substepReport = solver.failureReport(); + iteration++; + } + return report; +} - logException_(e, solverVerbose_); - // also catch errors in ISTL AMG that occur when time step is too large - } - catch (const Dune::MatrixBlockError& e) { - substepReport = solver.failureReport(); +#endif + +template <class TypeTag> +template <class Solver> +double +AdaptiveTimeStepping<TypeTag>::SubStepper<Solver>:: +suggestedNextTimestep_() const +{ + return this->adaptive_time_stepping_.suggestedNextStep(); +} - logException_(e, solverVerbose_); - // this can be thrown by ISTL's ILU0 in block mode, yet is not an ISTLError + + +/************************************************ + * Private class SubStepIteration public methods + ************************************************/ + +template<class TypeTag> +template<class Solver> +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +SubStepIteration( + SubStepper<Solver>& substepper, + AdaptiveSimulatorTimer& substep_timer, + const double original_time_step, + bool final_step +) + : substepper_{substepper} + , substep_timer_{substep_timer} + , original_time_step_{original_time_step} + , final_step_{final_step} + , adaptive_time_stepping_{substepper.getAdaptiveTimerStepper()} +{ +} + +template <class TypeTag> +template <class Solver> +SimulatorReport +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +run() +{ + auto& simulator = solver_().model().simulator(); + auto& problem = simulator.problem(); + // counter for solver restarts + int restarts = 0; + SimulatorReport report; + + // sub step time loop + while (!this->substep_timer_.done()) { + maybeUpdateTuningAndTimeStep_(); + const double dt = this->substep_timer_.currentStepLength(); + if (timeStepVerbose_()) { + detail::logTimer(this->substep_timer_); } + auto substep_report = runSubStep_(); + //Pass substep to eclwriter for summary output - simulator.problem().setSubStepReport(substepReport); - - report += substepReport; - - bool continue_on_uncoverged_solution = ignoreConvergenceFailure_ && - !substepReport.converged && - dt <= minTimeStep_; - - if (continue_on_uncoverged_solution && solverVerbose_) { - const auto msg = fmt::format( - "Solver failed to converge but timestep " - "{} is smaller or equal to {}\n" - "which is the minimum threshold given " - "by option --solver-min-time-step\n", - dt, minTimeStep_ - ); - OpmLog::problem(msg); - } + problem.setSubStepReport(substep_report); - if (substepReport.converged || continue_on_uncoverged_solution) { - - // advance by current dt - ++substepTimer; - - // create object to compute the time error, simply forwards the call to the model - SolutionTimeErrorSolverWrapper<Solver> relativeChange(solver); - - // compute new time step estimate - const int iterations = useNewtonIteration_ ? substepReport.total_newton_iterations - : substepReport.total_linear_iterations; - double dtEstimate = timeStepControl_->computeTimeStepSize(dt, iterations, relativeChange, - substepTimer.simulationTimeElapsed()); - - assert(dtEstimate > 0); - // limit the growth of the timestep size by the growth factor - dtEstimate = std::min(dtEstimate, double(maxGrowth_ * dt)); - assert(dtEstimate > 0); - // further restrict time step size growth after convergence problems - if (restarts > 0) { - dtEstimate = std::min(growthFactor_ * dt, dtEstimate); - // solver converged, reset restarts counter - restarts = 0; - } + report += substep_report; - if (timestepVerbose_) { - std::ostringstream ss; - substepReport.reportStep(ss); - OpmLog::info(ss.str()); - } + if (substep_report.converged || checkContinueOnUnconvergedSolution_(dt)) { + ++this->substep_timer_; // advance by current dt - // write data if outputWriter was provided - // if the time step is done we do not need - // to write it as this will be done by the simulator - // anyway. - if (!substepTimer.done()) { - time::StopWatch perfTimer; - perfTimer.start(); + const int iterations = getNumIterations_(substep_report); + auto dt_estimate = timeStepControlComputeEstimate_( + dt, iterations, this->substep_timer_.simulationTimeElapsed()); - problem.writeOutput(true); + assert(dt_estimate > 0); + dt_estimate = maybeRestrictTimeStepGrowth_(dt, dt_estimate, restarts); + restarts = 0; // solver converged, reset restarts counter - report.success.output_write_time += perfTimer.secsSinceStart(); + maybeReportSubStep_(substep_report); + if (this->final_step_ && this->substep_timer_.done()) { + // if the time step is done we do not need to write it as this will be done + // by the simulator anyway. + } + else { + report.success.output_write_time += writeOutput_(); } // set new time step length - substepTimer.provideTimeStepEstimate(dtEstimate); - - report.success.converged = substepTimer.done(); - substepTimer.setLastStepFailed(false); + setTimeStep_(dt_estimate); + report.success.converged = this->substep_timer_.done(); + this->substep_timer_.setLastStepFailed(false); } else { // in case of no convergence - substepTimer.setLastStepFailed(true); - - // If we have restarted (i.e. cut the timestep) too - // many times, we have failed and throw an exception. - if (restarts >= solverRestartMax_) { - const auto msg = fmt::format( - "Solver failed to converge after cutting timestep {} times.", - restarts - ); - if (solverVerbose_) { - OpmLog::error(msg); - } - // Use throw directly to prevent file and line - throw TimeSteppingBreakdown{msg}; + this->substep_timer_.setLastStepFailed(true); + checkTimeStepMaxRestartLimit_(restarts); + + const double new_time_step = restartFactor_() * dt; + checkTimeStepMinLimit_(new_time_step); + bool wells_shut = false; + if (new_time_step > minTimeStepBeforeClosingWells_()) { + chopTimeStep_(new_time_step); + } else { + wells_shut = chopTimeStepOrCloseFailingWells_(new_time_step); + } + if (wells_shut) { + setTimeStep_(dt); // retry the old timestep } + else { + restarts++; // only increase if no wells were shut + } + } + problem.setNextTimeStepSize(this->substep_timer_.currentStepLength()); + } + updateSuggestedNextStep_(); + return report; +} - // The new, chopped timestep. - const double newTimeStep = restartFactor_ * dt; +/************************************************ + * Private class SubStepIteration private methods + ************************************************/ - // If we have restarted (i.e. cut the timestep) too - // much, we have failed and throw an exception. - if (newTimeStep < minTimeStep_) { - const auto msg = fmt::format( - "Solver failed to converge after cutting timestep to {}\n" - "which is the minimum threshold given by option --solver-min-time-step\n", - minTimeStep_ - ); - if (solverVerbose_) { - OpmLog::error(msg); - } - // Use throw directly to prevent file and line - throw TimeSteppingBreakdown{msg}; - } - // Define utility function for chopping timestep. - auto chopTimestep = [&]() { - substepTimer.provideTimeStepEstimate(newTimeStep); - if (solverVerbose_) { - const auto msg = fmt::format( - "{}\nTimestep chopped to {} days\n", - causeOfFailure, - unit::convert::to(substepTimer.currentStepLength(), unit::day) - ); - OpmLog::problem(msg); - } - ++restarts; - }; +template<class TypeTag> +template<class Solver> +bool +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +checkContinueOnUnconvergedSolution_(double dt) const +{ + bool continue_on_uncoverged_solution = ignoreConvergenceFailure_() && dt <= minTimeStep_(); + if (continue_on_uncoverged_solution && solverVerbose_()) { + // NOTE: This method is only called if the solver failed to converge. + const auto msg = fmt::format( + "Solver failed to converge but timestep {} is smaller or equal to {}\n" + "which is the minimum threshold given by option --solver-min-time-step\n", + dt, minTimeStep_() + ); + OpmLog::problem(msg); + } + return continue_on_uncoverged_solution; +} - const double minimumChoppedTimestep = minTimeStepBeforeShuttingProblematicWells_; - if (newTimeStep > minimumChoppedTimestep) { - chopTimestep(); - } else { - // We are below the threshold, and will check if there are any - // wells we should close rather than chopping again. - std::set<std::string> failing_wells = detail::consistentlyFailingWells(solver.model().stepReports()); - if (failing_wells.empty()) { - // Found no wells to close, chop the timestep as above. - chopTimestep(); - } else { - // Close all consistently failing wells that are not under group control - std::vector<std::string> shut_wells; - for (const auto& well : failing_wells) { - bool was_shut = solver.model().wellModel().forceShutWellByName( - well, substepTimer.simulationTimeElapsed(), /*dont_shut_grup_wells =*/ true); - if (was_shut) { - shut_wells.push_back(well); - } - } - // If no wells are closed we also try to shut wells under group control - if (shut_wells.empty()) { - for (const auto& well : failing_wells) { - bool was_shut = solver.model().wellModel().forceShutWellByName( - well, substepTimer.simulationTimeElapsed(), /*dont_shut_grup_wells =*/ false); - if (was_shut) { - shut_wells.push_back(well); - } - } - } - // If still no wells are closed we must fall back to chopping again - if (shut_wells.empty()) { - chopTimestep(); - } else { - substepTimer.provideTimeStepEstimate(dt); - if (solverVerbose_) { - const std::string msg = - fmt::format("\nProblematic well(s) were shut: {}" - "(retrying timestep)\n", - fmt::join(shut_wells, " ")); - OpmLog::problem(msg); - } - } +template<class TypeTag> +template<class Solver> +void +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +checkTimeStepMaxRestartLimit_(const int restarts) const +{ + // If we have restarted (i.e. cut the timestep) too + // many times, we have failed and throw an exception. + if (restarts >= solverRestartMax_()) { + const auto msg = fmt::format( + "Solver failed to converge after cutting timestep {} times.", restarts + ); + if (solverVerbose_()) { + OpmLog::error(msg); + } + // Use throw directly to prevent file and line + throw TimeSteppingBreakdown{msg}; + } +} + +template<class TypeTag> +template<class Solver> +void +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +checkTimeStepMinLimit_(const int new_time_step) const +{ + // If we have restarted (i.e. cut the timestep) too + // much, we have failed and throw an exception. + if (new_time_step < minTimeStep_()) { + const auto msg = fmt::format( + "Solver failed to converge after cutting timestep to {}\n" + "which is the minimum threshold given by option --solver-min-time-step\n", + minTimeStep_() + ); + if (solverVerbose_()) { + OpmLog::error(msg); + } + // Use throw directly to prevent file and line + throw TimeSteppingBreakdown{msg}; + } +} + +template<class TypeTag> +template<class Solver> +void +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +chopTimeStep_(const double new_time_step) +{ + setTimeStep_(new_time_step); + if (solverVerbose_()) { + const auto msg = fmt::format("{}\nTimestep chopped to {} days\n", + this->cause_of_failure_, + unit::convert::to(this->substep_timer_.currentStepLength(), unit::day)); + OpmLog::problem(msg); + } +} + +template<class TypeTag> +template<class Solver> +bool +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +chopTimeStepOrCloseFailingWells_(const int new_time_step) +{ + bool wells_shut = false; + // We are below the threshold, and will check if there are any wells we should close + // rather than chopping again. + std::set<std::string> failing_wells = detail::consistentlyFailingWells( + solver_().model().stepReports()); + if (failing_wells.empty()) { + // Found no wells to close, chop the timestep + chopTimeStep_(new_time_step); + } else { + // Close all consistently failing wells that are not under group control + std::vector<std::string> shut_wells; + for (const auto& well : failing_wells) { + bool was_shut = solver_().model().wellModel().forceShutWellByName( + well, this->substep_timer_.simulationTimeElapsed(), /*dont_shut_grup_wells =*/ true); + if (was_shut) { + shut_wells.push_back(well); + } + } + // If no wells are closed we also try to shut wells under group control + if (shut_wells.empty()) { + for (const auto& well : failing_wells) { + bool was_shut = solver_().model().wellModel().forceShutWellByName( + well, this->substep_timer_.simulationTimeElapsed(), /*dont_shut_grup_wells =*/ false); + if (was_shut) { + shut_wells.push_back(well); } } } - problem.setNextTimeStepSize(substepTimer.currentStepLength()); + // If still no wells are closed we must fall back to chopping again + if (shut_wells.empty()) { + chopTimeStep_(new_time_step); + } else { + wells_shut = true; + if (solverVerbose_()) { + const std::string msg = + fmt::format("\nProblematic well(s) were shut: {}" + "(retrying timestep)\n", + fmt::join(shut_wells, " ")); + OpmLog::problem(msg); + } + } } + return wells_shut; +} - // store estimated time step for next reportStep - suggestedNextTimestep_ = substepTimer.currentStepLength(); - if (timestepVerbose_) { - std::ostringstream ss; - substepTimer.report(ss); - ss << "Suggested next step size = " << unit::convert::to(suggestedNextTimestep_, unit::day) << " (days)" << std::endl; - OpmLog::debug(ss.str()); - } +template<class TypeTag> +template<class Solver> +boost::posix_time::ptime +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +currentDateTime_() const +{ + return simulatorTimer_().currentDateTime(); +} - if (! std::isfinite(suggestedNextTimestep_)) { // check for NaN - suggestedNextTimestep_ = timestep; +template<class TypeTag> +template<class Solver> +int +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +getNumIterations_(const SimulatorReportSingle &substep_report) const +{ + if (useNewtonIteration_()) { + return substep_report.total_newton_iterations; + } + else { + return substep_report.total_linear_iterations; } - return report; } template<class TypeTag> -void AdaptiveTimeStepping<TypeTag>:: -updateTUNING(double max_next_tstep, const Tuning& tuning) +template<class Solver> +double +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +growthFactor_() const { - restartFactor_ = tuning.TSFCNV; - growthFactor_ = tuning.TFDIFF; - maxGrowth_ = tuning.TSFMAX; - maxTimeStep_ = tuning.TSMAXZ; - updateNEXTSTEP(max_next_tstep); - timestepAfterEvent_ = tuning.TMAXWC; + return this->adaptive_time_stepping_.growth_factor_; } template<class TypeTag> -void AdaptiveTimeStepping<TypeTag>:: -updateNEXTSTEP(double max_next_tstep) +template<class Solver> +bool +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +ignoreConvergenceFailure_() const { - // \Note Only update next suggested step if TSINIT was explicitly set in TUNING or NEXTSTEP is active. - if (max_next_tstep > 0) { - suggestedNextTimestep_ = max_next_tstep; + return adaptive_time_stepping_.ignore_convergence_failure_; +} + +template<class TypeTag> +template<class Solver> +double +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +maxGrowth_() const +{ + return this->adaptive_time_stepping_.max_growth_; +} + +template<class TypeTag> +template<class Solver> +void +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +maybeReportSubStep_(SimulatorReportSingle substep_report) const +{ + if (timeStepVerbose_()) { + std::ostringstream ss; + substep_report.reportStep(ss); + OpmLog::info(ss.str()); } } template<class TypeTag> -template<class Serializer> -void AdaptiveTimeStepping<TypeTag>:: -serializeOp(Serializer& serializer) +template<class Solver> +double +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +maybeRestrictTimeStepGrowth_(const double dt, double dt_estimate, const int restarts) const { - serializer(timeStepControlType_); - switch (timeStepControlType_) { - case TimeStepControlType::HardCodedTimeStep: - allocAndSerialize<HardcodedTimeStepControl>(serializer); - break; - case TimeStepControlType::PIDAndIterationCount: - allocAndSerialize<PIDAndIterationCountTimeStepControl>(serializer); - break; - case TimeStepControlType::SimpleIterationCount: - allocAndSerialize<SimpleIterationCountTimeStepControl>(serializer); - break; - case TimeStepControlType::PID: - allocAndSerialize<PIDTimeStepControl>(serializer); - break; + // limit the growth of the timestep size by the growth factor + dt_estimate = std::min(dt_estimate, double(maxGrowth_() * dt)); + assert(dt_estimate > 0); + // further restrict time step size growth after convergence problems + if (restarts > 0) { + dt_estimate = std::min(growthFactor_() * dt, dt_estimate); } - serializer(restartFactor_); - serializer(growthFactor_); - serializer(maxGrowth_); - serializer(maxTimeStep_); - serializer(minTimeStep_); - serializer(ignoreConvergenceFailure_); - serializer(solverRestartMax_); - serializer(solverVerbose_); - serializer(timestepVerbose_); - serializer(suggestedNextTimestep_); - serializer(fullTimestepInitially_); - serializer(timestepAfterEvent_); - serializer(useNewtonIteration_); - serializer(minTimeStepBeforeShuttingProblematicWells_); + + return dt_estimate; } +// The maybeUpdateTuning_() lambda callback is defined in SimulatorFullyImplicitBlackoil::runStep() +// It has to be called for each substep since TUNING might have been changed for next sub step due +// to ACTIONX (via NEXTSTEP) or WCYCLE keywords. template<class TypeTag> -AdaptiveTimeStepping<TypeTag> -AdaptiveTimeStepping<TypeTag>:: -serializationTestObjectHardcoded() +template<class Solver> +void +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +maybeUpdateTuningAndTimeStep_() { - return serializationTestObject_<HardcodedTimeStepControl>(); + // TODO: This function is currently only called if NEXTSTEP is activated from ACTIONX or + // if the WCYCLE keyword needs to modify the current timestep. So this method should rather + // be named maybeUpdateTimeStep_() or similar, since it should not update the tuning. However, + // the current definition of the maybeUpdateTuning_() callback is actually calling + // adaptiveTimeStepping_->updateTUNING(max_next_tstep, tuning) which is updating the tuning + // see SimulatorFullyImplicitBlackoil::runStep() for more details. + auto old_value = suggestedNextTimestep_(); + if (this->substepper_.maybeUpdateTuning_(this->substep_timer_.simulationTimeElapsed(), + this->substep_timer_.currentStepLength(), + this->substep_timer_.currentStepNum())) + { + // Either NEXTSTEP and WCYCLE wants to change the current time step, but they cannot + // change the current time step directly. Instead, they change the suggested next time step + // by calling updateNEXTSTEP() via the maybeUpdateTuning() callback. We now need to update + // the current time step to the new suggested time step and reset the suggested time step + // to the old value. + setTimeStep_(suggestedNextTimestep_()); + setSuggestedNextStep_(old_value); + } } template<class TypeTag> -AdaptiveTimeStepping<TypeTag> -AdaptiveTimeStepping<TypeTag>:: -serializationTestObjectPID() +template<class Solver> +double +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +minTimeStepBeforeClosingWells_() const { - return serializationTestObject_<PIDTimeStepControl>(); + return this->adaptive_time_stepping_.min_time_step_before_shutting_problematic_wells_; } template<class TypeTag> -AdaptiveTimeStepping<TypeTag> -AdaptiveTimeStepping<TypeTag>:: -serializationTestObjectPIDIt() +template<class Solver> +double +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +minTimeStep_() const { - return serializationTestObject_<PIDAndIterationCountTimeStepControl>(); + return this->adaptive_time_stepping_.min_time_step_; } template<class TypeTag> -AdaptiveTimeStepping<TypeTag> -AdaptiveTimeStepping<TypeTag>:: -serializationTestObjectSimple() +template<class Solver> +double +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +restartFactor_() const { - return serializationTestObject_<SimpleIterationCountTimeStepControl>(); + return this->adaptive_time_stepping_.restart_factor_; } template<class TypeTag> -bool -AdaptiveTimeStepping<TypeTag>:: -operator==(const AdaptiveTimeStepping<TypeTag>& rhs) const +template<class Solver> +SimulatorReportSingle +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +runSubStep_() { - if (timeStepControlType_ != rhs.timeStepControlType_ || - (timeStepControl_ && !rhs.timeStepControl_) || - (!timeStepControl_ && rhs.timeStepControl_)) { - return false; + SimulatorReportSingle substep_report; + + auto handleFailure = [this, &substep_report] + (const std::string& failure_reason, const std::exception& e, bool log_exception = true) + { + substep_report = solver_().failureReport(); + this->cause_of_failure_ = failure_reason; + if (log_exception && solverVerbose_()) { + std::string message; + message = "Caught Exception: "; + message += e.what(); + OpmLog::debug(message); + } + }; + + try { + substep_report = solver_().step(this->substep_timer_); + if (solverVerbose_()) { + // report number of linear iterations + OpmLog::debug("Overall linear iterations used: " + + std::to_string(substep_report.total_linear_iterations)); + } } - - bool result = false; - switch (timeStepControlType_) { - case TimeStepControlType::HardCodedTimeStep: - result = castAndComp<HardcodedTimeStepControl>(rhs); - break; - case TimeStepControlType::PIDAndIterationCount: - result = castAndComp<PIDAndIterationCountTimeStepControl>(rhs); - break; - case TimeStepControlType::SimpleIterationCount: - result = castAndComp<SimpleIterationCountTimeStepControl>(rhs); - break; - case TimeStepControlType::PID: - result = castAndComp<PIDTimeStepControl>(rhs); - break; + catch (const TooManyIterations& e) { + handleFailure("Solver convergence failure - Iteration limit reached", e); + } + catch (const ConvergenceMonitorFailure& e) { + handleFailure("Convergence monitor failure", e, /*log_exception=*/false); + } + catch (const LinearSolverProblem& e) { + handleFailure("Linear solver convergence failure", e); + } + catch (const NumericalProblem& e) { + handleFailure("Solver convergence failure - Numerical problem encountered", e); + } + catch (const std::runtime_error& e) { + handleFailure("Runtime error encountered", e); + } + catch (const Dune::ISTLError& e) { + handleFailure("ISTL error - Time step too large", e); + } + catch (const Dune::MatrixBlockError& e) { + handleFailure("Matrix block error", e); } - return result - && this->restartFactor_ == rhs.restartFactor_ - && this->growthFactor_ == rhs.growthFactor_ - && this->maxGrowth_ == rhs.maxGrowth_ - && this->maxTimeStep_ == rhs.maxTimeStep_ - && this->minTimeStep_ == rhs.minTimeStep_ - && this->ignoreConvergenceFailure_ == rhs.ignoreConvergenceFailure_ - && this->solverRestartMax_== rhs.solverRestartMax_ - && this->solverVerbose_ == rhs.solverVerbose_ - && this->fullTimestepInitially_ == rhs.fullTimestepInitially_ - && this->timestepAfterEvent_ == rhs.timestepAfterEvent_ - && this->useNewtonIteration_ == rhs.useNewtonIteration_ - && this->minTimeStepBeforeShuttingProblematicWells_ == - rhs.minTimeStepBeforeShuttingProblematicWells_; + return substep_report; } template<class TypeTag> -template<class Controller> -AdaptiveTimeStepping<TypeTag> -AdaptiveTimeStepping<TypeTag>:: -serializationTestObject_() +template<class Solver> +void +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +setTimeStep_(double dt_estimate) { - AdaptiveTimeStepping<TypeTag> result; + this->substep_timer_.provideTimeStepEstimate(dt_estimate); +} + +template<class TypeTag> +template<class Solver> +Solver& +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +solver_() const +{ + return this->substepper_.solver_; +} - result.restartFactor_ = 1.0; - result.growthFactor_ = 2.0; - result.maxGrowth_ = 3.0; - result.maxTimeStep_ = 4.0; - result.minTimeStep_ = 5.0; - result.ignoreConvergenceFailure_ = true; - result.solverRestartMax_ = 6; - result.solverVerbose_ = true; - result.timestepVerbose_ = true; - result.suggestedNextTimestep_ = 7.0; - result.fullTimestepInitially_ = true; - result.useNewtonIteration_ = true; - result.minTimeStepBeforeShuttingProblematicWells_ = 9.0; - result.timeStepControlType_ = Controller::Type; - result.timeStepControl_ = std::make_unique<Controller>(Controller::serializationTestObject()); - return result; +template<class TypeTag> +template<class Solver> +int +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +solverRestartMax_() const +{ + return this->adaptive_time_stepping_.solver_restart_max_; } template<class TypeTag> -void AdaptiveTimeStepping<TypeTag>:: -init_(const UnitSystem& unitSystem) +template<class Solver> +void +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +setSuggestedNextStep_(double step) { - std::tie(timeStepControlType_, - timeStepControl_, - useNewtonIteration_) = detail::createController(unitSystem); + this->adaptive_time_stepping_.setSuggestedNextStep(step); +} - // make sure growth factor is something reasonable - if (growthFactor_ < 1.0) { - OPM_THROW(std::runtime_error, - "Growth factor cannot be less than 1."); +template <class TypeTag> +template <class Solver> +const SimulatorTimer& +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +simulatorTimer_() const +{ + return this->substepper_.simulator_timer_; +} + +template <class TypeTag> +template <class Solver> +bool +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +solverVerbose_() const +{ + return this->adaptive_time_stepping_.solver_verbose_; +} + +template<class TypeTag> +template<class Solver> +boost::posix_time::ptime +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +startDateTime_() const +{ + return simulatorTimer_().startDateTime(); +} + +template <class TypeTag> +template <class Solver> +double +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +suggestedNextTimestep_() const +{ + return this->adaptive_time_stepping_.suggestedNextStep(); +} + +template <class TypeTag> +template <class Solver> +double +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +timeStepControlComputeEstimate_(const double dt, const int iterations, double elapsed) const +{ + // create object to compute the time error, simply forwards the call to the model + SolutionTimeErrorSolverWrapper<Solver> relative_change{solver_()}; + return this->adaptive_time_stepping_.time_step_control_->computeTimeStepSize( + dt, iterations, relative_change, elapsed); +} + +template <class TypeTag> +template <class Solver> +bool +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +timeStepVerbose_() const +{ + return this->adaptive_time_stepping_.timestep_verbose_; +} + +// The suggested time step is the stepsize that will be used as a first try for +// the next sub step. It is updated at the end of each substep. It can also be +// updated by the TUNING or NEXTSTEP keywords at the beginning of each report step or +// at the beginning of each substep by the ACTIONX keyword (via NEXTSTEP), this is +// done by the maybeUpdateTuning_() method which is called at the beginning of each substep +// (and the begginning of each report step). Note that the WCYCLE keyword can also update the +// suggested time step via the maybeUpdateTuning_() method. +template <class TypeTag> +template <class Solver> +void +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +updateSuggestedNextStep_() +{ + auto suggested_next_step = this->substep_timer_.currentStepLength(); + if (! std::isfinite(suggested_next_step)) { // check for NaN + suggested_next_step = this->original_time_step_; } + if (timeStepVerbose_()) { + std::ostringstream ss; + this->substep_timer_.report(ss); + ss << "Suggested next step size = " + << unit::convert::to(suggested_next_step, unit::day) << " (days)" << std::endl; + OpmLog::debug(ss.str()); + } + setSuggestedNextStep_(suggested_next_step); +} + +template <class TypeTag> +template <class Solver> +bool +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +useNewtonIteration_() const +{ + return this->adaptive_time_stepping_.use_newton_iteration_; +} + +template <class TypeTag> +template <class Solver> +double +AdaptiveTimeStepping<TypeTag>::SubStepIteration<Solver>:: +writeOutput_() const +{ + time::StopWatch perf_timer; + perf_timer.start(); + auto& problem = solver_().model().simulator().problem(); + problem.writeOutput(true); + return perf_timer.secsSinceStart(); +} + +/************************************************ + * Private class SolutionTimeErrorSolverWrapper + * **********************************************/ + +template<class TypeTag> +template<class Solver> +AdaptiveTimeStepping<TypeTag>::SolutionTimeErrorSolverWrapper<Solver>::SolutionTimeErrorSolverWrapper( + const Solver& solver +) + : solver_{solver} +{} + +template<class TypeTag> +template<class Solver> +double AdaptiveTimeStepping<TypeTag>::SolutionTimeErrorSolverWrapper<Solver>::relativeChange() const +{ + // returns: || u^n+1 - u^n || / || u^n+1 || + return solver_.model().relativeChange(); } } // namespace Opm -#endif +#endif // OPM_ADAPTIVE_TIME_STEPPING_IMPL_HPP diff --git a/opm/simulators/timestepping/SimulatorTimerInterface.hpp b/opm/simulators/timestepping/SimulatorTimerInterface.hpp index 6875db8711d..3948609bffe 100644 --- a/opm/simulators/timestepping/SimulatorTimerInterface.hpp +++ b/opm/simulators/timestepping/SimulatorTimerInterface.hpp @@ -40,14 +40,15 @@ namespace Opm /// destructor virtual ~SimulatorTimerInterface() {} - /// Current step number. This is the number of timesteps that - /// has been completed from the start of the run. The time - /// after initialization but before the simulation has started - /// is timestep number zero. - virtual int currentStepNum() const = 0; + // ----------------------------------------------------------- + // Pure virtual functions to be implemented by derived classes + // ----------------------------------------------------------- - /// Current report step number. This might differ from currentStepNum in case of sub stepping - virtual int reportStepNum() const { return currentStepNum(); } + /// advance time by currentStepLength + virtual void advance() = 0 ; + + /// return copy of current timer instance + virtual std::unique_ptr< SimulatorTimerInterface > clone () const = 0; /// Current step length. This is the length of the step /// the simulator will take in the next iteration. @@ -55,6 +56,25 @@ namespace Opm /// @note if done(), it is an error to call currentStepLength(). virtual double currentStepLength() const = 0; + /// Current step number. This is the number of timesteps that + /// has been completed from the start of the run. The time + /// after initialization but before the simulation has started + /// is timestep number zero. + virtual int currentStepNum() const = 0; + + /// Return true if timer indicates that simulation of timer interval is finished + virtual bool done() const = 0; + + /// Whether the current step is the first step. + virtual bool initialStep() const = 0; + + /// Return true if last time step failed + virtual bool lastStepFailed() const = 0; + + /// Time elapsed since the start of the simulation until the + /// beginning of the current time step [s]. + virtual double simulationTimeElapsed() const = 0; + /// Previous step length. This is the length of the step that /// was taken to arrive at this time. /// @@ -63,30 +83,13 @@ namespace Opm /// it is an error to call stepLengthTaken(). virtual double stepLengthTaken () const = 0; - /// Previous report step length. This is the length of the step that - /// was taken to arrive at this report step time. - /// - /// @note if no increments have been done (i.e. the timer is - /// still in its constructed state and reportStepNum() == 0), - /// it is an error to call stepLengthTaken(). - virtual double reportStepLengthTaken () const { return stepLengthTaken(); } - - /// Time elapsed since the start of the simulation until the - /// beginning of the current time step [s]. - virtual double simulationTimeElapsed() const = 0; - - /// advance time by currentStepLength - virtual void advance() = 0 ; - - /// Return true if timer indicates that simulation of timer interval is finished - virtual bool done() const = 0; - - /// Whether the current step is the first step. - virtual bool initialStep() const = 0; - /// Return start date of simulation virtual boost::posix_time::ptime startDateTime() const = 0; + // ----------------------------------------------------------------- + // Virtual functions (not pure) to allow for default implementations + // ----------------------------------------------------------------- + /// Return the current time as a posix time object. virtual boost::posix_time::ptime currentDateTime() const; @@ -94,11 +97,17 @@ namespace Opm /// 1970) until the current time step begins [s]. virtual time_t currentPosixTime() const; - /// Return true if last time step failed - virtual bool lastStepFailed() const = 0; + /// Previous report step length. This is the length of the step that + /// was taken to arrive at this report step time. + /// + /// @note if no increments have been done (i.e. the timer is + /// still in its constructed state and reportStepNum() == 0), + /// it is an error to call stepLengthTaken(). + virtual double reportStepLengthTaken () const { return stepLengthTaken(); } + + /// Current report step number. This might differ from currentStepNum in case of sub stepping + virtual int reportStepNum() const { return currentStepNum(); } - /// return copy of current timer instance - virtual std::unique_ptr< SimulatorTimerInterface > clone () const = 0; }; diff --git a/opm/simulators/utils/readDeck.cpp b/opm/simulators/utils/readDeck.cpp index bdb16c75a7c..aae46fcd094 100644 --- a/opm/simulators/utils/readDeck.cpp +++ b/opm/simulators/utils/readDeck.cpp @@ -54,7 +54,11 @@ #include <opm/input/eclipse/Schedule/Action/State.hpp> #include <opm/input/eclipse/Schedule/ArrayDimChecker.hpp> +#include <opm/input/eclipse/Schedule/ResCoup/ReservoirCouplingInfo.hpp> +#include <opm/input/eclipse/Schedule/ResCoup/MasterGroup.hpp> +#include <opm/input/eclipse/Schedule/ResCoup/Slaves.hpp> #include <opm/input/eclipse/Schedule/Schedule.hpp> + #include <opm/input/eclipse/Schedule/UDQ/UDQConfig.hpp> #include <opm/input/eclipse/Schedule/UDQ/UDQState.hpp> #include <opm/input/eclipse/Schedule/Well/WellTestState.hpp> @@ -151,7 +155,7 @@ namespace { if (schedule == nullptr) { schedule = std::make_shared<Opm::Schedule> (deck, eclipseState, parseContext, errorGuard, - std::move(python), lowActionParsingStrictness, /*slave_mode=*/false, + std::move(python), lowActionParsingStrictness, /*slaveMode=*/false, keepKeywords, outputInterval, init_state); } @@ -180,14 +184,14 @@ namespace { std::unique_ptr<Opm::UDQState>& udqState, std::unique_ptr<Opm::Action::State>& actionState, std::unique_ptr<Opm::WellTestState>& wtestState, - Opm::ErrorGuard& errorGuard) + Opm::ErrorGuard& errorGuard, + const bool slaveMode) { if (schedule == nullptr) { schedule = std::make_shared<Opm::Schedule> (deck, eclipseState, parseContext, - errorGuard, std::move(python), lowActionParsingStrictness, keepKeywords); + errorGuard, std::move(python), lowActionParsingStrictness, slaveMode, keepKeywords); } - udqState = std::make_unique<Opm::UDQState> ((*schedule)[0].udq().params().undefinedValue()); @@ -233,6 +237,24 @@ namespace { #endif } + void inconsistentScheduleError(const std::string& message) + { + OPM_THROW(std::logic_error, + fmt::format("Inconsistent SCHEDULE section: {}", message)); + } + + void checkScheduleKeywordConsistency(const Opm::Schedule& schedule) + { + const auto& final_state = schedule.back(); + const auto& rescoup = final_state.rescoup(); + if (rescoup.slaveCount() > 0 && rescoup.masterGroupCount() == 0) { + inconsistentScheduleError("SLAVES keyword without GRUPMAST keyword"); + } + if (rescoup.slaveCount() == 0 && rescoup.masterGroupCount() > 0) { + inconsistentScheduleError("GRUPMAST keyword without SLAVES keyword"); + } + } + void readOnIORank(Opm::Parallel::Communication comm, const std::string& deckFilename, const Opm::ParseContext* parseContext, @@ -249,7 +271,8 @@ namespace { const bool lowActionParsingStrictness, const bool keepKeywords, const std::optional<int>& outputInterval, - Opm::ErrorGuard& errorGuard) + Opm::ErrorGuard& errorGuard, + const bool slaveMode) { OPM_TIMEBLOCK(readDeck); if (((schedule == nullptr) || (summaryConfig == nullptr)) && @@ -282,9 +305,10 @@ namespace { lowActionParsingStrictness, keepKeywords, std::move(python), schedule, udqState, actionState, wtestState, - errorGuard); + errorGuard, slaveMode); } + checkScheduleKeywordConsistency(*schedule); eclipseState->appendAqufluxSchedule(schedule->getAquiferFluxSchedule()); if (Opm::OpmLog::hasBackend("STDOUT_LOGGER")) { @@ -480,7 +504,6 @@ Opm::setupLogging(Parallel::Communication& comm, } logFileStream << ".PRT"; debugFileStream << ".DBG"; - FileOutputMode output; { static std::map<std::string, FileOutputMode> stringToOutputMode = @@ -539,10 +562,10 @@ void Opm::readDeck(Opm::Parallel::Communication comm, const bool initFromRestart, const bool checkDeck, const bool keepKeywords, - const std::optional<int>& outputInterval) + const std::optional<int>& outputInterval, + const bool slaveMode) { auto errorGuard = std::make_unique<ErrorGuard>(); - int parseSuccess = 1; // > 0 is success std::string failureMessage; @@ -573,7 +596,7 @@ void Opm::readDeck(Opm::Parallel::Communication comm, eclipseState, schedule, udqState, actionState, wtestState, summaryConfig, std::move(python), initFromRestart, checkDeck, treatCriticalAsNonCritical, lowActionParsingStrictness, - keepKeywords, outputInterval, *errorGuard); + keepKeywords, outputInterval, *errorGuard, slaveMode); // Update schedule so that re-parsing after actions use same strictness assert(schedule); diff --git a/opm/simulators/utils/readDeck.hpp b/opm/simulators/utils/readDeck.hpp index 87da15315fe..dc7692ad471 100644 --- a/opm/simulators/utils/readDeck.hpp +++ b/opm/simulators/utils/readDeck.hpp @@ -99,7 +99,8 @@ void readDeck(Parallel::Communication comm, bool initFromRestart, bool checkDeck, bool keepKeywords, - const std::optional<int>& outputInterval); + const std::optional<int>& outputInterval, + bool slaveMode); void verifyValidCellGeometry(Parallel::Communication comm, const EclipseState& eclipseState);