REMORA
Regional Modeling of Oceans Refined Adaptively
Loading...
Searching...
No Matches
REMORA_DateClock.H
Go to the documentation of this file.
1#ifndef REMORA_DateClock_H
2#define REMORA_DateClock_H
3
4#include <algorithm>
5#include <cmath>
6#include <cstdio>
7#include <string>
8
9#include <AMReX_Extension.H>
10#include <AMReX_GpuQualifiers.H>
11
12/** \file REMORA_DateClock.H
13 *
14 * Model time to calendar date, ported from ROMS `dateclock.F`.
15 *
16 * The model clock is elapsed time since a reference date, and `remora.time_ref`
17 * both sets that date and picks the calendar, exactly as ROMS `TIME_REF` does:
18 *
19 * | `time_ref` | calendar | epoch | year length |
20 * |----------------|-----------------------|-----------------------|-------------|
21 * | `yyyymmdd.dd` | proleptic Gregorian | the date given | 365.2425 d |
22 * | `0` | proleptic Gregorian | 0001-01-01 00:00:00 | 365.2425 d |
23 * | `-1` | 360_day | 0000-12-30 00:00:00 | 360 d |
24 * | `-2` | Gregorian, truncated | 1968-05-23 00:00:00 | 365.25 d |
25 * Julian day (NASA)
26 *
27 * `time_ref` is passed in rather than read from a global, so these stay pure
28 * functions; ROMS keeps the equivalent in `Rclock` in `mod_scalars`. The
29 * reference date number is recomputed per call, which is a handful of integer
30 * operations, rather than cached.
31 *
32 * The arithmetic is deliberately a transcription rather than a tidier
33 * equivalent. Fortran `INT` and `AINT` truncate toward zero, as do a C++ cast
34 * to `int` and `std::trunc`, and C++ integer division truncates toward zero
35 * too, so each expression carries over term for term. Where ROMS uses `FLOOR`
36 * -- and in the `time_ref = -2` conversion it does, which rounds differently
37 * from `INT` on the negative side of the epoch -- `std::floor` is used here.
38 *
39 * Everything here is `double` regardless of the build's `amrex::Real`. A
40 * `yyyymmdd.dd` reference date carries eight significant integer digits, which a
41 * single-precision float (24-bit mantissa, exact only to 2^24 = 16777216) cannot
42 * hold, and a modern day number (about 737791 for 2020, 2440000 and up for the
43 * truncated Julian day) would keep only a fraction of a day of resolution.
44 *
45 * The day fraction is available from remora_caldate's `yday` for a caller that
46 * needs it. The reference date does carry a time of day -- `remora.time_ref`
47 * can name one -- and remora_ref_clock returns it.
48 *
49 * Two quirks of the `time_ref = -2` calendar carry over, both ROMS's and both
50 * confirmed against the Fortran rather than inferred:
51 *
52 * - `datenum` splits the year as `y01 = MyYear/100`, which truncates toward
53 * zero, so it disagrees with the true Julian day number for years before 0.
54 * `dateclock.F` documents `datenum(-4713,11,24) = 0`; its own code returns
55 * 1, and so does this. From year 1 on it agrees with the standard
56 * Fliegel-Van Flandern day number exactly.
57 * - `datevec` treats any day number below the 15 October 1582 changeover as a
58 * *truncated* one and adds the reference offset back, so it does not invert
59 * `datenum` before that date. That is the heuristic ROMS uses to accept both
60 * full and truncated Julian days in the same argument.
61 *
62 * Neither affects a model run in modern times, which is what this calendar is
63 * for, but both would bite anyone reaching further back with it.
64 */
65
66//! Reference day number of the 15 October 1582 Gregorian changeover, as ROMS
67//! spells it in datevec.
68static constexpr int remora_gregorian_daynum = 2299161;
69
70/** \brief Day of the year for a calendar date. ROMS `yearday`. */
72int remora_yearday (int year, int month, int day) noexcept
73{
74 const bool leap = (((year % 4) == 0) && ((year % 100) != 0)) || ((year % 400) == 0);
75 const int fac = leap ? 1 : 2;
76 return static_cast<int>((275.0 * month) / 9.0) - fac * ((month + 9) / 12) + day - 30;
77}
78
79/**
80 * \brief Fractional day number for a calendar date. ROMS `datenum`.
81 *
82 * Reference values, per calendar:
83 * - proleptic Gregorian: `datenum(0000,01,01) = 1`, `datenum(0001,01,01) = 367`.
84 * The origin is Matlab's `datenum(0000,00,00) = 0`, hence the 61-day offset.
85 * - 360_day: `datenum(0000,01,01) = 0`, `datenum(0001,01,01) = 360`.
86 * - truncated Julian: `datenum(-4713,11,24) = 0`, `datenum(1968,05,23) = 2440000`.
87 */
89double remora_datenum (double time_ref, int year, int month, int day,
90 int hour = 0, int minutes = 0,
91 double seconds = 0.0) noexcept
92{
93 const int cal = static_cast<int>(time_ref);
94 int my_day = 0;
95
96 if (cal == -2) {
97 // Julian day plus the Gregorian correction. Julian days formally start
98 // at noon; here they start at midnight, so this runs 12 hours ahead of
99 // the formal definition.
100 int my_year;
101 int my_month;
102 if (month > 2) {
103 my_year = year;
104 my_month = month - 3;
105 } else {
106 my_year = year - 1;
107 my_month = month + 9;
108 }
109 const int y01 = my_year / 100;
110 my_year -= y01 * 100;
111 my_day = (146097 * y01 / 4) + (1461 * my_year / 4) +
112 ((153 * my_month + 2) / 5) + day + 1721119;
113
114 } else if (cal == -1) {
115 // Every year is 360 days and every month 30.
116 my_day = year * 360 + (month - 1) * 30 + (day - 1);
117
118 } else {
119 // Gregorian and proleptic Gregorian, year length 365.2425.
120 constexpr int offset = 61;
121
122 const int my_month = (month + 9) % 12; // Mar=0, ..., Feb=11
123 const int my_year = year - static_cast<int>(0.1 * my_month); // Jan or Feb: back one
124
125 my_day = static_cast<int>(365.0 * my_year) +
126 static_cast<int>(0.25 * my_year) -
127 static_cast<int>(0.01 * my_year) +
128 static_cast<int>(0.0025 * my_year) +
129 static_cast<int>(0.1 * (my_month * 306.0 + 5.0)) +
130 (day - 1);
131
132 if ((year == 0) && (month == 0) && (day == 0)) {
133 my_day = 0;
134 } else if (my_day < 0) {
135 my_day += offset - 1;
136 } else {
137 my_day += offset;
138 }
139 }
140
141 return double(my_day) +
142 double(hour) / 24.0 +
143 double(minutes) / 1440.0 +
144 seconds / 86400.0;
145}
146
147/**
148 * \brief Reference date the model clock counts from, implied by \p time_ref.
149 *
150 * ROMS `ref_clock`'s date decode, the fields behind `Rclock%string`. The
151 * sentinel calendars carry ROMS's hardcoded dates: 0000-12-30 for 360_day,
152 * whose epoch is shifted back a day to undo a historical one-day offset,
153 * 1968-05-23 for the truncated Julian day, and 0001-01-01 for `time_ref = 0`.
154 */
156void remora_ref_clock (double time_ref, int& year, int& month, int& day,
157 int& hour, int& minute, int& second) noexcept
158{
159 const int cal = static_cast<int>(time_ref);
160
161 if (cal > 0) {
162 // Decode yyyymmdd.dd. The clamps are ROMS's, and they are what keeps a
163 // malformed value from indexing off the end of the calendar.
164 year = std::max(1, static_cast<int>(time_ref * 0.0001));
165 month = std::min(12, std::max(1, static_cast<int>(
166 (time_ref - double(year * 10000)) * 0.01)));
167 const double fday = time_ref - std::trunc(time_ref * 0.01) * 100.0;
168 day = std::max(1, static_cast<int>(fday));
169 const double sec = (fday - std::trunc(fday)) * 86400.0;
170 hour = static_cast<int>(sec / 3600.0);
171 minute = static_cast<int>(std::fmod(sec, 3600.0) / 60.0);
172 second = static_cast<int>(std::fmod(sec, 60.0));
173
174 } else if (cal == -1) {
175 year = 0; month = 12; day = 30; // 360_day, epoch shifted back a day
176 hour = 0; minute = 0; second = 0;
177
178 } else if (cal == -2) {
179 year = 1968; month = 5; day = 23; // truncated Julian day
180 hour = 0; minute = 0; second = 0;
181
182 } else {
183 year = 1; month = 1; day = 1; // proleptic Gregorian
184 hour = 0; minute = 0; second = 0;
185 }
186}
187
188/**
189 * \brief Day number of the reference date implied by \p time_ref.
190 *
191 * ROMS `ref_clock`, reduced to the one field most of this header needs:
192 * `Rclock%DateNumber(1)`. The two negative calendars carry hardcoded values in
193 * ROMS -- 359 for 360_day, whose epoch is shifted to 0000-12-30 to undo a
194 * historical one-day offset, and 2440000 for the truncated Julian day.
195 */
197double remora_ref_datenum (double time_ref) noexcept
198{
199 const int cal = static_cast<int>(time_ref);
200
201 if (cal == -1) {
202 return 359.0; // 0000-12-30 in the 360_day calendar
203 }
204 if (cal == -2) {
205 return 2440000.0; // 1968-05-23, truncated Julian offset
206 }
207
208 // Gregorian and proleptic Gregorian, including time_ref = 0, whose
209 // 0001-01-01 comes back from remora_ref_clock and equals 367.
210 int year, month, day, hour, minute, second;
212 return remora_datenum(time_ref, year, month, day, hour, minute,
213 double(second));
214}
215
216/**
217 * \brief Calendar date for a day (or second) number. ROMS `datevec`.
218 *
219 * \p is_day_units selects fractional days over fractional seconds, as ROMS's
220 * `IsDayUnits` does.
221 *
222 * ROMS's `time_ref = -2` branch can also run a proleptic *Julian* conversion,
223 * but its `ProlepticJulian` switch is a local hardcoded to `.FALSE.`, so that
224 * path is unreachable as shipped and is not ported.
225 */
227void remora_datevec (double time_ref, double date_number, bool is_day_units,
228 int& year, int& month, int& day) noexcept
229{
230 const int cal = static_cast<int>(time_ref);
231
232 if (cal == -2) {
233 // A value at or past the Gregorian changeover is taken to be a full
234 // Julian day number; anything smaller is a truncated one and gets the
235 // reference offset added back.
236 double my_date_number;
237 if (is_day_units) {
240 : date_number + remora_ref_datenum(time_ref);
241 } else {
243 (date_number >= double(remora_gregorian_daynum) * 86400.0)
244 ? date_number / 86400.0
245 : date_number / 86400.0 + remora_ref_datenum(time_ref);
246 }
247
248 // Proleptic Gregorian, origin 24 November 4713 BC. FLOOR throughout,
249 // not truncation: this calendar runs to dates before its own origin.
250 double jr = std::floor(my_date_number) - 1721119.0;
251 double js = 4.0 * jr - 1.0;
252 double yy = std::floor(js / 146097.0);
253 jr = js - 146097.0 * yy;
254 js = std::floor(jr * 0.25);
255 js = 4.0 * js + 3.0;
256 jr = std::floor(js / 1461.0);
257 const double dd = std::floor(((js - 1461.0 * jr) + 4.0) * 0.25);
258 js = 5.0 * dd - 3.0;
259 const double mo = std::floor(js / 153.0);
260 yy = yy * 100.0 + jr;
261
262 if (mo < 10.0) {
263 year = static_cast<int>(yy);
264 month = static_cast<int>(mo + 3.0);
265 } else {
266 year = static_cast<int>(yy + 1.0);
267 month = static_cast<int>(mo - 9.0);
268 }
269 day = static_cast<int>(((js - 153.0 * mo) + 5.0) * 0.2);
270
271 } else if (cal == -1) {
272 // 360_day: 12 months of 30 days, no leap years.
273 int yday;
274 if (is_day_units) {
275 year = static_cast<int>(date_number / 360.0);
276 yday = static_cast<int>(date_number - double(year * 360) + 1.0);
277 } else {
278 year = static_cast<int>(date_number / 31104000.0); // 360*86400
279 yday = static_cast<int>((date_number - double(year * 31104000) + 1.0) /
280 86400.0);
281 }
282 month = ((yday - 1) / 30) + 1;
283 day = ((yday - 1) % 30) + 1;
284
285 } else {
286 // Gregorian and proleptic Gregorian.
287 constexpr double offset = 61.0;
288
290 : date_number / 86400.0;
291 if (my_date_number < offset) { // adjust for the Matlab zero
293 } else { // origin, datenum(0,0,0) = 0
295 }
296
297 int my_year = static_cast<int>((10000.0 * std::trunc(my_date_number) + 14780.0) /
298 3652425.0);
299 auto days_before_year = [] (int y) {
300 return static_cast<int>(365.0 * y) + static_cast<int>(0.25 * y) -
301 static_cast<int>(0.01 * y) + static_cast<int>(0.0025 * y);
302 };
303 int my_day = static_cast<int>(my_date_number) - days_before_year(my_year);
304 if (my_day < 0) { // before 1 March; this keeps
305 my_year -= 1; // leap years easy
306 my_day = static_cast<int>(my_date_number) - days_before_year(my_year);
307 }
308
309 const int my_month = static_cast<int>((100.0 * my_day + 52.0) / 3060.0);
310 month = ((my_month + 2) % 12) + 1;
311 year = my_year + static_cast<int>((my_month + 2.0) / 12.0);
312 day = my_day - static_cast<int>(0.1 * (my_month * 306.0 + 5.0)) + 1;
313
314 // Matches Matlab "datestr" with the origin at 0000-00-00 00:00:00.
315 if (date_number == 0.0) {
316 year = 0;
317 month = 1;
318 day = 0;
319 }
320 }
321}
322
323/**
324 * \brief Model time in days to calendar date. ROMS `caldate`.
325 *
326 * @param[in] time_ref remora.time_ref; see the table at the top
327 * @param[in] current_time_days model time in days, as ROMS `tdays`
328 * @param[out] year year including century
329 * @param[out] month month of the year, 1 = January
330 * @param[out] day day of the month
331 * @param[out] yday day of the year, including the day fraction,
332 * so 00:00 on the first day of the year is
333 * exactly 1.0
334 */
336void remora_caldate (double time_ref, double current_time_days,
337 int& year, int& month, int& day, double& yday) noexcept
338{
339 const int cal = static_cast<int>(time_ref);
340 const double ref_date_number = remora_ref_datenum(time_ref);
341
342 // For the truncated Julian day, a model time already past the reference is
343 // taken to be an absolute day number rather than an elapsed interval.
344 double date_number;
345 if ((cal == -2) && (current_time_days >= ref_date_number)) {
347 } else {
349 }
350
351 const double day_fraction = std::abs(date_number - std::trunc(date_number));
352
353 remora_datevec(time_ref, date_number, true, year, month, day);
354
355 // The 360_day calendar has no leap years for yearday to reason about, so
356 // ROMS takes its day of year straight from the day number.
357 const int int_yday = (cal == -1)
358 ? static_cast<int>(date_number - double(year * 360) + 1.0)
360
361 yday = double(int_yday) + day_fraction;
362}
363
364/** \brief remora_caldate for callers that only want the year and day of year. */
366void remora_caldate (double time_ref, double current_time_days,
367 int& year, double& yday) noexcept
368{
369 int month = 0;
370 int day = 0;
372}
373
374/**
375 * \brief Whether \p time_ref names a calendar this header implements.
376 *
377 * ROMS's `CALENDAR` if-chain has no final `ELSE`, so a value below -2 leaves
378 * the date undefined rather than failing. Callers should reject it up front.
379 */
381bool remora_time_ref_is_valid (double time_ref) noexcept
382{
383 return static_cast<int>(time_ref) >= -2;
384}
385
386/**
387 * \brief CF calendar name for \p time_ref. ROMS `ref_clock`'s `Rclock%calendar`.
388 *
389 * Written next to every time stamp in NetCDF output, so a reader can tell which
390 * calendar the model clock is on.
391 */
393std::string remora_ref_calendar (double time_ref)
394{
395 const int cal = static_cast<int>(time_ref);
396
397 if (cal == -1) {
398 return "360_day";
399 }
400 if (cal == -2) {
401 return "gregorian";
402 }
403 return "proleptic_gregorian";
404}
405
406/**
407 * \brief Reference date as `YYYY-MM-DD hh:mm:ss`. ROMS `ref_clock`'s `Rclock%string`.
408 *
409 * The "since" half of a CF time-units attribute: prefix `days since` or
410 * `seconds since` to get the units of a time stamp on the model clock. Host
411 * only, since it builds a std::string.
412 */
414std::string remora_ref_date_string (double time_ref)
415{
416 int year, month, day, hour, minute, second;
418
419 // ROMS FORMAT (i4.4,'-',i2.2,'-',i2.2,1x,i2.2,':',i2.2,':',i2.2). Sized for
420 // the widest an int can print in all six fields, not the 19 characters a
421 // real date takes, so the compiler can see the format never truncates.
422 char buf[6 * 11 + 5 + 1];
423 std::snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d",
425 return std::string(buf);
426}
427
428#endif
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE void remora_ref_clock(double time_ref, int &year, int &month, int &day, int &hour, int &minute, int &second) noexcept
Reference date the model clock counts from, implied by time_ref.
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE int remora_yearday(int year, int month, int day) noexcept
Day of the year for a calendar date. ROMS yearday.
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE double remora_datenum(double time_ref, int year, int month, int day, int hour=0, int minutes=0, double seconds=0.0) noexcept
Fractional day number for a calendar date. ROMS datenum.
AMREX_FORCE_INLINE std::string remora_ref_date_string(double time_ref)
Reference date as YYYY-MM-DD hh:mm:ss. ROMS ref_clock's Rclockstring.
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE void remora_datevec(double time_ref, double date_number, bool is_day_units, int &year, int &month, int &day) noexcept
Calendar date for a day (or second) number. ROMS datevec.
static constexpr int remora_gregorian_daynum
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE double remora_ref_datenum(double time_ref) noexcept
Day number of the reference date implied by time_ref.
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE bool remora_time_ref_is_valid(double time_ref) noexcept
Whether time_ref names a calendar this header implements.
AMREX_FORCE_INLINE std::string remora_ref_calendar(double time_ref)
CF calendar name for time_ref. ROMS ref_clock's Rclockcalendar.
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE void remora_caldate(double time_ref, double current_time_days, int &year, int &month, int &day, double &yday) noexcept
Model time in days to calendar date. ROMS caldate.
mf_h setVal(geomdata.ProbHi(2))