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 <type_traits>
7
8#include <AMReX_REAL.H>
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 * Hours, minutes and seconds are not returned. ROMS derives them from the day
40 * fraction through the tolerant rounding helper in `round_mod`, and
41 * half-porting that would be worse than leaving it out; the day fraction is
42 * available from remora_caldate's `yday` for a caller that needs it.
43 *
44 * Two quirks of the `time_ref = -2` calendar carry over, both ROMS's and both
45 * confirmed against the Fortran rather than inferred:
46 *
47 * - `datenum` splits the year as `y01 = MyYear/100`, which truncates toward
48 * zero, so it disagrees with the true Julian day number for years before 0.
49 * `dateclock.F` documents `datenum(-4713,11,24) = 0`; its own code returns
50 * 1, and so does this. From year 1 on it agrees with the standard
51 * Fliegel-Van Flandern day number exactly.
52 * - `datevec` treats any day number below the 15 October 1582 changeover as a
53 * *truncated* one and adds the reference offset back, so it does not invert
54 * `datenum` before that date. That is the heuristic ROMS uses to accept both
55 * full and truncated Julian days in the same argument.
56 *
57 * Neither affects a model run in modern times, which is what this calendar is
58 * for, but both would bite anyone reaching further back with it.
59 */
60
61//! Reference day number of the 15 October 1582 Gregorian changeover, as ROMS
62//! spells it in datevec.
63static constexpr int remora_gregorian_daynum = 2299161;
64
65/** \brief Day of the year for a calendar date. ROMS `yearday`. */
67int remora_yearday (int year, int month, int day) noexcept
68{
69 const bool leap = (((year % 4) == 0) && ((year % 100) != 0)) || ((year % 400) == 0);
70 const int fac = leap ? 1 : 2;
71 return static_cast<int>((275.0 * month) / 9.0) - fac * ((month + 9) / 12) + day - 30;
72}
73
74/**
75 * \brief Fractional day number for a calendar date. ROMS `datenum`.
76 *
77 * Reference values, per calendar:
78 * - proleptic Gregorian: `datenum(0000,01,01) = 1`, `datenum(0001,01,01) = 367`.
79 * The origin is Matlab's `datenum(0000,00,00) = 0`, hence the 61-day offset.
80 * - 360_day: `datenum(0000,01,01) = 0`, `datenum(0001,01,01) = 360`.
81 * - truncated Julian: `datenum(-4713,11,24) = 0`, `datenum(1968,05,23) = 2440000`.
82 */
84amrex::Real remora_datenum (amrex::Real time_ref, int year, int month, int day,
85 int hour = 0, int minutes = 0,
86 amrex::Real seconds = amrex::Real(0.0)) noexcept
87{
88 const int cal = static_cast<int>(time_ref);
89 int my_day = 0;
90
91 if (cal == -2) {
92 // Julian day plus the Gregorian correction. Julian days formally start
93 // at noon; here they start at midnight, so this runs 12 hours ahead of
94 // the formal definition.
95 int my_year;
96 int my_month;
97 if (month > 2) {
98 my_year = year;
99 my_month = month - 3;
100 } else {
101 my_year = year - 1;
102 my_month = month + 9;
103 }
104 const int y01 = my_year / 100;
105 my_year -= y01 * 100;
106 my_day = (146097 * y01 / 4) + (1461 * my_year / 4) +
107 ((153 * my_month + 2) / 5) + day + 1721119;
108
109 } else if (cal == -1) {
110 // Every year is 360 days and every month 30.
111 my_day = year * 360 + (month - 1) * 30 + (day - 1);
112
113 } else {
114 // Gregorian and proleptic Gregorian, year length 365.2425.
115 constexpr int offset = 61;
116
117 const int my_month = (month + 9) % 12; // Mar=0, ..., Feb=11
118 const int my_year = year - static_cast<int>(0.1 * my_month); // Jan or Feb: back one
119
120 my_day = static_cast<int>(365.0 * my_year) +
121 static_cast<int>(0.25 * my_year) -
122 static_cast<int>(0.01 * my_year) +
123 static_cast<int>(0.0025 * my_year) +
124 static_cast<int>(0.1 * (my_month * 306.0 + 5.0)) +
125 (day - 1);
126
127 if ((year == 0) && (month == 0) && (day == 0)) {
128 my_day = 0;
129 } else if (my_day < 0) {
130 my_day += offset - 1;
131 } else {
132 my_day += offset;
133 }
134 }
135
136 return amrex::Real(my_day) +
137 amrex::Real(hour) / amrex::Real(24.0) +
138 amrex::Real(minutes) / amrex::Real(1440.0) +
139 seconds / amrex::Real(86400.0);
140}
141
142/**
143 * \brief Day number of the reference date implied by \p time_ref.
144 *
145 * ROMS `ref_clock`, reduced to the one field the rest of this header needs:
146 * `Rclock%DateNumber(1)`. The two negative calendars carry hardcoded values in
147 * ROMS -- 359 for 360_day, whose epoch is shifted to 0000-12-30 to undo a
148 * historical one-day offset, and 2440000 for the truncated Julian day.
149 */
151amrex::Real remora_ref_datenum (amrex::Real time_ref) noexcept
152{
153 const int cal = static_cast<int>(time_ref);
154
155 if (cal > 0) {
156 // Decode yyyymmdd.dd. The clamps are ROMS's, and they are what keeps a
157 // malformed value from indexing off the end of the calendar.
158 const int iyear = std::max(1, static_cast<int>(time_ref * amrex::Real(0.0001)));
159 const int month = std::min(12, std::max(1, static_cast<int>(
160 (time_ref - amrex::Real(iyear * 10000)) * amrex::Real(0.01))));
161 const amrex::Real day = time_ref -
162 std::trunc(time_ref * amrex::Real(0.01)) * amrex::Real(100.0);
163 const int iday = std::max(1, static_cast<int>(day));
164 const amrex::Real sec = (day - std::trunc(day)) * amrex::Real(86400.0);
165 const int ihour = static_cast<int>(sec / amrex::Real(3600.0));
166 const int minute = static_cast<int>(std::fmod(sec, amrex::Real(3600.0)) /
167 amrex::Real(60.0));
168 const int isec = static_cast<int>(std::fmod(sec, amrex::Real(60.0)));
169 return remora_datenum(time_ref, iyear, month, iday, ihour, minute,
170 amrex::Real(isec));
171 }
172 if (cal == -1) {
173 return amrex::Real(359.0); // 0000-12-30 in the 360_day calendar
174 }
175 if (cal == -2) {
176 return amrex::Real(2440000.0); // 1968-05-23, truncated Julian offset
177 }
178 return remora_datenum(time_ref, 1, 1, 1); // 0001-01-01, equals 367
179}
180
181/**
182 * \brief Calendar date for a day (or second) number. ROMS `datevec`.
183 *
184 * \p is_day_units selects fractional days over fractional seconds, as ROMS's
185 * `IsDayUnits` does.
186 *
187 * ROMS's `time_ref = -2` branch can also run a proleptic *Julian* conversion,
188 * but its `ProlepticJulian` switch is a local hardcoded to `.FALSE.`, so that
189 * path is unreachable as shipped and is not ported.
190 */
192void remora_datevec (amrex::Real time_ref, amrex::Real date_number, bool is_day_units,
193 int& year, int& month, int& day) noexcept
194{
195 const int cal = static_cast<int>(time_ref);
196
197 if (cal == -2) {
198 // A value at or past the Gregorian changeover is taken to be a full
199 // Julian day number; anything smaller is a truncated one and gets the
200 // reference offset added back.
201 amrex::Real my_date_number;
202 if (is_day_units) {
205 : date_number + remora_ref_datenum(time_ref);
206 } else {
208 (date_number >= amrex::Real(remora_gregorian_daynum) * amrex::Real(86400.0))
209 ? date_number / amrex::Real(86400.0)
210 : date_number / amrex::Real(86400.0) + remora_ref_datenum(time_ref);
211 }
212
213 // Proleptic Gregorian, origin 24 November 4713 BC. FLOOR throughout,
214 // not truncation: this calendar runs to dates before its own origin.
215 amrex::Real jr = std::floor(my_date_number) - amrex::Real(1721119.0);
216 amrex::Real js = amrex::Real(4.0) * jr - amrex::Real(1.0);
217 amrex::Real yy = std::floor(js / amrex::Real(146097.0));
218 jr = js - amrex::Real(146097.0) * yy;
219 js = std::floor(jr * amrex::Real(0.25));
220 js = amrex::Real(4.0) * js + amrex::Real(3.0);
221 jr = std::floor(js / amrex::Real(1461.0));
222 const amrex::Real dd =
223 std::floor(((js - amrex::Real(1461.0) * jr) + amrex::Real(4.0)) * amrex::Real(0.25));
224 js = amrex::Real(5.0) * dd - amrex::Real(3.0);
225 const amrex::Real mo = std::floor(js / amrex::Real(153.0));
226 yy = yy * amrex::Real(100.0) + jr;
227
228 if (mo < amrex::Real(10.0)) {
229 year = static_cast<int>(yy);
230 month = static_cast<int>(mo + amrex::Real(3.0));
231 } else {
232 year = static_cast<int>(yy + amrex::Real(1.0));
233 month = static_cast<int>(mo - amrex::Real(9.0));
234 }
235 day = static_cast<int>(((js - amrex::Real(153.0) * mo) + amrex::Real(5.0)) *
236 amrex::Real(0.2));
237
238 } else if (cal == -1) {
239 // 360_day: 12 months of 30 days, no leap years.
240 int yday;
241 if (is_day_units) {
242 year = static_cast<int>(date_number / amrex::Real(360.0));
243 yday = static_cast<int>(date_number - amrex::Real(year * 360) + amrex::Real(1.0));
244 } else {
245 year = static_cast<int>(date_number / amrex::Real(31104000.0)); // 360*86400
246 yday = static_cast<int>((date_number - amrex::Real(year * 31104000) +
247 amrex::Real(1.0)) / amrex::Real(86400.0));
248 }
249 month = ((yday - 1) / 30) + 1;
250 day = ((yday - 1) % 30) + 1;
251
252 } else {
253 // Gregorian and proleptic Gregorian.
254 constexpr amrex::Real offset = amrex::Real(61.0);
255
257 : date_number / amrex::Real(86400.0);
258 if (my_date_number < offset) { // adjust for the Matlab zero
259 my_date_number = my_date_number - offset + amrex::Real(1.0);
260 } else { // origin, datenum(0,0,0) = 0
262 }
263
264 int my_year = static_cast<int>((10000.0 * std::trunc(my_date_number) + 14780.0) /
265 3652425.0);
266 auto days_before_year = [] (int y) {
267 return static_cast<int>(365.0 * y) + static_cast<int>(0.25 * y) -
268 static_cast<int>(0.01 * y) + static_cast<int>(0.0025 * y);
269 };
270 int my_day = static_cast<int>(my_date_number) - days_before_year(my_year);
271 if (my_day < 0) { // before 1 March; this keeps
272 my_year -= 1; // leap years easy
273 my_day = static_cast<int>(my_date_number) - days_before_year(my_year);
274 }
275
276 const int my_month = static_cast<int>((100.0 * my_day + 52.0) / 3060.0);
277 month = ((my_month + 2) % 12) + 1;
278 year = my_year + static_cast<int>((my_month + 2.0) / 12.0);
279 day = my_day - static_cast<int>(0.1 * (my_month * 306.0 + 5.0)) + 1;
280
281 // Matches Matlab "datestr" with the origin at 0000-00-00 00:00:00.
282 if (date_number == amrex::Real(0.0)) {
283 year = 0;
284 month = 1;
285 day = 0;
286 }
287 }
288}
289
290/**
291 * \brief Model time in days to calendar date. ROMS `caldate`.
292 *
293 * @param[in] time_ref remora.time_ref; see the table at the top
294 * @param[in] current_time_days model time in days, as ROMS `tdays`
295 * @param[out] year year including century
296 * @param[out] month month of the year, 1 = January
297 * @param[out] day day of the month
298 * @param[out] yday day of the year, including the day fraction,
299 * so 00:00 on the first day of the year is
300 * exactly 1.0
301 */
303void remora_caldate (amrex::Real time_ref, amrex::Real current_time_days,
304 int& year, int& month, int& day, amrex::Real& yday) noexcept
305{
306 const int cal = static_cast<int>(time_ref);
307 const amrex::Real ref_date_number = remora_ref_datenum(time_ref);
308
309 // For the truncated Julian day, a model time already past the reference is
310 // taken to be an absolute day number rather than an elapsed interval.
311 amrex::Real date_number;
312 if ((cal == -2) && (current_time_days >= ref_date_number)) {
314 } else {
316 }
317
318 const amrex::Real day_fraction = std::abs(date_number - std::trunc(date_number));
319
320 remora_datevec(time_ref, date_number, true, year, month, day);
321
322 // The 360_day calendar has no leap years for yearday to reason about, so
323 // ROMS takes its day of year straight from the day number.
324 const int int_yday = (cal == -1)
325 ? static_cast<int>(date_number - amrex::Real(year * 360) + amrex::Real(1.0))
327
328 yday = amrex::Real(int_yday) + day_fraction;
329}
330
331/** \brief remora_caldate for callers that only want the year and day of year. */
333void remora_caldate (amrex::Real time_ref, amrex::Real current_time_days,
334 int& year, amrex::Real& yday) noexcept
335{
336 int month = 0;
337 int day = 0;
339}
340
341/**
342 * \brief Whether \p time_ref names a calendar this header implements.
343 *
344 * ROMS's `CALENDAR` if-chain has no final `ELSE`, so a value below -2 leaves
345 * the date undefined rather than failing. Callers should reject it up front.
346 */
348bool remora_time_ref_is_valid (amrex::Real time_ref) noexcept
349{
350 return static_cast<int>(time_ref) >= -2;
351}
352
353/**
354 * \brief Whether \p time_ref survives a round trip through amrex::Real.
355 *
356 * A yyyymmdd.dd date carries eight significant integer digits, and remora_ref_datenum
357 * pulls the year, month, and day back out of them arithmetically. That needs those digits
358 * to be exact. A single-precision Real has a 24-bit mantissa, so integers above 2^24
359 * (16777216) are not representable and every date from year 1678 on rounds to a
360 * neighbouring value -- which then decodes to a different calendar date with nothing to
361 * flag it. The three sentinel calendars (0, -1, -2) are small integers and are always
362 * fine.
363 */
365bool remora_time_ref_is_representable (amrex::Real time_ref) noexcept
366{
367 if (time_ref <= amrex::Real(0.0)) {
368 return true;
369 }
370 return std::is_same<amrex::Real, double>::value;
371}
372
373#endif
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE void remora_datevec(amrex::Real time_ref, amrex::Real date_number, bool is_day_units, int &year, int &month, int &day) noexcept
Calendar date for a day (or second) number. ROMS datevec.
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 bool remora_time_ref_is_valid(amrex::Real time_ref) noexcept
Whether time_ref names a calendar this header implements.
static constexpr int remora_gregorian_daynum
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE void remora_caldate(amrex::Real time_ref, amrex::Real current_time_days, int &year, int &month, int &day, amrex::Real &yday) noexcept
Model time in days to calendar date. ROMS caldate.
AMREX_FORCE_INLINE bool remora_time_ref_is_representable(amrex::Real time_ref) noexcept
Whether time_ref survives a round trip through amrex::Real.
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real remora_ref_datenum(amrex::Real time_ref) noexcept
Day number of the reference date implied by time_ref.
AMREX_GPU_HOST_DEVICE AMREX_FORCE_INLINE amrex::Real remora_datenum(amrex::Real time_ref, int year, int month, int day, int hour=0, int minutes=0, amrex::Real seconds=amrex::Real(0.0)) noexcept
Fractional day number for a calendar date. ROMS datenum.
mf_h setVal(geomdata.ProbHi(2))