diff --git a/opm/output/eclipse/InteHEAD.hpp b/opm/output/eclipse/InteHEAD.hpp index 1c0115d96..61741d9bb 100755 --- a/opm/output/eclipse/InteHEAD.hpp +++ b/opm/output/eclipse/InteHEAD.hpp @@ -21,6 +21,7 @@ #define OPM_INTEHEAD_HEADER_INCLUDED #include +#include #include #include @@ -58,15 +59,16 @@ namespace Opm { namespace RestartIO { int nplmix; }; - - struct Date { + struct TimePoint { int year; - int month; // 1..12 - int day; // 1..31 + int month; // 1..12 + int day; // 1..31 - int hour; // 0..23 - int minute; // 0..59 - int second; // 0..59 + int hour; // 0..23 + int minute; // 0..59 + int second; // 0..59 + + int microseconds; // 0..999999 }; struct Phases { @@ -99,7 +101,7 @@ namespace Opm { namespace RestartIO { InteHEAD& unitConventions(const UnitSystem& usys); InteHEAD& wellTableDimensions(const WellTableDim& wtdim); - InteHEAD& calenderDate(const Date& date); + InteHEAD& calenderDate(const TimePoint& date); InteHEAD& activePhases(const Phases& phases); InteHEAD& params_NWELZ(const int niwelz, const int nswelz, const int nxwelz, const int nzwelz); InteHEAD& params_NCON(const int niconz, const int nsconz, const int nxconz); @@ -120,6 +122,11 @@ namespace Opm { namespace RestartIO { std::vector data_; }; + std::time_t makeUTCTime(const std::tm& timePoint); + + InteHEAD::TimePoint + getSimulationTimePoint(const std::time_t start, + const double elapsed); }} // Opm::RestartIO #endif // OPM_INTEHEAD_HEADER_INCLUDED diff --git a/src/opm/output/eclipse/CreateInteHead.cpp b/src/opm/output/eclipse/CreateInteHead.cpp index 26f2cea11..dced225b4 100755 --- a/src/opm/output/eclipse/CreateInteHead.cpp +++ b/src/opm/output/eclipse/CreateInteHead.cpp @@ -33,7 +33,6 @@ #include #include -#include #include #include #include @@ -107,44 +106,6 @@ namespace { return US::Metric; } - Opm::RestartIO::InteHEAD::Date - getSimulationDate(const ::Opm::Schedule& sched, - const double simTime) - { - // Would have liked to use H.Hinnant's "date" library here - // (https://github.com/HowardHinnant/date), especially to avoid - // dealing with std::time_t and the threading issues of calendar - // function std::gmtime(). Library proposed (LEWG) for C++20. - // - // Pray that we're on a system for which std::time_t is 64 bits (or - // more) so that we at least don't run into 2k38 with the typical - // definition of time_point::time_since_epoch(system_clock::now()) - - using namespace std::chrono; - - const auto start = system_clock:: - from_time_t(sched.posixStartTime()); - - // Discard sub-second resolution. - const auto now = start + duration_cast( - duration(simTime)); - - const auto t = system_clock::to_time_t(now); - - // Not thread-safe (returns pointer to internal, static storage). - // May wish to add some locking here... - const auto timepoint = *std::localtime(&t); - - return { - timepoint.tm_year + 1900, - timepoint.tm_mon + 1, - timepoint.tm_mday , - timepoint.tm_hour , - timepoint.tm_min , - std::min(timepoint.tm_sec, 59), // Ignore leap seconds... - }; - } - Opm::RestartIO::InteHEAD::Phases getActivePhases(const ::Opm::Runspec& rspec) { @@ -256,7 +217,7 @@ createInteHead(const EclipseState& es, .numActive (static_cast(grid.getNumActive())) .unitConventions (getUnitConvention(es.getDeckUnitSystem())) .wellTableDimensions(getWellTableDims(rspec, sched, report_step)) - .calenderDate (getSimulationDate(sched, simTime)) + .calenderDate (getSimulationTimePoint(sched.posixStartTime(), simTime)) .activePhases (getActivePhases(rspec)) .params_NWELZ (155, 122, 130, 3) .params_NCON (25, 40, 58) diff --git a/src/opm/output/eclipse/InteHEAD.cpp b/src/opm/output/eclipse/InteHEAD.cpp index b234272fb..709b0d642 100755 --- a/src/opm/output/eclipse/InteHEAD.cpp +++ b/src/opm/output/eclipse/InteHEAD.cpp @@ -1,5 +1,10 @@ #include +#include +#include +#include +#include +#include #include #include @@ -491,17 +496,21 @@ Opm::RestartIO::InteHEAD::wellTableDimensions(const WellTableDim& wtdim) } Opm::RestartIO::InteHEAD& -Opm::RestartIO::InteHEAD::calenderDate(const Date& date) +Opm::RestartIO::InteHEAD::calenderDate(const TimePoint& timePoint) { - this->data_[DAY] = date.day; - this->data_[MONTH] = date.month; - this->data_[YEAR] = date.year; + this->data_[DAY] = timePoint.day; + this->data_[MONTH] = timePoint.month; + this->data_[YEAR] = timePoint.year; - this->data_[IHOURZ] = date.hour; - this->data_[IMINTS] = date.minute; + this->data_[IHOURZ] = timePoint.hour; + this->data_[IMINTS] = timePoint.minute; + + this->data_[IHOURZ] = timePoint.hour; + this->data_[IMINTS] = timePoint.minute; // Microseonds... - this->data_[ISECND] = (date.second * 1000) * 1000; + this->data_[ISECND] = + ((timePoint.second * 1000) * 1000) + timePoint.microseconds; return *this; } @@ -633,4 +642,66 @@ Opm::RestartIO::InteHEAD::regionDimensions(const RegDims& rdim) this->data_[NMFIPR] = rdim.nmfipr; return *this; -} \ No newline at end of file +} + +// ===================================================================== +// Free functions (calendar/time utilities) +// ===================================================================== + +namespace { + std::time_t advance(const std::time_t tp, const double sec) + { + using namespace std::chrono; + + using TP = time_point; + using DoubSec = duration; + + const auto t = system_clock::from_time_t(tp) + + duration_cast(DoubSec(sec)); + + return system_clock::to_time_t(t); + } +} + +std::time_t +Opm::RestartIO::makeUTCTime(const std::tm& timePoint) +{ + auto tp = timePoint; // Mutable copy. + const auto ltime = std::mktime(&tp); + auto tmval = *std::gmtime(<ime); // Mutable. + + // offset = ltime - tmval + // == #seconds by which 'ltime' is AHEAD of tmval. + const auto offset = + std::difftime(ltime, std::mktime(&tmval)); + + // Advance 'ltime' by 'offset' so that std::gmtime(return value) will + // have the same broken-down elements as 'tp'. + return advance(ltime, offset); +} + +Opm::RestartIO::InteHEAD::TimePoint +Opm::RestartIO::getSimulationTimePoint(const std::time_t start, + const double elapsed) +{ + const auto now = advance(start, elapsed); + const auto tp = *std::gmtime(&now); + + auto sec = 0.0; // Not really used here. + auto usec = std::floor(1.0e6 * std::modf(elapsed, &sec)); + + return { + // Y-m-d + tp.tm_year + 1900, + tp.tm_mon + 1, + tp.tm_mday , + + // H:M:S + tp.tm_hour , + tp.tm_min , + std::min(tp.tm_sec, 59), // Ignore leap seconds + + // Fractional seconds in microsecond resolution. + static_cast(usec), + }; +} diff --git a/tests/test_InteHEAD.cpp b/tests/test_InteHEAD.cpp index c2da4c6a0..45b6edba4 100755 --- a/tests/test_InteHEAD.cpp +++ b/tests/test_InteHEAD.cpp @@ -23,8 +23,52 @@ #include +#include +#include +#include +#include + +#include #include #include +#include +#include // partial_sum() +#include +#include + +namespace { + std::vector elapsedTime(const Opm::TimeMap& tmap) + { + auto elapsed = std::vector{}; + + elapsed.reserve(tmap.numTimesteps() + 1); + elapsed.push_back(0.0); + + for (auto nstep = tmap.numTimesteps(), + step = 0*nstep; step < nstep; ++step) + { + elapsed.push_back(tmap.getTimeStepLength(step)); + } + + std::partial_sum(std::begin(elapsed), std::end(elapsed), + std::begin(elapsed)); + + return elapsed; + } + + void expectDate(const Opm::RestartIO::InteHEAD::TimePoint& tp, + const int year, const int month, const int day) + { + BOOST_CHECK_EQUAL(tp.year , year); + BOOST_CHECK_EQUAL(tp.month , month); + BOOST_CHECK_EQUAL(tp.day , day); + + BOOST_CHECK_EQUAL(tp.hour , 0); + BOOST_CHECK_EQUAL(tp.minute , 0); + BOOST_CHECK_EQUAL(tp.second , 0); + BOOST_CHECK_EQUAL(tp.microseconds, 0); + } +} // Anonymous BOOST_AUTO_TEST_SUITE(Member_Functions) @@ -128,11 +172,11 @@ BOOST_AUTO_TEST_CASE(WellTableDimensions) BOOST_AUTO_TEST_CASE(CalendarDate) { - // 2015-04-09T11:22:33+0000 + // 2015-04-09T11:22:33.987654+0000 const auto ih = Opm::RestartIO::InteHEAD{} .calenderDate({ - 2015, 4, 9, 11, 22, 33 + 2015, 4, 9, 11, 22, 33, 987654, }); const auto& v = ih.data(); @@ -143,7 +187,7 @@ BOOST_AUTO_TEST_CASE(CalendarDate) BOOST_CHECK_EQUAL(v[207 - 1], 11); // Hour BOOST_CHECK_EQUAL(v[208 - 1], 22); // Minute - BOOST_CHECK_EQUAL(v[411 - 1], 33000000); // Second (in microseconds) + BOOST_CHECK_EQUAL(v[411 - 1], 33987654); // Second (in microseconds) } BOOST_AUTO_TEST_CASE(ActivePhases) @@ -364,5 +408,64 @@ BOOST_AUTO_TEST_CASE(regionDimensions) BOOST_CHECK_EQUAL(v[99], nmfipr); // NMFIPR } +BOOST_AUTO_TEST_CASE(SimulationDate) +{ + const auto input = std::string { R"( +RUNSPEC + +START + 1 JAN 2000 +/ + +SCHEDULE + +DATES + 1 'JAN' 2001 / +/ + +TSTEP +--Advance the simulater for TEN years: + 10*365.0D0 / +)" }; + + const auto tmap = ::Opm::TimeMap { + ::Opm::Parser{}.parseString(input) + }; + + const auto start = tmap.getStartTime(0); + const auto elapsed = elapsedTime(tmap); + + auto checkDate = [start, &elapsed] + (const std::vector::size_type i, + const std::array& expectYMD) -> void + { + using ::Opm::RestartIO::getSimulationTimePoint; + + expectDate(getSimulationTimePoint(start, elapsed[i]), + expectYMD[0], expectYMD[1], expectYMD[2]); + }; + + // START + checkDate(0, { 2000, 1, 1 }); // Start == 2000-01-01 + + // DATES (2000 being leap year is immaterial) + checkDate(1, { 2001, 1, 1 }); // RStep 1 == 2000-01-01 -> 2001-01-01 + + // TSTEP + checkDate(2, { 2002, 1, 1 }); // RStep 2 == 2001-01-01 -> 2002-01-01 + checkDate(3, { 2003, 1, 1 }); // RStep 3 == 2002-01-01 -> 2003-01-01 + checkDate(4, { 2004, 1, 1 }); // RStep 4 == 2003-01-01 -> 2004-01-01 + + // Leap year: 2004 + checkDate(5, { 2004, 12, 31 }); // RStep 5 == 2004-01-01 -> 2004-12-31 + checkDate(6, { 2005, 12, 31 }); // RStep 6 == 2004-12-31 -> 2005-12-31 + checkDate(7, { 2006, 12, 31 }); // RStep 7 == 2005-12-31 -> 2006-12-31 + checkDate(8, { 2007, 12, 31 }); // RStep 8 == 2006-12-31 -> 2007-12-31 + + // Leap year: 2008 + checkDate( 9, { 2008, 12, 30 }); // RStep 9 == 2007-12-31 -> 2008-12-30 + checkDate(10, { 2009, 12, 30 }); // RStep 10 == 2008-12-30 -> 2009-12-30 + checkDate(11, { 2010, 12, 30 }); // RStep 11 == 2009-12-30 -> 2010-12-30 +} BOOST_AUTO_TEST_SUITE_END()