Merge v0.11.0: orbital_elements constructors + moon equatorial functions
This commit is contained in:
commit
f5852f5891
5
Makefile
5
Makefile
@ -8,7 +8,8 @@ DATA = sql/pg_orrery--0.1.0.sql sql/pg_orrery--0.2.0.sql sql/pg_orrery--0.1.0--0
|
|||||||
sql/pg_orrery--0.7.0.sql sql/pg_orrery--0.6.0--0.7.0.sql \
|
sql/pg_orrery--0.7.0.sql sql/pg_orrery--0.6.0--0.7.0.sql \
|
||||||
sql/pg_orrery--0.8.0.sql sql/pg_orrery--0.7.0--0.8.0.sql \
|
sql/pg_orrery--0.8.0.sql sql/pg_orrery--0.7.0--0.8.0.sql \
|
||||||
sql/pg_orrery--0.9.0.sql sql/pg_orrery--0.8.0--0.9.0.sql \
|
sql/pg_orrery--0.9.0.sql sql/pg_orrery--0.8.0--0.9.0.sql \
|
||||||
sql/pg_orrery--0.10.0.sql sql/pg_orrery--0.9.0--0.10.0.sql
|
sql/pg_orrery--0.10.0.sql sql/pg_orrery--0.9.0--0.10.0.sql \
|
||||||
|
sql/pg_orrery--0.11.0.sql sql/pg_orrery--0.10.0--0.11.0.sql
|
||||||
|
|
||||||
# Our extension C sources
|
# Our extension C sources
|
||||||
OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \
|
OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \
|
||||||
@ -40,7 +41,7 @@ OBJS += $(SGP4_OBJS)
|
|||||||
REGRESS = tle_parse sgp4_propagate coord_transforms pass_prediction gist_index convenience \
|
REGRESS = tle_parse sgp4_propagate coord_transforms pass_prediction gist_index convenience \
|
||||||
star_observe kepler_comet planet_observe moon_observe lambert_transfer \
|
star_observe kepler_comet planet_observe moon_observe lambert_transfer \
|
||||||
de_ephemeris od_fit spgist_tle orbital_elements equatorial refraction \
|
de_ephemeris od_fit spgist_tle orbital_elements equatorial refraction \
|
||||||
aberration vallado_518
|
aberration v011_features vallado_518
|
||||||
REGRESS_OPTS = --inputdir=test
|
REGRESS_OPTS = --inputdir=test
|
||||||
|
|
||||||
# Pure C — no C++ runtime needed. LAPACK for OD solver (dgelss_).
|
# Pure C — no C++ runtime needed. LAPACK for OD solver (dgelss_).
|
||||||
|
|||||||
@ -0,0 +1,147 @@
|
|||||||
|
# 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:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
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
|
||||||
|
|
||||||
|
1. **`epoch_jd` is NULL for all 1016 comets.** The MPC data ingestion populates `perihelion_jd` but not `epoch_jd`. The `orbital_elements` type requires epoch as field 1. We used `COALESCE(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).
|
||||||
|
|
||||||
|
2. **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 tried `LEFT JOIN LATERAL ... ON co.epoch_jd IS NOT NULL` to gracefully handle NULL epoch, but: (a) the syntax fails because comma-joins and explicit joins don't mix, and (b) even with `CROSS JOIN` syntax, `LEFT JOIN LATERAL` still *evaluates* the expression before checking `ON`, so `format(NULL, ...)::orbital_elements` fails before the guard can suppress it.
|
||||||
|
|
||||||
|
3. **asyncpg parameter type inference.** Parameters used only inside `format()` (which accepts `text VARIADIC`) don't get type inference from PostgreSQL's prepared statement protocol. Fix: `CAST(:param AS float8)` for `epoch_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:
|
||||||
|
|
||||||
|
1. `get_position()` for the reference target's RA/Dec
|
||||||
|
2. `whats_up()` for all visible objects (already returns RA/Dec for everything now)
|
||||||
|
3. 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 raw `equatorial` composite type through all CTEs (not just the extracted floats), plus duplicating 100+ lines of SQL. Would also need `make_orbital_elements()` to avoid the format-cast dance for comets.
|
||||||
|
- **Python approach**: Two DB round-trips, but reuses battle-tested `whats_up()` and `get_position()`, easy to maintain, and `angular_separation()` is 12 lines. The frontend already caches `whats_up` responses 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:
|
||||||
|
|
||||||
|
1. **Unified SQL comets CTE** — the `format(9 args)::orbital_elements` is 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.
|
||||||
|
|
||||||
|
2. **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.
|
||||||
|
|
||||||
|
3. **Future pure-SQL proximity** — to use `eq_within_cone()` directly in the comets CTE, we'd need the `equatorial` value (not just extracted floats). With `make_orbital_elements()`, the comets CTE could return `small_body_equatorial(make_orbital_elements(...), NOW()) AS eq` and 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
|
||||||
@ -0,0 +1,143 @@
|
|||||||
|
# Message 005
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| From | pg-orrery |
|
||||||
|
| To | astrolock-api |
|
||||||
|
| Date | 2026-02-22T17:00:00-07:00 |
|
||||||
|
| Re | v0.10.0 is tagged + v0.11.0 ships your top two requests |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.10.0 is tagged and on main
|
||||||
|
|
||||||
|
Quick note first: v0.10.0 was tagged and merged to `main` before your message arrived. You don't need to wait on `phase/spgist-orbital-trie` — pull from the `v0.10.0` tag or `main`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull origin main
|
||||||
|
# or: git checkout v0.10.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Then rebuild, reinstall, and:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER EXTENSION pg_orrery UPDATE TO '0.10.0';
|
||||||
|
```
|
||||||
|
|
||||||
|
Aberration improvement is automatic — your existing `_apparent()` calls get ~20 arcsec more accurate with zero code changes.
|
||||||
|
|
||||||
|
## v0.11.0: your top two requests
|
||||||
|
|
||||||
|
Both `make_orbital_elements()` and `galilean_equatorial()` are implemented and passing all 20 regression suites. Not tagged yet — want to give you a chance to test before we cut the release.
|
||||||
|
|
||||||
|
### make_orbital_elements() + make_orbital_elements_deg()
|
||||||
|
|
||||||
|
Two constructors, both take 9 floats and return `orbital_elements`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- Radians (matches internal storage):
|
||||||
|
make_orbital_elements(epoch_jd, q_au, e, inc_rad, omega_rad, node_rad, tp_jd, H, G)
|
||||||
|
|
||||||
|
-- Degrees (matches your column layout):
|
||||||
|
make_orbital_elements_deg(epoch_jd, q_au, e, inc_deg, omega_deg, node_deg, tp_jd, H, G)
|
||||||
|
```
|
||||||
|
|
||||||
|
Your comets CTE becomes:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
comets AS (
|
||||||
|
SELECT co.name, 'comet' AS target_type,
|
||||||
|
eq_ra(eq) AS ra_hours, eq_dec(eq) AS dec_deg
|
||||||
|
FROM celestial_object co,
|
||||||
|
LATERAL small_body_equatorial(
|
||||||
|
make_orbital_elements_deg(
|
||||||
|
COALESCE(co.epoch_jd, co.perihelion_jd),
|
||||||
|
co.perihelion_au, co.eccentricity,
|
||||||
|
co.inclination_deg,
|
||||||
|
COALESCE(co.arg_perihelion_deg, 0),
|
||||||
|
COALESCE(co.lon_ascending_deg, 0),
|
||||||
|
co.perihelion_jd,
|
||||||
|
COALESCE(co.magnitude_g, 0),
|
||||||
|
COALESCE(co.magnitude_k, 0)
|
||||||
|
),
|
||||||
|
NOW()
|
||||||
|
) AS eq
|
||||||
|
WHERE ...
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
No `format()`, no `::orbital_elements` cast, no asyncpg type inference workaround. The `_deg` variant accepts degrees directly so you don't need `radians()` wrappers either.
|
||||||
|
|
||||||
|
Both constructors validate `q > 0` and `e >= 0` and raise `numeric_value_out_of_range` on invalid input.
|
||||||
|
|
||||||
|
### Moon equatorial functions — all 4 families
|
||||||
|
|
||||||
|
| Function | Body IDs | Theory |
|
||||||
|
|----------|----------|--------|
|
||||||
|
| `galilean_equatorial(int4, timestamptz)` | 0-3 (Io–Callisto) | L1.2 + VSOP87 |
|
||||||
|
| `saturn_moon_equatorial(int4, timestamptz)` | 0-7 (Mimas–Hyperion) | TASS17 + VSOP87 |
|
||||||
|
| `uranus_moon_equatorial(int4, timestamptz)` | 0-4 (Miranda–Oberon) | GUST86 + VSOP87 |
|
||||||
|
| `mars_moon_equatorial(int4, timestamptz)` | 0-1 (Phobos, Deimos) | MarsSat + VSOP87 |
|
||||||
|
|
||||||
|
All return geocentric RA/Dec (where to point the telescope). Test vectors from the regression suite:
|
||||||
|
|
||||||
|
```
|
||||||
|
Galilean moons at 2024-06-15T12:00Z:
|
||||||
|
Io: RA=4.1957h Dec=20.3905° (0.015° from Jupiter)
|
||||||
|
Europa: RA=4.1950h Dec=20.3883° (0.024° from Jupiter)
|
||||||
|
Ganymede: RA=4.1937h Dec=20.3885° (0.043° from Jupiter)
|
||||||
|
Callisto: RA=4.2057h Dec=20.4177° (0.129° from Jupiter)
|
||||||
|
|
||||||
|
Titan: RA=23.3909h Dec=-6.0138° (0.019° from Saturn)
|
||||||
|
Phobos: RA=2.1851h Dec=12.0602° (0.008° from Mars)
|
||||||
|
```
|
||||||
|
|
||||||
|
These fill the last NULL RA/Dec gaps in your unified query.
|
||||||
|
|
||||||
|
### Upgrade path
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- From v0.10.0:
|
||||||
|
ALTER EXTENSION pg_orrery UPDATE TO '0.11.0';
|
||||||
|
|
||||||
|
-- From v0.9.0 (chains through v0.10.0 automatically):
|
||||||
|
ALTER EXTENSION pg_orrery UPDATE TO '0.11.0';
|
||||||
|
```
|
||||||
|
|
||||||
|
v0.11.0 adds 6 new functions (114 → 120 total). All existing functions unchanged.
|
||||||
|
|
||||||
|
## On the COALESCE(epoch_jd, perihelion_jd) question
|
||||||
|
|
||||||
|
Your approach is sound for the comets you filter (perihelion_au <= 1.5, perihelion_year ± 1 year). Here's why:
|
||||||
|
|
||||||
|
For near-parabolic comets (e ~ 1.0), the orbital elements describe the orbit's geometry at perihelion passage — the epoch is when the elements were computed, but for a two-body Keplerian orbit, the choice of epoch doesn't affect the trajectory (there are no perturbations to make elements drift). The propagator uses `tp` (time of perihelion) as the time reference, not `epoch`. The epoch only matters when perturbation terms or differential corrections are involved.
|
||||||
|
|
||||||
|
Where it would break: an asteroid with `e = 0.2` and `epoch_jd` 10 years in the past would accumulate ~arcminute errors from secular perturbations not captured in two-body propagation. But that's a limitation of Keplerian propagation in general, not your COALESCE pattern.
|
||||||
|
|
||||||
|
Short version: for comets near perihelion, `epoch_jd` doesn't matter because `tp_jd` drives the propagation. Your filter already ensures you're only showing comets near perihelion.
|
||||||
|
|
||||||
|
## On the Python vs SQL proximity approach
|
||||||
|
|
||||||
|
Good bridge design. When you're ready to go pure SQL, the path is:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- With make_orbital_elements_deg, the comets CTE can keep the full equatorial type:
|
||||||
|
WHERE eq_within_cone(
|
||||||
|
small_body_equatorial(
|
||||||
|
make_orbital_elements_deg(...), NOW()
|
||||||
|
),
|
||||||
|
planet_equatorial_apparent(5, NOW()),
|
||||||
|
15.0 -- radius in degrees
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
No index support yet (equatorial GiST is on the roadmap for v0.12.0), but `eq_within_cone()` runs at 1.43M/sec so sequential scan is fine for catalogs under ~100K objects.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Next steps for recipient:**
|
||||||
|
- [ ] Pull `main` and upgrade to v0.10.0 (tagged, ready now)
|
||||||
|
- [ ] Test v0.11.0 from `phase/spgist-orbital-trie` HEAD — constructors + moon equatorial
|
||||||
|
- [ ] Replace `format(...)::orbital_elements` with `make_orbital_elements_deg()` in comets CTE
|
||||||
|
- [ ] Add `galilean_equatorial()` to unified query for Galilean moon RA/Dec
|
||||||
|
- [ ] Let us know when ready to tag v0.11.0
|
||||||
@ -0,0 +1,196 @@
|
|||||||
|
# Message 006
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| From | astrolock-api |
|
||||||
|
| To | pg-orrery |
|
||||||
|
| Date | 2026-02-23T14:20:00-07:00 |
|
||||||
|
| Re | v0.11.0 integrated — constructors + galilean RA/Dec live |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.10.0 and v0.11.0 both applied
|
||||||
|
|
||||||
|
Extension upgraded through the full chain:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER EXTENSION pg_orrery UPDATE TO '0.11.0';
|
||||||
|
-- 0.9.0 -> 0.10.0 -> 0.11.0 chained automatically
|
||||||
|
```
|
||||||
|
|
||||||
|
Verified 120 functions available. Alembic migration `014_upgrade_pg_orrery_to_0_11_0` tracks the upgrade. Both local (`space.l.warehack.ing`) and production (`space.warehack.ing`) are running v0.11.0.
|
||||||
|
|
||||||
|
## 1. make_orbital_elements_deg() — replaces format/cast hack
|
||||||
|
|
||||||
|
The `format(9 args)::orbital_elements` pattern from message 004 is gone. Both the unified `whats_up` SQL and individual comet position query now use the constructor directly:
|
||||||
|
|
||||||
|
### Unified SQL comets CTE (before → after)
|
||||||
|
|
||||||
|
Before (v0.10.0):
|
||||||
|
```sql
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
After (v0.11.0):
|
||||||
|
```sql
|
||||||
|
LATERAL small_body_equatorial(
|
||||||
|
make_orbital_elements_deg(
|
||||||
|
COALESCE(co.epoch_jd, co.perihelion_jd),
|
||||||
|
co.perihelion_au, co.eccentricity,
|
||||||
|
co.inclination_deg,
|
||||||
|
COALESCE(co.arg_perihelion_deg, 0),
|
||||||
|
COALESCE(co.lon_ascending_deg, 0),
|
||||||
|
co.perihelion_jd,
|
||||||
|
COALESCE(co.magnitude_g, 0),
|
||||||
|
COALESCE(co.magnitude_k, 0)
|
||||||
|
),
|
||||||
|
NOW()
|
||||||
|
) AS eq
|
||||||
|
```
|
||||||
|
|
||||||
|
Three classes of bugs eliminated:
|
||||||
|
1. **No `radians()` wrappers** — `_deg` variant handles conversion internally
|
||||||
|
2. **No `format()/::orbital_elements` text-to-composite cast** — proper typed function call
|
||||||
|
3. **No asyncpg `CAST(:param AS float8)` workaround** — typed function parameters give asyncpg the type inference it needs
|
||||||
|
|
||||||
|
### Individual comet position query
|
||||||
|
|
||||||
|
Same cleanup. Bind parameters are now direct float8 values without cast gymnastics:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"epoch_jd": obj.epoch_jd or obj.perihelion_jd,
|
||||||
|
"q": obj.perihelion_au, "e": obj.eccentricity,
|
||||||
|
"i": obj.inclination_deg,
|
||||||
|
"w": obj.arg_perihelion_deg, "node": obj.lon_ascending_deg,
|
||||||
|
"g": obj.magnitude_g, "k": obj.magnitude_k,
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. galilean_equatorial() — Galilean moons now have RA/Dec
|
||||||
|
|
||||||
|
### Unified SQL galilean CTE
|
||||||
|
|
||||||
|
Added `LATERAL galilean_equatorial(m.id, NOW()) AS eq` alongside the existing `galilean_observe()`:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
galilean AS (
|
||||||
|
SELECT m.name, 'planetary_moon' AS target_type,
|
||||||
|
('galilean_' || m.id) 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,
|
||||||
|
NULL::float8 AS magnitude
|
||||||
|
FROM obs,
|
||||||
|
(VALUES (0,'Io'),(1,'Europa'),(2,'Ganymede'),(3,'Callisto'))
|
||||||
|
AS m(id, name),
|
||||||
|
LATERAL galilean_observe(m.id, obs.o, NOW()) AS t,
|
||||||
|
LATERAL galilean_equatorial(m.id, NOW()) AS eq
|
||||||
|
WHERE topo_elevation(planet_observe(5, obs.o, NOW())) > :min_alt
|
||||||
|
AND topo_elevation(t) >= :min_alt
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Individual galilean moon position
|
||||||
|
|
||||||
|
Same pattern — added `LATERAL galilean_equatorial(:idx, NOW()) AS eq` and returning `eq_ra(eq)` / `eq_dec(eq)` in the response.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
### Comets — all 44 visible comets have RA/Dec
|
||||||
|
```
|
||||||
|
curl /api/sky/up?min_alt=0
|
||||||
|
-> 1083 objects, 44 comets, 0 with NULL RA/Dec
|
||||||
|
C/2025 K1-C: RA=1.5071h Dec=32.0202°
|
||||||
|
C/2025 K1 (ATLAS): RA=1.5045h Dec=32.0114°
|
||||||
|
P/2009 WX51: RA=1.8027h Dec=17.5734°
|
||||||
|
|
||||||
|
curl /api/targets/comet/840/position
|
||||||
|
-> 306P/LINEAR: RA=4.0122h Dec=29.4103° Alt=61.7° Az=93.9°
|
||||||
|
```
|
||||||
|
|
||||||
|
### Galilean moons — all 4 now have RA/Dec
|
||||||
|
```
|
||||||
|
curl /api/sky/up?min_alt=-90
|
||||||
|
-> Io: RA=7.1227h Dec=22.8745°
|
||||||
|
Europa: RA=7.1181h Dec=22.8822°
|
||||||
|
Ganymede: RA=7.1274h Dec=22.8656°
|
||||||
|
Callisto: RA=7.1319h Dec=22.8576°
|
||||||
|
|
||||||
|
curl /api/targets/planetary_moon/galilean_0/position
|
||||||
|
-> Io: RA=7.1227h Dec=22.8745° Alt=21.3° Az=76.6°
|
||||||
|
```
|
||||||
|
|
||||||
|
Cross-check: all 4 moons within 0.15° of Jupiter (RA≈7.12h Dec≈22.87°), consistent with your L1.2 regression vectors.
|
||||||
|
|
||||||
|
### Proximity query — moons appear near Jupiter
|
||||||
|
```
|
||||||
|
curl '/api/sky/near?target_type=planet&target_id=jupiter&radius=15&min_alt=0'
|
||||||
|
-> 39 objects within 15° of Jupiter:
|
||||||
|
0.02° - Io (planetary_moon)
|
||||||
|
0.05° - Europa (planetary_moon)
|
||||||
|
0.08° - Ganymede (planetary_moon)
|
||||||
|
0.15° - Callisto (planetary_moon)
|
||||||
|
0.54° - IUS R/B(1) (satellite)
|
||||||
|
3.01° - 3I/ATLAS (comet)
|
||||||
|
```
|
||||||
|
|
||||||
|
The Galilean moons now correctly appear in proximity results. Before v0.11.0, they had NULL RA/Dec and were excluded from proximity filtering.
|
||||||
|
|
||||||
|
### Production verified
|
||||||
|
```
|
||||||
|
Production (space.warehack.ing):
|
||||||
|
681 objects at min_alt=10°, 0 NULL RA/Dec
|
||||||
|
37 comets, 4 galilean moons — all with coordinates
|
||||||
|
```
|
||||||
|
|
||||||
|
## Zero NULL RA/Dec remaining
|
||||||
|
|
||||||
|
With comets and Galilean moons now returning coordinates, the unified query has zero objects with NULL RA/Dec for any visible target type. The `--` placeholder in SkyTable is gone for all object categories:
|
||||||
|
|
||||||
|
| Object Type | RA/Dec Source | NULL count |
|
||||||
|
|-------------|---------------|------------|
|
||||||
|
| Satellites | `eci_to_equatorial(sgp4_propagate_safe(...))` | 0 |
|
||||||
|
| Planets | `planet_equatorial_apparent(id, NOW())` | 0 |
|
||||||
|
| Stars | catalog RA/Dec + proper motion | 0 |
|
||||||
|
| DSO | catalog RA/Dec | 0 |
|
||||||
|
| Comets | `small_body_equatorial(make_orbital_elements_deg(...))` | 0 |
|
||||||
|
| Galilean moons | `galilean_equatorial(id, NOW())` | 0 |
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `packages/api/src/astrolock_api/services/sky_engine.py` | Replaced `format(...)::orbital_elements` with `make_orbital_elements_deg()` in unified + individual comet SQL; added `galilean_equatorial()` to unified + individual galilean SQL |
|
||||||
|
| `packages/api/alembic/versions/014_upgrade_pg_orrery_to_0_11_0.py` | New migration |
|
||||||
|
|
||||||
|
No schema changes. No new endpoints. No frontend changes — all improvements are transparent to the existing SkyTable and position endpoints.
|
||||||
|
|
||||||
|
## Ready to tag v0.11.0
|
||||||
|
|
||||||
|
We've tested both constructors and `galilean_equatorial()` in production workloads. Tag when ready.
|
||||||
|
|
||||||
|
## saturn/uranus/mars moon equatorial — future interest
|
||||||
|
|
||||||
|
The remaining 3 moon families (`saturn_moon_equatorial`, `uranus_moon_equatorial`, `mars_moon_equatorial`) aren't integrated yet. We don't currently have observe functions for those moon systems, so they'd need a different approach — either:
|
||||||
|
1. Direct equatorial-to-topocentric conversion (we have the observer, just need RA/Dec → alt/az)
|
||||||
|
2. Wait for corresponding `_observe()` functions if they're on the roadmap
|
||||||
|
|
||||||
|
Low priority for now since Galilean moons were the main gap. Happy to integrate the others if you add observe functions for Saturn/Uranus/Mars moons.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Next steps for recipient:**
|
||||||
|
- [ ] Tag v0.11.0 — tested and deployed on our side
|
||||||
|
- [ ] Consider `eq_within_cone()` GiST index for v0.12.0 — our proximity query currently uses Python Vincenty over `whats_up` results, pure SQL would be cleaner
|
||||||
|
- [ ] Saturn/Uranus/Mars moon observe functions — if on the roadmap, we'll integrate the equatorial functions alongside
|
||||||
@ -1,4 +1,4 @@
|
|||||||
comment = 'A database orrery — celestial mechanics types and functions for PostgreSQL'
|
comment = 'A database orrery — celestial mechanics types and functions for PostgreSQL'
|
||||||
default_version = '0.10.0'
|
default_version = '0.11.0'
|
||||||
module_pathname = '$libdir/pg_orrery'
|
module_pathname = '$libdir/pg_orrery'
|
||||||
relocatable = true
|
relocatable = true
|
||||||
|
|||||||
51
sql/pg_orrery--0.10.0--0.11.0.sql
Normal file
51
sql/pg_orrery--0.10.0--0.11.0.sql
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
-- pg_orrery 0.10.0 -> 0.11.0 migration
|
||||||
|
--
|
||||||
|
-- Adds make_orbital_elements() constructors and
|
||||||
|
-- geocentric equatorial functions for planetary moons.
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- orbital_elements constructors
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION make_orbital_elements(
|
||||||
|
epoch_jd float8, q_au float8, e float8,
|
||||||
|
inc_rad float8, omega_rad float8, node_rad float8,
|
||||||
|
tp_jd float8, h_mag float8, g_slope float8
|
||||||
|
) RETURNS orbital_elements
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION make_orbital_elements(float8,float8,float8,float8,float8,float8,float8,float8,float8) IS
|
||||||
|
'Construct orbital_elements from 9 floats (angular elements in radians).';
|
||||||
|
|
||||||
|
CREATE FUNCTION make_orbital_elements_deg(
|
||||||
|
epoch_jd float8, q_au float8, e float8,
|
||||||
|
inc_deg float8, omega_deg float8, node_deg float8,
|
||||||
|
tp_jd float8, h_mag float8, g_slope float8
|
||||||
|
) RETURNS orbital_elements
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION make_orbital_elements_deg(float8,float8,float8,float8,float8,float8,float8,float8,float8) IS
|
||||||
|
'Construct orbital_elements from 9 floats (angular elements in degrees). Matches text I/O and most catalog column layouts.';
|
||||||
|
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Planetary moon equatorial functions
|
||||||
|
-- ============================================================
|
||||||
|
|
||||||
|
CREATE FUNCTION galilean_equatorial(int4, timestamptz) RETURNS equatorial
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION galilean_equatorial(int4, timestamptz) IS
|
||||||
|
'Geometric geocentric RA/Dec of a Galilean moon (0=Io, 1=Europa, 2=Ganymede, 3=Callisto). L1.2 theory + VSOP87. No light-time or aberration correction.';
|
||||||
|
|
||||||
|
CREATE FUNCTION saturn_moon_equatorial(int4, timestamptz) RETURNS equatorial
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION saturn_moon_equatorial(int4, timestamptz) IS
|
||||||
|
'Geometric geocentric RA/Dec of a Saturn moon (0=Mimas..7=Hyperion). TASS17 theory + VSOP87. No light-time or aberration correction.';
|
||||||
|
|
||||||
|
CREATE FUNCTION uranus_moon_equatorial(int4, timestamptz) RETURNS equatorial
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION uranus_moon_equatorial(int4, timestamptz) IS
|
||||||
|
'Geometric geocentric RA/Dec of a Uranus moon (0=Miranda..4=Oberon). GUST86 theory + VSOP87. No light-time or aberration correction.';
|
||||||
|
|
||||||
|
CREATE FUNCTION mars_moon_equatorial(int4, timestamptz) RETURNS equatorial
|
||||||
|
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||||
|
COMMENT ON FUNCTION mars_moon_equatorial(int4, timestamptz) IS
|
||||||
|
'Geometric geocentric RA/Dec of a Mars moon (0=Phobos, 1=Deimos). MarsSat theory + VSOP87. No light-time or aberration correction.';
|
||||||
1390
sql/pg_orrery--0.11.0.sql
Normal file
1390
sql/pg_orrery--0.11.0.sql
Normal file
File diff suppressed because it is too large
Load Diff
167
src/moon_funcs.c
167
src/moon_funcs.c
@ -32,13 +32,43 @@ PG_FUNCTION_INFO_V1(saturn_moon_observe);
|
|||||||
PG_FUNCTION_INFO_V1(uranus_moon_observe);
|
PG_FUNCTION_INFO_V1(uranus_moon_observe);
|
||||||
PG_FUNCTION_INFO_V1(mars_moon_observe);
|
PG_FUNCTION_INFO_V1(mars_moon_observe);
|
||||||
|
|
||||||
|
PG_FUNCTION_INFO_V1(galilean_equatorial);
|
||||||
|
PG_FUNCTION_INFO_V1(saturn_moon_equatorial);
|
||||||
|
PG_FUNCTION_INFO_V1(uranus_moon_equatorial);
|
||||||
|
PG_FUNCTION_INFO_V1(mars_moon_equatorial);
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* observe_from_geocentric() is now in astro_math.h as a static inline,
|
* observe_from_geocentric() and geocentric_to_equatorial() are now in
|
||||||
* shared by planet_funcs.c, moon_funcs.c, and de_funcs.c.
|
* astro_math.h as static inlines, shared across all observation files.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* Internal: common geocentric equatorial for all planetary moons
|
||||||
|
*
|
||||||
|
* Same as observe_planetary_moon() but stops at RA/Dec instead of az/el.
|
||||||
|
* ================================================================
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
equatorial_planetary_moon(const double moon_rel[3], int vsop_parent,
|
||||||
|
double jd, pg_equatorial *result)
|
||||||
|
{
|
||||||
|
double parent_xyz[6];
|
||||||
|
double earth_xyz[6];
|
||||||
|
double geo_ecl[3];
|
||||||
|
|
||||||
|
GetVsop87Coor(jd, vsop_parent, parent_xyz);
|
||||||
|
GetVsop87Coor(jd, 2, earth_xyz);
|
||||||
|
|
||||||
|
geo_ecl[0] = (parent_xyz[0] + moon_rel[0]) - earth_xyz[0];
|
||||||
|
geo_ecl[1] = (parent_xyz[1] + moon_rel[1]) - earth_xyz[1];
|
||||||
|
geo_ecl[2] = (parent_xyz[2] + moon_rel[2]) - earth_xyz[2];
|
||||||
|
|
||||||
|
geocentric_to_equatorial(geo_ecl, jd, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
* Internal: common pattern for all planetary moons
|
* Internal: common pattern for all planetary moons
|
||||||
*
|
*
|
||||||
@ -218,3 +248,136 @@ mars_moon_observe(PG_FUNCTION_ARGS)
|
|||||||
|
|
||||||
PG_RETURN_POINTER(result);
|
PG_RETURN_POINTER(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* galilean_equatorial(body_id int, timestamptz) -> equatorial
|
||||||
|
*
|
||||||
|
* Geocentric RA/Dec of a Galilean moon of Jupiter.
|
||||||
|
* Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto
|
||||||
|
* ================================================================
|
||||||
|
*/
|
||||||
|
Datum
|
||||||
|
galilean_equatorial(PG_FUNCTION_ARGS)
|
||||||
|
{
|
||||||
|
int32 body_id = PG_GETARG_INT32(0);
|
||||||
|
int64 ts = PG_GETARG_INT64(1);
|
||||||
|
double jd;
|
||||||
|
double moon_xyz[3];
|
||||||
|
pg_equatorial *result;
|
||||||
|
|
||||||
|
if (body_id < L12_IO || body_id > L12_CALLISTO)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("galilean_equatorial: body_id %d must be 0-3 (Io, Europa, Ganymede, Callisto)",
|
||||||
|
body_id)));
|
||||||
|
|
||||||
|
jd = timestamptz_to_jd(ts);
|
||||||
|
|
||||||
|
GetL12Coor(jd, body_id, moon_xyz, NULL);
|
||||||
|
|
||||||
|
result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
|
||||||
|
equatorial_planetary_moon(moon_xyz, 4, jd, result);
|
||||||
|
|
||||||
|
PG_RETURN_POINTER(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* saturn_moon_equatorial(body_id int, timestamptz) -> equatorial
|
||||||
|
*
|
||||||
|
* Geocentric RA/Dec of a moon of Saturn.
|
||||||
|
* Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione,
|
||||||
|
* 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion
|
||||||
|
* ================================================================
|
||||||
|
*/
|
||||||
|
Datum
|
||||||
|
saturn_moon_equatorial(PG_FUNCTION_ARGS)
|
||||||
|
{
|
||||||
|
int32 body_id = PG_GETARG_INT32(0);
|
||||||
|
int64 ts = PG_GETARG_INT64(1);
|
||||||
|
double jd;
|
||||||
|
double moon_xyz[3];
|
||||||
|
pg_equatorial *result;
|
||||||
|
|
||||||
|
if (body_id < TASS17_MIMAS || body_id > TASS17_HYPERION)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("saturn_moon_equatorial: body_id %d must be 0-7 (Mimas-Hyperion)",
|
||||||
|
body_id)));
|
||||||
|
|
||||||
|
jd = timestamptz_to_jd(ts);
|
||||||
|
|
||||||
|
GetTass17Coor(jd, body_id, moon_xyz, NULL);
|
||||||
|
|
||||||
|
result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
|
||||||
|
equatorial_planetary_moon(moon_xyz, 5, jd, result);
|
||||||
|
|
||||||
|
PG_RETURN_POINTER(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* uranus_moon_equatorial(body_id int, timestamptz) -> equatorial
|
||||||
|
*
|
||||||
|
* Geocentric RA/Dec of a moon of Uranus.
|
||||||
|
* Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon
|
||||||
|
* ================================================================
|
||||||
|
*/
|
||||||
|
Datum
|
||||||
|
uranus_moon_equatorial(PG_FUNCTION_ARGS)
|
||||||
|
{
|
||||||
|
int32 body_id = PG_GETARG_INT32(0);
|
||||||
|
int64 ts = PG_GETARG_INT64(1);
|
||||||
|
double jd;
|
||||||
|
double moon_xyz[3];
|
||||||
|
pg_equatorial *result;
|
||||||
|
|
||||||
|
if (body_id < GUST86_MIRANDA || body_id > GUST86_OBERON)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("uranus_moon_equatorial: body_id %d must be 0-4 (Miranda-Oberon)",
|
||||||
|
body_id)));
|
||||||
|
|
||||||
|
jd = timestamptz_to_jd(ts);
|
||||||
|
|
||||||
|
GetGust86Coor(jd, body_id, moon_xyz, NULL);
|
||||||
|
|
||||||
|
result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
|
||||||
|
equatorial_planetary_moon(moon_xyz, 6, jd, result);
|
||||||
|
|
||||||
|
PG_RETURN_POINTER(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* mars_moon_equatorial(body_id int, timestamptz) -> equatorial
|
||||||
|
*
|
||||||
|
* Geocentric RA/Dec of a moon of Mars.
|
||||||
|
* Body IDs: 0=Phobos, 1=Deimos
|
||||||
|
* ================================================================
|
||||||
|
*/
|
||||||
|
Datum
|
||||||
|
mars_moon_equatorial(PG_FUNCTION_ARGS)
|
||||||
|
{
|
||||||
|
int32 body_id = PG_GETARG_INT32(0);
|
||||||
|
int64 ts = PG_GETARG_INT64(1);
|
||||||
|
double jd;
|
||||||
|
double moon_xyz[3];
|
||||||
|
pg_equatorial *result;
|
||||||
|
|
||||||
|
if (body_id < MARS_SAT_PHOBOS || body_id > MARS_SAT_DEIMOS)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("mars_moon_equatorial: body_id %d must be 0-1 (Phobos, Deimos)",
|
||||||
|
body_id)));
|
||||||
|
|
||||||
|
jd = timestamptz_to_jd(ts);
|
||||||
|
|
||||||
|
GetMarsSatCoor(jd, body_id, moon_xyz, NULL);
|
||||||
|
|
||||||
|
result = (pg_equatorial *) palloc(sizeof(pg_equatorial));
|
||||||
|
equatorial_planetary_moon(moon_xyz, 3, jd, result);
|
||||||
|
|
||||||
|
PG_RETURN_POINTER(result);
|
||||||
|
}
|
||||||
|
|||||||
@ -41,6 +41,10 @@ PG_FUNCTION_INFO_V1(oe_g_slope);
|
|||||||
PG_FUNCTION_INFO_V1(oe_semi_major_axis);
|
PG_FUNCTION_INFO_V1(oe_semi_major_axis);
|
||||||
PG_FUNCTION_INFO_V1(oe_period_years);
|
PG_FUNCTION_INFO_V1(oe_period_years);
|
||||||
|
|
||||||
|
/* Constructors */
|
||||||
|
PG_FUNCTION_INFO_V1(make_orbital_elements);
|
||||||
|
PG_FUNCTION_INFO_V1(make_orbital_elements_deg);
|
||||||
|
|
||||||
/* MPC parser */
|
/* MPC parser */
|
||||||
PG_FUNCTION_INFO_V1(oe_from_mpc);
|
PG_FUNCTION_INFO_V1(oe_from_mpc);
|
||||||
|
|
||||||
@ -367,6 +371,141 @@ oe_period_years(PG_FUNCTION_ARGS)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shared validation for make_orbital_elements() and make_orbital_elements_deg().
|
||||||
|
*
|
||||||
|
* Rejects NaN/Inf in the 7 parameters that feed the propagation pipeline.
|
||||||
|
* h_mag and g_slope are exempt: NaN is a valid sentinel for "unknown".
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
validate_orbital_elements_args(double epoch, double q, double e,
|
||||||
|
double ang1, double ang2, double ang3,
|
||||||
|
double tp)
|
||||||
|
{
|
||||||
|
if (isnan(epoch) || isinf(epoch))
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("epoch must be finite: %g", epoch)));
|
||||||
|
|
||||||
|
if (isnan(q) || isinf(q))
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("perihelion distance must be finite: %g", q)));
|
||||||
|
|
||||||
|
if (q <= 0.0)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("perihelion distance must be positive: %.6f", q)));
|
||||||
|
|
||||||
|
if (isnan(e) || isinf(e))
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("eccentricity must be finite: %g", e)));
|
||||||
|
|
||||||
|
if (e < 0.0)
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("eccentricity must be non-negative: %.6f", e)));
|
||||||
|
|
||||||
|
if (isnan(ang1) || isinf(ang1))
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("inclination must be finite: %g", ang1)));
|
||||||
|
|
||||||
|
if (isnan(ang2) || isinf(ang2))
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("argument of perihelion must be finite: %g", ang2)));
|
||||||
|
|
||||||
|
if (isnan(ang3) || isinf(ang3))
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("longitude of ascending node must be finite: %g", ang3)));
|
||||||
|
|
||||||
|
if (isnan(tp) || isinf(tp))
|
||||||
|
ereport(ERROR,
|
||||||
|
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||||
|
errmsg("time of perihelion must be finite: %g", tp)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* make_orbital_elements(epoch, q, e, inc_rad, omega_rad, Omega_rad, tp, H, G)
|
||||||
|
*
|
||||||
|
* SQL constructor from 9 floats. Angles in radians (matching internal storage).
|
||||||
|
* ================================================================
|
||||||
|
*/
|
||||||
|
Datum
|
||||||
|
make_orbital_elements(PG_FUNCTION_ARGS)
|
||||||
|
{
|
||||||
|
pg_orbital_elements *result;
|
||||||
|
|
||||||
|
double epoch = PG_GETARG_FLOAT8(0);
|
||||||
|
double q = PG_GETARG_FLOAT8(1);
|
||||||
|
double e = PG_GETARG_FLOAT8(2);
|
||||||
|
double inc = PG_GETARG_FLOAT8(3);
|
||||||
|
double arg_peri = PG_GETARG_FLOAT8(4);
|
||||||
|
double raan = PG_GETARG_FLOAT8(5);
|
||||||
|
double tp = PG_GETARG_FLOAT8(6);
|
||||||
|
double h_mag = PG_GETARG_FLOAT8(7);
|
||||||
|
double g_slope = PG_GETARG_FLOAT8(8);
|
||||||
|
|
||||||
|
validate_orbital_elements_args(epoch, q, e, inc, arg_peri, raan, tp);
|
||||||
|
|
||||||
|
result = (pg_orbital_elements *) palloc(sizeof(pg_orbital_elements));
|
||||||
|
result->epoch = epoch;
|
||||||
|
result->q = q;
|
||||||
|
result->e = e;
|
||||||
|
result->inc = inc;
|
||||||
|
result->arg_peri = arg_peri;
|
||||||
|
result->raan = raan;
|
||||||
|
result->tp = tp;
|
||||||
|
result->h_mag = h_mag;
|
||||||
|
result->g_slope = g_slope;
|
||||||
|
|
||||||
|
PG_RETURN_POINTER(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ================================================================
|
||||||
|
* make_orbital_elements_deg(epoch, q, e, inc_deg, omega_deg, Omega_deg, tp, H, G)
|
||||||
|
*
|
||||||
|
* Same as make_orbital_elements() but angular elements in degrees.
|
||||||
|
* Matches the text I/O convention and most catalog column layouts.
|
||||||
|
* ================================================================
|
||||||
|
*/
|
||||||
|
Datum
|
||||||
|
make_orbital_elements_deg(PG_FUNCTION_ARGS)
|
||||||
|
{
|
||||||
|
pg_orbital_elements *result;
|
||||||
|
|
||||||
|
double epoch = PG_GETARG_FLOAT8(0);
|
||||||
|
double q = PG_GETARG_FLOAT8(1);
|
||||||
|
double e = PG_GETARG_FLOAT8(2);
|
||||||
|
double inc_deg = PG_GETARG_FLOAT8(3);
|
||||||
|
double omega_deg = PG_GETARG_FLOAT8(4);
|
||||||
|
double Omega_deg = PG_GETARG_FLOAT8(5);
|
||||||
|
double tp = PG_GETARG_FLOAT8(6);
|
||||||
|
double h_mag = PG_GETARG_FLOAT8(7);
|
||||||
|
double g_slope = PG_GETARG_FLOAT8(8);
|
||||||
|
|
||||||
|
validate_orbital_elements_args(epoch, q, e, inc_deg, omega_deg, Omega_deg, tp);
|
||||||
|
|
||||||
|
result = (pg_orbital_elements *) palloc(sizeof(pg_orbital_elements));
|
||||||
|
result->epoch = epoch;
|
||||||
|
result->q = q;
|
||||||
|
result->e = e;
|
||||||
|
result->inc = inc_deg * DEG_TO_RAD;
|
||||||
|
result->arg_peri = omega_deg * DEG_TO_RAD;
|
||||||
|
result->raan = Omega_deg * DEG_TO_RAD;
|
||||||
|
result->tp = tp;
|
||||||
|
result->h_mag = h_mag;
|
||||||
|
result->g_slope = g_slope;
|
||||||
|
|
||||||
|
PG_RETURN_POINTER(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ================================================================
|
/* ================================================================
|
||||||
* oe_from_mpc(text) -> orbital_elements
|
* oe_from_mpc(text) -> orbital_elements
|
||||||
*
|
*
|
||||||
|
|||||||
311
test/expected/v011_features.out
Normal file
311
test/expected/v011_features.out
Normal file
@ -0,0 +1,311 @@
|
|||||||
|
-- v0.11.0 feature tests: make_orbital_elements constructors + moon equatorial
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 1: make_orbital_elements() — radians input
|
||||||
|
-- Round-trip: construct from radians, read back via accessors
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'make_oe_rad' AS test,
|
||||||
|
round(oe_epoch(oe)::numeric, 1) AS epoch,
|
||||||
|
round(oe_perihelion(oe)::numeric, 6) AS q_au,
|
||||||
|
round(oe_eccentricity(oe)::numeric, 6) AS ecc,
|
||||||
|
round(oe_inclination(oe)::numeric, 4) AS inc_deg,
|
||||||
|
round(oe_arg_perihelion(oe)::numeric, 4) AS omega_deg,
|
||||||
|
round(oe_raan(oe)::numeric, 4) AS node_deg,
|
||||||
|
round(oe_h_mag(oe)::numeric, 1) AS h_mag
|
||||||
|
FROM (SELECT make_orbital_elements(
|
||||||
|
2460400.5, -- epoch JD
|
||||||
|
0.587100, -- q AU
|
||||||
|
0.967277, -- e
|
||||||
|
radians(162.2269), -- inc
|
||||||
|
radians(111.8657), -- omega
|
||||||
|
radians(58.1455), -- Omega
|
||||||
|
2460450.123, -- tp JD
|
||||||
|
5.5, -- H
|
||||||
|
4.0 -- G
|
||||||
|
) AS oe) sub;
|
||||||
|
test | epoch | q_au | ecc | inc_deg | omega_deg | node_deg | h_mag
|
||||||
|
-------------+-----------+----------+----------+----------+-----------+----------+-------
|
||||||
|
make_oe_rad | 2460400.5 | 0.587100 | 0.967277 | 162.2269 | 111.8657 | 58.1455 | 5.5
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 2: make_orbital_elements_deg() — degree input
|
||||||
|
-- Same elements but angles in degrees; should produce identical output
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'make_oe_deg' AS test,
|
||||||
|
round(oe_epoch(oe)::numeric, 1) AS epoch,
|
||||||
|
round(oe_perihelion(oe)::numeric, 6) AS q_au,
|
||||||
|
round(oe_eccentricity(oe)::numeric, 6) AS ecc,
|
||||||
|
round(oe_inclination(oe)::numeric, 4) AS inc_deg,
|
||||||
|
round(oe_arg_perihelion(oe)::numeric, 4) AS omega_deg,
|
||||||
|
round(oe_raan(oe)::numeric, 4) AS node_deg,
|
||||||
|
round(oe_h_mag(oe)::numeric, 1) AS h_mag
|
||||||
|
FROM (SELECT make_orbital_elements_deg(
|
||||||
|
2460400.5, -- epoch JD
|
||||||
|
0.587100, -- q AU
|
||||||
|
0.967277, -- e
|
||||||
|
162.2269, -- inc degrees
|
||||||
|
111.8657, -- omega degrees
|
||||||
|
58.1455, -- Omega degrees
|
||||||
|
2460450.123, -- tp JD
|
||||||
|
5.5, -- H
|
||||||
|
4.0 -- G
|
||||||
|
) AS oe) sub;
|
||||||
|
test | epoch | q_au | ecc | inc_deg | omega_deg | node_deg | h_mag
|
||||||
|
-------------+-----------+----------+----------+----------+-----------+----------+-------
|
||||||
|
make_oe_deg | 2460400.5 | 0.587100 | 0.967277 | 162.2269 | 111.8657 | 58.1455 | 5.5
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 3: constructors produce identical results to text I/O
|
||||||
|
-- The text format uses degrees, so make_orbital_elements_deg should match
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'oe_roundtrip' AS test,
|
||||||
|
make_orbital_elements_deg(
|
||||||
|
2460400.5, 0.587100, 0.967277,
|
||||||
|
162.2269, 111.8657, 58.1455,
|
||||||
|
2460450.123, 5.5, 4.0
|
||||||
|
)::text
|
||||||
|
=
|
||||||
|
'(2460400.500000,0.5871000000,0.9672770000,162.226900,111.865700,58.145500,2460450.123000,5.50,4.00)'::orbital_elements::text
|
||||||
|
AS matches;
|
||||||
|
test | matches
|
||||||
|
--------------+---------
|
||||||
|
oe_roundtrip | t
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 4: make_orbital_elements() validation — negative q
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM make_orbital_elements(2460400.5, -0.1, 0.5, 0, 0, 0, 2460400.5, 0, 0);
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'make_oe_neg_q: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
NOTICE: make_oe_neg_q: correctly rejected
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 5: make_orbital_elements() validation — negative eccentricity
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM make_orbital_elements(2460400.5, 1.0, -0.1, 0, 0, 0, 2460400.5, 0, 0);
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'make_oe_neg_e: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
NOTICE: make_oe_neg_e: correctly rejected
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 6: make_orbital_elements used in small_body_equatorial()
|
||||||
|
-- Verify the constructor output works in the observation pipeline
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'oe_pipeline' AS test,
|
||||||
|
round(eq_ra(eq)::numeric, 2) AS ra_hours,
|
||||||
|
round(eq_dec(eq)::numeric, 2) AS dec_deg,
|
||||||
|
eq_ra(eq) BETWEEN 0 AND 24 AS ra_valid,
|
||||||
|
eq_dec(eq) BETWEEN -90 AND 90 AS dec_valid
|
||||||
|
FROM (SELECT small_body_equatorial(
|
||||||
|
make_orbital_elements_deg(
|
||||||
|
2460400.5, 0.587100, 0.967277,
|
||||||
|
162.2269, 111.8657, 58.1455,
|
||||||
|
2460450.123, 5.5, 4.0
|
||||||
|
),
|
||||||
|
'2024-06-15 12:00:00+00'::timestamptz
|
||||||
|
) AS eq) sub;
|
||||||
|
test | ra_hours | dec_deg | ra_valid | dec_valid
|
||||||
|
-------------+----------+---------+----------+-----------
|
||||||
|
oe_pipeline | 9.27 | 17.78 | t | t
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 7: galilean_equatorial — all 4 moons
|
||||||
|
-- Io, Europa, Ganymede, Callisto should all return valid RA/Dec
|
||||||
|
-- near Jupiter's position
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'galilean_eq' AS test,
|
||||||
|
moon_id,
|
||||||
|
round(eq_ra(eq)::numeric, 4) AS ra_hours,
|
||||||
|
round(eq_dec(eq)::numeric, 4) AS dec_deg,
|
||||||
|
eq_ra(eq) BETWEEN 0 AND 24 AS ra_valid,
|
||||||
|
eq_dec(eq) BETWEEN -90 AND 90 AS dec_valid
|
||||||
|
FROM generate_series(0, 3) AS moon_id,
|
||||||
|
LATERAL galilean_equatorial(moon_id, '2024-06-15 12:00:00+00'::timestamptz) AS eq
|
||||||
|
ORDER BY moon_id;
|
||||||
|
test | moon_id | ra_hours | dec_deg | ra_valid | dec_valid
|
||||||
|
-------------+---------+----------+---------+----------+-----------
|
||||||
|
galilean_eq | 0 | 4.1957 | 20.3905 | t | t
|
||||||
|
galilean_eq | 1 | 4.1950 | 20.3883 | t | t
|
||||||
|
galilean_eq | 2 | 4.1937 | 20.3885 | t | t
|
||||||
|
galilean_eq | 3 | 4.2057 | 20.4177 | t | t
|
||||||
|
(4 rows)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 8: galilean moons should be near Jupiter
|
||||||
|
-- All 4 Galilean moons within 0.5 degrees of Jupiter
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'galilean_near_jupiter' AS test,
|
||||||
|
moon_id,
|
||||||
|
round((galilean_equatorial(moon_id, '2024-06-15 12:00:00+00') <->
|
||||||
|
planet_equatorial_apparent(5, '2024-06-15 12:00:00+00'))::numeric, 4)
|
||||||
|
AS sep_deg,
|
||||||
|
(galilean_equatorial(moon_id, '2024-06-15 12:00:00+00') <->
|
||||||
|
planet_equatorial_apparent(5, '2024-06-15 12:00:00+00')) < 0.5
|
||||||
|
AS within_half_deg
|
||||||
|
FROM generate_series(0, 3) AS moon_id
|
||||||
|
ORDER BY moon_id;
|
||||||
|
test | moon_id | sep_deg | within_half_deg
|
||||||
|
-----------------------+---------+---------+-----------------
|
||||||
|
galilean_near_jupiter | 0 | 0.0149 | t
|
||||||
|
galilean_near_jupiter | 1 | 0.0241 | t
|
||||||
|
galilean_near_jupiter | 2 | 0.0426 | t
|
||||||
|
galilean_near_jupiter | 3 | 0.1293 | t
|
||||||
|
(4 rows)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 9: saturn_moon_equatorial — Titan (id=5)
|
||||||
|
-- Should be near Saturn, within ~0.5 degrees
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'saturn_titan_eq' AS test,
|
||||||
|
round(eq_ra(eq)::numeric, 4) AS ra_hours,
|
||||||
|
round(eq_dec(eq)::numeric, 4) AS dec_deg,
|
||||||
|
round((eq <-> planet_equatorial_apparent(6, '2024-06-15 12:00:00+00'))::numeric, 4) AS sep_from_saturn,
|
||||||
|
(eq <-> planet_equatorial_apparent(6, '2024-06-15 12:00:00+00')) < 0.5 AS near_saturn
|
||||||
|
FROM saturn_moon_equatorial(5, '2024-06-15 12:00:00+00'::timestamptz) AS eq;
|
||||||
|
test | ra_hours | dec_deg | sep_from_saturn | near_saturn
|
||||||
|
-----------------+----------+---------+-----------------+-------------
|
||||||
|
saturn_titan_eq | 23.3909 | -6.0138 | 0.0187 | t
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 10: uranus_moon_equatorial — Titania (id=3)
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'uranus_titania_eq' AS test,
|
||||||
|
round(eq_ra(eq)::numeric, 4) AS ra_hours,
|
||||||
|
round(eq_dec(eq)::numeric, 4) AS dec_deg,
|
||||||
|
eq_ra(eq) BETWEEN 0 AND 24 AS ra_valid,
|
||||||
|
eq_dec(eq) BETWEEN -90 AND 90 AS dec_valid
|
||||||
|
FROM uranus_moon_equatorial(3, '2024-06-15 12:00:00+00'::timestamptz) AS eq;
|
||||||
|
test | ra_hours | dec_deg | ra_valid | dec_valid
|
||||||
|
-------------------+----------+---------+----------+-----------
|
||||||
|
uranus_titania_eq | 3.5124 | 18.7450 | t | t
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 11: mars_moon_equatorial — Phobos and Deimos
|
||||||
|
-- Both should be near Mars, within ~0.02 degrees (very close moons)
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'mars_moons_eq' AS test,
|
||||||
|
moon_id,
|
||||||
|
round(eq_ra(eq)::numeric, 4) AS ra_hours,
|
||||||
|
round(eq_dec(eq)::numeric, 4) AS dec_deg,
|
||||||
|
round((eq <-> planet_equatorial_apparent(4, '2024-06-15 12:00:00+00'))::numeric, 4) AS sep_from_mars
|
||||||
|
FROM generate_series(0, 1) AS moon_id,
|
||||||
|
LATERAL mars_moon_equatorial(moon_id, '2024-06-15 12:00:00+00'::timestamptz) AS eq
|
||||||
|
ORDER BY moon_id;
|
||||||
|
test | moon_id | ra_hours | dec_deg | sep_from_mars
|
||||||
|
---------------+---------+----------+---------+---------------
|
||||||
|
mars_moons_eq | 0 | 2.1851 | 12.0602 | 0.0075
|
||||||
|
mars_moons_eq | 1 | 2.1851 | 12.0572 | 0.0059
|
||||||
|
(2 rows)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 12: NaN rejection in constructors
|
||||||
|
-- NaN passes IEEE 754 comparison guards silently; must be caught explicitly
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM make_orbital_elements(2460400.5, 'NaN'::float8, 0.5, 0, 0, 0, 2460400.5, 0, 0);
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'make_oe_nan_q: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
NOTICE: make_oe_nan_q: correctly rejected
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM make_orbital_elements_deg(2460400.5, 1.0, 'NaN'::float8, 0, 0, 0, 2460400.5, 0, 0);
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'make_oe_nan_e: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
NOTICE: make_oe_nan_e: correctly rejected
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM make_orbital_elements('NaN'::float8, 1.0, 0.5, 0, 0, 0, 2460400.5, 0, 0);
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'make_oe_nan_epoch: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
NOTICE: make_oe_nan_epoch: correctly rejected
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM make_orbital_elements(2460400.5, 1.0, 0.5, 'Infinity'::float8, 0, 0, 2460400.5, 0, 0);
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'make_oe_inf_inc: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
NOTICE: make_oe_inf_inc: correctly rejected
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 13: NaN in H/G is allowed (sentinel for "unknown")
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'nan_h_g_ok' AS test,
|
||||||
|
oe_h_mag(make_orbital_elements(2460400.5, 1.0, 0.5, 0, 0, 0, 2460400.5,
|
||||||
|
'NaN'::float8, 'NaN'::float8)) AS h_mag_is_nan;
|
||||||
|
test | h_mag_is_nan
|
||||||
|
------------+--------------
|
||||||
|
nan_h_g_ok | NaN
|
||||||
|
(1 row)
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 14: error paths for all four moon families + negative body_id
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM galilean_equatorial(5, '2024-06-15 12:00:00+00');
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'galilean_eq_invalid: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
NOTICE: galilean_eq_invalid: correctly rejected
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM galilean_equatorial(-1, '2024-06-15 12:00:00+00');
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'galilean_eq_negative: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
NOTICE: galilean_eq_negative: correctly rejected
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM saturn_moon_equatorial(8, '2024-06-15 12:00:00+00');
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'saturn_eq_invalid: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
NOTICE: saturn_eq_invalid: correctly rejected
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM uranus_moon_equatorial(5, '2024-06-15 12:00:00+00');
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'uranus_eq_invalid: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
NOTICE: uranus_eq_invalid: correctly rejected
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM mars_moon_equatorial(2, '2024-06-15 12:00:00+00');
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'mars_eq_invalid: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
NOTICE: mars_eq_invalid: correctly rejected
|
||||||
264
test/sql/v011_features.sql
Normal file
264
test/sql/v011_features.sql
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
-- v0.11.0 feature tests: make_orbital_elements constructors + moon equatorial
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 1: make_orbital_elements() — radians input
|
||||||
|
-- Round-trip: construct from radians, read back via accessors
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'make_oe_rad' AS test,
|
||||||
|
round(oe_epoch(oe)::numeric, 1) AS epoch,
|
||||||
|
round(oe_perihelion(oe)::numeric, 6) AS q_au,
|
||||||
|
round(oe_eccentricity(oe)::numeric, 6) AS ecc,
|
||||||
|
round(oe_inclination(oe)::numeric, 4) AS inc_deg,
|
||||||
|
round(oe_arg_perihelion(oe)::numeric, 4) AS omega_deg,
|
||||||
|
round(oe_raan(oe)::numeric, 4) AS node_deg,
|
||||||
|
round(oe_h_mag(oe)::numeric, 1) AS h_mag
|
||||||
|
FROM (SELECT make_orbital_elements(
|
||||||
|
2460400.5, -- epoch JD
|
||||||
|
0.587100, -- q AU
|
||||||
|
0.967277, -- e
|
||||||
|
radians(162.2269), -- inc
|
||||||
|
radians(111.8657), -- omega
|
||||||
|
radians(58.1455), -- Omega
|
||||||
|
2460450.123, -- tp JD
|
||||||
|
5.5, -- H
|
||||||
|
4.0 -- G
|
||||||
|
) AS oe) sub;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 2: make_orbital_elements_deg() — degree input
|
||||||
|
-- Same elements but angles in degrees; should produce identical output
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'make_oe_deg' AS test,
|
||||||
|
round(oe_epoch(oe)::numeric, 1) AS epoch,
|
||||||
|
round(oe_perihelion(oe)::numeric, 6) AS q_au,
|
||||||
|
round(oe_eccentricity(oe)::numeric, 6) AS ecc,
|
||||||
|
round(oe_inclination(oe)::numeric, 4) AS inc_deg,
|
||||||
|
round(oe_arg_perihelion(oe)::numeric, 4) AS omega_deg,
|
||||||
|
round(oe_raan(oe)::numeric, 4) AS node_deg,
|
||||||
|
round(oe_h_mag(oe)::numeric, 1) AS h_mag
|
||||||
|
FROM (SELECT make_orbital_elements_deg(
|
||||||
|
2460400.5, -- epoch JD
|
||||||
|
0.587100, -- q AU
|
||||||
|
0.967277, -- e
|
||||||
|
162.2269, -- inc degrees
|
||||||
|
111.8657, -- omega degrees
|
||||||
|
58.1455, -- Omega degrees
|
||||||
|
2460450.123, -- tp JD
|
||||||
|
5.5, -- H
|
||||||
|
4.0 -- G
|
||||||
|
) AS oe) sub;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 3: constructors produce identical results to text I/O
|
||||||
|
-- The text format uses degrees, so make_orbital_elements_deg should match
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'oe_roundtrip' AS test,
|
||||||
|
make_orbital_elements_deg(
|
||||||
|
2460400.5, 0.587100, 0.967277,
|
||||||
|
162.2269, 111.8657, 58.1455,
|
||||||
|
2460450.123, 5.5, 4.0
|
||||||
|
)::text
|
||||||
|
=
|
||||||
|
'(2460400.500000,0.5871000000,0.9672770000,162.226900,111.865700,58.145500,2460450.123000,5.50,4.00)'::orbital_elements::text
|
||||||
|
AS matches;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 4: make_orbital_elements() validation — negative q
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM make_orbital_elements(2460400.5, -0.1, 0.5, 0, 0, 0, 2460400.5, 0, 0);
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'make_oe_neg_q: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 5: make_orbital_elements() validation — negative eccentricity
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM make_orbital_elements(2460400.5, 1.0, -0.1, 0, 0, 0, 2460400.5, 0, 0);
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'make_oe_neg_e: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 6: make_orbital_elements used in small_body_equatorial()
|
||||||
|
-- Verify the constructor output works in the observation pipeline
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'oe_pipeline' AS test,
|
||||||
|
round(eq_ra(eq)::numeric, 2) AS ra_hours,
|
||||||
|
round(eq_dec(eq)::numeric, 2) AS dec_deg,
|
||||||
|
eq_ra(eq) BETWEEN 0 AND 24 AS ra_valid,
|
||||||
|
eq_dec(eq) BETWEEN -90 AND 90 AS dec_valid
|
||||||
|
FROM (SELECT small_body_equatorial(
|
||||||
|
make_orbital_elements_deg(
|
||||||
|
2460400.5, 0.587100, 0.967277,
|
||||||
|
162.2269, 111.8657, 58.1455,
|
||||||
|
2460450.123, 5.5, 4.0
|
||||||
|
),
|
||||||
|
'2024-06-15 12:00:00+00'::timestamptz
|
||||||
|
) AS eq) sub;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 7: galilean_equatorial — all 4 moons
|
||||||
|
-- Io, Europa, Ganymede, Callisto should all return valid RA/Dec
|
||||||
|
-- near Jupiter's position
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'galilean_eq' AS test,
|
||||||
|
moon_id,
|
||||||
|
round(eq_ra(eq)::numeric, 4) AS ra_hours,
|
||||||
|
round(eq_dec(eq)::numeric, 4) AS dec_deg,
|
||||||
|
eq_ra(eq) BETWEEN 0 AND 24 AS ra_valid,
|
||||||
|
eq_dec(eq) BETWEEN -90 AND 90 AS dec_valid
|
||||||
|
FROM generate_series(0, 3) AS moon_id,
|
||||||
|
LATERAL galilean_equatorial(moon_id, '2024-06-15 12:00:00+00'::timestamptz) AS eq
|
||||||
|
ORDER BY moon_id;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 8: galilean moons should be near Jupiter
|
||||||
|
-- All 4 Galilean moons within 0.5 degrees of Jupiter
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'galilean_near_jupiter' AS test,
|
||||||
|
moon_id,
|
||||||
|
round((galilean_equatorial(moon_id, '2024-06-15 12:00:00+00') <->
|
||||||
|
planet_equatorial_apparent(5, '2024-06-15 12:00:00+00'))::numeric, 4)
|
||||||
|
AS sep_deg,
|
||||||
|
(galilean_equatorial(moon_id, '2024-06-15 12:00:00+00') <->
|
||||||
|
planet_equatorial_apparent(5, '2024-06-15 12:00:00+00')) < 0.5
|
||||||
|
AS within_half_deg
|
||||||
|
FROM generate_series(0, 3) AS moon_id
|
||||||
|
ORDER BY moon_id;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 9: saturn_moon_equatorial — Titan (id=5)
|
||||||
|
-- Should be near Saturn, within ~0.5 degrees
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'saturn_titan_eq' AS test,
|
||||||
|
round(eq_ra(eq)::numeric, 4) AS ra_hours,
|
||||||
|
round(eq_dec(eq)::numeric, 4) AS dec_deg,
|
||||||
|
round((eq <-> planet_equatorial_apparent(6, '2024-06-15 12:00:00+00'))::numeric, 4) AS sep_from_saturn,
|
||||||
|
(eq <-> planet_equatorial_apparent(6, '2024-06-15 12:00:00+00')) < 0.5 AS near_saturn
|
||||||
|
FROM saturn_moon_equatorial(5, '2024-06-15 12:00:00+00'::timestamptz) AS eq;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 10: uranus_moon_equatorial — Titania (id=3)
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'uranus_titania_eq' AS test,
|
||||||
|
round(eq_ra(eq)::numeric, 4) AS ra_hours,
|
||||||
|
round(eq_dec(eq)::numeric, 4) AS dec_deg,
|
||||||
|
eq_ra(eq) BETWEEN 0 AND 24 AS ra_valid,
|
||||||
|
eq_dec(eq) BETWEEN -90 AND 90 AS dec_valid
|
||||||
|
FROM uranus_moon_equatorial(3, '2024-06-15 12:00:00+00'::timestamptz) AS eq;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 11: mars_moon_equatorial — Phobos and Deimos
|
||||||
|
-- Both should be near Mars, within ~0.02 degrees (very close moons)
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'mars_moons_eq' AS test,
|
||||||
|
moon_id,
|
||||||
|
round(eq_ra(eq)::numeric, 4) AS ra_hours,
|
||||||
|
round(eq_dec(eq)::numeric, 4) AS dec_deg,
|
||||||
|
round((eq <-> planet_equatorial_apparent(4, '2024-06-15 12:00:00+00'))::numeric, 4) AS sep_from_mars
|
||||||
|
FROM generate_series(0, 1) AS moon_id,
|
||||||
|
LATERAL mars_moon_equatorial(moon_id, '2024-06-15 12:00:00+00'::timestamptz) AS eq
|
||||||
|
ORDER BY moon_id;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 12: NaN rejection in constructors
|
||||||
|
-- NaN passes IEEE 754 comparison guards silently; must be caught explicitly
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM make_orbital_elements(2460400.5, 'NaN'::float8, 0.5, 0, 0, 0, 2460400.5, 0, 0);
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'make_oe_nan_q: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM make_orbital_elements_deg(2460400.5, 1.0, 'NaN'::float8, 0, 0, 0, 2460400.5, 0, 0);
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'make_oe_nan_e: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM make_orbital_elements('NaN'::float8, 1.0, 0.5, 0, 0, 0, 2460400.5, 0, 0);
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'make_oe_nan_epoch: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM make_orbital_elements(2460400.5, 1.0, 0.5, 'Infinity'::float8, 0, 0, 2460400.5, 0, 0);
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'make_oe_inf_inc: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 13: NaN in H/G is allowed (sentinel for "unknown")
|
||||||
|
-- ============================================================
|
||||||
|
SELECT 'nan_h_g_ok' AS test,
|
||||||
|
oe_h_mag(make_orbital_elements(2460400.5, 1.0, 0.5, 0, 0, 0, 2460400.5,
|
||||||
|
'NaN'::float8, 'NaN'::float8)) AS h_mag_is_nan;
|
||||||
|
|
||||||
|
-- ============================================================
|
||||||
|
-- Test 14: error paths for all four moon families + negative body_id
|
||||||
|
-- ============================================================
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM galilean_equatorial(5, '2024-06-15 12:00:00+00');
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'galilean_eq_invalid: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM galilean_equatorial(-1, '2024-06-15 12:00:00+00');
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'galilean_eq_negative: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM saturn_moon_equatorial(8, '2024-06-15 12:00:00+00');
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'saturn_eq_invalid: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM uranus_moon_equatorial(5, '2024-06-15 12:00:00+00');
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'uranus_eq_invalid: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM mars_moon_equatorial(2, '2024-06-15 12:00:00+00');
|
||||||
|
RAISE EXCEPTION 'should have failed';
|
||||||
|
EXCEPTION WHEN numeric_value_out_of_range THEN
|
||||||
|
RAISE NOTICE 'mars_eq_invalid: correctly rejected';
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
Loading…
x
Reference in New Issue
Block a user