Body-agnostic rise/transit/set engine.
More...
|
| namespace | detail |
| namespace | sun |
| | Solar conventions and entry points: standard altitudes, twilight constants, and the transit-centered API wired to the Sun's ephemeris.
|
| namespace | moon |
| | Lunar conventions and entry points: the Ch.15 standard altitude and the UT-day (almanac-style) API wired to the ELP2000-82B ephemeris.
|
|
| struct | Result |
| | The result of a rise/transit/set calculation for one date. More...
|
|
| concept | BodyProvider |
| | A body position provider: apparent equatorial coordinates at one instant.
|
|
| using | GeoLocation |
| | Backward-compatible name for the common observer location type.
|
|
| enum class | Polar : uint8_t { NONE
, DAY
, NIGHT
} |
| | The whole-day topology of a date: whether the body crosses h₀ at all, and if not, which side of h₀ it stays on. More...
|
|
| auto | hour_angle_at_altitude (const toolbox::AngleDeg &δ, const toolbox::AngleDeg &φ, const toolbox::AngleDeg &h0) -> std::optional< toolbox::AngleDeg > |
| | Compute the hour angle at which a body of declination δ reaches altitude h₀, as seen from latitude φ.
|
| template<BodyProvider P> |
| auto | rise_set_jde (const double transit_jde_tt, const bool is_rise, const GeoLocation &location, const toolbox::AngleDeg &h0, const P &provider) -> std::optional< double > |
| | Compute the instant at which a body crosses altitude h₀, before (rise) or after (set) a given transit.
|
| template<BodyProvider P> |
| auto | calculate_around_transit (const double transit_jde_tt, const GeoLocation &location, const toolbox::AngleDeg &h0, const P &provider) -> Result |
| | Compute rise, transit, and set around a known transit (transit-centered window).
|
| template<BodyProvider P> |
| auto | transit_in_window (const double t0_jde_tt, const double t1_jde_tt, const GeoLocation &location, const P &provider) -> std::optional< double > |
| | Find a body's first upper culmination inside a time window, if there is one.
|
| template<BodyProvider P> |
| auto | calculate_day (const std::chrono::year_month_day &ymd_ut1, const GeoLocation &location, const toolbox::AngleDeg &h0, const P &provider) -> Result |
| | Compute rise, transit, and set for one UT1 calendar day (almanac semantics).
|
|
| constexpr double | COS_H0_CLAMP_TOLERANCE |
| | How far past ±1 cos(H₀) may land and still count as a grazing event rather than a polar day/night verdict.
|
| constexpr double | POLAR_DENOMINATOR_EPSILON |
| | Below this |cos φ · cos δ| the hour-angle equation is treated as degenerate (observer at a geographic pole, or the body at a celestial pole).
|
| constexpr double | TRANSIT_BRACKET_HALF_WIDTH_DAYS |
| | The half-width of the root bracket around a transit estimate, in days.
|
| constexpr double | RISE_SET_BRACKET_HALF_WIDTH_DAYS |
| | The half-width of the first-try root bracket around the rise/set estimate, in days.
|
| constexpr double | LOWER_CULMINATION_OFFSET_DAYS |
| | The nominal offset of the adjacent lower culmination from a transit, in days — the mean-time estimate used to center the altitude-minimum search window.
|
| constexpr double | MIN_SEARCH_HALF_WIDTH_DAYS |
| | The half-width of the altitude-minimum search window around the nominal lower culmination, in days.
|
| constexpr double | TRANSIT_RESIDUAL_GUARD_DEG |
| | The residual guard on a transit root, in degrees of hour angle.
|
| constexpr double | RISE_SET_RESIDUAL_GUARD_DEG |
| | The residual guard on a rise/set root, in degrees of altitude.
|
| constexpr double | EXTREMUM_SEARCH_TOLERANCE_DAYS |
| | Convergence tolerance for the golden-section extremum searches, in days.
|
| constexpr double | EDGE_PROBE_TOLERANCE_DAYS |
| | Coarser tolerance for the edge-cell probes of find_extrema, in days.
|
Body-agnostic rise/transit/set engine.
- Note
- The engine knows nothing about any particular body: a body is injected as a BodyProvider (jde_tt → apparent equatorial coordinates), and the event altitude h₀ is injected by the caller. Per-body conventions (standard altitudes, ephemeris wiring) live in the sun / moon sub-namespaces; no planet provider is wired into this layer.
◆ GeoLocation
Backward-compatible name for the common observer location type.
- Note
- Longitude is positive east. This is the opposite of Meeus's west-positive convention used by sidereal::local_apparent; the negation happens inside this namespace.
-
+180° and -180° name the same meridian but are NOT interchangeable for the date-anchored solar APIs (sun::transit_jde, sun::calculate): the date input is a UT1 date, so they select transit-centered windows one day apart (local mean noon at +180° is 0h UT of that date; at -180° it is 24h UT). Pick the sign matching the intended UT window — deliberate, same behavior as other UT-date APIs. For the UT-day APIs (calculate_day, moon::calculate) the window is determined by ymd_ut1 alone, and the two signs give identical results.
◆ Polar
The whole-day topology of a date: whether the body crosses h₀ at all, and if not, which side of h₀ it stays on.
- Note
- First-class member of Result — the event topology (normal / polar day / polar night) is part of the result's type, not inferred from counting empty optionals (design constraint from issue #62).
◆ calculate_around_transit()
Compute rise, transit, and set around a known transit (transit-centered window).
- Parameters
-
| transit_jde_tt | The transit instant anchoring the window, as a julian ephemeris day on the TT scale. |
| location | The observer's location. |
| h0 | The event altitude. |
| provider | The body's position provider. |
- Returns
- The three instants (JDE, TT scale) and the polar topology; see Result's notes.
- Exceptions
-
| std::invalid_argument | / std::runtime_error See the generic rise_set_jde, whose validation and residual-guard failures propagate through this function. |
- Note
- The rise is searched between the preceding altitude minimum and the transit, the set between the transit and the following one — the window is a half-period on each side, NOT a calendar day. For the Moon this means consecutive dates' results tile the timeline without gaps or double-counts only through their own transits; callers wanting almanac-style "events on this UT date" semantics should use calculate_day.
◆ calculate_day()
template<BodyProvider P>
| auto astro::rise_set::calculate_day |
( |
const std::chrono::year_month_day & | ymd_ut1, |
|
|
const GeoLocation & | location, |
|
|
const toolbox::AngleDeg & | h0, |
|
|
const P & | provider )->Result |
|
inlinenodiscard |
Compute rise, transit, and set for one UT1 calendar day (almanac semantics).
- Parameters
-
| ymd_ut1 | The date, interpreted on the UT1 scale (callers handle time zones). The window is [0h, 24h) UT1 of this date. |
| location | The observer's location. |
| h0 | The event altitude. |
| provider | The body's position provider. |
- Returns
- The events inside the UT day (JDE, TT scale) and the polar topology. Any of the three instants may be absent: slow bodies (the Moon) regularly have UT dates with no rise, no set, or no transit — that absence is calendar arithmetic, NOT a polar verdict. polar is DAY/NIGHT only when no crossing exists and the altitude stays entirely on one side of h₀ for the whole day.
- Note
- At most one event of each kind is reported. Days with two crossings of the same kind exist — lunar high-latitude days near a standstill (the Moon's declination swings fast enough that it can rise twice in one UT day; USNO lists both in the same cell), and any body whose diurnal period dips below 24 h. On such days the LATER crossing is reported. This library reports one event per cell; almanacs that list both keep the earlier one visible too.
-
Mechanism: the day is partitioned at its detected altitude extrema (detail::find_extrema), each segment's crossing is decided by a directed sign check, and the polar verdict compares the day's global extremes (window ends included) against h₀.
-
Boundary attribution: the window is [0h, 24h), and the crossing checks are strict sign comparisons, so an event landing EXACTLY on a UT midnight is not owned by either adjacent day. That is a measure-zero edge (the TT/UT1 conversions make an exact landing practically unreachable for real ephemerides), accepted deliberately rather than given a fake-precision rounding rule.
- Exceptions
-
| std::invalid_argument | If ymd_ut1 is invalid, location is out of range, or h0 is not finite or outside [-90°, 90°], or a time conversion leaves its supported years. |
| std::runtime_error | If the altitude straddles h₀ inside the day but no crossing solve converged, if a bracketed crossing failed the residual guard, or if the transit solve failed its own residual guard (see transit_in_window) — a numerical failure must not read as "no event". |
◆ hour_angle_at_altitude()
Compute the hour angle at which a body of declination δ reaches altitude h₀, as seen from latitude φ.
- Parameters
-
| δ | The body's declination. |
| φ | The observer's geographic latitude. |
| h0 | The target altitude. |
- Returns
- The (positive) hour angle H₀ ∈ [0°, 180°]; the body is at h₀ at hour angles ±H₀. Returns nullopt when the altitude is never reached (polar day/night) or when the equation degenerates — |cos φ · cos δ| < POLAR_DENOMINATOR_EPSILON, i.e. the observer at a geographic pole or the body at/near a celestial pole.
- Exceptions
-
| std::invalid_argument | If any argument is not finite or lies outside [-90°, 90°] (outside that domain sin/cos alias and (15.1) returns a physically meaningless H₀). |
- See also
- Jean Meeus, "Astronomical Algorithms", Second Edition, Chapter 15, Formula (15.1).
◆ rise_set_jde()
template<BodyProvider P>
| auto astro::rise_set::rise_set_jde |
( |
const double | transit_jde_tt, |
|
|
const bool | is_rise, |
|
|
const GeoLocation & | location, |
|
|
const toolbox::AngleDeg & | h0, |
|
|
const P & | provider )->std::optional< double > |
|
inlinenodiscard |
Compute the instant at which a body crosses altitude h₀, before (rise) or after (set) a given transit.
- Parameters
-
| transit_jde_tt | The transit instant, as a julian ephemeris day on the TT scale. |
| is_rise | True for the upward crossing before transit, false for the downward crossing after it. |
| location | The observer's location. |
| h0 | The crossing altitude. |
| provider | The body's position provider. |
- Returns
- The crossing instant (JDE, TT scale), or nullopt when the body does not cross h₀ between the transit and the adjacent altitude minimum (polar day/night).
- Exceptions
-
| std::invalid_argument | If transit_jde_tt is not finite, location is out of range, or h0 is not finite or outside [-90°, 90°], or a time conversion leaves its supported years. |
| std::runtime_error | If a directed sign change proved a crossing exists but the solve failed the residual guard — a numerical failure must not read as a polar verdict. |
- Note
- Solves (altitude − h₀) = 0 directly: no hand-written dh/dH derivative, no hour-angle sign convention — the bracket side selects the event. Existence is decided by the directed sign check at the true altitude minimum (detail::min_altitude_jde, golden-section — exact also under dδ/dt ≠ 0); the H₀-based bracket only accelerates.
◆ transit_in_window()
template<BodyProvider P>
| auto astro::rise_set::transit_in_window |
( |
const double | t0_jde_tt, |
|
|
const double | t1_jde_tt, |
|
|
const GeoLocation & | location, |
|
|
const P & | provider )->std::optional< double > |
|
inlinenodiscard |
Find a body's first upper culmination inside a time window, if there is one.
- Parameters
-
| t0_jde_tt | The window start (inclusive), as a julian ephemeris day on the TT scale. |
| t1_jde_tt | The window end (exclusive), same scale. |
| location | The observer's location. |
| provider | The body's position provider. |
- Returns
- The first transit instant in the window (JDE, TT scale), or nullopt when no transit falls in the window — a regular event for slow bodies (the Moon transits ~24.84 h apart, so a 1-day window without a transit occurs roughly one day in 29). If the window holds more than one transit (possible when it is not shorter than the body's transit period — the solar day dips below 24 h around the equinoxes, so even a 1-day window can contain two solar transits), the FIRST one is returned.
- Exceptions
-
| std::invalid_argument | If location is out of range, or the window is not finite and positive, or a time conversion leaves its supported years. |
| std::runtime_error | If the polished root fails the hour-angle residual guard — the estimate's bracket held no root, which is a numerical failure and must not read as an eventless window. |
- Note
- The estimate is Meeus Ch.15's m₀ in secant form: the hour angle sweeps at (sidereal rate − dα/dt), and dα/dt is measured from the provider over a fixed 1-day span from t0_jde_tt — the rate is a property of the body, not of the window, so the estimate is window-length-independent. The span aliases only if |dα/dt| ever reached 180°/day; the library's own providers stay an order of magnitude below that, and a custom provider that does not is outside this premise. Near the top of the representable date range the 1-day sample can itself leave the range and throw (see @throw).
◆ COS_H0_CLAMP_TOLERANCE
| double astro::rise_set::COS_H0_CLAMP_TOLERANCE |
|
inlineconstexpr |
How far past ±1 cos(H₀) may land and still count as a grazing event rather than a polar day/night verdict.
- Note
- Roundoff in (sin h₀ - sin φ sin δ) / (cos φ cos δ) is a few ulps (~1e-16); 1e-9 keeps a wide margin over that while staying far below any physically meaningful excess.
◆ EDGE_PROBE_TOLERANCE_DAYS
| double astro::rise_set::EDGE_PROBE_TOLERANCE_DAYS |
|
inlineconstexpr |
Coarser tolerance for the edge-cell probes of find_extrema, in days.
- Note
- The probes only locate a cut point — crossings are re-solved by Newton afterwards, and the polar verdict's altitude values need ~1e-3° — so ~0.1 s of time precision is far more than enough, and the coarser target cuts the probe's evaluation count by ~40% (20 vs 34 iterations on a 15-min cell).
◆ EXTREMUM_SEARCH_TOLERANCE_DAYS
| double astro::rise_set::EXTREMUM_SEARCH_TOLERANCE_DAYS |
|
inlineconstexpr |
Convergence tolerance for the golden-section extremum searches, in days.
- Note
- 1e-9 day ≈ 0.1 ms of time — far below the precision the rise/set residual guard (1e-3°) cares about. In the representable range's ordinary part this tolerance is what ends the search (≤ 40 iterations for any bracket here); past JDE 2²³ (≈ year 18255) the double ulp exceeds it and only the iteration cap in golden_section_argmin ends the loop (see that function's note).
◆ LOWER_CULMINATION_OFFSET_DAYS
| double astro::rise_set::LOWER_CULMINATION_OFFSET_DAYS |
|
inlineconstexpr |
The nominal offset of the adjacent lower culmination from a transit, in days — the mean-time estimate used to center the altitude-minimum search window.
- Note
- This is only a search-window center, not a solved quantity: for the Sun the true altitude minimum deviates from the nominal half-day mark by ≤ ~16 s (measured peak 16.00 s over the supported span), and the window half-width below dwarfs that.
◆ MIN_SEARCH_HALF_WIDTH_DAYS
| double astro::rise_set::MIN_SEARCH_HALF_WIDTH_DAYS |
|
inlineconstexpr |
The half-width of the altitude-minimum search window around the nominal lower culmination, in days.
- Note
- For the Sun the true minimum sits within ~16 s of the nominal half-day mark — a 500x margin (held by RiseSet.MinSearchWindowRetainsMargin, #126). The minimum is found by golden-section search over this window, NOT by solving H = ±180°: with dδ/dt ≠ 0 (the Moon runs ±13°/day) the altitude extremum wanders off the lower culmination by degrees of hour angle, and anchoring the existence check to H = 180° leaves a blind band of order 0.01°–0.05° of altitude that silently mislabels grazing days as polar (#62).
◆ POLAR_DENOMINATOR_EPSILON
| double astro::rise_set::POLAR_DENOMINATOR_EPSILON |
|
inlineconstexpr |
Below this |cos φ · cos δ| the hour-angle equation is treated as degenerate (observer at a geographic pole, or the body at a celestial pole).
◆ RISE_SET_BRACKET_HALF_WIDTH_DAYS
| double astro::rise_set::RISE_SET_BRACKET_HALF_WIDTH_DAYS |
|
inlineconstexpr |
The half-width of the first-try root bracket around the rise/set estimate, in days.
- Note
- The estimate extrapolates H₀ computed from the declination at transit; for the Sun out to |φ| = 65° the δ drift between transit and the event moves the true root by ≤ 3.5 min, so ±72 min is a comfortable margin. Near the polar boundary the extrapolation degrades without bound — which is why this bracket is only an accelerator: the decider is the sign check at the true altitude extremum (see rise_set_jde). Held out to |φ| = 65° by RiseSet.RiseSetBracketRetainsMargin (#126).
◆ RISE_SET_RESIDUAL_GUARD_DEG
| double astro::rise_set::RISE_SET_RESIDUAL_GUARD_DEG |
|
inlineconstexpr |
The residual guard on a rise/set root, in degrees of altitude.
- Note
- Robustness only: after a directed sign check the bracketed Newton solve converges to residuals ~1e-7°; this guard (4 orders looser) exists so that a degenerate solve — newton_method may return a best-effort iterate when f′ collapses — never ships a wrong instant: a straddle that proved a root exists throws instead of mislabeling the day.
◆ TRANSIT_BRACKET_HALF_WIDTH_DAYS
| double astro::rise_set::TRANSIT_BRACKET_HALF_WIDTH_DAYS |
|
inlineconstexpr |
The half-width of the root bracket around a transit estimate, in days.
- Note
- For the Sun the estimate is local mean noon; the true transit deviates from it by the equation of time only (≤ 20.4 min ≈ 0.0142 day across the supported span — a 7.1x margin). Held by RiseSet.TransitBracketCoversTheEquationOfTime (#126).
◆ TRANSIT_RESIDUAL_GUARD_DEG
| double astro::rise_set::TRANSIT_RESIDUAL_GUARD_DEG |
|
inlineconstexpr |
The residual guard on a transit root, in degrees of hour angle.
- Note
- Same philosophy as RISE_SET_RESIDUAL_GUARD_DEG: a converged Newton solve leaves |H| ~ 1e-7°, so this guard (4 orders looser) only fires when the bracket held no root and newton_method returned a best-effort iterate — a numerical failure that must never ship as a fake transit or a fake eventless window.