Time Shield Library
C++ library for working with time
Loading...
Searching...
No Matches
MoonPhase.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2#pragma once
3#ifndef _TIME_SHIELD_MOON_PHASE_HPP_INCLUDED
4#define _TIME_SHIELD_MOON_PHASE_HPP_INCLUDED
5
9
11#include "types.hpp"
12
13#include <array>
14#include <cmath>
15#include <cstddef>
16
17namespace time_shield {
18
19namespace astronomy {
20
23 double phase = 0.0;
24 double illumination = 0.0;
25 double age_days = 0.0;
26 double distance_km = 0.0;
27 double diameter_deg = 0.0;
28 double age_deg = 0.0;
29 double phase_angle_rad = 0.0;
30 double phase_sin = 0.0;
31 double phase_cos = 0.0;
32 double sun_distance_km = 0.0;
33 double sun_diameter_deg = 0.0;
34 };
35
47
68 class MoonPhase {
69 public:
70 using quarters_unix_s_t = std::array<double, 8>;
71 static constexpr double kDefaultQuarterWindow_s = 43200.0;
72
76 MoonPhaseResult compute(double unix_utc_s) const noexcept {
77 MoonPhaseResult out{};
78 const double jd = julian_day_from_unix_seconds(unix_utc_s);
79
80 // --- Sun position ---
81 const double day = jd - kEpochJd;
82 const double N = fix_angle((360.0 / 365.2422) * day);
83 const double M = fix_angle(N + kElonge - kElongp);
84
85 double Ec = kepler(M, kEccent);
86 Ec = std::sqrt((1.0 + kEccent) / (1.0 - kEccent)) * std::tan(Ec / 2.0);
87 Ec = 2.0 * rad2deg(std::atan(Ec));
88 const double lambda_sun = fix_angle(Ec + kElongp);
89
90 const double F = ((1.0 + kEccent * std::cos(deg2rad(Ec))) / (1.0 - kEccent * kEccent));
91 const double sun_dist = kSunSmax / F;
92 const double sun_ang = F * kSunAngSiz;
93
94 // --- Moon position ---
95 const double ml = fix_angle(13.1763966 * day + kMmLong);
96 const double MM = fix_angle(ml - 0.1114041 * day - kMmLongp);
97
98 const double Ev = 1.2739 * std::sin(deg2rad(2.0 * (ml - lambda_sun) - MM));
99 const double Ae = 0.1858 * std::sin(deg2rad(M));
100 const double A3 = 0.37 * std::sin(deg2rad(M));
101 const double MmP = MM + Ev - Ae - A3;
102
103 const double mEc = 6.2886 * std::sin(deg2rad(MmP));
104 const double A4 = 0.214 * std::sin(deg2rad(2.0 * MmP));
105 const double lP = ml + Ev + mEc - Ae + A4;
106
107 const double V = 0.6583 * std::sin(deg2rad(2.0 * (lP - lambda_sun)));
108 const double lPP = lP + V;
109
110 // --- Phase ---
111 const double moon_age_deg_wrapped = fix_angle(lPP - lambda_sun);
112 const double moon_age_rad = deg2rad(moon_age_deg_wrapped);
113 const double illum = (1.0 - std::cos(moon_age_rad)) / 2.0;
114
115 const double moon_dist = (kMsMax * (1.0 - kMecc * kMecc))
116 / (1.0 + kMecc * std::cos(deg2rad(MmP + mEc)));
117
118 const double moon_dfrac = moon_dist / kMsMax;
119 const double moon_ang = kMAngSiz / moon_dfrac;
120
121 out.phase = moon_age_deg_wrapped / 360.0;
122 out.illumination = illum;
123 out.age_days = kSynMonth * out.phase;
124 out.distance_km = moon_dist;
125 out.diameter_deg = moon_ang;
126 out.age_deg = moon_age_deg_wrapped;
127 out.phase_angle_rad = moon_age_rad;
128 out.phase_sin = std::sin(moon_age_rad);
129 out.phase_cos = std::cos(moon_age_rad);
130 out.sun_distance_km = sun_dist;
131 out.sun_diameter_deg = sun_ang;
132 return out;
133 }
134
138 double compute_phase(double unix_utc_s) const noexcept {
139 return compute(unix_utc_s).phase;
140 }
141
145 quarters_unix_s_t quarter_times_unix(double unix_utc_s) const noexcept {
146 const double sdate = julian_day_from_unix_seconds(unix_utc_s);
147 double adate = sdate - 45.0;
148
149 const double ats = unix_utc_s - 86400.0 * 45.0;
150 const int yy = year_from_unix_seconds(ats);
151 const int mm = month_from_unix_seconds(ats);
152
153 // IMPORTANT: use floating division
154 double k1 = std::floor((yy + ((mm - 1) * (1.0 / 12.0)) - 1900.0) * 12.3685);
155 double k2 = 0.0;
156
157 double nt1 = mean_phase_jd(adate, k1);
158 adate = nt1;
159
160 while (true) {
161 adate += kSynMonth;
162 k2 = k1 + 1.0;
163
164 double nt2 = mean_phase_jd(adate, k2);
165 if (std::abs(nt2 - sdate) < 0.75) {
166 nt2 = true_phase_jd(k2, 0.0); // new moon correction
167 }
168
169 if (nt1 <= sdate && nt2 > sdate) {
170 break;
171 }
172
173 nt1 = nt2;
174 k1 = k2;
175 }
176
177 const double dates_jd[8] = {
178 true_phase_jd(k1, 0.0),
179 true_phase_jd(k1, 0.25),
180 true_phase_jd(k1, 0.5),
181 true_phase_jd(k1, 0.75),
182 true_phase_jd(k2, 0.0),
183 true_phase_jd(k2, 0.25),
184 true_phase_jd(k2, 0.5),
185 true_phase_jd(k2, 0.75)
186 };
187
188 quarters_unix_s_t out{};
189 for (std::size_t i = 0; i < 8; ++i) {
190 out[i] = jd_to_unix_seconds(dates_jd[i]);
191 }
192 return out;
193 }
194
198 quarters_unix_s_t quarter_times(double unix_utc_s) const noexcept {
199 return quarter_times_unix(unix_utc_s);
200 }
201
205 MoonQuarterInstants quarter_instants_unix(double unix_utc_s) const noexcept {
206 const auto quarters = quarter_times_unix(unix_utc_s);
208 out.previous_new_unix_s = quarters[0];
209 out.previous_first_quarter_unix_s = quarters[1];
210 out.previous_full_unix_s = quarters[2];
211 out.previous_last_quarter_unix_s = quarters[3];
212 out.next_new_unix_s = quarters[4];
213 out.next_first_quarter_unix_s = quarters[5];
214 out.next_full_unix_s = quarters[6];
215 out.next_last_quarter_unix_s = quarters[7];
216 return out;
217 }
218
223 bool is_new_moon_window(double unix_utc_s, double window_seconds = kDefaultQuarterWindow_s) const noexcept {
224 const auto instants = quarter_instants_unix(unix_utc_s);
225 return is_within_window(unix_utc_s, instants.previous_new_unix_s, instants.next_new_unix_s, window_seconds);
226 }
227
232 bool is_full_moon_window(double unix_utc_s, double window_seconds = kDefaultQuarterWindow_s) const noexcept {
233 const auto instants = quarter_instants_unix(unix_utc_s);
234 return is_within_window(unix_utc_s, instants.previous_full_unix_s, instants.next_full_unix_s, window_seconds);
235 }
236
241 bool is_first_quarter_window(double unix_utc_s, double window_seconds = kDefaultQuarterWindow_s) const noexcept {
242 const auto instants = quarter_instants_unix(unix_utc_s);
243 return is_within_window(unix_utc_s, instants.previous_first_quarter_unix_s, instants.next_first_quarter_unix_s, window_seconds);
244 }
245
250 bool is_last_quarter_window(double unix_utc_s, double window_seconds = kDefaultQuarterWindow_s) const noexcept {
251 const auto instants = quarter_instants_unix(unix_utc_s);
252 return is_within_window(unix_utc_s, instants.previous_last_quarter_unix_s, instants.next_last_quarter_unix_s, window_seconds);
253 }
254
255 private:
256 static double julian_day_from_unix_seconds(double unix_utc_s) noexcept {
257 return 2440587.5 + unix_utc_s / 86400.0;
258 }
259
260 static int year_from_unix_seconds(double unix_utc_s) noexcept {
261 return static_cast<int>(year_of<year_t>(static_cast<ts_t>(unix_utc_s)));
262 }
263
264 static int month_from_unix_seconds(double unix_utc_s) noexcept {
265 return static_cast<int>(month_of_year<int>(static_cast<ts_t>(unix_utc_s)));
266 }
267
268 static double jd_to_unix_seconds(double julian_day) noexcept {
269 return (julian_day - 2440587.5) * 86400.0;
270 }
271
272 // --- Constants (1980 January 0.0) ---
273 static constexpr double kEpochJd = 2444238.5;
274
275 static constexpr double kElonge = 278.833540;
276 static constexpr double kElongp = 282.596403;
277 static constexpr double kEccent = 0.016718;
278 static constexpr double kSunSmax = 1.495985e8; // km
279 static constexpr double kSunAngSiz= 0.533128; // deg
280
281 static constexpr double kMmLong = 64.975464;
282 static constexpr double kMmLongp = 349.383063;
283 [[maybe_unused]] static constexpr double kMNode = 151.950429;
284 [[maybe_unused]] static constexpr double kMInc = 5.145396;
285 static constexpr double kMecc = 0.054900;
286 static constexpr double kMAngSiz = 0.5181; // deg
287 static constexpr double kMsMax = 384401.0; // km
288 [[maybe_unused]] static constexpr double kMParallax= 0.9507; // deg
289 static constexpr double kSynMonth = 29.53058868;
290
291 static constexpr double kPi = 3.14159265358979323846;
292
293 static double deg2rad(double deg) noexcept { return deg * (kPi / 180.0); }
294 static double rad2deg(double rad) noexcept { return rad * (180.0 / kPi); }
295
296 static double fix_angle(double a) noexcept {
297 a = std::fmod(a, 360.0);
298 if (a < 0.0) a += 360.0;
299 return a;
300 }
301
302 static double kepler(double m_deg, double ecc) noexcept {
303 constexpr double eps = 1e-6;
304 const double m = deg2rad(m_deg);
305 double e = m;
306 for (int i = 0; i < 50; ++i) {
307 const double delta = e - ecc * std::sin(e) - m;
308 e -= delta / (1.0 - ecc * std::cos(e));
309 if (std::abs(delta) <= eps) break;
310 }
311 return e;
312 }
313
314 double mean_phase_jd(double julian_day, double lunation_index) const noexcept {
315 const double jt = (julian_day - 2415020.0) / 36525.0;
316 const double t2 = jt * jt;
317 const double t3 = t2 * jt;
318
319 return 2415020.75933 + kSynMonth * lunation_index
320 + 0.0001178 * t2
321 - 0.000000155 * t3
322 + 0.00033 * std::sin(deg2rad(166.56 + 132.87 * jt - 0.009173 * t2));
323 }
324
325 double true_phase_jd(double lunation_index, double phase_fraction) const noexcept {
326 // This algorithm is designed for phase_fraction in {0, 0.25, 0.5, 0.75}.
327 const double kx = lunation_index + phase_fraction;
328 const double t = kx / 1236.85;
329 const double t2 = t * t;
330 const double t3 = t2 * t;
331
332 double pt = 2415020.75933
333 + kSynMonth * kx
334 + 0.0001178 * t2
335 - 0.000000155 * t3
336 + 0.00033 * std::sin(deg2rad(166.56 + 132.87 * t - 0.009173 * t2));
337
338 const double m = 359.2242 + 29.10535608 * kx - 0.0000333 * t2 - 0.00000347 * t3;
339 const double mprime = 306.0253 + 385.81691806 * kx + 0.0107306 * t2 + 0.00001236 * t3;
340 const double f = 21.2964 + 390.67050646 * kx - 0.0016528 * t2 - 0.00000239 * t3;
341
342 // Corrections (same structure as common ports of moontool)
343 if (phase_fraction < 0.01 || std::abs(phase_fraction - 0.5) < 0.01) {
344 pt += (0.1734 - 0.000393 * t) * std::sin(deg2rad(m))
345 + 0.0021 * std::sin(deg2rad(2 * m))
346 - 0.4068 * std::sin(deg2rad(mprime))
347 + 0.0161 * std::sin(deg2rad(2 * mprime))
348 - 0.0004 * std::sin(deg2rad(3 * mprime))
349 + 0.0104 * std::sin(deg2rad(2 * f))
350 - 0.0051 * std::sin(deg2rad(m + mprime))
351 - 0.0074 * std::sin(deg2rad(m - mprime))
352 + 0.0004 * std::sin(deg2rad(2 * f + m))
353 - 0.0004 * std::sin(deg2rad(2 * f - m))
354 - 0.0006 * std::sin(deg2rad(2 * f + mprime))
355 + 0.0010 * std::sin(deg2rad(2 * f - mprime))
356 + 0.0005 * std::sin(deg2rad(m + 2 * mprime));
357 return pt;
358 }
359
360 if (std::abs(phase_fraction - 0.25) < 0.01 || std::abs(phase_fraction - 0.75) < 0.01) {
361 pt += (0.1721 - 0.0004 * t) * std::sin(deg2rad(m))
362 + 0.0021 * std::sin(deg2rad(2 * m))
363 - 0.6280 * std::sin(deg2rad(mprime))
364 + 0.0089 * std::sin(deg2rad(2 * mprime))
365 - 0.0004 * std::sin(deg2rad(3 * mprime))
366 + 0.0079 * std::sin(deg2rad(2 * f))
367 - 0.0119 * std::sin(deg2rad(m + mprime))
368 - 0.0047 * std::sin(deg2rad(m - mprime))
369 + 0.0003 * std::sin(deg2rad(2 * f + m))
370 - 0.0004 * std::sin(deg2rad(2 * f - m))
371 - 0.0006 * std::sin(deg2rad(2 * f + mprime))
372 + 0.0021 * std::sin(deg2rad(2 * f - mprime))
373 + 0.0003 * std::sin(deg2rad(m + 2 * mprime))
374 + 0.0004 * std::sin(deg2rad(m - 2 * mprime))
375 - 0.0003 * std::sin(deg2rad(2 * m + mprime));
376
377 if (phase_fraction < 0.5) {
378 pt += 0.0028 - 0.0004 * std::cos(deg2rad(m)) + 0.0003 * std::cos(deg2rad(mprime));
379 } else {
380 pt += -0.0028 + 0.0004 * std::cos(deg2rad(m)) - 0.0003 * std::cos(deg2rad(mprime));
381 }
382 return pt;
383 }
384
385 // Fallback: return uncorrected estimate
386 return pt;
387 }
388
389 static bool is_within_window(double unix_utc_s, double previous_instant, double next_instant, double window_seconds) noexcept {
390 const double prev_delta = std::abs(unix_utc_s - previous_instant);
391 const double next_delta = std::abs(unix_utc_s - next_instant);
392 return (prev_delta <= window_seconds) || (next_delta <= window_seconds);
393 }
394 };
395
396} // namespace astronomy
397
400
401} // namespace time_shield
402
403#endif // _TIME_SHIELD_MOON_PHASE_HPP_INCLUDED
Moon phase calculator (geocentric approximation).
Definition MoonPhase.hpp:68
static constexpr double kMInc
static constexpr double kSynMonth
bool is_new_moon_window(double unix_utc_s, double window_seconds=kDefaultQuarterWindow_s) const noexcept
Check whether timestamp is inside a window around new moon.
double mean_phase_jd(double julian_day, double lunation_index) const noexcept
static constexpr double kMNode
static constexpr double kMmLong
static bool is_within_window(double unix_utc_s, double previous_instant, double next_instant, double window_seconds) noexcept
static constexpr double kSunSmax
static constexpr double kElongp
static constexpr double kPi
bool is_first_quarter_window(double unix_utc_s, double window_seconds=kDefaultQuarterWindow_s) const noexcept
Check whether timestamp is inside a window around first quarter.
static double julian_day_from_unix_seconds(double unix_utc_s) noexcept
MoonPhaseResult compute(double unix_utc_s) const noexcept
Compute full set of Moon phase parameters for given UTC timestamp.
Definition MoonPhase.hpp:76
static constexpr double kElonge
MoonQuarterInstants quarter_instants_unix(double unix_utc_s) const noexcept
Quarter instants around the provided timestamp as a structured result (Unix UTC seconds).
static constexpr double kMsMax
static constexpr double kMmLongp
static double jd_to_unix_seconds(double julian_day) noexcept
static constexpr double kEccent
static constexpr double kMParallax
quarters_unix_s_t quarter_times(double unix_utc_s) const noexcept
Compatibility wrapper returning quarter instants as Unix UTC seconds.
quarters_unix_s_t quarter_times_unix(double unix_utc_s) const noexcept
Compute quarter/new/full instants around given timestamp.
static double fix_angle(double a) noexcept
static constexpr double kMecc
static double rad2deg(double rad) noexcept
double compute_phase(double unix_utc_s) const noexcept
Compute only phase fraction in [0..1) for given UTC timestamp.
std::array< double, 8 > quarters_unix_s_t
Quarter instants as Unix UTC seconds ({new, firstQ, full, lastQ} for previous and next cycles).
Definition MoonPhase.hpp:70
static double deg2rad(double deg) noexcept
static constexpr double kDefaultQuarterWindow_s
Default window around phase events (12h).
Definition MoonPhase.hpp:71
static double kepler(double m_deg, double ecc) noexcept
static constexpr double kMAngSiz
static int month_from_unix_seconds(double unix_utc_s) noexcept
static int year_from_unix_seconds(double unix_utc_s) noexcept
static constexpr double kSunAngSiz
static constexpr double kEpochJd
bool is_last_quarter_window(double unix_utc_s, double window_seconds=kDefaultQuarterWindow_s) const noexcept
Check whether timestamp is inside a window around last quarter.
bool is_full_moon_window(double unix_utc_s, double window_seconds=kDefaultQuarterWindow_s) const noexcept
Check whether timestamp is inside a window around full moon.
double true_phase_jd(double lunation_index, double phase_fraction) const noexcept
Conversions involving DateTimeStruct and day boundary helpers.
TIME_SHIELD_CONSTEXPR T year_of(ts_t ts=time_shield::ts())
Get the year from the timestamp.
TIME_SHIELD_CONSTEXPR T month_of_year(ts_t ts) noexcept
Get the month of the year.
int64_t ts_t
Unix timestamp in seconds since 1970‑01‑01T00:00:00Z.
Definition types.hpp:48
Main namespace for the Time Shield library.
astronomy::MoonPhase MoonPhaseCalculator
Convenience alias for the geocentric Moon phase calculator.
Result of Moon phase computation (geocentric approximation).
Definition MoonPhase.hpp:22
double age_deg
Phase angle in degrees (0..360).
Definition MoonPhase.hpp:28
double distance_km
Distance to Moon in km (approx).
Definition MoonPhase.hpp:26
double diameter_deg
Angular diameter of Moon in degrees (approx).
Definition MoonPhase.hpp:27
double phase
Phase fraction in [0..1). 0=new moon, 0.5=full moon.
Definition MoonPhase.hpp:23
double phase_sin
sin(phase_angle_rad) helper for continuous signal.
Definition MoonPhase.hpp:30
double phase_angle_rad
Phase angle in radians (0..2*pi).
Definition MoonPhase.hpp:29
double age_days
Age of the Moon in days since new moon (approx).
Definition MoonPhase.hpp:25
double phase_cos
cos(phase_angle_rad) helper for continuous signal.
Definition MoonPhase.hpp:31
double illumination
Illuminated fraction in [0..1].
Definition MoonPhase.hpp:24
Lunar quarter instants (Unix UTC seconds, floating).
Definition MoonPhase.hpp:37
double previous_new_unix_s
Previous new moon instant (Unix UTC seconds, double).
Definition MoonPhase.hpp:38
double previous_first_quarter_unix_s
Previous first quarter instant (Unix UTC seconds, double).
Definition MoonPhase.hpp:39
double previous_full_unix_s
Previous full moon instant (Unix UTC seconds, double).
Definition MoonPhase.hpp:40
double next_last_quarter_unix_s
Next last quarter instant (Unix UTC seconds, double).
Definition MoonPhase.hpp:45
double next_new_unix_s
Next new moon instant (Unix UTC seconds, double).
Definition MoonPhase.hpp:42
double previous_last_quarter_unix_s
Previous last quarter instant (Unix UTC seconds, double).
Definition MoonPhase.hpp:41
double next_first_quarter_unix_s
Next first quarter instant (Unix UTC seconds, double).
Definition MoonPhase.hpp:43
double next_full_unix_s
Next full moon instant (Unix UTC seconds, double).
Definition MoonPhase.hpp:44
Type definitions for time-related units and formats.