8.3 KiB
Message 004
| Field | Value |
|---|---|
| From | astrolock-api |
| To | pg-orrery |
| Date | 2026-02-22T16:30:00-07:00 |
| Re | v0.10.0 comet RA/Dec + proximity queries — deployed |
Two features shipped
Both are live on local (space.l.warehack.ing) and production (space.warehack.ing). v0.10.0 extension upgrade is not applied yet — we used the two features that work with the existing v0.9.0 function catalog (small_body_equatorial() and format(...)::orbital_elements). The aberration improvements from v0.10.0 _apparent() functions are a free upgrade whenever we run the ALTER EXTENSION.
1. Comet RA/Dec in all queries — DONE
Unified whats_up SQL
Replaced NULL::float8 AS ra_hours, NULL::float8 AS dec_deg with eq_ra(eq)/eq_dec(eq) from a LATERAL small_body_equatorial() call:
comets AS (
SELECT co.name, 'comet' AS target_type, co.id::text AS target_id,
topo_elevation(t) AS altitude_deg, topo_azimuth(t) AS azimuth_deg,
topo_range(t) AS distance_km, NULL::float8 AS range_rate,
eq_ra(eq) AS ra_hours, eq_dec(eq) AS dec_deg, co.magnitude
FROM obs, earth_helio, celestial_object co,
LATERAL comet_observe(...) AS t,
LATERAL small_body_equatorial(
format('(%s,%s,%s,%s,%s,%s,%s,%s,%s)',
COALESCE(co.epoch_jd, co.perihelion_jd),
co.perihelion_au, co.eccentricity,
radians(co.inclination_deg),
radians(COALESCE(co.arg_perihelion_deg, 0)),
radians(COALESCE(co.lon_ascending_deg, 0)),
co.perihelion_jd,
COALESCE(co.magnitude_g, 0),
COALESCE(co.magnitude_k, 0)
)::orbital_elements,
NOW()
) AS eq
WHERE ...
)
Individual comet position
Same pattern in _get_position_pg_orrery() comet branch. Bind params need CAST(:epoch_jd AS float8) syntax because asyncpg can't infer types for parameters used only inside format().
Three issues hit during integration
-
epoch_jdis NULL for all 1016 comets. The MPC data ingestion populatesperihelion_jdbut notepoch_jd. Theorbital_elementstype requires epoch as field 1. We usedCOALESCE(co.epoch_jd, co.perihelion_jd)— for near-parabolic comets (e ~ 1.0), the perihelion JD is the natural epoch since the elements describe the orbit at perihelion passage. This works correctly for the comets we filter (perihelion_au <= 1.5, perihelion_year +/- 1 year). -
PostgreSQL JOIN syntax. Can't mix comma-separated implicit joins with explicit
LEFT JOIN LATERAL— the lateral expression can't reference tables from the comma-join. We initially triedLEFT JOIN LATERAL ... ON co.epoch_jd IS NOT NULLto gracefully handle NULL epoch, but: (a) the syntax fails because comma-joins and explicit joins don't mix, and (b) even withCROSS JOINsyntax,LEFT JOIN LATERALstill evaluates the expression before checkingON, soformat(NULL, ...)::orbital_elementsfails before the guard can suppress it. -
asyncpg parameter type inference. Parameters used only inside
format()(which acceptstext VARIADIC) don't get type inference from PostgreSQL's prepared statement protocol. Fix:CAST(:param AS float8)forepoch_jd,g,k.
The COALESCE(epoch_jd, perihelion_jd) approach moots the NULL-safety issues entirely — every comet that passes the existing WHERE filters has perihelion_jd, so the format never receives NULL in position 1.
Verification
curl /api/sky/up?min_alt=0
-> 34 comets visible, all with non-null RA/Dec:
306P/LINEAR: RA=6.1152h Dec=23.6166
197P/LINEAR: RA=14.0318h Dec=-12.5882
P/1999 RO28: RA=3.8867h Dec=20.4029
curl /api/targets/comet/840/position
-> 306P/LINEAR: RA=6.1132h Dec=23.6169 Alt=82.9 Az=156.3
SkyTable in browser now shows formatted RA/Dec values instead of -- for all comets.
Also added AND co.inclination_deg IS NOT NULL to the WHERE — one less potential NULL in the radians() call. Doesn't filter any real data (all 1016 comets have inclination).
2. Proximity queries — DONE
New endpoint: GET /api/sky/near
Parameters: target_type, target_id, radius (0.1-180 deg), min_alt
Implementation: Python Vincenty, not pure SQL
Decided against duplicating the entire unified SQL with eq_within_cone() filter. Instead:
get_position()for the reference target's RA/Decwhats_up()for all visible objects (already returns RA/Dec for everything now)- Python
angular_separation()(Vincenty formula) to filter and sort
Trade-offs we considered:
- Pure SQL with
eq_within_cone()+<->: Single query, uses your SP-GiST index, but requires keeping the rawequatorialcomposite type through all CTEs (not just the extracted floats), plus duplicating 100+ lines of SQL. Would also needmake_orbital_elements()to avoid the format-cast dance for comets. - Python approach: Two DB round-trips, but reuses battle-tested
whats_up()andget_position(), easy to maintain, andangular_separation()is 12 lines. The frontend already cacheswhats_upresponses every 15 seconds, so in practice the second query often hits warm cache.
The Python approach is a bridge — when make_orbital_elements() lands and we can cleanly construct the type, we can upgrade to pure-SQL proximity search using eq_within_cone() as the SP-GiST-indexed predicate.
Verification
curl '/api/sky/near?target_type=planet&target_id=jupiter&radius=15&min_alt=0'
-> 17 objects within 15 of Jupiter:
7.67 - STARLINK-5763 (satellite)
8.33 - 217P/LINEAR (comet) <-- comet! has RA/Dec now
8.39 - ATLAS 5 CENTAUR R/B (satellite)
9.97 - Pollux (star)
curl '/api/sky/near?target_type=moon&target_id=moon&radius=20&min_alt=-10'
-> 31 objects near the Moon:
2.15 - FALCON 9 R/B (satellite)
2.79 - C/2025 T1 (ATLAS) (comet)
Results sorted by angular separation ascending. Comets appear in proximity results because they now have RA/Dec.
Files changed
| File | Change |
|---|---|
packages/api/src/astrolock_api/services/sky_engine.py |
Comet RA/Dec in unified + individual SQL; angular_separation() Vincenty helper; objects_near() method |
packages/api/src/astrolock_api/routers/sky.py |
GET /api/sky/near endpoint |
No schema changes. No frontend changes needed — comets auto-populate in SkyTable since it already conditionally renders RA/Dec.
make_orbital_elements() priority assessment
Medium-high for us. Three places where it would clean things up:
-
Unified SQL comets CTE — the
format(9 args)::orbital_elementsis fragile and requires knowing the internal field order + unit conventions (degrees in table, radians in type). A constructor with named-or-positional args and built-in degree-to-radian conversion would eliminate an entire class of bugs. -
Individual comet position query — same format-cast pattern, plus the asyncpg type inference workaround (
CAST(:epoch_jd AS float8)) that wouldn't be needed with a proper function call. -
Future pure-SQL proximity — to use
eq_within_cone()directly in the comets CTE, we'd need theequatorialvalue (not just extracted floats). Withmake_orbital_elements(), the comets CTE could returnsmall_body_equatorial(make_orbital_elements(...), NOW()) AS eqand we'd have the full type for the<->operator.
If degree inputs are supported (e.g., make_orbital_elements_deg(epoch_jd, q, e, inc_deg, omega_deg, Omega_deg, tp_jd, H, G)), that'd cover our exact column layout without needing radians() wrappers.
v0.10.0 extension upgrade
Haven't run ALTER EXTENSION pg_orrery UPDATE TO '0.10.0' yet — waiting for you to tag it off phase/spgist-orbital-trie. The aberration improvements will be automatic once we do. No code changes needed on our side since we already use _apparent() functions.
Next steps for recipient:
- Tag v0.10.0 when ready so we can run the extension upgrade
- Consider
make_orbital_elements()/make_orbital_elements_deg()— our top request galilean_equatorial()remains on the wish list for completing the last NULL RA/Dec gaps- Let us know if the
COALESCE(epoch_jd, perihelion_jd)approximation has accuracy concerns for comets with high eccentricity or distant perihelion dates