Spec: topocentric positions of the Sun and the Moon #10

Open
opened 2026-10-23 17:56:26 -05:00 by jeff · 1 comment
Owner

Problem Statement

Everything zig-astro computes today is geocentric: where the Sun and the Moon appear from the Earth's center. Nobody stands there. For the Moon the difference is large: its parallax reaches about 1°, two of its own diameters, so a geocentric position cannot say whether the Moon is above a given horizon, which way to point a telescope, or when an occultation happens. Nor can the library answer the questions an observer asks first — how high is the Moon, and in which direction — because it has no sidereal time, no notion of an observer, and no horizontal coordinates. Someone wanting them would have to do the work themselves, at exactly the places such code goes wrong: longitude counted west where east was meant, azimuth from south where north was meant, geodetic latitude mistaken for geocentric, UT confused with TT in the Earth's rotation, and refraction applied or not without saying which.

Solution

zig-astro adds topocentric positions — apparent positions as seen by an observer on the Earth's surface — for the Sun and the Moon, and the machinery beneath them, built generically so that the planets and satellites to come use it unchanged:

  • sidereal time, mean and apparent, at Greenwich;
  • an observer: geodetic latitude, longitude positive east, and height above the WGS 84 ellipsoid;
  • a generic conversion of any apparent geocentric position and distance into a topocentric one: equatorial and ecliptic coordinates, the hour angle, horizontal coordinates (azimuth from north through east, and the airless altitude), and the topocentric distance;
  • refraction, as an explicit, separate step between the airless and the observed altitude, under a stated pressure and temperature;
  • the topocentric Sun, the topocentric Moon, and the Moon's topocentric libration.

The library's conventions follow the IAU and WGS 84 rather than Meeus where the two differ (ADR 0003).

User Stories

  1. As an amateur astronomer, I want the Moon's azimuth and altitude from my backyard at an instant, so that I know where to look.
  2. As an amateur astronomer, I want the Sun's azimuth and altitude from my location, so that I can plan around it.
  3. As a developer, I want to describe an observer by latitude, longitude and height, so that every topocentric calculation takes the same value.
  4. As a developer, I want longitude to count positive east, so that coordinates copied from a map or a GPS receiver work unchanged.
  5. As a developer reading Meeus alongside the code, I want the departure from his west-positive longitude and south-based azimuth documented, so that I don't take it for a bug.
  6. As a developer, I want azimuth counted from north through east in [0°, 360°), so that it matches compasses, almanacs and JPL Horizons.
  7. As a developer, I want the observer's latitude to be geodetic, so that the latitude on any map is the right one to give.
  8. As a developer, I want height measured above the WGS 84 ellipsoid, and to know that height above sea level is good enough, so that I don't have to look up the geoid.
  9. As a developer, I want a latitude beyond ±90° to give NaN rather than a plausible wrong answer, so that bad input is visible.
  10. As a developer, I want any finite longitude to be accepted and wrapped, so that −180° and 180° and 540° mean the same thing.
  11. As a developer, I want negative heights accepted, so that an observer at the Dead Sea is not an error.
  12. As a developer, I want Greenwich mean sidereal time at an instant, so that I can point an equatorial mount.
  13. As a developer, I want Greenwich apparent sidereal time, including the equation of the equinoxes, so that it matches apparent right ascensions.
  14. As a developer, I want sidereal time to accept an instant in either time scale, so that I don't have to convert to UT myself.
  15. As a developer, I want the local sidereal time and hour angle of a body for my observer, so that I can tell how far it is from the meridian.
  16. As a developer, I want a body's topocentric right ascension and declination, so that I can point a telescope at the Moon to better than its own diameter.
  17. As a developer, I want topocentric ecliptic longitude and latitude, so that I can compare the Moon's place against stars and the Sun as seen from my site.
  18. As a developer, I want the topocentric distance, so that I can compute the Moon's apparent size from my location.
  19. As a developer adding a new body later, I want a generic conversion from any apparent geocentric equatorial position and distance to a topocentric one, so that a planet or satellite needs no parallax code of its own.
  20. As a developer, I want the topocentric result to carry its equatorial, ecliptic, hour-angle, horizontal and distance values together, so that I don't recompute shared pieces.
  21. As a developer, I want the distance in kilometers for every body in the topocentric result, so that the type is the same for the Sun and the Moon.
  22. As a developer, I want the horizontal coordinates to give the airless altitude, so that I always know whether refraction has been applied.
  23. As a developer, I want a function that converts an airless altitude to an observed altitude, so that I can predict where the Moon will actually be seen near the horizon.
  24. As a developer, I want the inverse, from observed to airless altitude, so that I can reduce a measurement.
  25. As a developer, I want to give the pressure and temperature, defaulting to 1010 mb and 10 °C, so that I can account for the weather when it matters and ignore it when it doesn't.
  26. As a developer, I want refraction to be zero well below the horizon, so that a body under the Earth doesn't get an absurd correction.
  27. As a developer, I want refraction never to be negative, so that it only ever raises a body.
  28. As a lunar observer, I want the Moon's topocentric libration, so that I know which limb features are favorably placed from my site tonight.
  29. As a lunar observer, I want the topocentric position angle of the axis, so that I can orient a map to the Moon as I see it.
  30. As a developer, I want the topocentric libration in the same type as the geocentric one, so that code using one can take the other.
  31. As a developer, I want the topocentric Sun and Moon to be thin wrappers over their geocentric positions, so that the two can never disagree about anything but the viewpoint.
  32. As a developer, I want the effects left out — diurnal aberration, polar motion, UT1 − UTC, deflection of the vertical — named in the documentation with their sizes, so that I know the error budget.
  33. As a developer, I want each function's accuracy stated against JPL Horizons in its documentation, so that I can decide whether it is good enough.
  34. As a developer, I want non-finite instants to give NaN everywhere, as the rest of the library does, so that errors propagate visibly.
  35. As a maintainer, I want every result checked against Meeus's worked examples and JPL Horizons from several sites, so that sign and convention errors cannot hide.
  36. As a maintainer, I want fuzz targets for the new functions, so that edge cases at the poles, the antimeridian, the zenith and the nadir are covered.
  37. As a reader of the README, I want an example of the Moon's altitude and azimuth from a named site, so that I can see the API at a glance.
  38. As a maintainer, I want the Horizons tooling to fetch topocentric tables, so that the reference data can be regenerated.

Implementation Decisions

  • Sidereal time goes in the Earth module: Greenwich mean sidereal time from Meeus eq. 12.4, and apparent sidereal time adding the equation of the equinoxes, Δψ cos ε, with the true obliquity. A TT instant is converted to UT through the ΔT model, as positions already accept either scale.
  • A new observer module holds:
    • the Observer type: a plain struct of geodetic latitude, longitude positive east (both angles), and height in meters above the WGS 84 ellipsoid (a = 6378.137 km, 1/f = 298.257223563). No validating constructor. ρ sin φ′ and ρ cos φ′ come from Meeus ch. 11 with the WGS 84 constants.
    • the generic topocentric conversion: given an instant, an observer, an apparent geocentric equatorial position and a distance in kilometers, it returns a Topocentric result with equatorial, ecliptic, hour_angle, horizontal and distance_km. Parallax uses the rigorous formulas (eqs. 40.6 and 40.7), not the approximations; the ecliptic coordinates follow Meeus's topocentric ecliptic method (ch. 40) or are converted from the topocentric equatorial ones with the true obliquity. The hour angle is local apparent sidereal time minus right ascension, topocentric.
    • refraction from airless to observed altitude (Sæmundsson, eq. 16.4) and its inverse (Bennett, eq. 16.3), both scaled by an Atmosphere of pressure in millibars and temperature in °C, defaulting to 1010 mb and 10 °C. Refraction is zero below an airless altitude of about −2°, the exact cutoff to be checked against what Horizons does.
  • Horizontal coordinates become a new type in the coordinate module: azimuth from north through east in [0°, 360°), altitude in [−90°, +90°].
  • The Sun module gains a topocentric function, taking an instant and an observer, that passes its apparent position through the generic conversion with the distance in kilometers (1 au = 149,597,870.7 km). The geocentric position keeps its distance in astronomical units.
  • The Moon module gains the same.
  • The libration module gains a topocentric libration, returning the existing libration type, by feeding the topocentric ecliptic longitude and latitude to the same selenographic reduction the geocentric libration uses, and the topocentric right ascension to the position angle of the axis. Meeus's approximate corrections are not used.
  • Conventions departing from Meeus — east longitude, azimuth from north — are recorded in ADR 0003 and warned of in doc comments.
  • Diurnal aberration (≤ 0.32″), polar motion, UT1 − UTC and deflection of the vertical are left out and named in the doc comments.
  • An out-of-range latitude makes every topocentric result NaN; finite longitudes wrap; finite heights of any sign are accepted.

Testing Decisions

  • Tests go through the public astro module only, from the test directory, as the existing ones do; no internal hooks are added.
  • Meeus's worked examples, read from the page images: 11.a (Palomar's ρ sin φ′ and ρ cos φ′), 12.a and 12.b (sidereal time), 13.b (Venus's horizontal coordinates from Washington, with his azimuth turned by 180° and his longitude's sign flipped), 40.a (Mars from Palomar, through the generic conversion), the refraction examples of ch. 16, and the topocentric libration example of ch. 53 if there is one.
  • JPL Horizons topocentric tables for the Sun and the Moon at 201 instants over 1900–2100 from three sites — Palomar, Tromsø and Cerro Paranal — with right ascension and declination, azimuth and elevation airless and refracted, and the Moon's topocentric sub-observer point and north pole angle. The Horizons site coordinates are given as geodetic, matching the observer.
  • Tolerances are set only after measuring: the worst error seen is recorded in a comment beside each, and the tolerance is set just above it. Where an error is systematic rather than scatter (as with the lunar frame tilt in the geocentric libration), it is explained, not just absorbed.
  • Property tests: a TT instant and the same moment in UT give the same sidereal time; the topocentric shift stays within the parallax bound; the distance changes by no more than the Earth's radius plus the height; Bennett inverts Sæmundsson within what Meeus states; non-finite instants and out-of-range latitudes give NaN.
  • Fuzz targets for sidereal time, the topocentric Sun and Moon, refraction, and topocentric libration, with seeds at the poles, the antimeridian, the zenith and the nadir.
  • Prior art: the existing Meeus-example tests, the Horizons comparisons with their parser and recorded worst errors, and the fuzz targets with their seeds.

Out of Scope

  • Rising, setting and transit, which are an event search and get their own spec.
  • The Moon's topocentric semidiameter (ch. 55).
  • The topocentric phase: illuminated fraction, phase angle and bright limb as seen from the site.
  • Diurnal aberration, polar motion, UT1 − UTC and deflection of the vertical.
  • Ellipsoids other than WGS 84, and geoid heights.
  • Planets and artificial satellites themselves; only the generic conversion they will use.

Further Notes

  • The glossary entries added for this work: geocentric, topocentric, observer, geodetic latitude, parallax, sidereal time, hour angle, horizontal coordinates, airless altitude, observed altitude, refraction; and apparent position no longer names a viewpoint.
  • ADR 0003 records the departure from Meeus's conventions.
  • The README gains the observer module in its table and an example of the Moon's altitude and azimuth from a named site; the Horizons tooling gains topocentric targets, whose tables carry the existing Horizons REUSE annotation.
## Problem Statement Everything zig-astro computes today is **geocentric**: where the Sun and the Moon appear from the Earth's center. Nobody stands there. For the Moon the difference is large: its **parallax** reaches about 1°, two of its own diameters, so a geocentric position cannot say whether the Moon is above a given horizon, which way to point a telescope, or when an occultation happens. Nor can the library answer the questions an observer asks first — how high is the Moon, and in which direction — because it has no **sidereal time**, no notion of an **observer**, and no **horizontal coordinates**. Someone wanting them would have to do the work themselves, at exactly the places such code goes wrong: longitude counted west where east was meant, azimuth from south where north was meant, geodetic latitude mistaken for geocentric, UT confused with TT in the Earth's rotation, and refraction applied or not without saying which. ## Solution zig-astro adds **topocentric** positions — apparent positions as seen by an **observer** on the Earth's surface — for the Sun and the Moon, and the machinery beneath them, built generically so that the planets and satellites to come use it unchanged: - **sidereal time**, mean and apparent, at Greenwich; - an **observer**: **geodetic latitude**, longitude positive east, and height above the WGS 84 ellipsoid; - a generic conversion of any apparent geocentric position and distance into a topocentric one: equatorial and ecliptic coordinates, the **hour angle**, **horizontal coordinates** (azimuth from north through east, and the **airless altitude**), and the topocentric distance; - **refraction**, as an explicit, separate step between the airless and the **observed altitude**, under a stated pressure and temperature; - the topocentric Sun, the topocentric Moon, and the Moon's **topocentric libration**. The library's conventions follow the IAU and WGS 84 rather than Meeus where the two differ (ADR 0003). ## User Stories 1. As an amateur astronomer, I want the Moon's azimuth and altitude from my backyard at an instant, so that I know where to look. 2. As an amateur astronomer, I want the Sun's azimuth and altitude from my location, so that I can plan around it. 3. As a developer, I want to describe an observer by latitude, longitude and height, so that every topocentric calculation takes the same value. 4. As a developer, I want longitude to count positive east, so that coordinates copied from a map or a GPS receiver work unchanged. 5. As a developer reading Meeus alongside the code, I want the departure from his west-positive longitude and south-based azimuth documented, so that I don't take it for a bug. 6. As a developer, I want azimuth counted from north through east in [0°, 360°), so that it matches compasses, almanacs and JPL Horizons. 7. As a developer, I want the observer's latitude to be geodetic, so that the latitude on any map is the right one to give. 8. As a developer, I want height measured above the WGS 84 ellipsoid, and to know that height above sea level is good enough, so that I don't have to look up the geoid. 9. As a developer, I want a latitude beyond ±90° to give NaN rather than a plausible wrong answer, so that bad input is visible. 10. As a developer, I want any finite longitude to be accepted and wrapped, so that −180° and 180° and 540° mean the same thing. 11. As a developer, I want negative heights accepted, so that an observer at the Dead Sea is not an error. 12. As a developer, I want Greenwich mean sidereal time at an instant, so that I can point an equatorial mount. 13. As a developer, I want Greenwich apparent sidereal time, including the equation of the equinoxes, so that it matches apparent right ascensions. 14. As a developer, I want sidereal time to accept an instant in either time scale, so that I don't have to convert to UT myself. 15. As a developer, I want the local sidereal time and hour angle of a body for my observer, so that I can tell how far it is from the meridian. 16. As a developer, I want a body's topocentric right ascension and declination, so that I can point a telescope at the Moon to better than its own diameter. 17. As a developer, I want topocentric ecliptic longitude and latitude, so that I can compare the Moon's place against stars and the Sun as seen from my site. 18. As a developer, I want the topocentric distance, so that I can compute the Moon's apparent size from my location. 19. As a developer adding a new body later, I want a generic conversion from any apparent geocentric equatorial position and distance to a topocentric one, so that a planet or satellite needs no parallax code of its own. 20. As a developer, I want the topocentric result to carry its equatorial, ecliptic, hour-angle, horizontal and distance values together, so that I don't recompute shared pieces. 21. As a developer, I want the distance in kilometers for every body in the topocentric result, so that the type is the same for the Sun and the Moon. 22. As a developer, I want the horizontal coordinates to give the airless altitude, so that I always know whether refraction has been applied. 23. As a developer, I want a function that converts an airless altitude to an observed altitude, so that I can predict where the Moon will actually be seen near the horizon. 24. As a developer, I want the inverse, from observed to airless altitude, so that I can reduce a measurement. 25. As a developer, I want to give the pressure and temperature, defaulting to 1010 mb and 10 °C, so that I can account for the weather when it matters and ignore it when it doesn't. 26. As a developer, I want refraction to be zero well below the horizon, so that a body under the Earth doesn't get an absurd correction. 27. As a developer, I want refraction never to be negative, so that it only ever raises a body. 28. As a lunar observer, I want the Moon's topocentric libration, so that I know which limb features are favorably placed from my site tonight. 29. As a lunar observer, I want the topocentric position angle of the axis, so that I can orient a map to the Moon as I see it. 30. As a developer, I want the topocentric libration in the same type as the geocentric one, so that code using one can take the other. 31. As a developer, I want the topocentric Sun and Moon to be thin wrappers over their geocentric positions, so that the two can never disagree about anything but the viewpoint. 32. As a developer, I want the effects left out — diurnal aberration, polar motion, UT1 − UTC, deflection of the vertical — named in the documentation with their sizes, so that I know the error budget. 33. As a developer, I want each function's accuracy stated against JPL Horizons in its documentation, so that I can decide whether it is good enough. 34. As a developer, I want non-finite instants to give NaN everywhere, as the rest of the library does, so that errors propagate visibly. 35. As a maintainer, I want every result checked against Meeus's worked examples and JPL Horizons from several sites, so that sign and convention errors cannot hide. 36. As a maintainer, I want fuzz targets for the new functions, so that edge cases at the poles, the antimeridian, the zenith and the nadir are covered. 37. As a reader of the README, I want an example of the Moon's altitude and azimuth from a named site, so that I can see the API at a glance. 38. As a maintainer, I want the Horizons tooling to fetch topocentric tables, so that the reference data can be regenerated. ## Implementation Decisions - **Sidereal time** goes in the Earth module: Greenwich mean sidereal time from Meeus eq. 12.4, and apparent sidereal time adding the equation of the equinoxes, Δψ cos ε, with the true obliquity. A TT instant is converted to UT through the ΔT model, as positions already accept either scale. - A new **observer** module holds: - the **Observer** type: a plain struct of geodetic latitude, longitude positive east (both angles), and height in meters above the WGS 84 ellipsoid (a = 6378.137 km, 1/f = 298.257223563). No validating constructor. ρ sin φ′ and ρ cos φ′ come from Meeus ch. 11 with the WGS 84 constants. - the generic topocentric conversion: given an instant, an observer, an apparent geocentric equatorial position and a distance in kilometers, it returns a **Topocentric** result with `equatorial`, `ecliptic`, `hour_angle`, `horizontal` and `distance_km`. Parallax uses the rigorous formulas (eqs. 40.6 and 40.7), not the approximations; the ecliptic coordinates follow Meeus's topocentric ecliptic method (ch. 40) or are converted from the topocentric equatorial ones with the true obliquity. The hour angle is local apparent sidereal time minus right ascension, topocentric. - **refraction** from airless to observed altitude (Sæmundsson, eq. 16.4) and its inverse (Bennett, eq. 16.3), both scaled by an **Atmosphere** of pressure in millibars and temperature in °C, defaulting to 1010 mb and 10 °C. Refraction is zero below an airless altitude of about −2°, the exact cutoff to be checked against what Horizons does. - **Horizontal coordinates** become a new type in the coordinate module: azimuth from north through east in [0°, 360°), altitude in [−90°, +90°]. - The Sun module gains a topocentric function, taking an instant and an observer, that passes its apparent position through the generic conversion with the distance in kilometers (1 au = 149,597,870.7 km). The geocentric position keeps its distance in astronomical units. - The Moon module gains the same. - The libration module gains a topocentric libration, returning the existing libration type, by feeding the topocentric ecliptic longitude and latitude to the same selenographic reduction the geocentric libration uses, and the topocentric right ascension to the position angle of the axis. Meeus's approximate corrections are not used. - Conventions departing from Meeus — east longitude, azimuth from north — are recorded in ADR 0003 and warned of in doc comments. - Diurnal aberration (≤ 0.32″), polar motion, UT1 − UTC and deflection of the vertical are left out and named in the doc comments. - An out-of-range latitude makes every topocentric result NaN; finite longitudes wrap; finite heights of any sign are accepted. ## Testing Decisions - Tests go through the public `astro` module only, from the test directory, as the existing ones do; no internal hooks are added. - Meeus's worked examples, read from the page images: 11.a (Palomar's ρ sin φ′ and ρ cos φ′), 12.a and 12.b (sidereal time), 13.b (Venus's horizontal coordinates from Washington, with his azimuth turned by 180° and his longitude's sign flipped), 40.a (Mars from Palomar, through the generic conversion), the refraction examples of ch. 16, and the topocentric libration example of ch. 53 if there is one. - JPL Horizons topocentric tables for the Sun and the Moon at 201 instants over 1900–2100 from three sites — Palomar, Tromsø and Cerro Paranal — with right ascension and declination, azimuth and elevation airless and refracted, and the Moon's topocentric sub-observer point and north pole angle. The Horizons site coordinates are given as geodetic, matching the observer. - Tolerances are set only after measuring: the worst error seen is recorded in a comment beside each, and the tolerance is set just above it. Where an error is systematic rather than scatter (as with the lunar frame tilt in the geocentric libration), it is explained, not just absorbed. - Property tests: a TT instant and the same moment in UT give the same sidereal time; the topocentric shift stays within the parallax bound; the distance changes by no more than the Earth's radius plus the height; Bennett inverts Sæmundsson within what Meeus states; non-finite instants and out-of-range latitudes give NaN. - Fuzz targets for sidereal time, the topocentric Sun and Moon, refraction, and topocentric libration, with seeds at the poles, the antimeridian, the zenith and the nadir. - Prior art: the existing Meeus-example tests, the Horizons comparisons with their parser and recorded worst errors, and the fuzz targets with their seeds. ## Out of Scope - Rising, setting and transit, which are an event search and get their own spec. - The Moon's topocentric semidiameter (ch. 55). - The topocentric phase: illuminated fraction, phase angle and bright limb as seen from the site. - Diurnal aberration, polar motion, UT1 − UTC and deflection of the vertical. - Ellipsoids other than WGS 84, and geoid heights. - Planets and artificial satellites themselves; only the generic conversion they will use. ## Further Notes - The glossary entries added for this work: geocentric, topocentric, observer, geodetic latitude, parallax, sidereal time, hour angle, horizontal coordinates, airless altitude, observed altitude, refraction; and apparent position no longer names a viewpoint. - ADR 0003 records the departure from Meeus's conventions. - The README gains the observer module in its table and an example of the Moon's altitude and azimuth from a named site; the Horizons tooling gains topocentric targets, whose tables carry the existing Horizons REUSE annotation.
Author
Owner

Changed from this spec while working #14: refraction below −1° airless is held at its value there (as Horizons does) rather than zeroed below −2°, and the inverse, airless from observed, inverts Sæmundsson's formula exactly rather than using Bennett's. See #14 and 0c94159.

Changed from this spec while working #14: refraction below −1° airless is held at its value there (as Horizons does) rather than zeroed below −2°, and the inverse, airless from observed, inverts Sæmundsson's formula exactly rather than using Bennett's. See #14 and 0c94159.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jeff/zig-astro#10
No description provided.