CelestialCalendar 1.0.0 · unreleased development @ 67a7c4c79a4a4a6aa7e703c9e6f0039ae34bee82
Loading...
Searching...
No Matches
astro::rise_set Namespace Reference

Body-agnostic rise/transit/set engine. More...

Namespaces

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.

Classes

struct  Result
 The result of a rise/transit/set calculation for one date. More...

Concepts

concept  BodyProvider
 A body position provider: apparent equatorial coordinates at one instant.

Typedefs

using GeoLocation
 Backward-compatible name for the common observer location type.

Enumerations

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...

Functions

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).

Variables

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.

Detailed Description

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.

Typedef Documentation

◆ 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.

Enumeration Type Documentation

◆ Polar

enum class astro::rise_set::Polar : uint8_t
strong

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).
Enumerator
NONE 
DAY 
NIGHT 

Function Documentation

◆ calculate_around_transit()

template<BodyProvider P>
auto astro::rise_set::calculate_around_transit ( const double transit_jde_tt,
const GeoLocation & location,
const toolbox::AngleDeg & h0,
const P & provider )->Result
inlinenodiscard

Compute rise, transit, and set around a known transit (transit-centered window).

Parameters
transit_jde_ttThe transit instant anchoring the window, as a julian ephemeris day on the TT scale.
locationThe observer's location.
h0The event altitude.
providerThe 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_ut1The date, interpreted on the UT1 scale (callers handle time zones). The window is [0h, 24h) UT1 of this date.
locationThe observer's location.
h0The event altitude.
providerThe 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_argumentIf 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_errorIf 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()

auto astro::rise_set::hour_angle_at_altitude ( const toolbox::AngleDeg & δ,
const toolbox::AngleDeg & φ,
const toolbox::AngleDeg & h0 )->std::optional< toolbox::AngleDeg >
inlinenodiscard

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.
h0The 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_argumentIf 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_ttThe transit instant, as a julian ephemeris day on the TT scale.
is_riseTrue for the upward crossing before transit, false for the downward crossing after it.
locationThe observer's location.
h0The crossing altitude.
providerThe 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_argumentIf 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_errorIf 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_ttThe window start (inclusive), as a julian ephemeris day on the TT scale.
t1_jde_ttThe window end (exclusive), same scale.
locationThe observer's location.
providerThe 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_argumentIf location is out of range, or the window is not finite and positive, or a time conversion leaves its supported years.
std::runtime_errorIf 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).

Variable Documentation

◆ 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.