Merge rename/pg-orrery: v0.5.0 OD solver enhancements
This commit is contained in:
commit
6e17513885
11
.gitignore
vendored
11
.gitignore
vendored
@ -17,3 +17,14 @@ log/
|
||||
*~
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Test artifacts
|
||||
test/matrix-logs/
|
||||
test/test_de_reader
|
||||
test/test_od_math
|
||||
test/test_od_iod
|
||||
|
||||
# Docs site
|
||||
docs/node_modules/
|
||||
docs/dist/
|
||||
docs/.astro/
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
||||
[submodule "lib/sat_code"]
|
||||
path = lib/sat_code
|
||||
url = https://github.com/Bill-Gray/sat_code.git
|
||||
393
CLAUDE.md
393
CLAUDE.md
@ -1,147 +1,348 @@
|
||||
# pg_orbit — PostgreSQL Extension for Orbital Mechanics
|
||||
# pg_orrery — A Database Orrery for PostgreSQL
|
||||
|
||||
## What This Is
|
||||
A PostgreSQL extension that makes TLE/orbital data first-class types — the way PostGIS does for geographic data. Native C extension using PGXS, wrapping Bill Gray's `sat_code` SGP4/SDP4 implementation.
|
||||
A database orrery — celestial mechanics types and functions for PostgreSQL. Native C extension using PGXS, 68 SQL functions, 7 custom types, covering satellites (SGP4/SDP4), planets (VSOP87 + optional JPL DE441), Moon (ELP2000-82B), 19 planetary moons (L1.2/TASS17/GUST86/MarsSat), stars, comets, Jupiter radio bursts, and interplanetary Lambert transfers.
|
||||
|
||||
**Current version:** 0.3.0 on branch `phase/solar-system-expansion`
|
||||
**Repository:** https://git.supported.systems/warehack.ing/pg_orrery
|
||||
**Documentation:** https://pg-orrery.warehack.ing
|
||||
|
||||
## Build System
|
||||
```bash
|
||||
make # Compile with PGXS
|
||||
make install # Install to PostgreSQL extensions dir
|
||||
make installcheck # Run regression tests
|
||||
make PG_CONFIG=/usr/bin/pg_config # Compile with PGXS
|
||||
sudo make install PG_CONFIG=/usr/bin/pg_config # Install extension
|
||||
make installcheck PG_CONFIG=/usr/bin/pg_config # Run 13 regression test suites
|
||||
```
|
||||
|
||||
Requires: PostgreSQL 14+ development headers (`pg_config` in PATH), GCC, Make.
|
||||
Requires: PostgreSQL 17 development headers, GCC, Make.
|
||||
|
||||
### Docker
|
||||
```bash
|
||||
make docker-build # Build standalone image (pg17 + pg_orrery)
|
||||
make docker-test # Smoke test the image
|
||||
make docker-push # Push to git.supported.systems registry
|
||||
```
|
||||
|
||||
Image: `git.supported.systems/warehack.ing/pg_orrery:pg17`
|
||||
|
||||
## Project Layout
|
||||
```
|
||||
pg_orbit.control # Extension metadata
|
||||
Makefile # PGXS build
|
||||
pg_orrery.control # Extension metadata (version 0.3.0)
|
||||
Makefile # PGXS build + Docker targets
|
||||
sql/
|
||||
pg_orbit--0.1.0.sql # All type/function/operator definitions
|
||||
pg_orrery--0.1.0.sql # v0.1.0: satellite types/functions/operators
|
||||
pg_orrery--0.2.0.sql # v0.2.0: solar system (57 functions)
|
||||
pg_orrery--0.3.0.sql # v0.3.0: complete extension (68 functions)
|
||||
pg_orrery--0.1.0--0.2.0.sql # Migration: v0.1.0 → v0.2.0 (adds solar system)
|
||||
pg_orrery--0.2.0--0.3.0.sql # Migration: v0.2.0 → v0.3.0 (adds DE ephemeris)
|
||||
src/
|
||||
pg_orbit.c # PG_MODULE_MAGIC entry point
|
||||
tle_type.c # TLE custom type (input/output/binary/accessors)
|
||||
eci_type.c # ECI position type + geodetic/topocentric types
|
||||
observer_type.c # Observer location type with flexible parsing
|
||||
sgp4_funcs.c # sgp4_propagate(), sgp4_propagate_series()
|
||||
coord_funcs.c # eci_to_geodetic(), eci_to_topocentric(), subsatellite_point()
|
||||
pass_funcs.c # next_pass(), predict_passes(), pass_visible()
|
||||
gist_tle.c # GiST operator class for altitude-band indexing
|
||||
types.h # Shared struct definitions
|
||||
lib/
|
||||
sat_code/ # Bill Gray's SGP4 (MIT license, git submodule)
|
||||
pg_orrery.c # PG_MODULE_MAGIC + _PG_init() (GUC registration)
|
||||
types.h # All struct definitions + constants + DE body ID mapping
|
||||
astro_math.h # Shared astronomical helpers + observe_from_geocentric()
|
||||
# --- Satellite (v0.1.0) ---
|
||||
tle_type.c # TLE custom type (I/O, binary, 15 accessors)
|
||||
eci_type.c # ECI position type + geodetic/topocentric types
|
||||
observer_type.c # Observer type with flexible string parsing
|
||||
sgp4_funcs.c # sgp4_propagate(), _safe(), _series(), tle_distance()
|
||||
coord_funcs.c # eci_to_geodetic(), eci_to_topocentric(), ground_track()
|
||||
pass_funcs.c # next_pass(), predict_passes(), pass_visible()
|
||||
gist_tle.c # GiST operator class (&&, <->)
|
||||
# --- Solar System (v0.2.0) ---
|
||||
vsop87.c / vsop87.h # VSOP87 planetary ephemeris (Bretagnon 1988)
|
||||
elp82b.c / elp82b.h # ELP2000-82B lunar ephemeris (Chapront 1988)
|
||||
precession.c / precession.h # IAU 1976 precession (Lieske 1979)
|
||||
sidereal_time.c / .h # GMST calculation (Vallado Eq. 3-47)
|
||||
elliptic_to_rectangular.c/.h # Orbital element conversions
|
||||
planet_funcs.c # planet_observe(), planet_heliocentric(), sun/moon_observe()
|
||||
star_funcs.c # star_observe(), star_observe_safe()
|
||||
kepler_funcs.c # kepler_propagate(), comet_observe()
|
||||
l12.c / l12.h # L1.2 Galilean moon theory (Lieske 1998)
|
||||
tass17.c / tass17.h # TASS 1.7 Saturn moon theory (Vienne & Duriez 1995)
|
||||
gust86.c / gust86.h # GUST86 Uranus moon theory (Laskar & Jacobson 1987)
|
||||
marssat.c / marssat.h # MarsSat Mars moon theory (Jacobson 2014)
|
||||
moon_funcs.c # galilean/saturn/uranus/mars_moon_observe()
|
||||
radio_funcs.c # io_phase_angle(), jupiter_cml(), burst_probability()
|
||||
lambert.c / lambert.h # Lambert transfer solver (Izzo 2015)
|
||||
transfer_funcs.c # lambert_transfer(), lambert_c3()
|
||||
# --- JPL DE Ephemeris (v0.3.0) ---
|
||||
de_reader.h / de_reader.c # Clean-room JPL DE binary reader (Chebyshev/Clenshaw)
|
||||
eph_provider.h / eph_provider.c # Provider dispatch, GUC, lazy init, frame rotation
|
||||
de_funcs.c # All _de() SQL function implementations
|
||||
sgp4/ # Vendored SGP4/SDP4 (Bill Gray's sat_code, MIT license)
|
||||
sgp4.c # Near-earth propagator (period < 225 min)
|
||||
sdp4.c # Deep-space propagator (period >= 225 min)
|
||||
deep.c # Lunar/solar perturbations, resonance integration
|
||||
common.c # Shared initialization (Brouwer mean elements, Kepler solver)
|
||||
basics.c # select_ephemeris(), FMod2p()
|
||||
get_el.c # TLE parsing (parse_elements(), checksum)
|
||||
tle_out.c # TLE output (write_elements_in_tle_format())
|
||||
norad.h / norad_in.h # Public + internal headers
|
||||
PROVENANCE.md # Vendoring decision, modifications, verification
|
||||
LICENSE # MIT license (Bill Gray / Project Pluto)
|
||||
test/
|
||||
sql/ # Regression test SQL
|
||||
expected/ # Expected output
|
||||
data/
|
||||
vallado_518.csv # 518 verification test vectors
|
||||
sql/ # 13 regression test suites
|
||||
expected/ # Expected output
|
||||
data/vallado_518.json # 518 Vallado test vectors (AIAA 2006-6753-Rev1)
|
||||
docs/
|
||||
DESIGN.md # Architecture decisions, theory-to-code mappings
|
||||
DESIGN.md # Architecture decisions, theory-to-code mappings
|
||||
Dockerfile # Starlight docs site (Astro + Caddy)
|
||||
package.json # Docs site dependencies
|
||||
astro.config.mjs # Starlight configuration
|
||||
src/content/docs/ # MDX documentation pages
|
||||
```
|
||||
|
||||
## Type System
|
||||
|
||||
### Core Types (all varlena or fixed-size, stored in tuples)
|
||||
All types are fixed-size, `STORAGE = plain`, `ALIGNMENT = double`. No TOAST overhead.
|
||||
|
||||
| Type | Storage | Description |
|
||||
|------|---------|-------------|
|
||||
| `tle` | ~160 bytes fixed | Parsed mean elements (not raw text) |
|
||||
| `eci_position` | 48 bytes | x,y,z + vx,vy,vz (km, km/s) in TEME |
|
||||
| `geodetic` | 24 bytes | lat, lon (radians), alt (km) above WGS-84 |
|
||||
| `topocentric` | 32 bytes | azimuth, elevation, range, range_rate |
|
||||
| `observer` | 24 bytes | lat, lon (radians), alt_m (meters) |
|
||||
| `pass_event` | 56 bytes | AOS/MAX/LOS times + max_el + AOS/LOS az |
|
||||
| Type | Bytes | Description |
|
||||
|------|-------|-------------|
|
||||
| `tle` | 112 | Parsed mean orbital elements for SGP4/SDP4 |
|
||||
| `eci_position` | 48 | x,y,z + vx,vy,vz (km, km/s) in TEME frame |
|
||||
| `geodetic` | 24 | lat, lon (radians), alt (km) above WGS-84 |
|
||||
| `topocentric` | 32 | azimuth, elevation, range, range_rate |
|
||||
| `observer` | 24 | lat, lon (radians), alt_m (meters) |
|
||||
| `pass_event` | 48 | AOS/MAX/LOS times + max_el + AOS/LOS azimuth |
|
||||
| `heliocentric` | 24 | x, y, z in AU (ecliptic J2000 frame) |
|
||||
|
||||
### TLE Internal Struct
|
||||
Stores all parsed mean elements from the two-line format:
|
||||
- epoch (Julian date, float64)
|
||||
- inclination, eccentricity, RAAN, arg_perigee, mean_anomaly (radians, float64)
|
||||
- mean_motion (rev/day, float64), mean_motion_dot, mean_motion_ddot
|
||||
- bstar (drag coefficient, float64)
|
||||
- norad_id (int32), elset_num (int32), rev_num (int32)
|
||||
- classification (char), intl_designator (8 chars)
|
||||
- ephemeris_type (int8)
|
||||
## Function Domains (68 total)
|
||||
|
||||
| Domain | Theory | Key Functions | Count |
|
||||
|--------|--------|---------------|-------|
|
||||
| Satellite | SGP4/SDP4 (Brouwer 1959) | `observe()`, `predict_passes()`, `ground_track()` | 22 |
|
||||
| Planets | VSOP87 (Bretagnon 1988) | `planet_observe()`, `planet_heliocentric()` | 3 |
|
||||
| Sun/Moon | VSOP87 + ELP2000-82B | `sun_observe()`, `moon_observe()` | 2 |
|
||||
| Planetary moons | L1.2, TASS17, GUST86, MarsSat | `galilean_observe()`, `saturn_moon_observe()` | 4 |
|
||||
| Stars | J2000 + IAU 1976 precession | `star_observe()`, `star_observe_safe()` | 2 |
|
||||
| Comets/asteroids | Two-body Keplerian | `kepler_propagate()`, `comet_observe()` | 2 |
|
||||
| Jupiter radio | Carr et al. (1983) | `jupiter_burst_probability()` | 3 |
|
||||
| Transfers | Lambert (Izzo 2015) | `lambert_transfer()`, `lambert_c3()` | 2 |
|
||||
| DE ephemeris | JPL DE440/441 (optional) | `planet_observe_de()`, `moon_observe_de()` | 11 |
|
||||
| GiST index | Altitude-band approximation | `&&` (overlap), `<->` (distance) | 8 |
|
||||
| Diagnostics | -- | `pg_orrery_ephemeris_info()` | 1 |
|
||||
|
||||
All functions are `PARALLEL SAFE`. VSOP87/ELP82B functions are `IMMUTABLE` (compiled-in coefficients). DE functions are `STABLE` (external file dependency).
|
||||
|
||||
## Body IDs
|
||||
|
||||
### Planets (VSOP87 convention)
|
||||
| ID | Body | ID | Body |
|
||||
|----|------|----|------|
|
||||
| 0 | Sun | 5 | Jupiter |
|
||||
| 1 | Mercury | 6 | Saturn |
|
||||
| 2 | Venus | 7 | Uranus |
|
||||
| 3 | Earth | 8 | Neptune |
|
||||
| 4 | Mars | 10 | Moon |
|
||||
|
||||
### Planetary Moons (per-family indexing)
|
||||
- **Galilean** (0-3): Io, Europa, Ganymede, Callisto
|
||||
- **Saturn** (0-7): Mimas, Enceladus, Tethys, Dione, Rhea, Titan, Iapetus, Hyperion
|
||||
- **Uranus** (0-4): Miranda, Ariel, Umbriel, Titania, Oberon
|
||||
- **Mars** (0-1): Phobos, Deimos
|
||||
|
||||
## Constant Chain of Custody
|
||||
|
||||
**This is the most critical design constraint.**
|
||||
|
||||
TLEs are mean elements fitted using WGS-72 constants. The elements absorb geodetic model biases — using WGS-84 constants for propagation silently corrupts position accuracy by kilometers.
|
||||
**The most critical design constraint.** TLEs absorb geodetic model biases — using wrong constants silently corrupts positions by kilometers.
|
||||
|
||||
### Rules
|
||||
1. **SGP4 propagation**: WGS-72 constants ONLY (mu, ae, J2, J3, J4, ke)
|
||||
2. **Coordinate output** (geodetic, topocentric): Convert to WGS-84 (a=6378.137km, f=1/298.257223563)
|
||||
3. **TEME frame**: Use only 4 of 106 IAU-80 nutation terms (matching SGP4's internal model)
|
||||
4. **Never mix**: WGS-72 propagation + WGS-84 output. No other combination.
|
||||
2. **Coordinate output** (geodetic, topocentric): WGS-84 (a=6378.137km, f=1/298.257223563)
|
||||
3. **TEME frame**: Only 4 of 106 IAU-80 nutation terms (matching SGP4's internal model)
|
||||
4. **Solar system pipeline**: IAU 1976 precession, J2000 obliquity, GMST from Vallado Eq. 3-47
|
||||
5. **Never mix**: WGS-72 propagation + WGS-84 output. No other combination.
|
||||
6. **DE frame rotation**: DE positions (ICRS equatorial) pass through `equatorial_to_ecliptic()` at the provider boundary before entering the observation pipeline
|
||||
7. **Same-provider rule**: Both target and Earth must come from the same provider in any geocentric computation (never mix DE target with VSOP87 Earth)
|
||||
8. **DE AU consistency**: Verify DE header AU matches compiled-in `AU_KM` (149597870.7) at init time
|
||||
|
||||
### WGS-72 Constants (from Hoots & Roehrich STR#3)
|
||||
### WGS-72 Constants (from Hoots & Roehrich STR#3, propagation only)
|
||||
```c
|
||||
#define WGS72_MU 398600.8 /* km^3/s^2 */
|
||||
#define WGS72_AE 6378.135 /* km */
|
||||
#define WGS72_MU 398600.8 /* km^3/s^2 */
|
||||
#define WGS72_AE 6378.135 /* km */
|
||||
#define WGS72_J2 0.001082616
|
||||
#define WGS72_J3 -0.00000253881
|
||||
#define WGS72_J4 -0.00000165597
|
||||
#define WGS72_KE 0.0743669161 /* (min)^(-1), = sqrt(mu) * 60 / ae^(3/2) */
|
||||
#define WGS72_XPDOTP 1440.0 / (2.0 * M_PI) /* min/rev */
|
||||
#define WGS72_KE 0.0743669161331734132 /* (min)^(-1) */
|
||||
```
|
||||
|
||||
### WGS-84 Constants (for output only)
|
||||
### WGS-84 Constants (coordinate output only)
|
||||
```c
|
||||
#define WGS84_A 6378.137 /* km */
|
||||
#define WGS84_A 6378.137 /* km */
|
||||
#define WGS84_F (1.0 / 298.257223563)
|
||||
#define WGS84_E2 (WGS84_F * (2.0 - WGS84_F))
|
||||
```
|
||||
|
||||
## sat_code Submodule
|
||||
|
||||
Bill Gray's SGP4 implementation: https://github.com/Bill-Gray/sat_code
|
||||
|
||||
Key files we use:
|
||||
- `sgp4.c` / `sgp4.h` — SGP4/SDP4 propagator
|
||||
- `norad.h` — TLE struct definitions and constants
|
||||
|
||||
The submodule lives at `lib/sat_code/`. To initialize:
|
||||
```bash
|
||||
git submodule update --init
|
||||
```
|
||||
|
||||
### Integration Pattern
|
||||
### Astronomical Constants
|
||||
```c
|
||||
#include "lib/sat_code/norad.h"
|
||||
|
||||
// Parse TLE lines into sat_code's tle_t struct
|
||||
// Call SGP4_init() once per TLE
|
||||
// Call SGP4() with minutes-since-epoch for each propagation
|
||||
#define AU_KM 149597870.7 /* IAU 2012 */
|
||||
#define GAUSS_K 0.01720209895 /* AU^(3/2)/day */
|
||||
#define OBLIQUITY_J2000 0.40909280422232897 /* 23.4392911 deg in radians */
|
||||
#define J2000_JD 2451545.0 /* 2000 Jan 1.5 TT */
|
||||
```
|
||||
|
||||
## JPL DE Ephemeris (Optional)
|
||||
|
||||
v0.3.0 adds optional JPL DE440/441 ephemeris support (~0.1 milliarcsecond accuracy) alongside the existing VSOP87 pipeline (~1 arcsecond). DE functions are separate `_de()` variants — existing VSOP87 functions are completely unchanged.
|
||||
|
||||
### Architecture
|
||||
|
||||
- **Clean-room DE reader** (`de_reader.c`): ~250 lines of C. Parses the JPL binary format, evaluates Chebyshev polynomials via Clenshaw recurrence. No GPL dependency (avoids Bill Gray's `jpl_eph`).
|
||||
- **Per-backend lazy init**: Each PostgreSQL backend opens its own file descriptor on first `_de()` call. Never opens in `_PG_init()` (postmaster context). Safe for `PARALLEL SAFE`.
|
||||
- **VSOP87 fallback**: Every `_de()` function falls back to its VSOP87/ELP82B equivalent when DE is unavailable.
|
||||
- **STABLE volatility**: DE functions are `STABLE` (not `IMMUTABLE`) because output depends on an external file. Existing VSOP87 functions remain `IMMUTABLE`.
|
||||
|
||||
### GUC Configuration
|
||||
|
||||
```sql
|
||||
-- Set the path to a JPL DE binary file (requires superuser)
|
||||
ALTER SYSTEM SET pg_orrery.ephemeris_path = '/var/lib/postgres/de441.bin';
|
||||
SELECT pg_reload_conf();
|
||||
|
||||
-- Check which provider is active
|
||||
SELECT * FROM pg_orrery_ephemeris_info();
|
||||
```
|
||||
|
||||
| GUC | Type | Default | Context |
|
||||
|-----|------|---------|---------|
|
||||
| `pg_orrery.ephemeris_path` | string | `''` (empty = VSOP87 only) | `SIGHUP` (superuser only) |
|
||||
|
||||
### DE Function Variants
|
||||
|
||||
Every `_de()` function mirrors an existing VSOP87 function:
|
||||
|
||||
| DE Function | VSOP87 Equivalent | Volatility |
|
||||
|-------------|-------------------|------------|
|
||||
| `planet_heliocentric_de()` | `planet_heliocentric()` | STABLE |
|
||||
| `planet_observe_de()` | `planet_observe()` | STABLE |
|
||||
| `sun_observe_de()` | `sun_observe()` | STABLE |
|
||||
| `moon_observe_de()` | `moon_observe()` | STABLE |
|
||||
| `lambert_transfer_de()` | `lambert_transfer()` | STABLE |
|
||||
| `lambert_c3_de()` | `lambert_c3()` | STABLE |
|
||||
| `galilean_observe_de()` | `galilean_observe()` | STABLE |
|
||||
| `saturn_moon_observe_de()` | `saturn_moon_observe()` | STABLE |
|
||||
| `uranus_moon_observe_de()` | `uranus_moon_observe()` | STABLE |
|
||||
| `mars_moon_observe_de()` | `mars_moon_observe()` | STABLE |
|
||||
| `pg_orrery_ephemeris_info()` | — | STABLE |
|
||||
|
||||
## Vendored SGP4/SDP4
|
||||
|
||||
Bill Gray's sat_code (MIT license): https://github.com/Bill-Gray/sat_code
|
||||
|
||||
Vendored into `src/sgp4/` from upstream commit `ff7b98957dfa2979700a482bde9de9542807293e`. The `.cpp` files were renamed to `.c` — the code is valid C99 with zero C++ features (no classes, templates, namespaces, exceptions, or STL). This eliminates the `g++` and `-lstdc++` dependencies.
|
||||
|
||||
Modifications from upstream are minimal and documented in `src/sgp4/PROVENANCE.md`:
|
||||
- Renamed `.cpp` → `.c` (no code changes — already valid C99)
|
||||
- Stripped Win32 `DLL_FUNC`/`__stdcall` decorators
|
||||
- Removed `extern "C"` wrapper (now compiled as C)
|
||||
- Removed unused SGP/SGP8/SDP8 model prototypes
|
||||
- Added forward declarations (`-Wmissing-prototypes`)
|
||||
- Changed bare `inline` to `static inline` for C99 compliance
|
||||
- Added equation citations referencing STR#3 and Vallado Rev-1
|
||||
|
||||
All numerical logic is byte-identical to upstream. Verified against 518 Vallado test vectors (AIAA 2006-6753-Rev1).
|
||||
|
||||
## Testing
|
||||
|
||||
### Vallado 518 Test Vectors
|
||||
The definitive SGP4 verification dataset. Each row: NORAD ID, minutes since epoch, expected x,y,z,vx,vy,vz. All 518 must pass to machine epsilon before any other work proceeds.
|
||||
13 regression test suites via `make installcheck`:
|
||||
|
||||
### Regression Tests
|
||||
Standard PostgreSQL `make installcheck` framework:
|
||||
- `test/sql/*.sql` — test queries
|
||||
- `test/expected/*.out` — expected output
|
||||
- Tests run against a temporary database
|
||||
| Suite | What it tests |
|
||||
|-------|--------------|
|
||||
| tle_parse | TLE I/O round-trip, malformed input rejection, all 15 accessors |
|
||||
| sgp4_propagate | SGP4/SDP4, propagation series, tle_distance |
|
||||
| coord_transforms | TEME-to-geodetic, TEME-to-topocentric, ground_track |
|
||||
| pass_prediction | predict_passes, next_pass, pass_visible, min elevation filter |
|
||||
| gist_index | `&&` overlap, `<->` distance, GiST index scan, KNN ordering |
|
||||
| convenience | observe(), observe_safe(), tle_from_lines(), observer_from_geodetic() |
|
||||
| star_observe | Star observation, IAU 1976 precession, heliocentric type I/O |
|
||||
| kepler_comet | Keplerian propagation (elliptic/parabolic/hyperbolic), comet_observe |
|
||||
| planet_observe | VSOP87 planets, sun_observe, moon_observe (ELP2000-82B) |
|
||||
| moon_observe | Galilean/Saturn/Uranus/Mars moons, Io phase, Jupiter CML, burst probability |
|
||||
| lambert_transfer | Lambert solver, lambert_c3, pork chop grid, error handling |
|
||||
| de_ephemeris | DE function fallback to VSOP87, cross-provider consistency, error handling |
|
||||
| vallado_518 | 518 Vallado test vectors (AIAA 2006-6753-Rev1), per-satellite breakdown |
|
||||
|
||||
### Test Categories
|
||||
1. **tle_parse** — TLE input/output round-trip, malformed input rejection
|
||||
2. **sgp4_propagate** — Vallado vectors, edge cases (deep space, high eccentricity)
|
||||
3. **coord_transforms** — TEME->geodetic, TEME->topocentric accuracy
|
||||
4. **pass_prediction** — Known ISS passes, edge cases (polar, retrograde)
|
||||
5. **gist_index** — Index scan vs sequential scan equivalence
|
||||
### PG Version Matrix
|
||||
|
||||
Test all 13 regression suites + DE reader unit test across PostgreSQL 14-18 using Docker:
|
||||
|
||||
```bash
|
||||
make test-matrix # Full matrix (PG 14-18)
|
||||
make test-pg18 # Single version
|
||||
PG_TEST_VERSIONS="16 17" make test-matrix # Subset
|
||||
make test-matrix-clean # Remove logs + test images
|
||||
```
|
||||
|
||||
Logs saved to `test/matrix-logs/pg${ver}.log`. The script reuses the Dockerfile `builder` stage as the test engine — no additional test infrastructure.
|
||||
|
||||
**Adding a new PG version:** Update `PG_TEST_VERSIONS` default in `Makefile` and `PG_VERSIONS` default in `test/pg-version-matrix.sh`.
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
- `_safe()` variants (`sgp4_propagate_safe`, `observe_safe`, `star_observe_safe`) return NULL on error instead of raising exceptions. Use these for batch queries over potentially invalid data.
|
||||
- SGP4 error codes: -1 (nearly parabolic), -2 (negative semi-major axis/decayed), -3/-4 (orbit within Earth, returns with NOTICE), -5 (negative mean motion), -6 (convergence failure)
|
||||
- Pass prediction: propagation failures return -pi elevation (below horizon), shedding the failed timestep without aborting the scan.
|
||||
- Input validation: same-body Lambert check, arrival-before-departure, invalid body_id, RA out of [0,24), negative perihelion distance.
|
||||
|
||||
## Documentation Site
|
||||
|
||||
**Live:** https://pg-orrery.warehack.ing
|
||||
|
||||
Starlight docs at `docs/` — 36 MDX pages covering all domains.
|
||||
|
||||
Sections: Getting Started, Guides (9 domain walkthroughs incl. DE ephemeris), Workflow Translation (Skyfield/Horizons/GMAT/Radio Jupiter Pro comparisons), Reference (all 68 functions incl. DE variants), Architecture (Hamilton's principles, constant custody, observation pipeline), Performance (benchmarks).
|
||||
|
||||
### Local Development
|
||||
```bash
|
||||
cd docs && npm run dev # Dev server on :4321
|
||||
cd docs && npm run build # Static build to dist/
|
||||
```
|
||||
|
||||
### Production Deployment
|
||||
|
||||
The docs site deploys to the `warehack.ing` VPS (`149.28.126.25`) which runs caddy-docker-proxy with wildcard DNS for `*.warehack.ing`.
|
||||
|
||||
**Deploy (or redeploy after changes):**
|
||||
```bash
|
||||
ssh -A warehack-ing@pg-orrery.warehack.ing
|
||||
cd ~/pg_orrery
|
||||
git pull origin phase/solar-system-expansion # or the current branch
|
||||
cd docs
|
||||
make prod # builds image + starts container
|
||||
```
|
||||
|
||||
**First-time setup on VPS:**
|
||||
```bash
|
||||
ssh -A warehack-ing@pg-orrery.warehack.ing
|
||||
git clone git@git.supported.systems:warehack.ing/pg_orrery.git
|
||||
cd pg_orrery && git checkout phase/solar-system-expansion
|
||||
cat > docs/.env << 'EOF'
|
||||
COMPOSE_PROJECT_NAME=pg-orrery-docs
|
||||
NODE_ENV=production
|
||||
VITE_HMR_HOST=pg-orrery.warehack.ing
|
||||
EOF
|
||||
cd docs && make prod
|
||||
```
|
||||
|
||||
**Makefile targets:**
|
||||
- `make prod` — build + start production (Caddy serves static files)
|
||||
- `make dev` — build + start dev mode (hot-reload, volume mounts)
|
||||
- `make down` — stop containers
|
||||
- `make restart` — stop + start production
|
||||
- `make clean` — stop + remove volumes
|
||||
- `make logs` — tail container logs
|
||||
|
||||
**Infrastructure:** Container `pg-orrery-docs` joins external `caddy` network. caddy-docker-proxy reads labels to auto-configure reverse proxy + TLS (Let's Encrypt via Vultr DNS challenge). TLS cert provisioning takes ~2 minutes on first deploy.
|
||||
|
||||
**Do NOT run the docs container locally** if also deployed on the VPS — competing ACME DNS challenges will corrupt each other's TXT records.
|
||||
|
||||
## Coding Style
|
||||
- Standard PostgreSQL extension C style
|
||||
- `ereport(ERROR, ...)` for user-facing errors, never `elog(ERROR, ...)`
|
||||
- All memory allocation through `palloc`/`pfree` (PostgreSQL memory contexts)
|
||||
- All memory via `palloc`/`pfree` (PostgreSQL memory contexts)
|
||||
- Comments explain "why", not "what"
|
||||
- No global mutable state — all computation from function arguments
|
||||
- Functions that call `SGP4()` must handle the error return code
|
||||
- No global mutable state — all computation from function arguments (exceptions: per-backend DE handle via `on_proc_exit`; 3 statics in vendored `deep.c` + 1 cache in `sdp4.c`, safe in PostgreSQL's fork model, never modified by pg_orrery)
|
||||
- Every function handling SGP4 must check the error return code
|
||||
- All functions marked `PARALLEL SAFE`
|
||||
- DE functions: always fall back to VSOP87/ELP82B on any error
|
||||
|
||||
## Git Conventions
|
||||
- One commit per logical change
|
||||
- Branch per phase: `phase/1-tle-sgp4`, `phase/2-coordinates`, etc.
|
||||
- Tag releases: `v0.1.0`, `v0.2.0`, etc.
|
||||
- Branch per phase: `phase/solar-system-expansion`
|
||||
- Tag releases: `v0.1.0`, `v0.2.0`
|
||||
- Commit messages: imperative mood, no AI attribution
|
||||
|
||||
39
Dockerfile
39
Dockerfile
@ -20,11 +20,12 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
apt-get update && apt-get install -y --no-install-recommends \
|
||||
postgresql-${PG_MAJOR} \
|
||||
postgresql-server-dev-${PG_MAJOR} \
|
||||
gcc g++ make && \
|
||||
gcc make \
|
||||
liblapack-dev libblas-dev && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy source tree (submodule content included as regular files)
|
||||
WORKDIR /build/pg_orbit
|
||||
WORKDIR /build/pg_orrery
|
||||
COPY . .
|
||||
|
||||
ENV PG_CONFIG=/usr/lib/postgresql/${PG_MAJOR}/bin/pg_config
|
||||
@ -35,15 +36,20 @@ RUN make PG_CONFIG=${PG_CONFIG}
|
||||
# Install to system location (needed for installcheck)
|
||||
RUN make PG_CONFIG=${PG_CONFIG} install
|
||||
|
||||
# Run all 6 regression test suites against a throwaway cluster
|
||||
# Run all 14 regression test suites against a throwaway cluster
|
||||
RUN su postgres -c "/usr/lib/postgresql/${PG_MAJOR}/bin/initdb -D /tmp/pgtest" && \
|
||||
su postgres -c "/usr/lib/postgresql/${PG_MAJOR}/bin/pg_ctl -D /tmp/pgtest -l /tmp/pgtest.log start" && \
|
||||
su postgres -c "/usr/lib/postgresql/${PG_MAJOR}/bin/createuser -s root" && \
|
||||
make PG_CONFIG=${PG_CONFIG} installcheck && \
|
||||
su postgres -c "/usr/lib/postgresql/${PG_MAJOR}/bin/pg_ctl -D /tmp/pgtest stop"
|
||||
|
||||
# Capture artifacts under /pg_orbit prefix for the next stage
|
||||
RUN make PG_CONFIG=${PG_CONFIG} DESTDIR=/pg_orbit install
|
||||
# Standalone unit tests (no PostgreSQL dependency)
|
||||
RUN make test-de-reader
|
||||
RUN make test-od-math
|
||||
RUN make test-od-iod
|
||||
|
||||
# Capture artifacts under /pg_orrery prefix for the next stage
|
||||
RUN make PG_CONFIG=${PG_CONFIG} DESTDIR=/pg_orrery install
|
||||
|
||||
|
||||
# ── Stage 2: Minimal artifact (COPY --from target) ──────────
|
||||
@ -51,13 +57,30 @@ RUN make PG_CONFIG=${PG_CONFIG} DESTDIR=/pg_orbit install
|
||||
# Downstream images (TimescaleDB-HA, vanilla PG) pull from here.
|
||||
FROM scratch AS artifact
|
||||
|
||||
COPY --from=builder /pg_orbit/ /
|
||||
COPY docker/020_install_pg_orbit.sh /docker-entrypoint-initdb.d/
|
||||
COPY --from=builder /pg_orrery/ /
|
||||
COPY docker/020_install_pg_orrery.sh /docker-entrypoint-initdb.d/
|
||||
|
||||
|
||||
# ── Stage 3: Standalone dev/test image ───────────────────────
|
||||
# Ready-to-run PostgreSQL with pg_orbit pre-installed.
|
||||
# Ready-to-run PostgreSQL with pg_orrery pre-installed.
|
||||
# For development, CI, and standalone experiments.
|
||||
#
|
||||
# Optional DE ephemeris at runtime (recommended):
|
||||
# docker run -v /path/to/de440.bin:/var/lib/postgresql/pg_orrery/de440.bin pg_orrery
|
||||
# Then: ALTER SYSTEM SET pg_orrery.ephemeris_path = '/var/lib/postgresql/pg_orrery/de440.bin';
|
||||
#
|
||||
# Or bake into the image (115 MB for DE440, 3.1 GB for DE441):
|
||||
# Place the DE file in the build context, then:
|
||||
# docker build --build-arg DE_FILE=de440.bin -t pg_orrery:de440 .
|
||||
FROM postgres:${PG_MAJOR}-bookworm AS standalone
|
||||
|
||||
# LAPACK/BLAS runtime for OD solver (dgelss_)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
liblapack3 libblas3 && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=artifact / /
|
||||
|
||||
# Create the pg_orrery data directory for DE ephemeris files
|
||||
RUN mkdir -p /var/lib/postgresql/pg_orrery && \
|
||||
chown postgres:postgres /var/lib/postgresql/pg_orrery
|
||||
|
||||
99
Makefile
99
Makefile
@ -1,43 +1,94 @@
|
||||
MODULE_big = pg_orbit
|
||||
EXTENSION = pg_orbit
|
||||
DATA = sql/pg_orbit--0.1.0.sql
|
||||
MODULE_big = pg_orrery
|
||||
EXTENSION = pg_orrery
|
||||
DATA = sql/pg_orrery--0.1.0.sql sql/pg_orrery--0.2.0.sql sql/pg_orrery--0.1.0--0.2.0.sql \
|
||||
sql/pg_orrery--0.3.0.sql sql/pg_orrery--0.2.0--0.3.0.sql \
|
||||
sql/pg_orrery--0.4.0.sql sql/pg_orrery--0.3.0--0.4.0.sql \
|
||||
sql/pg_orrery--0.5.0.sql sql/pg_orrery--0.4.0--0.5.0.sql
|
||||
|
||||
# Our extension C sources
|
||||
OBJS = src/pg_orbit.o src/tle_type.o src/eci_type.o src/observer_type.o \
|
||||
src/sgp4_funcs.o src/coord_funcs.o src/pass_funcs.o src/gist_tle.o
|
||||
OBJS = src/pg_orrery.o src/tle_type.o src/eci_type.o src/observer_type.o \
|
||||
src/sgp4_funcs.o src/coord_funcs.o src/pass_funcs.o src/gist_tle.o \
|
||||
src/star_funcs.o src/kepler_funcs.o \
|
||||
src/vsop87.o src/elp82b.o src/elliptic_to_rectangular.o \
|
||||
src/precession.o src/sidereal_time.o src/planet_funcs.o \
|
||||
src/tass17.o src/gust86.o src/marssat.o src/l12.o \
|
||||
src/moon_funcs.o src/radio_funcs.o \
|
||||
src/lambert.o src/transfer_funcs.o \
|
||||
src/de_reader.o src/eph_provider.o src/de_funcs.o \
|
||||
src/od_math.o src/od_iod.o src/od_solver.o src/od_funcs.o
|
||||
|
||||
# sat_code C++ sources (compiled with g++, linked with extern "C" symbols)
|
||||
SAT_CODE_DIR = lib/sat_code
|
||||
SAT_CODE_SRCS = $(SAT_CODE_DIR)/sgp4.cpp $(SAT_CODE_DIR)/sdp4.cpp \
|
||||
$(SAT_CODE_DIR)/deep.cpp $(SAT_CODE_DIR)/common.cpp \
|
||||
$(SAT_CODE_DIR)/basics.cpp $(SAT_CODE_DIR)/get_el.cpp \
|
||||
$(SAT_CODE_DIR)/tle_out.cpp
|
||||
SAT_CODE_OBJS = $(SAT_CODE_SRCS:.cpp=.o)
|
||||
# Vendored SGP4/SDP4 sources (pure C, from Bill Gray's sat_code, MIT license)
|
||||
SGP4_DIR = src/sgp4
|
||||
SGP4_SRCS = $(SGP4_DIR)/sgp4.c $(SGP4_DIR)/sdp4.c \
|
||||
$(SGP4_DIR)/deep.c $(SGP4_DIR)/common.c \
|
||||
$(SGP4_DIR)/basics.c $(SGP4_DIR)/get_el.c \
|
||||
$(SGP4_DIR)/tle_out.c
|
||||
SGP4_OBJS = $(SGP4_SRCS:.c=.o)
|
||||
|
||||
OBJS += $(SAT_CODE_OBJS)
|
||||
OBJS += $(SGP4_OBJS)
|
||||
|
||||
# Regression tests
|
||||
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 \
|
||||
de_ephemeris od_fit vallado_518
|
||||
REGRESS_OPTS = --inputdir=test
|
||||
|
||||
# Need C++ runtime for sat_code
|
||||
SHLIB_LINK += -lstdc++ -lm
|
||||
# Pure C — no C++ runtime needed. LAPACK for OD solver (dgelss_).
|
||||
SHLIB_LINK += -lm -llapack -lblas
|
||||
|
||||
# Compiler flags
|
||||
PG_CPPFLAGS = -I$(SAT_CODE_DIR)
|
||||
PG_CPPFLAGS = -I$(SGP4_DIR)
|
||||
|
||||
# Use PGXS
|
||||
PG_CONFIG ?= pg_config
|
||||
PGXS := $(shell $(PG_CONFIG) --pgxs)
|
||||
include $(PGXS)
|
||||
|
||||
# Rule for compiling sat_code C++ files
|
||||
$(SAT_CODE_DIR)/%.o: $(SAT_CODE_DIR)/%.cpp
|
||||
$(CXX) $(CXXFLAGS) -fPIC -I$(SAT_CODE_DIR) -c -o $@ $<
|
||||
# ── Standalone DE reader unit test (no PostgreSQL dependency) ──
|
||||
# Generates a synthetic DE binary, exercises Chebyshev evaluation,
|
||||
# header parsing, Earth derivation, error paths.
|
||||
test-de-reader: test/test_de_reader.c src/de_reader.c src/de_reader.h
|
||||
$(CC) -Wall -Werror -Isrc -o test/test_de_reader $< src/de_reader.c -lm
|
||||
./test/test_de_reader
|
||||
|
||||
.PHONY: test-de-reader
|
||||
|
||||
# ── Standalone OD math unit test (no PostgreSQL dependency) ──
|
||||
# Element converters, inverse coordinate transforms, Brouwer/Kozai inverse.
|
||||
test-od-math: test/test_od_math.c src/od_math.c src/od_math.h
|
||||
$(CC) -Wall -Werror -Isrc -o test/test_od_math $< src/od_math.c -lm
|
||||
./test/test_od_math
|
||||
|
||||
.PHONY: test-od-math
|
||||
|
||||
# ── Standalone IOD unit test (no PostgreSQL dependency) ──
|
||||
# Gibbs method: 3-position orbit recovery, coplanarity checks.
|
||||
test-od-iod: test/test_od_iod.c src/od_iod.c src/od_iod.h src/od_math.c src/od_math.h
|
||||
$(CC) -Wall -Werror -Isrc -o test/test_od_iod $< src/od_iod.c src/od_math.c -lm
|
||||
./test/test_od_iod
|
||||
|
||||
.PHONY: test-od-iod
|
||||
|
||||
# ── PG version test matrix ─────────────────────────────────
|
||||
PG_TEST_VERSIONS ?= 14 15 16 17 18
|
||||
|
||||
test-matrix:
|
||||
PG_VERSIONS="$(PG_TEST_VERSIONS)" bash test/pg-version-matrix.sh
|
||||
|
||||
test-pg%:
|
||||
PG_VERSIONS="$*" bash test/pg-version-matrix.sh
|
||||
|
||||
test-matrix-clean:
|
||||
rm -rf test/matrix-logs
|
||||
@for v in $(PG_TEST_VERSIONS); do \
|
||||
docker rmi "pg_orrery-test:pg$$v" 2>/dev/null || true; \
|
||||
done
|
||||
|
||||
.PHONY: test-matrix test-matrix-clean
|
||||
|
||||
# ── Docker packaging ────────────────────────────────────────
|
||||
REGISTRY ?= git.supported.systems/warehack.ing
|
||||
IMAGE ?= pg_orbit
|
||||
IMAGE ?= pg_orrery
|
||||
PG_MAJOR ?= 17
|
||||
TAG ?= pg$(PG_MAJOR)
|
||||
|
||||
@ -53,14 +104,14 @@ docker-push:
|
||||
|
||||
docker-test:
|
||||
@echo "Smoke-testing standalone image..."
|
||||
docker run --rm -d --name pg_orbit_test \
|
||||
docker run --rm -d --name pg_orrery_test \
|
||||
-e POSTGRES_PASSWORD=test $(REGISTRY)/$(IMAGE):$(TAG)
|
||||
@echo "Waiting for PostgreSQL to initialize..."
|
||||
@sleep 10
|
||||
docker exec pg_orbit_test psql -U postgres -tAc \
|
||||
docker exec pg_orrery_test psql -U postgres -tAc \
|
||||
"SELECT tle_norad_id(E'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025\n2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle);" \
|
||||
| grep -q 25544
|
||||
@docker stop pg_orbit_test
|
||||
@docker stop pg_orrery_test
|
||||
@echo "Smoke test passed."
|
||||
|
||||
.PHONY: docker-build docker-push docker-test
|
||||
|
||||
428
README.md
428
README.md
@ -1,305 +1,229 @@
|
||||
# pg_orbit
|
||||
# pg_orrery
|
||||
|
||||
Orbital mechanics types and functions for PostgreSQL.
|
||||
*It's not rocket science.* (It's celestial mechanics. But now it's just SQL.)
|
||||
|
||||
pg_orbit adds native SQL types for TLEs, orbital positions, ground stations, and
|
||||
satellite passes. It wraps Bill Gray's [sat_code](https://github.com/Bill-Gray/sat_code)
|
||||
(MIT) for SGP4/SDP4 propagation, provides coordinate transforms between inertial
|
||||
and ground-referenced frames, predicts passes over observer locations, and supports
|
||||
GiST-indexed conjunction screening on altitude bands.
|
||||
An orrery is a clockwork model of the solar system — brass gears turning planets in their courses. pg_orrery is the same idea, built from Keplerian parameters and SQL instead of wheelwork. Where a mechanical orrery approximates orbits with gear ratios, a database orrery computes them from the six orbital elements that define each trajectory.
|
||||
|
||||
Think PostGIS, but for objects in orbit.
|
||||
68 functions. 7 custom types. All `PARALLEL SAFE`. Optional JPL DE440/441 support
|
||||
for sub-arcsecond accuracy; core functions have zero external dependencies at runtime.
|
||||
|
||||
**[Documentation](https://pg-orrery.warehack.ing)** · [Source](https://git.supported.systems/warehack.ing/pg_orrery)
|
||||
|
||||
## Installation
|
||||
|
||||
Requirements:
|
||||
- PostgreSQL 14+ development headers (`pg_config` in PATH)
|
||||
- GCC and Make
|
||||
- C++ compiler (for sat_code)
|
||||
### Docker (recommended)
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/...
|
||||
cd pg_orbit
|
||||
make
|
||||
sudo make install
|
||||
docker run -d --name pg_orrery \
|
||||
-e POSTGRES_PASSWORD=orbit \
|
||||
-p 5499:5432 \
|
||||
git.supported.systems/warehack.ing/pg_orrery:pg17
|
||||
```
|
||||
|
||||
Then in your database:
|
||||
```bash
|
||||
psql -h localhost -p 5499 -U postgres -c "CREATE EXTENSION pg_orrery;"
|
||||
```
|
||||
|
||||
### Build from Source
|
||||
|
||||
Requires PostgreSQL 17 development headers and a C/C++ toolchain.
|
||||
|
||||
```bash
|
||||
git clone https://git.supported.systems/warehack.ing/pg_orrery.git
|
||||
cd pg_orrery
|
||||
git submodule update --init
|
||||
make PG_CONFIG=/usr/bin/pg_config
|
||||
sudo make install PG_CONFIG=/usr/bin/pg_config
|
||||
```
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION pg_orbit;
|
||||
```
|
||||
|
||||
If you cloned without `--recurse-submodules`, initialize the sat_code dependency:
|
||||
|
||||
```bash
|
||||
git submodule update --init
|
||||
CREATE EXTENSION pg_orrery;
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
**Where is Jupiter right now?**
|
||||
|
||||
```sql
|
||||
-- Create a table with a TLE column
|
||||
CREATE TABLE satellites (
|
||||
norad_id int PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
tle tle NOT NULL
|
||||
);
|
||||
|
||||
-- Insert a TLE (standard two-line format, concatenated with newline)
|
||||
INSERT INTO satellites VALUES (25544, 'ISS',
|
||||
'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9002
|
||||
2 25544 51.6400 208.5000 0006000 30.0000 330.0000 15.50000000400000');
|
||||
|
||||
-- Propagate to a point in time
|
||||
SELECT sgp4_propagate(tle, now()) FROM satellites WHERE norad_id = 25544;
|
||||
|
||||
-- Subsatellite point (nadir)
|
||||
SELECT subsatellite_point(tle, now()) FROM satellites WHERE norad_id = 25544;
|
||||
|
||||
-- All passes over Boulder, CO in the next 24 hours (min 10 deg elevation)
|
||||
SELECT sat.name, p.*
|
||||
FROM satellites sat,
|
||||
LATERAL predict_passes(sat.tle, '40.0N 105.3W 1655m'::observer,
|
||||
now(), now() + '24h', 10.0) p
|
||||
ORDER BY pass_aos_time(p);
|
||||
|
||||
-- Conjunction screening with GiST index
|
||||
CREATE INDEX ON satellites USING gist (tle);
|
||||
|
||||
SELECT a.name, b.name, tle_distance(a.tle, b.tle, now())
|
||||
FROM satellites a, satellites b
|
||||
WHERE a.tle && b.tle AND a.norad_id < b.norad_id
|
||||
AND tle_distance(a.tle, b.tle, now()) < 10.0;
|
||||
SELECT topo_azimuth(t) AS az,
|
||||
topo_elevation(t) AS el,
|
||||
topo_range(t) / 149597870.7 AS distance_au
|
||||
FROM planet_observe(5, '40.0N 105.3W 1655m'::observer, now()) t;
|
||||
```
|
||||
|
||||
**What's the entire solar system doing?**
|
||||
|
||||
```sql
|
||||
SELECT body_id,
|
||||
CASE body_id
|
||||
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
|
||||
WHEN 3 THEN 'Earth' WHEN 4 THEN 'Mars'
|
||||
WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn'
|
||||
WHEN 7 THEN 'Uranus' WHEN 8 THEN 'Neptune'
|
||||
END AS name,
|
||||
round(helio_distance(planet_heliocentric(body_id, now()))::numeric, 4) AS distance_au
|
||||
FROM generate_series(1, 8) AS body_id;
|
||||
```
|
||||
|
||||
**Predict ISS passes over your location:**
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT pass_aos(p) AS rise_time,
|
||||
pass_max_el(p) AS max_elevation,
|
||||
pass_los(p) AS set_time
|
||||
FROM iss, predict_passes(tle, '40.0N 105.3W 1655m'::observer,
|
||||
now(), now() + interval '24 hours', 10.0) p;
|
||||
```
|
||||
|
||||
**When will Jupiter produce radio bursts tonight?**
|
||||
|
||||
```sql
|
||||
SELECT t,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
)::numeric, 3) AS burst_prob
|
||||
FROM generate_series(now(), now() + interval '12 hours', interval '10 minutes') AS t
|
||||
WHERE jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
) > 0.3;
|
||||
```
|
||||
|
||||
**Plan an Earth-Mars transfer:**
|
||||
|
||||
```sql
|
||||
SELECT round(c3_departure::numeric, 2) AS c3_depart_km2s2,
|
||||
round(tof_days::numeric, 1) AS flight_days,
|
||||
round(transfer_sma::numeric, 4) AS sma_au
|
||||
FROM lambert_transfer(3, 4, '2028-10-01'::timestamptz, '2029-06-15'::timestamptz);
|
||||
```
|
||||
|
||||
## What It Covers
|
||||
|
||||
| Domain | Theory | Key Functions | Accuracy |
|
||||
|---|---|---|---|
|
||||
| Satellites | SGP4/SDP4 (Brouwer 1959) | `observe()`, `predict_passes()` | ~1 km (LEO, fresh TLE) |
|
||||
| Planets | VSOP87 (Bretagnon 1988) | `planet_observe()`, `planet_heliocentric()` | ~1 arcsecond |
|
||||
| Sun | VSOP87 (Earth vector, inverted) | `sun_observe()` | ~1 arcsecond |
|
||||
| Moon | ELP2000-82B (Chapront 1988) | `moon_observe()` | ~10 arcseconds |
|
||||
| Planetary moons | L1.2, TASS17, GUST86, MarsSat | `galilean_observe()`, etc. | ~1-10 arcseconds |
|
||||
| Stars | J2000 catalog + precession | `star_observe()` | Limited by catalog |
|
||||
| Comets/asteroids | Two-body Keplerian | `kepler_propagate()`, `comet_observe()` | Varies with eccentricity |
|
||||
| Jupiter radio | Carr et al. (1983) sources | `jupiter_burst_probability()` | Empirical probability |
|
||||
| Transfers | Lambert (Izzo 2015) | `lambert_transfer()`, `lambert_c3()` | Ballistic two-body |
|
||||
|
||||
## Types
|
||||
|
||||
| Type | Size | Description |
|
||||
|------|------|-------------|
|
||||
| `tle` | 112 bytes | Parsed mean orbital elements (epoch, Keplerian elements, drag terms, identifiers). Stored as a fixed-size struct, not raw text. |
|
||||
| `eci_position` | 48 bytes | Position (km) and velocity (km/s) in the True Equator Mean Equinox (TEME) frame. |
|
||||
| `geodetic` | 24 bytes | Latitude/longitude (degrees) and altitude (km) on the WGS-84 ellipsoid. |
|
||||
| `topocentric` | 32 bytes | Azimuth, elevation (degrees), slant range (km), and range rate (km/s) relative to an observer. |
|
||||
| `observer` | 24 bytes | Ground station location. Accepts human-readable input: `'40.0N 105.3W 1655m'` or decimal degrees. |
|
||||
| `pass_event` | 48 bytes | Satellite pass with AOS/MAX/LOS times, max elevation, and AOS/LOS azimuths. |
|
||||
| Type | Bytes | Description |
|
||||
|------|-------|-------------|
|
||||
| `tle` | 112 | Parsed mean orbital elements for SGP4/SDP4 propagation |
|
||||
| `eci_position` | 48 | Position and velocity in TEME frame (km, km/s) |
|
||||
| `geodetic` | 24 | Latitude, longitude, altitude on WGS-84 ellipsoid |
|
||||
| `topocentric` | 32 | Azimuth, elevation, range, range rate relative to observer |
|
||||
| `observer` | 24 | Ground location. Input: `'40.0N 105.3W 1655m'` or decimal degrees |
|
||||
| `pass_event` | 48 | Satellite pass with AOS/TCA/LOS times and azimuths |
|
||||
| `heliocentric` | 24 | Position in AU, ecliptic J2000 frame |
|
||||
|
||||
### Input Formats
|
||||
All types are fixed-size with `STORAGE = plain`. No TOAST overhead.
|
||||
|
||||
**tle** -- Standard two-line format (lines joined by newline):
|
||||
## Body IDs
|
||||
|
||||
```sql
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9002
|
||||
2 25544 51.6400 208.5000 0006000 30.0000 330.0000 15.50000000400000'::tle;
|
||||
```
|
||||
Planets follow the VSOP87 convention. Planetary moons use per-family indexing.
|
||||
|
||||
**observer** -- Flexible ground station input:
|
||||
|
||||
```sql
|
||||
-- Compass notation with altitude
|
||||
SELECT '40.0N 105.3W 1655m'::observer;
|
||||
|
||||
-- Decimal degrees (positive East, altitude in meters)
|
||||
SELECT '40.0 -105.3 1655'::observer;
|
||||
```
|
||||
|
||||
## Functions
|
||||
|
||||
### TLE Accessors
|
||||
|
||||
| Function | Returns | Description |
|
||||
|----------|---------|-------------|
|
||||
| `tle_norad_id(tle)` | `int4` | NORAD catalog number |
|
||||
| `tle_epoch(tle)` | `float8` | Epoch as Julian date (UTC) |
|
||||
| `tle_inclination(tle)` | `float8` | Inclination in degrees |
|
||||
| `tle_eccentricity(tle)` | `float8` | Eccentricity (dimensionless) |
|
||||
| `tle_raan(tle)` | `float8` | Right ascension of ascending node (degrees) |
|
||||
| `tle_arg_perigee(tle)` | `float8` | Argument of perigee (degrees) |
|
||||
| `tle_mean_anomaly(tle)` | `float8` | Mean anomaly (degrees) |
|
||||
| `tle_mean_motion(tle)` | `float8` | Mean motion (rev/day) |
|
||||
| `tle_bstar(tle)` | `float8` | B* drag coefficient (1/earth-radii) |
|
||||
| `tle_period(tle)` | `float8` | Orbital period (minutes) |
|
||||
| `tle_perigee(tle)` | `float8` | Perigee altitude (km above WGS-72 ellipsoid) |
|
||||
| `tle_apogee(tle)` | `float8` | Apogee altitude (km above WGS-72 ellipsoid) |
|
||||
| `tle_age(tle, timestamptz)` | `float8` | TLE age in days (positive = past epoch) |
|
||||
| `tle_intl_desig(tle)` | `text` | International designator (COSPAR ID) |
|
||||
|
||||
### Propagation
|
||||
|
||||
| Function | Returns | Description |
|
||||
|----------|---------|-------------|
|
||||
| `sgp4_propagate(tle, timestamptz)` | `eci_position` | Propagate to a point in time. Uses SGP4 for near-earth, SDP4 for deep-space. |
|
||||
| `sgp4_propagate_series(tle, start, stop, step)` | `SETOF (t, x, y, z, vx, vy, vz)` | Time series of TEME positions at regular intervals. |
|
||||
| `tle_distance(tle, tle, timestamptz)` | `float8` | Euclidean distance (km) between two objects at a reference time. |
|
||||
|
||||
### Coordinate Transforms
|
||||
|
||||
| Function | Returns | Description |
|
||||
|----------|---------|-------------|
|
||||
| `eci_to_geodetic(eci_position, timestamptz)` | `geodetic` | TEME to WGS-84 geodetic (lat/lon/alt). Requires time for Earth rotation. |
|
||||
| `eci_to_topocentric(eci_position, observer, timestamptz)` | `topocentric` | TEME to observer-relative az/el/range. |
|
||||
| `subsatellite_point(tle, timestamptz)` | `geodetic` | Nadir point on WGS-84 ellipsoid. Propagates internally. |
|
||||
| `ground_track(tle, start, stop, step)` | `SETOF (t, lat, lon, alt)` | Ground track as time series of subsatellite points. |
|
||||
|
||||
### ECI Accessors
|
||||
|
||||
| Function | Returns | Description |
|
||||
|----------|---------|-------------|
|
||||
| `eci_x(eci_position)` | `float8` | X position (km, TEME) |
|
||||
| `eci_y(eci_position)` | `float8` | Y position (km, TEME) |
|
||||
| `eci_z(eci_position)` | `float8` | Z position (km, TEME) |
|
||||
| `eci_vx(eci_position)` | `float8` | X velocity (km/s) |
|
||||
| `eci_vy(eci_position)` | `float8` | Y velocity (km/s) |
|
||||
| `eci_vz(eci_position)` | `float8` | Z velocity (km/s) |
|
||||
| `eci_speed(eci_position)` | `float8` | Velocity magnitude (km/s) |
|
||||
| `eci_altitude(eci_position)` | `float8` | Geocentric altitude (km) |
|
||||
|
||||
### Topocentric Accessors
|
||||
|
||||
| Function | Returns | Description |
|
||||
|----------|---------|-------------|
|
||||
| `topo_azimuth(topocentric)` | `float8` | Azimuth in degrees (0=N, 90=E, 180=S, 270=W) |
|
||||
| `topo_elevation(topocentric)` | `float8` | Elevation in degrees (0=horizon, 90=zenith) |
|
||||
| `topo_range(topocentric)` | `float8` | Slant range (km) |
|
||||
| `topo_range_rate(topocentric)` | `float8` | Range rate (km/s, positive = receding) |
|
||||
|
||||
### Pass Prediction
|
||||
|
||||
| Function | Returns | Description |
|
||||
|----------|---------|-------------|
|
||||
| `next_pass(tle, observer, timestamptz)` | `pass_event` | Next pass over observer. Searches up to 7 days. |
|
||||
| `predict_passes(tle, observer, start, stop [, min_el])` | `SETOF pass_event` | All passes in a time window. Optional minimum elevation (degrees). |
|
||||
| `pass_visible(tle, observer, start, stop)` | `boolean` | True if any pass occurs in the time window. |
|
||||
|
||||
### Pass Event Accessors
|
||||
|
||||
| Function | Returns | Description |
|
||||
|----------|---------|-------------|
|
||||
| `pass_aos_time(pass_event)` | `timestamptz` | Acquisition of signal time |
|
||||
| `pass_max_el_time(pass_event)` | `timestamptz` | Maximum elevation time |
|
||||
| `pass_los_time(pass_event)` | `timestamptz` | Loss of signal time |
|
||||
| `pass_max_elevation(pass_event)` | `float8` | Maximum elevation (degrees) |
|
||||
| `pass_aos_azimuth(pass_event)` | `float8` | AOS azimuth (degrees, 0=N) |
|
||||
| `pass_los_azimuth(pass_event)` | `float8` | LOS azimuth (degrees, 0=N) |
|
||||
| `pass_duration(pass_event)` | `interval` | Pass duration (LOS - AOS) |
|
||||
|
||||
### Operators
|
||||
|
||||
| Operator | Operands | Description |
|
||||
|----------|----------|-------------|
|
||||
| `&&` | `tle, tle` | Altitude band overlap. Necessary (not sufficient) condition for conjunction. |
|
||||
| `<->` | `tle, tle` | Minimum altitude-band separation in km. Returns 0 if bands overlap. |
|
||||
|
||||
Both operators are supported by the GiST `tle_ops` operator class for indexed scans.
|
||||
| ID | Planet | | Galilean (0-3) | Saturn (0-7) | Uranus (0-4) | Mars (0-1) |
|
||||
|----|--------|-|----------------|--------------|--------------|------------|
|
||||
| 1 | Mercury | | 0: Io | 0: Mimas | 0: Miranda | 0: Phobos |
|
||||
| 2 | Venus | | 1: Europa | 1: Enceladus | 1: Ariel | 1: Deimos |
|
||||
| 3 | Earth | | 2: Ganymede | 2: Tethys | 2: Umbriel | |
|
||||
| 4 | Mars | | 3: Callisto | 3: Dione | 3: Titania | |
|
||||
| 5 | Jupiter | | | 4: Rhea | 4: Oberon | |
|
||||
| 6 | Saturn | | | 5: Titan | | |
|
||||
| 7 | Uranus | | | 6: Iapetus | | |
|
||||
| 8 | Neptune | | | 7: Hyperion | | |
|
||||
|
||||
## GiST Indexing
|
||||
|
||||
The `tle_ops` operator class indexes TLEs by their altitude band (perigee to apogee).
|
||||
This provides fast filtering for conjunction screening: only pairs whose altitude
|
||||
bands overlap can possibly be close to each other.
|
||||
The `tle_ops` operator class indexes TLEs by altitude band for conjunction screening:
|
||||
|
||||
```sql
|
||||
-- Create the index
|
||||
CREATE INDEX idx_tle_alt ON satellites USING gist (tle);
|
||||
CREATE INDEX ON satellites USING gist (tle);
|
||||
|
||||
-- The && operator triggers index scans
|
||||
EXPLAIN SELECT a.name, b.name
|
||||
-- Find objects in overlapping altitude shells
|
||||
SELECT a.name, b.name
|
||||
FROM satellites a, satellites b
|
||||
WHERE a.tle && b.tle AND a.norad_id < b.norad_id;
|
||||
|
||||
-- KNN ordering by altitude-band distance
|
||||
SELECT name, tle <-> (SELECT tle FROM satellites WHERE norad_id = 25544) AS sep
|
||||
FROM satellites
|
||||
ORDER BY tle <-> (SELECT tle FROM satellites WHERE norad_id = 25544)
|
||||
-- K-nearest-neighbor by altitude separation
|
||||
SELECT name, round((tle <-> iss.tle)::numeric, 0) AS alt_sep_km
|
||||
FROM satellites, (SELECT tle FROM satellites WHERE norad_id = 25544) iss
|
||||
ORDER BY tle <-> iss.tle
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
The index reduces conjunction candidate pairs from O(n^2) to the set of objects with
|
||||
intersecting altitude bands, which is then refined by computing actual `tle_distance()`
|
||||
at a specific time.
|
||||
## Performance
|
||||
|
||||
## Geodetic Constants
|
||||
Measured on PostgreSQL 17, single backend:
|
||||
|
||||
TLEs are mean elements fitted using WGS-72 constants. Using WGS-84 constants for
|
||||
propagation introduces kilometer-scale position errors because the elements absorb
|
||||
geodetic model biases during the fitting process.
|
||||
|
||||
pg_orbit enforces this:
|
||||
- **Propagation (SGP4/SDP4):** WGS-72 constants only (mu, ae, J2, J3, J4, ke)
|
||||
- **Coordinate output (geodetic, topocentric):** WGS-84 (a=6378.137 km, f=1/298.257223563)
|
||||
|
||||
These are not interchangeable. Mixing them is a silent accuracy loss.
|
||||
|
||||
## Building from Source
|
||||
|
||||
```bash
|
||||
# Build (requires pg_config in PATH)
|
||||
make
|
||||
|
||||
# Install to PostgreSQL extension directory
|
||||
sudo make install
|
||||
|
||||
# Run regression tests against a live database
|
||||
make installcheck
|
||||
```
|
||||
|
||||
Override `pg_config` location if needed:
|
||||
|
||||
```bash
|
||||
make PG_CONFIG=/usr/lib/postgresql/16/bin/pg_config
|
||||
sudo make PG_CONFIG=/usr/lib/postgresql/16/bin/pg_config install
|
||||
```
|
||||
|
||||
### Project Layout
|
||||
|
||||
```
|
||||
pg_orbit.control Extension metadata (version 0.1.0)
|
||||
Makefile PGXS build
|
||||
sql/
|
||||
pg_orbit--0.1.0.sql Type, function, operator, and GiST definitions
|
||||
src/
|
||||
pg_orbit.c PG_MODULE_MAGIC entry point
|
||||
tle_type.c TLE input/output/binary/accessors
|
||||
eci_type.c ECI position type
|
||||
observer_type.c Observer type with flexible parsing
|
||||
sgp4_funcs.c SGP4 propagation and distance
|
||||
coord_funcs.c Coordinate transforms (TEME/geodetic/topocentric)
|
||||
pass_funcs.c Pass prediction (next_pass, predict_passes)
|
||||
gist_tle.c GiST operator class for altitude-band indexing
|
||||
types.h Shared struct definitions and constants
|
||||
lib/
|
||||
sat_code/ Bill Gray's SGP4/SDP4 (MIT, git submodule)
|
||||
test/
|
||||
sql/ Regression test SQL
|
||||
expected/ Expected output
|
||||
data/
|
||||
vallado_518.csv 518 verification test vectors (Vallado et al.)
|
||||
```
|
||||
| Operation | Count | Time | Rate |
|
||||
|---|---|---|---|
|
||||
| TLE propagation (SGP4) | 12,000 | 17ms | 706K/sec |
|
||||
| Planet observation (VSOP87) | 875 | 57ms | 15.4K/sec |
|
||||
| Moon observation (Galilean) | 1,000 | 63ms | 15.9K/sec |
|
||||
| Star observation | 500 | 0.7ms | 714K/sec |
|
||||
| Lambert transfer solve | 100 | 0.1ms | 800K/sec |
|
||||
| Pork chop plot (150x150) | 22,500 | 8.3s | 2.7K/sec |
|
||||
|
||||
## Testing
|
||||
|
||||
pg_orbit uses the standard PostgreSQL regression test framework.
|
||||
12 regression test suites covering all domains:
|
||||
|
||||
```bash
|
||||
make installcheck
|
||||
make installcheck PG_CONFIG=/usr/bin/pg_config
|
||||
```
|
||||
|
||||
Test categories:
|
||||
Tests: TLE parsing, SGP4/SDP4 propagation, coordinate transforms, pass prediction,
|
||||
GiST indexing, convenience functions, star observation, Keplerian propagation,
|
||||
planet observation, moon observation, Lambert transfers, and DE ephemeris.
|
||||
|
||||
| Suite | Coverage |
|
||||
|-------|----------|
|
||||
| `tle_parse` | TLE input/output round-trip, malformed input rejection |
|
||||
| `sgp4_propagate` | Vallado 518 test vectors, deep-space and high-eccentricity edge cases |
|
||||
| `coord_transforms` | TEME to geodetic, TEME to topocentric accuracy |
|
||||
| `pass_prediction` | Known ISS passes, polar and retrograde orbits |
|
||||
| `gist_index` | Index scan vs. sequential scan result equivalence |
|
||||
## Documentation
|
||||
|
||||
The Vallado 518 test vectors are the standard SGP4 verification dataset. Each row
|
||||
contains a NORAD ID, minutes since epoch, and expected position/velocity. All 518
|
||||
must pass to machine epsilon.
|
||||
Full documentation at the [pg_orrery docs site](https://pg-orrery.warehack.ing),
|
||||
built with [Starlight](https://starlight.astro.build). Includes guides, workflow
|
||||
translations (from Skyfield, JPL Horizons, GMAT, Radio Jupiter Pro), complete
|
||||
function reference, architecture notes, and benchmarks.
|
||||
|
||||
## What pg_orrery Is Not
|
||||
|
||||
**Not a GUI.** Use Stellarium, GPredict, or STK for visualization.
|
||||
|
||||
**Not sub-arcsecond.** VSOP87 gives ~1 arcsecond — good for observation planning,
|
||||
not for dish pointing at GHz frequencies. For that, use SPICE or Skyfield with DE441.
|
||||
|
||||
**Not a TLE source.** Bring your own from Space-Track, CelesTrak, or any provider.
|
||||
|
||||
**Not a replacement for SPICE.** No BSP kernels, no aberration corrections at IAU 2000A
|
||||
level. pg_orrery trades those last few milliarcseconds for SQL-speed computation joined
|
||||
with your existing data.
|
||||
|
||||
**Not a full mission design tool.** The Lambert solver handles ballistic two-body
|
||||
transfers. For low-thrust, gravity assists, or multi-body optimization, use GMAT.
|
||||
|
||||
## Upgrading from v0.1.0
|
||||
|
||||
```sql
|
||||
ALTER EXTENSION pg_orrery UPDATE TO '0.2.0';
|
||||
```
|
||||
|
||||
Adds all solar system functions while preserving existing TLE data and satellite functions.
|
||||
|
||||
## License
|
||||
|
||||
[PostgreSQL License](LICENSE). Copyright (c) 2025, Ryan Malloy.
|
||||
|
||||
The bundled sat_code library is separately licensed under the MIT license.
|
||||
The bundled [sat_code](https://github.com/Bill-Gray/sat_code) library is separately
|
||||
licensed under the MIT license.
|
||||
|
||||
7
TODO
Normal file
7
TODO
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
- Gauss method for angles-only initial orbit determination
|
||||
(eliminates seed requirement for sensors without ranging)
|
||||
- Weighted observations (per-obs covariance weighting for
|
||||
heterogeneous sensor fusion)
|
||||
- Range rate fitting in topocentric mode (currently reserved
|
||||
via vel_ecef in residual computation)
|
||||
@ -1,10 +1,10 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Create the pg_orbit extension on first container startup.
|
||||
# Create the pg_orrery extension on first container startup.
|
||||
# The 020_ prefix orders this after TimescaleDB's own init scripts
|
||||
# (000_, 001_, 010_) when used in timescaledb-ha images.
|
||||
|
||||
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "${POSTGRES_DB:-postgres}" <<-'EOSQL'
|
||||
CREATE EXTENSION IF NOT EXISTS pg_orbit;
|
||||
CREATE EXTENSION IF NOT EXISTS pg_orrery;
|
||||
EOSQL
|
||||
241
docs/DESIGN.md
241
docs/DESIGN.md
@ -1,8 +1,8 @@
|
||||
# pg_orbit Design Document
|
||||
# pg_orrery Design Document
|
||||
|
||||
Internal architecture notes. Documents WHY decisions were made,
|
||||
not how to use the extension. Intended audience: future maintainers
|
||||
who need to modify pg_orbit without breaking physical correctness.
|
||||
who need to modify pg_orrery without breaking physical correctness.
|
||||
|
||||
|
||||
## 1. Constant Chain of Custody
|
||||
@ -49,20 +49,20 @@ prediction error of the TLE by an order of magnitude.
|
||||
|
||||
### Constant Inventory
|
||||
|
||||
| Constant | Source Paper | Value | pg_orbit Location | sat_code Location |
|
||||
|----------|-------------|-------|-------------------|-------------------|
|
||||
| ae (equatorial radius) | Hoots & Roehrich STR#3 | 6378.135 km | `types.h:20` (WGS72_AE) | `norad_in.h:64` (earth_radius_in_km) |
|
||||
| J2 | Hoots & Roehrich STR#3 | 0.001082616 | `types.h:21` (WGS72_J2) | `norad_in.h:69` (xj2) |
|
||||
| J3 | Hoots & Roehrich STR#3 | -2.53881e-6 | `types.h:22` (WGS72_J3) | `norad_in.h:63` (xj3) |
|
||||
| J4 | Hoots & Roehrich STR#3 | -1.65597e-6 | `types.h:23` (WGS72_J4) | `norad_in.h:79` (xj4) |
|
||||
| ke | Hoots & Roehrich STR#3 | 0.0743669161331734132 min^-1 | `types.h:24` (WGS72_KE) | `norad_in.h:83` (xke) |
|
||||
| mu | Hoots & Roehrich STR#3 | 398600.8 km^3/s^2 | `types.h:19` (WGS72_MU) | (implicit in xke) |
|
||||
| Constant | Source Paper | Value | pg_orrery Location | Vendored SGP4 Location |
|
||||
|----------|-------------|-------|-------------------|------------------------|
|
||||
| ae (equatorial radius) | Hoots & Roehrich STR#3 | 6378.135 km | `types.h` (WGS72_AE) | `src/sgp4/norad_in.h` (earth_radius_in_km) |
|
||||
| J2 | Hoots & Roehrich STR#3 | 0.001082616 | `types.h` (WGS72_J2) | `src/sgp4/norad_in.h` (xj2) |
|
||||
| J3 | Hoots & Roehrich STR#3 | -2.53881e-6 | `types.h` (WGS72_J3) | `src/sgp4/norad_in.h` (xj3) |
|
||||
| J4 | Hoots & Roehrich STR#3 | -1.65597e-6 | `types.h` (WGS72_J4) | `src/sgp4/norad_in.h` (xj4) |
|
||||
| ke | Hoots & Roehrich STR#3 | 0.0743669161331734132 min^-1 | `types.h` (WGS72_KE) | `src/sgp4/norad_in.h` (xke) |
|
||||
| mu | Hoots & Roehrich STR#3 | 398600.8 km^3/s^2 | `types.h` (WGS72_MU) | (implicit in xke) |
|
||||
| WGS-84 a | NIMA TR8350.2 | 6378.137 km | `types.h:31` (WGS84_A) | -- |
|
||||
| WGS-84 f | NIMA TR8350.2 | 1/298.257223563 | `types.h:32` (WGS84_F) | -- |
|
||||
|
||||
Note that `types.h` carries a parallel copy of the WGS-72 constants
|
||||
even though sat_code defines them in `norad_in.h`. This is intentional:
|
||||
`types.h` is the single header for all pg_orbit C sources, and
|
||||
`types.h` is the single header for all pg_orrery C sources, and
|
||||
`norad_in.h` is an internal sat_code header not meant for external
|
||||
consumers. The GiST index (`gist_tle.c`) and TLE accessor functions
|
||||
(`tle_type.c`) need KE and AE without pulling in sat_code internals.
|
||||
@ -84,7 +84,7 @@ but wrong in principle, and the error compounds in index operations.
|
||||
|
||||
## 2. SGP4 Implementation Choice
|
||||
|
||||
pg_orbit wraps Bill Gray's `sat_code` library (MIT license, Project Pluto).
|
||||
pg_orrery wraps Bill Gray's `sat_code` library (MIT license, Project Pluto).
|
||||
|
||||
### Why sat_code over alternatives
|
||||
|
||||
@ -112,38 +112,37 @@ across function invocations.
|
||||
|
||||
3. **Includes deep-space SDP4.** Many SGP4 implementations only handle
|
||||
near-earth orbits (period < 225 minutes). sat_code includes the full
|
||||
SDP4 with lunar/solar perturbations via `deep.cpp`, handling GEO,
|
||||
SDP4 with lunar/solar perturbations via `deep.c`, handling GEO,
|
||||
Molniya, and GPS orbits.
|
||||
|
||||
4. **MIT license.** Compatible with the PostgreSQL License for embedding
|
||||
in a shared library.
|
||||
|
||||
5. **Actively maintained.** Used in Bill Gray's Find_Orb production
|
||||
astrometry software. Bug fixes reach us through the git submodule.
|
||||
astrometry software.
|
||||
|
||||
### Build Integration
|
||||
|
||||
The Makefile compiles sat_code's `.cpp` files with `g++` and links the
|
||||
resulting `.o` files into the PostgreSQL shared library alongside our C
|
||||
sources. The `-lstdc++` link flag pulls in the C++ runtime. This is the
|
||||
same pattern used by PostGIS for GEOS integration (C extension linking
|
||||
C++ library objects).
|
||||
The SGP4/SDP4 source is vendored into `src/sgp4/` — the `.cpp` files
|
||||
renamed to `.c` (the code is valid C99 with zero C++ features). The
|
||||
Makefile compiles everything with `gcc` and links with `-lm` only. No
|
||||
C++ compiler or runtime is required.
|
||||
|
||||
```
|
||||
src/*.c --> gcc --> .o --|
|
||||
lib/sat_code/*.cpp -> g++ -> .o --|--> pg_orbit.so
|
||||
-lstdc++ -lm
|
||||
src/*.c --[gcc]--> .o --|
|
||||
src/sgp4/*.c --[gcc]--> .o --|--> pg_orrery.so
|
||||
-lm
|
||||
```
|
||||
|
||||
The `-I$(SAT_CODE_DIR)` flag lets our C files `#include "norad.h"`
|
||||
directly.
|
||||
The `-I$(SGP4_DIR)` flag lets our C files `#include "norad.h"` directly.
|
||||
Provenance is recorded in `src/sgp4/PROVENANCE.md`.
|
||||
|
||||
|
||||
## 3. Type System Design
|
||||
|
||||
### Design Principles
|
||||
|
||||
Every pg_orbit type is fixed-size, not varlena. This means:
|
||||
Every pg_orrery type is fixed-size, not varlena. This means:
|
||||
|
||||
- No TOAST overhead (no detoasting on access)
|
||||
- Direct pointer access via `PG_GETARG_POINTER(n)` -- no copy
|
||||
@ -203,7 +202,7 @@ Six doubles: x, y, z (km), vx, vy, vz (km/s).
|
||||
SGP4 outputs velocity in km/min. We convert to km/s at the boundary
|
||||
(`sgp4_funcs.c`, lines 181-183: `vel[i] / 60.0`). This conversion
|
||||
happens exactly once, at the point where the pg_eci struct is populated.
|
||||
Internally, all velocity in pg_orbit is km/s.
|
||||
Internally, all velocity in pg_orrery is km/s.
|
||||
|
||||
### Geodetic Type (24 bytes)
|
||||
|
||||
@ -484,11 +483,16 @@ no `new`, no static buffers.
|
||||
|
||||
### No Global Mutable State
|
||||
|
||||
There are no file-scope variables, no static locals that accumulate
|
||||
state, no caches. Every function computes from its arguments alone.
|
||||
This is required for `PARALLEL SAFE` (all pg_orbit functions are
|
||||
declared PARALLEL SAFE) and avoids cross-session contamination in
|
||||
a multi-backend PostgreSQL server.
|
||||
For v0.1.0/v0.2.0 functions, there are no file-scope variables, no
|
||||
static locals that accumulate state, no caches. Every function computes
|
||||
from its arguments alone.
|
||||
|
||||
The v0.3.0 DE ephemeris layer introduces per-backend static state in
|
||||
`eph_provider.c` (file descriptor, coefficient cache, init flags). This
|
||||
is safe because each backend gets its own copy after fork(). The handle
|
||||
is cleaned up via `on_proc_exit()`. All pg_orrery functions remain
|
||||
`PARALLEL SAFE` -- parallel workers each open their own DE handle
|
||||
independently.
|
||||
|
||||
sat_code itself has no global mutable state. The propagator state is
|
||||
entirely in the `params[]` array and the `tle_t` struct, both of which
|
||||
@ -521,7 +525,7 @@ the per-row propagation loop.
|
||||
|
||||
sat_code returns integer error codes from SGP4() and SDP4():
|
||||
|
||||
| Code | Constant | Severity | Meaning | pg_orbit Response |
|
||||
| Code | Constant | Severity | Meaning | pg_orrery Response |
|
||||
|------|----------|----------|---------|-------------------|
|
||||
| 0 | -- | OK | Normal | Return result |
|
||||
| -1 | SXPX_ERR_NEARLY_PARABOLIC | Fatal | eccentricity >= 1 | `ereport(ERROR)` |
|
||||
@ -568,18 +572,18 @@ initialize the propagator.
|
||||
## 9. Theory-to-Code Mapping
|
||||
|
||||
This table maps key equations from the SGP4 theory papers to their
|
||||
implementation in pg_orbit and sat_code.
|
||||
implementation in pg_orrery and the vendored SGP4 code.
|
||||
|
||||
| Theory | Paper | What | Code Location |
|
||||
|--------|-------|------|---------------|
|
||||
| Mean element recovery | Brouwer (1959) | Recover original mean motion (xnodp) and semi-major axis (aodp) from input TLE elements, removing secular J2 perturbations | `sat_code/common.cpp:sxpall_common_init()` lines 17-35 |
|
||||
| Secular perturbations | Lane & Cranford (1969), Hoots & Roehrich STR#3 | Secular rates of mean anomaly, argument of perigee, and RAAN due to J2, J4 | `sat_code/common.cpp:sxpx_common_init()` lines 86-101 |
|
||||
| Atmospheric drag | Hoots & Roehrich STR#3 | B* formulation of drag, C1/C2/C4 coefficients, perigee-dependent s parameter | `sat_code/common.cpp:sxpx_common_init()` lines 47-84; `sat_code/sgp4.cpp:SGP4_init()` |
|
||||
| Short-period perturbations | Lane & Cranford (1969), Brouwer (1959) | Oscillatory corrections to radius, argument of latitude, node, and inclination | `sat_code/common.cpp:sxpx_posn_vel()` lines 121-229 |
|
||||
| Kepler equation | Classical | Newton-Raphson with second-order correction, bounded first step | `sat_code/common.cpp:sxpx_posn_vel()` lines 175-208 |
|
||||
| Deep-space resonance | Hujsak (1979) | Lunar and solar gravitational perturbations, geopotential resonance for 12-hour and 24-hour orbits | `sat_code/deep.cpp:Deep_dpinit()`, `Deep_dpsec()`, `Deep_dpper()` |
|
||||
| Near-earth propagation | Hoots & Roehrich STR#3 | SGP4 main loop: secular + short-period + drag terms | `sat_code/sgp4.cpp:SGP4()` |
|
||||
| Deep-space propagation | Hoots & Roehrich STR#3 | SDP4: SGP4 core + deep-space secular/periodic corrections | `sat_code/sdp4.cpp:SDP4()` |
|
||||
| Mean element recovery | Brouwer (1959) | Recover original mean motion (xnodp) and semi-major axis (aodp) from input TLE elements, removing secular J2 perturbations | `src/sgp4/common.c:sxpall_common_init()` lines 17-35 |
|
||||
| Secular perturbations | Lane & Cranford (1969), Hoots & Roehrich STR#3 | Secular rates of mean anomaly, argument of perigee, and RAAN due to J2, J4 | `src/sgp4/common.c:sxpx_common_init()` lines 86-101 |
|
||||
| Atmospheric drag | Hoots & Roehrich STR#3 | B* formulation of drag, C1/C2/C4 coefficients, perigee-dependent s parameter | `src/sgp4/common.c:sxpx_common_init()` lines 47-84; `src/sgp4/sgp4.c:SGP4_init()` |
|
||||
| Short-period perturbations | Lane & Cranford (1969), Brouwer (1959) | Oscillatory corrections to radius, argument of latitude, node, and inclination | `src/sgp4/common.c:sxpx_posn_vel()` lines 121-229 |
|
||||
| Kepler equation | Classical | Newton-Raphson with second-order correction, bounded first step | `src/sgp4/common.c:sxpx_posn_vel()` lines 175-208 |
|
||||
| Deep-space resonance | Hujsak (1979) | Lunar and solar gravitational perturbations, geopotential resonance for 12-hour and 24-hour orbits | `src/sgp4/deep.c:Deep_dpinit()`, `Deep_dpsec()`, `Deep_dpper()` |
|
||||
| Near-earth propagation | Hoots & Roehrich STR#3 | SGP4 main loop: secular + short-period + drag terms | `src/sgp4/sgp4.c:SGP4()` |
|
||||
| Deep-space propagation | Hoots & Roehrich STR#3 | SDP4: SGP4 core + deep-space secular/periodic corrections | `src/sgp4/sdp4.c:SDP4()` |
|
||||
| Semi-major axis from n | Kepler's third law | a = (KE / n)^(2/3) in earth radii | `src/tle_type.c:tle_perigee()` line 415; `src/gist_tle.c:tle_to_alt_range()` line 76 |
|
||||
| GMST | Vallado (2013) Eq. 3-47 | Greenwich Mean Sidereal Time from Julian date | `src/coord_funcs.c:gmst_from_jd()` lines 59-73; `src/pass_funcs.c:gmst_from_jd()` lines 129-151 |
|
||||
| TEME to ECEF | Vallado (2013) | Z-axis rotation by -GMST, velocity cross-product correction | `src/coord_funcs.c:teme_to_ecef()` lines 83-103; `src/pass_funcs.c:teme_to_ecef()` lines 157-179 |
|
||||
@ -587,4 +591,157 @@ implementation in pg_orbit and sat_code.
|
||||
| Topocentric transform | Standard SEZ | ECEF range vector rotated to South-East-Zenith, azimuth from north | `src/coord_funcs.c:ecef_to_topocentric()` lines 163-188 |
|
||||
| Observer to ECEF | Geodesy standard | WGS-84 ellipsoid surface point to Cartesian | `src/coord_funcs.c:observer_to_ecef()` lines 143-156 |
|
||||
| Range rate | Dot product | Projection of relative velocity onto line-of-sight unit vector | `src/coord_funcs.c:eci_to_topocentric()` line 618 |
|
||||
| Near/deep selection | Hoots & Roehrich STR#3 | Period threshold: 225 minutes (n < 2*pi/225 rad/min) | `sat_code/norad.h:select_ephemeris()` |
|
||||
| Near/deep selection | Hoots & Roehrich STR#3 | Period threshold: 225 minutes (n < 2*pi/225 rad/min) | `src/sgp4/norad.h:select_ephemeris()` |
|
||||
|
||||
|
||||
## 10. JPL DE Ephemeris Architecture (v0.3.0)
|
||||
|
||||
v0.3.0 adds optional JPL DE440/441 ephemeris support (~0.1 milliarcsecond
|
||||
accuracy) without modifying any existing VSOP87/ELP82B code path. This
|
||||
section documents the architectural decisions specific to DE integration.
|
||||
|
||||
### The Fundamental Tension
|
||||
|
||||
pg_orrery's core properties (compiled-in coefficients, no file I/O, no
|
||||
mutable state) are precisely what DE441 challenges. A ~3GB binary file
|
||||
introduces file dependency, per-backend state (file descriptor,
|
||||
coefficient cache), and OS-level file descriptor management across
|
||||
PostgreSQL's multi-process model.
|
||||
|
||||
The architecture resolves this by treating DE as an **additive layer**:
|
||||
new `_de()` function variants alongside existing functions. Existing
|
||||
functions are untouched. VSOP87 is the bulkhead -- always compiled in,
|
||||
always works, always IMMUTABLE.
|
||||
|
||||
### Why Separate Functions (Not a GUC Switch)
|
||||
|
||||
Considered and rejected: a single `planet_observe()` that dispatches to
|
||||
DE or VSOP87 based on a GUC setting.
|
||||
|
||||
The problem is volatility. VSOP87 functions are IMMUTABLE -- their data
|
||||
is compiled into the binary. PostgreSQL can:
|
||||
- Cache results in expression indexes (almanac tables, pork chop grids)
|
||||
- Constant-fold during planning (bake results into prepared statements)
|
||||
|
||||
If `planet_observe()` dispatched to DE based on a GUC, it would need to
|
||||
be STABLE, losing these optimizations for ALL users, even those without
|
||||
a DE file. Separate `_de()` variants let VSOP87 users keep IMMUTABLE
|
||||
and DE users get STABLE -- honest volatility for both.
|
||||
|
||||
### Clean-Room DE Reader Design
|
||||
|
||||
The JPL DE binary format (used by DE405, DE430, DE440, DE441) consists
|
||||
of fixed-size records of `double` values:
|
||||
|
||||
- **Record 1 (header):** Start/end JD, record interval, number of
|
||||
coefficients, AU value, Earth-Moon mass ratio (EMRAT), coefficient
|
||||
layout table (3 values per body: offset, ncoeff, nsub)
|
||||
- **Record 2:** Constant values
|
||||
- **Records 3+:** Chebyshev polynomial coefficients covering a time
|
||||
interval for all 13 body groups
|
||||
|
||||
The reader is implemented in ~250 lines of C (`de_reader.c`), using:
|
||||
- Raw POSIX I/O (`open`/`lseek`/`read`, not `fread`) to avoid libc
|
||||
buffering issues across fork
|
||||
- Clenshaw recurrence for Chebyshev evaluation (~15 lines)
|
||||
- Single-record coefficient cache (most queries hit consecutive times)
|
||||
|
||||
#### Why Not jpl_eph
|
||||
|
||||
Bill Gray's `jpl_eph` (GPL-2+) would be the obvious choice, but:
|
||||
1. GPL-2+ license constrains pg_orrery's licensing flexibility
|
||||
2. Uses global statics (`static int init_err_code`)
|
||||
3. Written in C++ (`jpleph.cpp`); pg_orrery is pure C
|
||||
4. pg_orrery only needs position queries, not velocity or nutation
|
||||
|
||||
The format is well-documented and the algorithm is straightforward.
|
||||
A clean-room implementation in ~250 lines avoids all four issues.
|
||||
|
||||
### Per-Backend Lazy Initialization
|
||||
|
||||
PostgreSQL parallel workers are forked from the **postmaster, not the
|
||||
leader**. They don't inherit the leader's file descriptors or initialized
|
||||
handles.
|
||||
|
||||
Each backend (including parallel workers) gets its own independently-
|
||||
opened handle via lazy init on first `_de()` call:
|
||||
|
||||
```
|
||||
Backend A calls planet_observe_de() -> opens own fd, own cache
|
||||
Worker W1 calls planet_observe_de() -> opens own fd, own cache
|
||||
Worker W2 calls planet_observe_de() -> opens own fd, own cache
|
||||
```
|
||||
|
||||
Static per-backend variables (`de_handle_ptr`, `de_init_attempted`) are
|
||||
safe because after `fork()`, each process gets its own copy-on-write
|
||||
address space.
|
||||
|
||||
**Critical rule:** Never open the DE file in `_PG_init()`. That runs in
|
||||
the postmaster, and the file descriptor would be inherited by all children
|
||||
with undefined stream behavior. Always lazy init.
|
||||
|
||||
Cleanup is via `on_proc_exit(eph_cleanup, 0)`, registered in `_PG_init()`.
|
||||
|
||||
### ICRS-to-Ecliptic Frame Rotation
|
||||
|
||||
DE ephemerides return positions in the ICRS equatorial frame. The
|
||||
pg_orrery observation pipeline expects ecliptic J2000. The conversion
|
||||
happens at the provider boundary in `eph_provider.c`:
|
||||
|
||||
```
|
||||
DE position (ICRS equatorial) -> equatorial_to_ecliptic() -> ecliptic J2000
|
||||
^
|
||||
already in astro_math.h since v0.2.0
|
||||
```
|
||||
|
||||
This rotation is applied to BOTH the target body and Earth positions
|
||||
before they enter `observe_from_geocentric()`. The frame conversion
|
||||
happens exactly once, at the provider boundary.
|
||||
|
||||
### Earth Position from DE
|
||||
|
||||
DE ephemerides don't store Earth directly. They store the Earth-Moon
|
||||
Barycenter (EMB, body 3) and the Moon (body 10). Earth's position is
|
||||
derived:
|
||||
|
||||
```
|
||||
Earth = EMB - Moon / (1 + EMRAT)
|
||||
```
|
||||
|
||||
where EMRAT (~81.300587) is the Earth-Moon mass ratio from the DE
|
||||
header. This calculation happens in `eph_provider.c:de_get_earth_helio_ecliptic()`.
|
||||
|
||||
### Constant Chain of Custody Extension
|
||||
|
||||
Three new rules for the DE pipeline:
|
||||
|
||||
| Rule | What | Why |
|
||||
|------|------|-----|
|
||||
| 6 | DE positions pass through `equatorial_to_ecliptic()` at the provider boundary | DE returns ICRS equatorial; the observation pipeline expects ecliptic J2000 |
|
||||
| 7 | Both target and Earth must come from the same provider | Mixing DE Mars with VSOP87 Earth introduces frame inconsistency |
|
||||
| 8 | DE header AU must match compiled-in `AU_KM` (149597870.7) | AU defines the length scale; a mismatch corrupts all distance calculations |
|
||||
|
||||
### Fallback Strategy
|
||||
|
||||
Every `_de()` function follows this pattern:
|
||||
|
||||
1. Try to get position from DE
|
||||
2. If DE succeeds, use DE result
|
||||
3. If DE fails (no file, JD out of range, I/O error):
|
||||
- If DE was explicitly configured (GUC set), emit `ereport(NOTICE)`
|
||||
- Fall back to VSOP87/ELP82B equivalent
|
||||
- Return the VSOP87 result
|
||||
|
||||
When `pg_orrery.ephemeris_path` is empty (default), DE functions fall
|
||||
back silently -- no NOTICE, no overhead, identical results to the
|
||||
non-DE variants.
|
||||
|
||||
### Lauren Bugs (DE-Specific)
|
||||
|
||||
| Bug | What "Can't Happen" | How It Happens | Mitigation |
|
||||
|-----|---------------------|----------------|------------|
|
||||
| L1 | File won't change mid-query | DBA replaces file; new parallel worker opens new version | Per-backend handle is stable for backend lifetime |
|
||||
| L2 | File will always be there | Docker restart with ephemeral volume | VSOP87 fallback always available |
|
||||
| L3 | File is always valid | Partial download, bit rot, wrong endianness | Canary validation: Earth at J2000.0 ~1 AU from Sun; byte-order detection via AU constant |
|
||||
| L4 | All backends use same version | Primary has DE441, replica has DE440 | AU consistency check; log ephemeris version at NOTICE |
|
||||
| L5 | read() always succeeds | NFS timeout, disk error, fd limit | Every `read()` return checked; propagate as `ereport(ERROR)` |
|
||||
|
||||
61
docs/Dockerfile
Normal file
61
docs/Dockerfile
Normal file
@ -0,0 +1,61 @@
|
||||
ARG NODE_VERSION=22
|
||||
|
||||
# ── Stage 1: Build ──────────────────────────────────────────────
|
||||
FROM node:${NODE_VERSION}-slim AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install dependencies first (cache layer)
|
||||
COPY package.json package-lock.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci
|
||||
|
||||
# Copy source and build
|
||||
COPY . .
|
||||
ENV ASTRO_TELEMETRY_DISABLED=1
|
||||
RUN npm run build
|
||||
|
||||
# ── Stage 2: Production ────────────────────────────────────────
|
||||
FROM caddy:2-alpine AS production
|
||||
|
||||
COPY --from=build /app/dist /srv
|
||||
COPY <<'CADDYFILE' /etc/caddy/Caddyfile
|
||||
:3000 {
|
||||
root * /srv
|
||||
file_server
|
||||
try_files {path} {path}/ /404.html
|
||||
encode gzip
|
||||
|
||||
header {
|
||||
X-Content-Type-Options nosniff
|
||||
X-Frame-Options DENY
|
||||
Referrer-Policy strict-origin-when-cross-origin
|
||||
}
|
||||
|
||||
header /docs/_astro/* {
|
||||
Cache-Control "public, max-age=31536000, immutable"
|
||||
}
|
||||
}
|
||||
CADDYFILE
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
|
||||
CMD wget -qO- http://127.0.0.1:3000/ || exit 1
|
||||
|
||||
# ── Stage 3: Development ───────────────────────────────────────
|
||||
FROM node:${NODE_VERSION}-slim AS development
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV ASTRO_TELEMETRY_DISABLED=1
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["npm", "run", "dev"]
|
||||
32
docs/Makefile
Normal file
32
docs/Makefile
Normal file
@ -0,0 +1,32 @@
|
||||
.PHONY: dev prod build down logs clean restart
|
||||
|
||||
COMPOSE = docker compose
|
||||
|
||||
# Start in development mode with hot-reload
|
||||
dev:
|
||||
$(COMPOSE) -f docker-compose.yml -f docker-compose.dev.yml up -d --build
|
||||
@$(COMPOSE) -f docker-compose.yml -f docker-compose.dev.yml logs -f
|
||||
|
||||
# Start in production mode (static build served by Caddy)
|
||||
prod:
|
||||
$(COMPOSE) up -d --build
|
||||
@$(COMPOSE) logs --tail=30
|
||||
|
||||
# Build image without starting
|
||||
build:
|
||||
$(COMPOSE) build
|
||||
|
||||
# Stop services
|
||||
down:
|
||||
$(COMPOSE) down
|
||||
|
||||
# Tail logs
|
||||
logs:
|
||||
$(COMPOSE) logs -f
|
||||
|
||||
# Stop and remove volumes
|
||||
clean:
|
||||
$(COMPOSE) down -v --remove-orphans
|
||||
|
||||
# Restart in production mode
|
||||
restart: down prod
|
||||
@ -3,7 +3,7 @@
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| From | craft-api |
|
||||
| To | pg-orbit |
|
||||
| To | pg-orrery |
|
||||
| Date | 2026-02-15T17:00:00-07:00 |
|
||||
| Re | Consumer use cases and API feedback on first draft |
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
|
||||
Craft is a satellite tracking + radio astronomy platform at `~/claude/ham/satellite/astrolock/`. We run a FastAPI backend backed by TimescaleDB-HA. Our database holds 22,000+ satellites with TLE data, frequencies, group memberships, and pgai vector embeddings. The frontend renders a live sky view via `/api/sky/up`.
|
||||
|
||||
The sky engine (`packages/api/src/astrolock_api/services/sky_engine.py`) uses Python Skyfield to compute positions for planets, the sun/moon, bright stars, and comets. Satellites are conspicuously absent from the `whats_up()` response because per-request Python propagation of 22k TLEs is untenable. pg_orbit is the solution.
|
||||
The sky engine (`packages/api/src/astrolock_api/services/sky_engine.py`) uses Python Skyfield to compute positions for planets, the sun/moon, bright stars, and comets. Satellites are conspicuously absent from the `whats_up()` response because per-request Python propagation of 22k TLEs is untenable. pg_orrery is the solution.
|
||||
|
||||
## What We Love About the First Draft
|
||||
|
||||
@ -139,7 +139,7 @@ Our API passes observer coordinates as floats from the `observer_location` table
|
||||
|
||||
### P0 -- Unblocks `/api/sky/up`
|
||||
|
||||
| Use Case | Query Pattern | pg_orbit Functions |
|
||||
| Use Case | Query Pattern | pg_orrery Functions |
|
||||
|----------|--------------|-------------------|
|
||||
| What satellites are overhead? | `WHERE topo_elevation(observe(...)) >= :min_alt` | `observe()` (new), `topo_elevation()` |
|
||||
| Single satellite position | `observe(tle_from_lines(:l1, :l2), :obs, NOW())` | `observe()` (new), `tle_from_lines()` (new) |
|
||||
@ -147,7 +147,7 @@ Our API passes observer coordinates as floats from the `observer_location` table
|
||||
|
||||
### P1 -- Enables pass prediction and materialized views
|
||||
|
||||
| Use Case | Query Pattern | pg_orbit Functions |
|
||||
| Use Case | Query Pattern | pg_orrery Functions |
|
||||
|----------|--------------|-------------------|
|
||||
| Upcoming passes for a group | `LATERAL predict_passes(tle, :obs, NOW(), NOW()+'24h', 10.0)` | `predict_passes()`, `tle_from_lines()` (new) |
|
||||
| Next pass for a satellite | `next_pass(tle_from_lines(:l1, :l2), :obs, NOW())` | `next_pass()`, `tle_from_lines()` (new) |
|
||||
@ -157,7 +157,7 @@ Our API passes observer coordinates as floats from the `observer_location` table
|
||||
|
||||
### P2 -- Batch Doppler, ground tracks, conjunction screening
|
||||
|
||||
| Use Case | Query Pattern | pg_orbit Functions |
|
||||
| Use Case | Query Pattern | pg_orrery Functions |
|
||||
|----------|--------------|-------------------|
|
||||
| Doppler correction | `f.frequency_mhz * (1 - topo_range_rate(observe(...))/299792.458)` | `observe()` (new), `topo_range_rate()` |
|
||||
| Ground track overlay | `LATERAL ground_track(tle, :start, :stop, '30s')` | `ground_track()` |
|
||||
@ -166,7 +166,7 @@ Our API passes observer coordinates as floats from the `observer_location` table
|
||||
|
||||
### P2 -- PostGIS integration (future)
|
||||
|
||||
| Use Case | Query Pattern | pg_orbit Functions |
|
||||
| Use Case | Query Pattern | pg_orrery Functions |
|
||||
|----------|--------------|-------------------|
|
||||
| Satellites over a region | `WHERE ST_Contains(:geom, ST_Point(geodetic_lon(g), geodetic_lat(g)))` | `ground_track()`, geodetic accessors |
|
||||
| Footprint circles | `ST_Buffer(ST_Point(lon, lat), footprint_radius)` | `subsatellite_point()`, `geodetic_lat/lon()` |
|
||||
@ -241,7 +241,7 @@ ORDER BY s.name, f.frequency_mhz;
|
||||
|
||||
We can provide:
|
||||
|
||||
1. **ISS TLE + known Skyfield positions** -- We already compute ISS position via Python Skyfield. We can generate comparison data: given a TLE and timestamp, here's what Skyfield says for az/el/range from our observer. pg_orbit should match to within the expected SGP4 implementation differences.
|
||||
1. **ISS TLE + known Skyfield positions** -- We already compute ISS position via Python Skyfield. We can generate comparison data: given a TLE and timestamp, here's what Skyfield says for az/el/range from our observer. pg_orrery should match to within the expected SGP4 implementation differences.
|
||||
|
||||
2. **Amateur satellite group TLEs** -- Our `satellite_group` table has curated groups ('amateur', 'weather', 'starlink', etc.). We can provide a batch of TLEs for pass prediction testing.
|
||||
|
||||
@ -264,4 +264,4 @@ We can provide:
|
||||
- [ ] Confirm whether `tle_in()` validates checksums and what happens on bad input
|
||||
- [ ] Clarify NULL-vs-error behavior for failed propagation
|
||||
- [ ] Consider a `_safe` variant of propagation functions that returns NULL on error
|
||||
- [ ] Reply with `002-pg-orbit-*.md` when ready
|
||||
- [ ] Reply with `002-pg-orrery-*.md` when ready
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| From | pg-orbit |
|
||||
| From | pg-orrery |
|
||||
| To | craft-api |
|
||||
| Date | 2026-02-15T18:45:00-07:00 |
|
||||
| Re | All three convenience functions shipped, plus _safe variants |
|
||||
@ -150,10 +150,10 @@ Drop the test data as `003-craft-test-data.md` in this thread and we'll integrat
|
||||
## Build & Install
|
||||
|
||||
```bash
|
||||
cd ~/claude/pg_orbit
|
||||
cd ~/claude/pg_orrery
|
||||
make clean && make # Zero warnings
|
||||
sudo make install
|
||||
psql -c "DROP EXTENSION IF EXISTS pg_orbit CASCADE; CREATE EXTENSION pg_orbit;"
|
||||
psql -c "DROP EXTENSION IF EXISTS pg_orrery CASCADE; CREATE EXTENSION pg_orrery;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| From | craft-api |
|
||||
| To | pg-orbit |
|
||||
| To | pg-orrery |
|
||||
| Date | 2026-02-15T19:30:00-07:00 |
|
||||
| Re | Test data package -- ISS vectors, edge cases, cross-verification script |
|
||||
|
||||
@ -224,10 +224,10 @@ Save as `test/skyfield_verify.py` or run standalone. Uses `uv run` with inline s
|
||||
# ]
|
||||
# ///
|
||||
"""
|
||||
Cross-verification tool for pg_orbit.
|
||||
Cross-verification tool for pg_orrery.
|
||||
|
||||
Computes satellite position via Skyfield/sgp4 and outputs JSON
|
||||
for comparison against pg_orbit's observe() function.
|
||||
for comparison against pg_orrery's observe() function.
|
||||
|
||||
Usage:
|
||||
uv run skyfield_verify.py --tle1 "1 25544U ..." --tle2 "2 25544 ..." \
|
||||
@ -363,7 +363,7 @@ def run_batch(args):
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Cross-verify satellite positions against pg_orbit"
|
||||
description="Cross-verify satellite positions against pg_orrery"
|
||||
)
|
||||
sub = parser.add_subparsers(dest="mode")
|
||||
|
||||
@ -411,11 +411,11 @@ Batch mode expects a CSV with columns: `tle1,tle2,lat,lon,alt,timestamp`
|
||||
uv run skyfield_verify.py batch --csv test_vectors.csv --output skyfield_results.json
|
||||
```
|
||||
|
||||
Then compare `skyfield_results.json` against pg_orbit output for the same inputs.
|
||||
Then compare `skyfield_results.json` against pg_orrery output for the same inputs.
|
||||
|
||||
Expected agreement between Skyfield and pg_orbit: ~0.01 deg angular, ~1 km range, ~0.001 km/s range rate. Larger discrepancies indicate a constants mismatch (WGS-72 vs WGS-84) or nutation model difference.
|
||||
Expected agreement between Skyfield and pg_orrery: ~0.01 deg angular, ~1 km range, ~0.001 km/s range rate. Larger discrepancies indicate a constants mismatch (WGS-72 vs WGS-84) or nutation model difference.
|
||||
|
||||
## 5. Suggested pg_orbit Regression SQL
|
||||
## 5. Suggested pg_orrery Regression SQL
|
||||
|
||||
### 5a. Round-trip: tle_from_lines extracts correct NORAD ID
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| From | pg-orbit |
|
||||
| From | pg-orrery |
|
||||
| To | craft-api |
|
||||
| Date | 2026-02-15T19:10:00-07:00 |
|
||||
| Re | Test results -- all pass, plus 2-D GiST index upgrade |
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
## Test Results Summary
|
||||
|
||||
All suggested regression tests from message 003 pass against pg_orbit v0.1.0 on PostgreSQL 17 (port 5499).
|
||||
All suggested regression tests from message 003 pass against pg_orrery v0.1.0 on PostgreSQL 17 (port 5499).
|
||||
|
||||
| Test | Result | Notes |
|
||||
|------|--------|-------|
|
||||
@ -76,11 +76,11 @@ If Craft wants to explicitly filter by observer latitude (stronger pruning), we
|
||||
|
||||
Your `skyfield_verify.py` script is excellent. I have not run it yet (it requires `uv run` with sgp4/skyfield/numpy). The expected agreement tolerances you specified (0.05 deg angular, 5 km range, 0.01 km/s range rate) are reasonable for SGP4 implementations using the same WGS-72 constants.
|
||||
|
||||
One note: Skyfield's `altaz()` uses the full IAU-80 nutation model (106 terms) for TEME→ITRF conversion, while pg_orbit uses only the 4 terms that match SGP4's internal model (per Hoots & Roehrich STR#3). This can introduce ~0.01-0.03 deg differences in azimuth at certain times. The range/range-rate agreement should be tighter since those are less sensitive to frame rotation.
|
||||
One note: Skyfield's `altaz()` uses the full IAU-80 nutation model (106 terms) for TEME→ITRF conversion, while pg_orrery uses only the 4 terms that match SGP4's internal model (per Hoots & Roehrich STR#3). This can introduce ~0.01-0.03 deg differences in azimuth at certain times. The range/range-rate agreement should be tighter since those are less sensitive to frame rotation.
|
||||
|
||||
## Next Steps
|
||||
|
||||
Your edge case TLEs (Vela 1, Molniya, decayed, JPSS-1, GOES-16) are already verified individually. They would be good additions to pg_orbit's regression suite as permanent test cases.
|
||||
Your edge case TLEs (Vela 1, Molniya, decayed, JPSS-1, GOES-16) are already verified individually. They would be good additions to pg_orrery's regression suite as permanent test cases.
|
||||
|
||||
The amateur satellite batch is particularly valuable for testing `observe_safe()` in the pattern Craft's API will use — `LATERAL observe_safe(...)` over a table of TLEs.
|
||||
|
||||
|
||||
@ -3,9 +3,9 @@
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| From | craft-api |
|
||||
| To | pg-orbit |
|
||||
| To | pg-orrery |
|
||||
| Date | 2026-02-15T20:00:00-07:00 |
|
||||
| Re | Integration complete — install pg_orbit on Craft database |
|
||||
| Re | Integration complete — install pg_orrery on Craft database |
|
||||
|
||||
---
|
||||
|
||||
@ -15,13 +15,13 @@ Craft-side integration is done. The sky engine (`sky_engine.py`) already calls `
|
||||
|
||||
## What's Left
|
||||
|
||||
pg_orbit needs to be installed on the Craft database (TimescaleDB-HA, PostgreSQL 17, port 5499):
|
||||
pg_orrery needs to be installed on the Craft database (TimescaleDB-HA, PostgreSQL 17, port 5499):
|
||||
|
||||
```bash
|
||||
cd ~/claude/pg_orbit
|
||||
cd ~/claude/pg_orrery
|
||||
make clean && make
|
||||
sudo make install
|
||||
psql -p 5499 -d astrolock -c "CREATE EXTENSION IF NOT EXISTS pg_orbit;"
|
||||
psql -p 5499 -d astrolock -c "CREATE EXTENSION IF NOT EXISTS pg_orrery;"
|
||||
```
|
||||
|
||||
Once installed, `/api/sky/up` will include satellites automatically — no code changes, no restart needed. The `observe_safe()` query runs on next request.
|
||||
|
||||
164
docs/astro.config.mjs
Normal file
164
docs/astro.config.mjs
Normal file
@ -0,0 +1,164 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
import starlight from "@astrojs/starlight";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import remarkMath from "remark-math";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
import mermaid from "astro-mermaid";
|
||||
import icon from "astro-icon";
|
||||
import opengraphImages from "astro-opengraph-images";
|
||||
import { pgOrreryOgImage } from "./src/og-renderer.js";
|
||||
import * as fs from "fs";
|
||||
|
||||
export default defineConfig({
|
||||
site: "https://pg-orrery.warehack.ing",
|
||||
integrations: [
|
||||
icon(),
|
||||
mermaid(),
|
||||
starlight({
|
||||
title: "pg_orrery",
|
||||
description:
|
||||
"It's not rocket science. A database orrery — celestial mechanics for PostgreSQL.",
|
||||
favicon: "/favicon.svg",
|
||||
logo: {
|
||||
src: "./src/assets/pg-orrery-logo.svg",
|
||||
replacesTitle: true,
|
||||
},
|
||||
social: [
|
||||
{
|
||||
icon: "github",
|
||||
label: "Gitea",
|
||||
href: "https://git.supported.systems/warehack.ing/pg_orrery",
|
||||
},
|
||||
],
|
||||
customCss: [
|
||||
"./src/styles/custom.css",
|
||||
"./src/styles/katex-fixes.css",
|
||||
"katex/dist/katex.min.css",
|
||||
],
|
||||
head: [
|
||||
{
|
||||
tag: "meta",
|
||||
attrs: {
|
||||
name: "theme-color",
|
||||
content: "#0a0e17",
|
||||
},
|
||||
},
|
||||
],
|
||||
components: {
|
||||
Head: "./src/components/Head.astro",
|
||||
},
|
||||
sidebar: [
|
||||
{
|
||||
label: "Getting Started",
|
||||
items: [
|
||||
{ label: "What is pg_orrery?", slug: "getting-started/what-is-pg-orrery" },
|
||||
{ label: "Installation", slug: "getting-started/installation" },
|
||||
{ label: "Quick Start", slug: "getting-started/quick-start" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Guides",
|
||||
items: [
|
||||
{ label: "Tracking Satellites", slug: "guides/tracking-satellites" },
|
||||
{ label: "Observing the Solar System", slug: "guides/observing-solar-system" },
|
||||
{ label: "Planetary Moon Tracking", slug: "guides/planetary-moons" },
|
||||
{ label: "Star Catalogs in SQL", slug: "guides/star-catalogs" },
|
||||
{ label: "Comet & Asteroid Tracking", slug: "guides/comets-asteroids" },
|
||||
{ label: "Jupiter Radio Burst Prediction", slug: "guides/jupiter-radio-bursts" },
|
||||
{ label: "Interplanetary Trajectories", slug: "guides/interplanetary-trajectories" },
|
||||
{ label: "Conjunction Screening", slug: "guides/conjunction-screening" },
|
||||
{ label: "JPL DE Ephemeris", slug: "guides/de-ephemeris" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Workflow Translation",
|
||||
items: [
|
||||
{ label: "From Skyfield to SQL", slug: "workflow/from-skyfield" },
|
||||
{ label: "From JPL Horizons to SQL", slug: "workflow/from-jpl-horizons" },
|
||||
{ label: "From GMAT to SQL", slug: "workflow/from-gmat" },
|
||||
{ label: "From Radio Jupiter Pro to SQL", slug: "workflow/from-radio-jupiter-pro" },
|
||||
{ label: "The SQL Advantage", slug: "workflow/sql-advantage" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Reference",
|
||||
items: [
|
||||
{ label: "Types", slug: "reference/types" },
|
||||
{ label: "Functions: Satellite", slug: "reference/functions-satellite" },
|
||||
{ label: "Functions: Solar System", slug: "reference/functions-solar-system" },
|
||||
{ label: "Functions: Moons", slug: "reference/functions-moons" },
|
||||
{ label: "Functions: Stars & Comets", slug: "reference/functions-stars-comets" },
|
||||
{ label: "Functions: Radio", slug: "reference/functions-radio" },
|
||||
{ label: "Functions: Transfers", slug: "reference/functions-transfers" },
|
||||
{ label: "Functions: DE Ephemeris", slug: "reference/functions-de" },
|
||||
{ label: "Operators & GiST Index", slug: "reference/operators-gist" },
|
||||
{ label: "Body ID Reference", slug: "reference/body-ids" },
|
||||
{ label: "Constants & Accuracy", slug: "reference/constants-accuracy" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Architecture",
|
||||
items: [
|
||||
{ label: "Design Principles", slug: "architecture/design-principles" },
|
||||
{ label: "Constant Chain of Custody", slug: "architecture/constant-chain-of-custody" },
|
||||
{ label: "Observation Pipeline", slug: "architecture/observation-pipeline" },
|
||||
{ label: "Theory-to-Code Mapping", slug: "architecture/theory-to-code" },
|
||||
{ label: "Memory & Thread Safety", slug: "architecture/memory-thread-safety" },
|
||||
{ label: "SGP4 Integration", slug: "architecture/sgp4-integration" },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Performance",
|
||||
items: [
|
||||
{ label: "Benchmarks", slug: "performance/benchmarks" },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
opengraphImages({
|
||||
options: {
|
||||
fonts: [
|
||||
{
|
||||
name: "Inter",
|
||||
weight: 400,
|
||||
style: "normal",
|
||||
data: fs.readFileSync(
|
||||
"node_modules/@fontsource/inter/files/inter-latin-400-normal.woff"
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "Inter",
|
||||
weight: 700,
|
||||
style: "normal",
|
||||
data: fs.readFileSync(
|
||||
"node_modules/@fontsource/inter/files/inter-latin-700-normal.woff"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
render: pgOrreryOgImage,
|
||||
}),
|
||||
],
|
||||
|
||||
markdown: {
|
||||
remarkPlugins: [remarkMath],
|
||||
rehypePlugins: [rehypeKatex],
|
||||
},
|
||||
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
server: {
|
||||
host: "0.0.0.0",
|
||||
...(process.env.VITE_HMR_HOST && {
|
||||
hmr: {
|
||||
host: process.env.VITE_HMR_HOST,
|
||||
protocol: "wss",
|
||||
clientPort: 443,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
telemetry: false,
|
||||
devToolbar: { enabled: false },
|
||||
});
|
||||
21
docs/docker-compose.dev.yml
Normal file
21
docs/docker-compose.dev.yml
Normal file
@ -0,0 +1,21 @@
|
||||
services:
|
||||
docs:
|
||||
build:
|
||||
target: development
|
||||
volumes:
|
||||
- ./src:/app/src
|
||||
- ./public:/app/public
|
||||
- ./astro.config.mjs:/app/astro.config.mjs
|
||||
- ./package.json:/app/package.json
|
||||
environment:
|
||||
- VITE_HMR_HOST=${VITE_HMR_HOST:-pg-orrery.warehack.ing}
|
||||
labels:
|
||||
# WebSocket / HMR support for dev hot-reload
|
||||
caddy.reverse_proxy.flush_interval: "-1"
|
||||
caddy.reverse_proxy.transport: http
|
||||
caddy.reverse_proxy.transport.read_timeout: "0"
|
||||
caddy.reverse_proxy.transport.write_timeout: "0"
|
||||
caddy.reverse_proxy.transport.keepalive: 5m
|
||||
caddy.reverse_proxy.transport.keepalive_idle_conns: "10"
|
||||
caddy.reverse_proxy.stream_timeout: 24h
|
||||
caddy.reverse_proxy.stream_close_delay: 5s
|
||||
22
docs/docker-compose.yml
Normal file
22
docs/docker-compose.yml
Normal file
@ -0,0 +1,22 @@
|
||||
services:
|
||||
docs:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
container_name: pg-orrery-docs
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- "3000"
|
||||
networks:
|
||||
- caddy
|
||||
environment:
|
||||
- ASTRO_TELEMETRY_DISABLED=1
|
||||
labels:
|
||||
caddy: pg-orrery.warehack.ing
|
||||
caddy.reverse_proxy: "{{upstreams 3000}}"
|
||||
caddy.encode: gzip
|
||||
|
||||
networks:
|
||||
caddy:
|
||||
external: true
|
||||
11797
docs/package-lock.json
generated
Normal file
11797
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
docs/package.json
Normal file
32
docs/package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "pg-orrery-docs",
|
||||
"type": "module",
|
||||
"version": "2026.02.16",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "astro dev --host 0.0.0.0 --port 3000",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview --port 3000"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/starlight": "^0.37.6",
|
||||
"@fontsource/inter": "^5.0.0",
|
||||
"@fontsource/jetbrains-mono": "^5.0.0",
|
||||
"@iconify-json/lucide": "^1.2.91",
|
||||
"astro": "^5.17.2",
|
||||
"astro-icon": "^1.1.5",
|
||||
"astro-mermaid": "^1.3.1",
|
||||
"astro-opengraph-images": "^1.14.3",
|
||||
"astro-seo-meta": "^5.2.0",
|
||||
"katex": "^0.16.28",
|
||||
"react": "^19.2.4",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"sharp": "^0.33.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
6
docs/public/favicon.svg
Normal file
6
docs/public/favicon.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="none">
|
||||
<ellipse cx="20" cy="20" rx="15" ry="7" stroke="#f59e0b" stroke-width="1.5" transform="rotate(-20 20 20)" opacity="0.6"/>
|
||||
<ellipse cx="20" cy="20" rx="13" ry="5" stroke="#fbbf24" stroke-width="1" transform="rotate(35 20 20)" opacity="0.35"/>
|
||||
<circle cx="20" cy="20" r="4" fill="#f59e0b"/>
|
||||
<circle cx="32" cy="15" r="1.8" fill="#fbbf24"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 426 B |
99
docs/src/assets/hero-elephant-orbit.svg
Normal file
99
docs/src/assets/hero-elephant-orbit.svg
Normal file
@ -0,0 +1,99 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 600 400" fill="none">
|
||||
<defs>
|
||||
<!-- Orbital glow -->
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="3" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<filter id="starGlow">
|
||||
<feGaussianBlur stdDeviation="1.5" result="blur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="blur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
<!-- Planet gradient -->
|
||||
<radialGradient id="planetGrad" cx="45%" cy="40%">
|
||||
<stop offset="0%" stop-color="#f59e0b"/>
|
||||
<stop offset="60%" stop-color="#d97706"/>
|
||||
<stop offset="100%" stop-color="#92400e"/>
|
||||
</radialGradient>
|
||||
<!-- Elephant gradient -->
|
||||
<linearGradient id="elephantGrad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#94a3b8"/>
|
||||
<stop offset="50%" stop-color="#64748b"/>
|
||||
<stop offset="100%" stop-color="#475569"/>
|
||||
</linearGradient>
|
||||
<!-- Orbit trail gradient -->
|
||||
<linearGradient id="orbitTrail" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stop-color="#f59e0b" stop-opacity="0"/>
|
||||
<stop offset="40%" stop-color="#f59e0b" stop-opacity="0.15"/>
|
||||
<stop offset="100%" stop-color="#fbbf24" stop-opacity="0.6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background stars -->
|
||||
<circle cx="45" cy="30" r="1" fill="#fbbf24" opacity="0.4"/>
|
||||
<circle cx="120" cy="80" r="0.8" fill="#e2e8f0" opacity="0.3"/>
|
||||
<circle cx="530" cy="50" r="1.2" fill="#fbbf24" opacity="0.5"/>
|
||||
<circle cx="480" cy="340" r="0.8" fill="#e2e8f0" opacity="0.3"/>
|
||||
<circle cx="70" cy="350" r="1" fill="#e2e8f0" opacity="0.25"/>
|
||||
<circle cx="560" cy="150" r="0.7" fill="#fbbf24" opacity="0.35"/>
|
||||
<circle cx="200" cy="370" r="0.9" fill="#e2e8f0" opacity="0.2"/>
|
||||
<circle cx="400" cy="25" r="1.1" fill="#fbbf24" opacity="0.4"/>
|
||||
<circle cx="150" cy="180" r="0.6" fill="#e2e8f0" opacity="0.2"/>
|
||||
<circle cx="510" cy="250" r="0.8" fill="#fbbf24" opacity="0.3"/>
|
||||
<circle cx="30" cy="200" r="1" fill="#e2e8f0" opacity="0.15"/>
|
||||
<circle cx="350" cy="380" r="0.7" fill="#fbbf24" opacity="0.25"/>
|
||||
|
||||
<!-- Outer orbit (behind planet) - dashed, subtle -->
|
||||
<ellipse cx="300" cy="200" rx="220" ry="90" stroke="#f59e0b" stroke-width="0.8" stroke-dasharray="4 6" transform="rotate(-12 300 200)" opacity="0.15"/>
|
||||
|
||||
<!-- Primary orbit ellipse (behind planet portion) -->
|
||||
<ellipse cx="300" cy="200" rx="180" ry="75" stroke="url(#orbitTrail)" stroke-width="2" transform="rotate(-12 300 200)" opacity="0.5"/>
|
||||
|
||||
<!-- Central planet/star -->
|
||||
<circle cx="300" cy="200" r="45" fill="url(#planetGrad)" filter="url(#glow)"/>
|
||||
<!-- Planet surface detail -->
|
||||
<ellipse cx="290" cy="190" rx="20" ry="8" fill="#fbbf24" opacity="0.15" transform="rotate(-10 290 190)"/>
|
||||
<ellipse cx="310" cy="210" rx="15" ry="5" fill="#92400e" opacity="0.2" transform="rotate(5 310 210)"/>
|
||||
|
||||
<!-- Primary orbit ellipse (in front of planet) - brighter section -->
|
||||
<path d="M 135 230 A 180 75 -12 0 0 465 170" stroke="#fbbf24" stroke-width="2" transform="rotate(-12 300 200)" fill="none" opacity="0.6" stroke-linecap="round"/>
|
||||
|
||||
<!-- PostgreSQL Elephant (Slonik) - orbiting at upper-right of orbit -->
|
||||
<!-- Positioned at roughly the 2 o'clock position on the orbit -->
|
||||
<g transform="translate(438, 135) scale(0.55) rotate(15)">
|
||||
<!-- Body -->
|
||||
<ellipse cx="0" cy="10" rx="38" ry="30" fill="url(#elephantGrad)"/>
|
||||
<!-- Head -->
|
||||
<circle cx="-30" cy="-8" r="22" fill="url(#elephantGrad)"/>
|
||||
<!-- Ear -->
|
||||
<ellipse cx="-42" cy="-15" rx="14" ry="18" fill="#475569" stroke="#64748b" stroke-width="1"/>
|
||||
<ellipse cx="-42" cy="-15" rx="9" ry="12" fill="#334155" opacity="0.6"/>
|
||||
<!-- Trunk - curling forward and down -->
|
||||
<path d="M -48 0 C -58 5, -62 20, -55 30 C -50 36, -42 34, -40 28" stroke="#64748b" stroke-width="7" fill="none" stroke-linecap="round"/>
|
||||
<!-- Eye -->
|
||||
<circle cx="-24" cy="-12" r="3" fill="#0a0e17"/>
|
||||
<circle cx="-23" cy="-13" r="1" fill="#fbbf24" opacity="0.8"/>
|
||||
<!-- Tusk -->
|
||||
<path d="M -38 4 C -42 12, -38 18, -34 16" stroke="#e2e8f0" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Legs (2 visible) -->
|
||||
<rect x="-12" y="30" width="8" height="20" rx="4" fill="#475569"/>
|
||||
<rect x="12" y="30" width="8" height="20" rx="4" fill="#475569"/>
|
||||
<!-- Tail -->
|
||||
<path d="M 36 5 C 45 0, 48 8, 44 14" stroke="#64748b" stroke-width="2.5" fill="none" stroke-linecap="round"/>
|
||||
<!-- Orbital velocity trail from elephant -->
|
||||
<path d="M 40 10 C 55 8, 70 12, 85 18" stroke="#fbbf24" stroke-width="1.5" fill="none" opacity="0.4" stroke-linecap="round"/>
|
||||
<path d="M 40 15 C 52 14, 62 18, 72 24" stroke="#fbbf24" stroke-width="1" fill="none" opacity="0.25" stroke-linecap="round"/>
|
||||
</g>
|
||||
|
||||
<!-- Small satellite dot (on opposite side of orbit) -->
|
||||
<circle cx="165" cy="258" r="3" fill="#fbbf24" filter="url(#starGlow)" opacity="0.7"/>
|
||||
|
||||
<!-- Inner orbit ring (like Saturn's ring on the planet) -->
|
||||
<ellipse cx="300" cy="200" rx="60" ry="12" stroke="#fbbf24" stroke-width="1" transform="rotate(-5 300 200)" opacity="0.25"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
12
docs/src/assets/pg-orrery-logo.svg
Normal file
12
docs/src/assets/pg-orrery-logo.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 220 40" fill="none">
|
||||
<!-- Orbital ellipse -->
|
||||
<ellipse cx="20" cy="20" rx="15" ry="7" stroke="#f59e0b" stroke-width="1.5" transform="rotate(-20 20 20)" opacity="0.6"/>
|
||||
<!-- Second orbit (inclined) -->
|
||||
<ellipse cx="20" cy="20" rx="13" ry="5" stroke="#fbbf24" stroke-width="1" transform="rotate(35 20 20)" opacity="0.35"/>
|
||||
<!-- Central body -->
|
||||
<circle cx="20" cy="20" r="4" fill="#f59e0b"/>
|
||||
<!-- Satellite dot on orbit -->
|
||||
<circle cx="32" cy="15" r="1.8" fill="#fbbf24"/>
|
||||
<!-- "pg_orrery" text -->
|
||||
<text x="44" y="27" font-family="Inter, system-ui, sans-serif" font-size="22" font-weight="700" fill="#e2e8f0" letter-spacing="0.02em">pg_orrery</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 729 B |
25
docs/src/components/Head.astro
Normal file
25
docs/src/components/Head.astro
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
/**
|
||||
* Extends Starlight's built-in Head with OG image and Twitter card tags.
|
||||
*
|
||||
* Starlight already generates: og:title, og:type, og:url, og:locale,
|
||||
* og:description, og:site_name, twitter:card, meta description.
|
||||
*
|
||||
* We add: og:image, twitter:image, twitter:title, twitter:description.
|
||||
*/
|
||||
import Default from "@astrojs/starlight/components/Head.astro";
|
||||
import { getImagePath } from "astro-opengraph-images";
|
||||
|
||||
const route = Astro.locals.starlightRoute;
|
||||
const title = route?.entry?.data?.title ?? "pg_orrery";
|
||||
const description =
|
||||
route?.entry?.data?.description ||
|
||||
"It's not rocket science. A database orrery — celestial mechanics for PostgreSQL.";
|
||||
const ogImageUrl = getImagePath({ url: Astro.url, site: Astro.site });
|
||||
---
|
||||
|
||||
<Default {...Astro.props}><slot /></Default>
|
||||
<meta property="og:image" content={ogImageUrl} />
|
||||
<meta name="twitter:image" content={ogImageUrl} />
|
||||
<meta name="twitter:title" content={title} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
6
docs/src/content.config.ts
Normal file
6
docs/src/content.config.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { defineCollection } from "astro:content";
|
||||
import { docsSchema } from "@astrojs/starlight/schema";
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ schema: docsSchema() }),
|
||||
};
|
||||
176
docs/src/content/docs/architecture/constant-chain-of-custody.mdx
Normal file
176
docs/src/content/docs/architecture/constant-chain-of-custody.mdx
Normal file
@ -0,0 +1,176 @@
|
||||
---
|
||||
title: Constant Chain of Custody
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
This is the single most critical design constraint in pg_orrery. Get it wrong and positions silently drift by kilometers. There is no runtime check that can detect this class of error after the fact.
|
||||
|
||||
## The problem
|
||||
|
||||
Two-Line Elements are not raw orbital measurements. They are *mean* elements produced by a differential correction process that fits observed positions against an SGP4 propagator running with a specific set of geopotential constants --- WGS-72. The mean elements absorb geodetic model biases: the eccentricity, inclination, and mean motion encode corrections that only make physical sense when propagated with the same constants used to generate them.
|
||||
|
||||
Substituting WGS-84 constants into the propagator does not "upgrade" accuracy. It breaks the internal consistency of the element set. The resulting position error can exceed the natural prediction error of the TLE by an order of magnitude.
|
||||
|
||||
<Aside type="danger" title="This is not theoretical">
|
||||
The WGS-72 equatorial radius is 6378.135 km. The WGS-84 equatorial radius is 6378.137 km. The 2-meter difference looks negligible, but it enters every altitude computation, every semi-major axis derivation, and every GiST index key. Mixing the two constants corrupts results in ways that pass casual inspection but fail against reference implementations.
|
||||
</Aside>
|
||||
|
||||
## The rules
|
||||
|
||||
Four rules govern constant usage across the entire codebase. No exceptions.
|
||||
|
||||
### Rule 1: WGS-72 for SGP4/SDP4 propagation
|
||||
|
||||
All propagation uses WGS-72 constants: $\mu$, $a_e$, $J_2$, $J_3$, $J_4$, $k_e$. These flow through the vendored `src/sgp4/norad_in.h` defines and are never overridden. The functions `SGP4_init()`, `SGP4()`, `SDP4_init()`, and `SDP4()` operate entirely in the WGS-72 domain.
|
||||
|
||||
### Rule 2: WGS-84 for coordinate output
|
||||
|
||||
Geodetic latitude, longitude, and altitude use the WGS-84 ellipsoid ($a = 6378.137$ km, $f = 1/298.257223563$). This is the modern standard for ground-station positioning, GPS receivers, and mapping services. The conversion from ECEF to geodetic in `coord_funcs.c:ecef_to_geodetic()` uses WGS-84.
|
||||
|
||||
### Rule 3: Reduced TEME nutation
|
||||
|
||||
The SGP4 output frame (True Equator, Mean Equinox) uses only 4 of the 106 IAU-80 nutation terms. Applying the full nutation model would "correct" for effects that SGP4 already accounts for internally, introducing error rather than removing it.
|
||||
|
||||
### Rule 4: No other combination is valid
|
||||
|
||||
WGS-72 for propagation, WGS-84 for output. Perigee and apogee altitudes use WGS-72 because they derive from mean elements. Geodetic altitude uses WGS-84 because it converts a physical position. There is no scenario where mixing these is correct.
|
||||
|
||||
## Constant inventory
|
||||
|
||||
The complete set of constants, with provenance and location in both pg_orrery and the vendored SGP4 code.
|
||||
|
||||
### WGS-72 constants (propagation domain)
|
||||
|
||||
Source: Hoots & Roehrich, "Models for Propagation of NORAD Element Sets," Spacetrack Report No. 3, 1980.
|
||||
|
||||
| Constant | Symbol | Value | `types.h` | `norad_in.h` |
|
||||
|----------|--------|-------|-----------|--------------|
|
||||
| Gravitational parameter | $\mu$ | $398600.8\ \text{km}^3/\text{s}^2$ | `WGS72_MU` | (implicit in $k_e$) |
|
||||
| Equatorial radius | $a_e$ | $6378.135\ \text{km}$ | `WGS72_AE` | `earth_radius_in_km` |
|
||||
| Zonal harmonic $J_2$ | $J_2$ | $0.001082616$ | `WGS72_J2` | `xj2` |
|
||||
| Zonal harmonic $J_3$ | $J_3$ | $-2.53881 \times 10^{-6}$ | `WGS72_J3` | `xj3` |
|
||||
| Zonal harmonic $J_4$ | $J_4$ | $-1.65597 \times 10^{-6}$ | `WGS72_J4` | `xj4` |
|
||||
| Derived rate constant | $k_e$ | $0.0743669161\ \text{min}^{-1}$ | `WGS72_KE` | `xke` |
|
||||
|
||||
The rate constant $k_e$ is derived from $\mu$ and $a_e$:
|
||||
|
||||
$$
|
||||
k_e = \frac{\sqrt{\mu} \times 60}{a_e^{3/2}}
|
||||
$$
|
||||
|
||||
The factor of 60 converts from seconds to minutes, matching the SGP4 convention of radians per minute for mean motion.
|
||||
|
||||
### WGS-84 constants (output domain)
|
||||
|
||||
Source: NIMA TR8350.2, "Department of Defense World Geodetic System 1984."
|
||||
|
||||
| Constant | Symbol | Value | `types.h` |
|
||||
|----------|--------|-------|-----------|
|
||||
| Equatorial radius | $a$ | $6378.137\ \text{km}$ | `WGS84_A` |
|
||||
| Flattening | $f$ | $1/298.257223563$ | `WGS84_F` |
|
||||
| Eccentricity squared | $e^2$ | $f(2 - f)$ | `WGS84_E2` |
|
||||
|
||||
## Why two copies of AE?
|
||||
|
||||
`types.h` carries a parallel copy of the WGS-72 constants even though the vendored SGP4 code defines them in `norad_in.h`. This is intentional.
|
||||
|
||||
`types.h` is the single header for all pg_orrery C sources. `norad_in.h` is an internal SGP4 header in `src/sgp4/` not meant for external consumers. The GiST index (`gist_tle.c`) and TLE accessor functions (`tle_type.c`) need $k_e$ and $a_e$ without pulling in sat_code internals. The values **must** be identical.
|
||||
|
||||
The perigee and apogee altitude computations derive from mean elements:
|
||||
|
||||
$$
|
||||
a_{er} = \left(\frac{k_e}{n}\right)^{2/3} \quad \text{[earth radii]}
|
||||
$$
|
||||
|
||||
$$
|
||||
\text{perigee}_\text{km} = a_{er} \cdot (1 - e) \cdot a_e - a_e
|
||||
$$
|
||||
|
||||
$$
|
||||
\text{apogee}_\text{km} = a_{er} \cdot (1 + e) \cdot a_e - a_e
|
||||
$$
|
||||
|
||||
These **must** use WGS-72 $a_e$ (6378.135 km), not WGS-84 (6378.137 km), because $n$ is a mean motion fitted against the WGS-72 geopotential. Using the wrong radius shifts every altitude by 2 meters. The error compounds in GiST index operations where altitude-band overlap determines whether two orbits are candidates for conjunction screening.
|
||||
|
||||
## Where the boundary lives
|
||||
|
||||
The WGS-72/WGS-84 boundary is crossed in exactly two places in the codebase:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="TEME to Geodetic">
|
||||
`coord_funcs.c:ecef_to_geodetic()` converts ECEF Cartesian coordinates (derived from WGS-72 propagation through GMST rotation) to geodetic latitude, longitude, and altitude on the WGS-84 ellipsoid. This is the correct boundary --- the ECEF position is a physical location in space, and WGS-84 is the standard for expressing that location as geodetic coordinates.
|
||||
</TabItem>
|
||||
<TabItem label="Observer to ECEF">
|
||||
`coord_funcs.c:observer_to_ecef()` converts a ground station's geodetic coordinates (on WGS-84, as entered by the user) to ECEF Cartesian for the topocentric transform. The observer's position is a real-world location defined in WGS-84; converting it to ECEF puts it in the same Cartesian frame as the satellite.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Everything upstream of these functions operates in the WGS-72 domain. Everything downstream operates on physical positions that have already been converted. The boundary is narrow, explicit, and documented in the source comments.
|
||||
|
||||
## The GMST question
|
||||
|
||||
The GMST computation uses the IAU 1982 formula (Vallado Eq. 3-47):
|
||||
|
||||
$$
|
||||
\text{GMST} = 67310.54841 + (876600 \times 3600 + 8640184.812866) \cdot T_{UT1} + 0.093104 \cdot T_{UT1}^2 - 6.2 \times 10^{-6} \cdot T_{UT1}^3
|
||||
$$
|
||||
|
||||
where $T_{UT1} = (JD - 2451545.0) / 36525.0$, and the result is in seconds of time, converted to radians by multiplying by $\pi / 43200$ and normalized to $[0, 2\pi)$.
|
||||
|
||||
pg_orrery deliberately does **not** use a higher-precision GMST model (e.g., IAU 2000A). The SGP4 output is only accurate to the precision of its own GMST model. Applying a more precise rotation would not improve the final position and could introduce a systematic offset between the propagated TEME position and the Earth-fixed frame.
|
||||
|
||||
This is the constant chain of custody in action: match the precision of the input, not the precision available in the literature.
|
||||
|
||||
## Rules 6-8: DE Ephemeris Pipeline (v0.3.0)
|
||||
|
||||
The v0.3.0 DE integration adds three rules to the chain of custody.
|
||||
|
||||
### Rule 6: ICRS-to-ecliptic frame rotation at the provider boundary
|
||||
|
||||
DE ephemerides return positions in the ICRS equatorial frame. The observation pipeline expects ecliptic J2000. The conversion uses `equatorial_to_ecliptic()` from `astro_math.h`, applied in `eph_provider.c` before the position enters the shared observation pipeline. This rotation is applied to both the target body and Earth.
|
||||
|
||||
### Rule 7: Same-provider consistency
|
||||
|
||||
Both the target body and Earth must come from the same provider in any geocentric computation. If DE succeeds for the target but fails for Earth (or vice versa), the entire computation falls back to VSOP87. This prevents mixing DE Mars with VSOP87 Earth, which would introduce a frame-dependent systematic error at the arcsecond level.
|
||||
|
||||
```c
|
||||
/* From de_funcs.c: both positions or neither */
|
||||
if (eph_de_planet(body_id, jd, target_xyz) &&
|
||||
eph_de_earth(jd, earth_xyz))
|
||||
{
|
||||
/* Use DE positions for both */
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Fall back to VSOP87 for both */
|
||||
}
|
||||
```
|
||||
|
||||
### Rule 8: AU consistency verification
|
||||
|
||||
The DE header contains an AU value (in km). At init time, `eph_provider.c` verifies this matches pg_orrery's compiled-in `AU_KM` constant (149597870.7 km, IAU 2012). A mismatch would corrupt every distance calculation. If they disagree, the DE file is rejected and fallback to VSOP87 activates with a log message.
|
||||
|
||||
<Aside type="note" title="For maintainers">
|
||||
If you are modifying `eph_provider.c` or `de_funcs.c`, remember that Rule 7 is the critical invariant. Never return a DE position for one body and a VSOP87 position for another within the same geocentric computation. The conditional must gate both positions atomically.
|
||||
</Aside>
|
||||
|
||||
## Verification
|
||||
|
||||
The chain of custody is verified through the Vallado 518 test vectors --- 518 reference propagations across a range of orbit types (LEO, MEO, GEO, deep-space, high-eccentricity). Every test vector must match to machine epsilon before any other development proceeds.
|
||||
|
||||
If a code change causes even one vector to drift, the constant chain has been broken somewhere. The test suite is the enforcement mechanism for the design constraint.
|
||||
|
||||
```sql
|
||||
-- Verify against Vallado test vector (ISS-like orbit)
|
||||
-- Expected: position match within 1e-8 km
|
||||
SELECT eci_x(sgp4_propagate(tle, epoch + interval '720 minutes'))
|
||||
FROM vallado_test_vectors
|
||||
WHERE norad_id = 25544;
|
||||
```
|
||||
|
||||
<Aside type="note" title="For maintainers">
|
||||
If you are modifying any function in `tle_type.c`, `gist_tle.c`, or `coord_funcs.c`, check which constant set you are using. If you find yourself reaching for `WGS84_A` in a function that computes from mean elements, stop. You are about to break the chain.
|
||||
</Aside>
|
||||
187
docs/src/content/docs/architecture/design-principles.mdx
Normal file
187
docs/src/content/docs/architecture/design-principles.mdx
Normal file
@ -0,0 +1,187 @@
|
||||
---
|
||||
title: Design Principles
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem, Steps } from "@astrojs/starlight/components";
|
||||
|
||||
pg_orrery is engineering software that computes physical quantities. A wrong answer delivered confidently is worse than no answer at all. The design principles that govern the extension trace directly to Margaret Hamilton's work on the Apollo guidance computer --- software that could not afford to be approximately correct.
|
||||
|
||||
These principles are not aspirational. They are enforced structurally in the code.
|
||||
|
||||
## Development before the fact
|
||||
|
||||
Hamilton's most fundamental principle: design the system correctly from the start, rather than patching it after deployment. In pg_orrery, this manifests as the **constant chain of custody** --- the strict separation between WGS-72 constants (used for SGP4 propagation) and WGS-84 constants (used for coordinate output).
|
||||
|
||||
This separation was not bolted on after a bug was found. It was the first architectural decision, made before any code was written. The `types.h` header carries both constant sets with explicit comments about which functions may use which set.
|
||||
|
||||
```c
|
||||
/* WGS-72 constants (for SGP4 propagation ONLY) */
|
||||
#define WGS72_MU 398600.8 /* km^3/s^2 */
|
||||
#define WGS72_AE 6378.135 /* km */
|
||||
|
||||
/* WGS-84 constants (for coordinate output ONLY) */
|
||||
#define WGS84_A 6378.137 /* km */
|
||||
#define WGS84_F (1.0 / 298.257223563)
|
||||
```
|
||||
|
||||
The 2-meter difference between WGS-72 and WGS-84 equatorial radii looks insignificant. It compounds through index operations, altitude computations, and conjunction screening. Getting this wrong would not produce a crash --- it would produce subtly wrong results that pass every test except comparison with an independent reference implementation.
|
||||
|
||||
See [Constant Chain of Custody](/architecture/constant-chain-of-custody/) for the full treatment.
|
||||
|
||||
## Error detection by design
|
||||
|
||||
The Apollo guidance computer did not wait for failures to announce themselves. It classified errors by severity and responded proportionally. pg_orrery follows the same pattern across three mechanisms.
|
||||
|
||||
### The `_safe()` function variants
|
||||
|
||||
Every propagation function that can fail has a `_safe()` variant that returns `NULL` instead of raising a PostgreSQL `ERROR`. This lets callers handle failure in SQL without `BEGIN/EXCEPTION` blocks:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Standard (aborts on error)">
|
||||
```sql
|
||||
-- Raises ERROR if TLE has decayed past validity
|
||||
SELECT sgp4_propagate(tle, now())
|
||||
FROM catalog;
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Safe (returns NULL)">
|
||||
```sql
|
||||
-- Returns NULL for invalid propagations, continues scan
|
||||
SELECT sgp4_propagate_safe(tle, now())
|
||||
FROM catalog
|
||||
WHERE sgp4_propagate_safe(tle, now()) IS NOT NULL;
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### SGP4 error classification
|
||||
|
||||
The vendored SGP4/SDP4 library returns six distinct error codes. pg_orrery classifies them into two categories based on physical meaning:
|
||||
|
||||
| Code | Meaning | Severity | Response |
|
||||
|------|---------|----------|----------|
|
||||
| -1 | Nearly parabolic orbit ($e \geq 1$) | Fatal | `ereport(ERROR)` |
|
||||
| -2 | Negative semi-major axis (decayed) | Fatal | `ereport(ERROR)` |
|
||||
| -3 | Orbit within Earth | Warning | `ereport(NOTICE)`, return result |
|
||||
| -4 | Perigee within Earth | Warning | `ereport(NOTICE)`, return result |
|
||||
| -5 | Negative mean motion | Fatal | `ereport(ERROR)` |
|
||||
| -6 | Kepler equation diverged | Fatal | `ereport(ERROR)` |
|
||||
|
||||
The distinction between warnings and errors is physical, not numerical. A satellite with perigee below Earth's surface is plausible during reentry --- the state vector is still mathematically valid. A negative semi-major axis means the model has broken down entirely.
|
||||
|
||||
### Input validation at storage time
|
||||
|
||||
TLE parsing errors are caught in `tle_in()`, not during propagation. Invalid TLEs never enter the database. A marginal TLE might parse correctly but fail during propagator initialization --- that failure surfaces at query time with a clear error message.
|
||||
|
||||
## Priority-driven execution
|
||||
|
||||
The Apollo computer had a priority scheduler that shed low-priority tasks under overload rather than crashing. pg_orrery applies a similar principle in pass prediction: **failures degrade gracefully instead of aborting the scan**.
|
||||
|
||||
When `elevation_at_jd()` encounters a propagation error during the coarse scan, it returns $-\pi$ radians --- well below any physical horizon elevation. The scan treats this as "satellite below horizon" and continues searching.
|
||||
|
||||
```c
|
||||
static double
|
||||
elevation_at_jd(/* ... */)
|
||||
{
|
||||
int err = propagate_tle(&sat, tsince, pos, vel);
|
||||
if (err < -2) /* hard errors: treat as below horizon */
|
||||
return -M_PI;
|
||||
/* ... compute actual elevation ... */
|
||||
}
|
||||
```
|
||||
|
||||
This matters because a TLE might be valid for the first three days of a seven-day search window and then decay past model validity. The pass finder should return the three days of valid passes, not abort the entire query.
|
||||
|
||||
## Ultra-reliable software
|
||||
|
||||
Hamilton defined ultra-reliable software as software that behaves correctly under all possible input combinations, including combinations the designer did not anticipate. pg_orrery achieves this through four structural guarantees.
|
||||
|
||||
### Zero global mutable state
|
||||
|
||||
For v0.1.0/v0.2.0 functions, there are no file-scope variables, no static locals, no caches. Every function computes from its arguments alone. The v0.3.0 DE ephemeris layer introduces per-backend static state (a file descriptor and coefficient cache in `eph_provider.c`), but each backend gets its own copy after `fork()` --- no shared state between processes. All 68 pg_orrery functions carry the `PARALLEL SAFE` declaration, meaning the query planner can distribute work across multiple CPU cores without coordination.
|
||||
|
||||
### Fixed-size types
|
||||
|
||||
All seven pg_orrery types use `STORAGE = plain` and fixed `INTERNALLENGTH`. No TOAST, no detoasting, no variable-length headers. The `tle` type is exactly 112 bytes. Direct pointer access via `PG_GETARG_POINTER(n)` --- no copies, no allocations on read.
|
||||
|
||||
### Deterministic memory
|
||||
|
||||
All heap allocation goes through `palloc()`/`pfree()`. No `malloc()`, no `new`, no static buffers. PostgreSQL's memory context system owns every byte, and frees it automatically when the query completes.
|
||||
|
||||
### Reproducible computation
|
||||
|
||||
Given the same TLE and timestamp, pg_orrery produces the same result on every platform, every time. No floating-point non-determinism from threading, no stale caches, no accumulated state from previous calls.
|
||||
|
||||
## Software engineering as discipline
|
||||
|
||||
Hamilton insisted that software engineering was a real engineering discipline, not an ad hoc craft. For pg_orrery, this means every equation in the codebase traces to a published, peer-reviewed source.
|
||||
|
||||
The [Theory-to-Code Mapping](/architecture/theory-to-code/) page provides the complete table. A sample:
|
||||
|
||||
| Equation | Source | Code |
|
||||
|----------|--------|------|
|
||||
| SGP4/SDP4 propagation | Hoots & Roehrich, STR#3 (1980) | `src/sgp4/sgp4.c`, `sdp4.c` |
|
||||
| VSOP87 planetary positions | Bretagnon & Francou (1988) | `src/vsop87.c` |
|
||||
| GMST computation | Vallado (2013) Eq. 3-47 | `src/coord_funcs.c:gmst_from_jd()` |
|
||||
| Lambert solver | Izzo (2015) | `src/lambert.c` |
|
||||
| Precession J2000 to date | Lieske et al. (1977) | `src/precession.c` |
|
||||
|
||||
Every constant has a provenance. Every algorithm has a citation. If a future maintainer needs to understand why `0.40909280422232897` appears in `types.h`, the comment says "23.4392911 degrees in radians" and the design document traces it to the IAU value for the obliquity of the ecliptic at J2000.
|
||||
|
||||
## Systems thinking
|
||||
|
||||
Hamilton's approach to the Apollo software was holistic --- she understood that modifying one subsystem could cascade through the entire stack. pg_orrery embodies this through the **observation pipeline**, a seven-stage flow from heliocentric coordinates to topocentric azimuth and elevation.
|
||||
|
||||
<Steps>
|
||||
1. VSOP87 heliocentric ecliptic J2000 position for the target body (AU)
|
||||
2. VSOP87 heliocentric ecliptic J2000 position for Earth
|
||||
3. Geocentric ecliptic = target minus Earth
|
||||
4. Ecliptic-to-equatorial rotation by J2000 obliquity ($23.4392911\degree$)
|
||||
5. IAU 1976 precession from J2000 to the date of observation
|
||||
6. GMST for sidereal time (Vallado Eq. 3-47, IAU 1982)
|
||||
7. Equatorial-to-horizontal transform for the observer's latitude and longitude
|
||||
</Steps>
|
||||
|
||||
You cannot modify stage 4 without understanding what stage 3 produces and what stage 5 expects. You cannot swap the GMST model without understanding that the SGP4 output is only accurate to the precision of its own internal GMST --- applying a higher-precision rotation would not improve accuracy and could introduce systematic offsets.
|
||||
|
||||
See [Observation Pipeline](/architecture/observation-pipeline/) for the full flow with equations.
|
||||
|
||||
## The "Lauren Bug"
|
||||
|
||||
<Aside type="tip" title="The name">
|
||||
Hamilton named this class of error after her daughter Lauren, who as a young child pressed unexpected key sequences on the Apollo simulator and crashed it. The lesson: if a child can trigger it, an astronaut under stress certainly will. Design for the input you did not expect.
|
||||
</Aside>
|
||||
|
||||
pg_orrery defends against three categories of unexpected input that would silently produce wrong results in a naive implementation.
|
||||
|
||||
### Same-body Lambert transfer
|
||||
|
||||
What happens when someone computes a transfer from Earth to Earth?
|
||||
|
||||
```sql
|
||||
SELECT * FROM lambert_transfer(3, 3, '2028-01-01', '2028-06-01');
|
||||
```
|
||||
|
||||
The departure and arrival positions are the same body at different times. The Lambert solver would converge on a trivial solution that does not represent a physical transfer orbit. pg_orrery validates `dep_body_id != arr_body_id` and returns an error before invoking the solver.
|
||||
|
||||
### Arrival before departure
|
||||
|
||||
```sql
|
||||
SELECT * FROM lambert_transfer(3, 4, '2029-06-15', '2028-10-01');
|
||||
```
|
||||
|
||||
A negative time of flight. The Lambert solver might converge on a mathematically valid but physically meaningless retrograde solution. pg_orrery checks `arr_time > dep_time` and returns an error.
|
||||
|
||||
### Observer on the observed body
|
||||
|
||||
When computing the topocentric observation of Earth (body ID 3), the geocentric vector is zero --- the observer is on the body being observed. Division by zero in the range computation. pg_orrery catches this case and returns a clear error rather than NaN or infinity propagating through the rest of the pipeline.
|
||||
|
||||
These are not edge cases in the traditional sense. They are the inputs that a SQL user will inevitably produce when exploring the system with ad hoc queries, and they must produce clear errors rather than silently wrong results.
|
||||
|
||||
### DE ephemeris file dependency (v0.3.0)
|
||||
|
||||
The v0.3.0 DE integration introduces five new failure domains --- external file dependency, per-backend mutable state, OS-level file descriptor management, frame boundary crossings, and volatility contract management --- into a system that previously had none.
|
||||
|
||||
Each is handled by the principle of graceful degradation: DE functions fall back to VSOP87 on any DE-specific failure. The existing VSOP87 pipeline is the bulkhead --- always compiled in, always works, always IMMUTABLE. See the [DE Ephemeris guide](/guides/de-ephemeris/) for details on the fallback strategy.
|
||||
190
docs/src/content/docs/architecture/memory-thread-safety.mdx
Normal file
190
docs/src/content/docs/architecture/memory-thread-safety.mdx
Normal file
@ -0,0 +1,190 @@
|
||||
---
|
||||
title: Memory & Thread Safety
|
||||
sidebar:
|
||||
order: 5
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
PostgreSQL extensions run inside a shared-memory, multi-process server. A function that leaks memory degrades the entire backend. A function that uses global state cannot be parallelized. pg_orrery is designed to be a well-behaved citizen: all memory goes through PostgreSQL's allocator, and no mutable state survives between function calls.
|
||||
|
||||
## Allocation strategy
|
||||
|
||||
All heap allocation goes through `palloc()` / `pfree()`. No `malloc()`, no `new`, no static buffers. This is not a convention --- it is a hard requirement. PostgreSQL's memory context system tracks every allocation and frees entire contexts at transaction boundaries, query completion, or error recovery. Using `malloc()` would create memory that PostgreSQL cannot reclaim on error, leading to gradual backend bloat.
|
||||
|
||||
### Single-shot propagation
|
||||
|
||||
Functions like `sgp4_propagate()` and `tle_distance()` follow the simplest pattern:
|
||||
|
||||
```c
|
||||
double *params = palloc(sizeof(double) * N_SAT_PARAMS);
|
||||
|
||||
/* Initialize and propagate */
|
||||
SGP4_init(params, &sat);
|
||||
err = SGP4(tsince, &sat, params, pos, vel);
|
||||
|
||||
pfree(params);
|
||||
```
|
||||
|
||||
The `params` array (~92 doubles, ~736 bytes) lives in the current memory context. It is allocated before propagation and freed before the function returns. If an `ereport(ERROR)` fires between `palloc` and `pfree`, PostgreSQL's error recovery frees the current context automatically.
|
||||
|
||||
### Set-returning functions
|
||||
|
||||
SRF functions like `sgp4_propagate_series()`, `ground_track()`, and `predict_passes()` must maintain state across multiple calls. They use PostgreSQL's `multi_call_memory_ctx`:
|
||||
|
||||
```c
|
||||
typedef struct {
|
||||
tle_t sat;
|
||||
double params[N_SAT_PARAMS]; /* embedded, not separate allocation */
|
||||
int is_deep;
|
||||
double epoch_jd;
|
||||
int64 start_ts;
|
||||
int64 step_usec;
|
||||
} propagate_series_ctx;
|
||||
```
|
||||
|
||||
<Aside type="note" title="Embedded arrays">
|
||||
The `params` array is embedded directly in the context struct, not allocated separately. This puts everything in a single `palloc` call --- fewer allocations, better cache locality during the per-row propagation loop, and simpler cleanup.
|
||||
</Aside>
|
||||
|
||||
The lifecycle:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="First call">
|
||||
```c
|
||||
funcctx = SRF_FIRSTCALL_INIT();
|
||||
oldctx = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
|
||||
|
||||
ctx = palloc(sizeof(propagate_series_ctx));
|
||||
/* Copy TLE and observer into ctx */
|
||||
/* Initialize propagator */
|
||||
|
||||
MemoryContextSwitchTo(oldctx);
|
||||
funcctx->user_fctx = ctx;
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Subsequent calls">
|
||||
```c
|
||||
funcctx = SRF_PERCALL_SETUP();
|
||||
ctx = funcctx->user_fctx;
|
||||
|
||||
/* Propagate to next timestep using ctx->params */
|
||||
/* Return result or SRF_RETURN_DONE */
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Cleanup">
|
||||
PostgreSQL frees `multi_call_memory_ctx` automatically when the SRF completes (either by returning `SRF_RETURN_DONE` or via error recovery). No explicit cleanup code needed.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Type I/O functions
|
||||
|
||||
Input functions (`tle_in`, `eci_in`, etc.) allocate the result struct with `palloc()` in the current context. PostgreSQL manages the lifecycle --- the struct may be copied into a tuple for storage or used transiently for a computation.
|
||||
|
||||
```c
|
||||
pg_tle *result = (pg_tle *) palloc(sizeof(pg_tle));
|
||||
/* Parse input text into result */
|
||||
PG_RETURN_POINTER(result);
|
||||
```
|
||||
|
||||
## Minimal global mutable state
|
||||
|
||||
The v0.1.0/v0.2.0 functions have zero global mutable state --- no file-scope variables, no static locals, no caches. Every function computes from its arguments alone.
|
||||
|
||||
The v0.3.0 DE ephemeris layer introduces a controlled exception: per-backend static variables in `eph_provider.c` (`de_handle_ptr`, `de_init_attempted`, `de_init_success`). These are safe because each PostgreSQL backend (including parallel workers) gets its own copy after `fork()` --- no shared state between processes. The handle is opened lazily on first `_de()` call and cleaned up via `on_proc_exit()`. See the [DE Ephemeris guide](/guides/de-ephemeris/) for details.
|
||||
|
||||
This guarantee has three consequences:
|
||||
|
||||
### PARALLEL SAFE
|
||||
|
||||
All 68 pg_orrery functions are declared `PARALLEL SAFE` in the SQL definition. This tells PostgreSQL's query planner that the function can be executed in parallel worker processes without coordination. For bulk operations like propagating 12,000 TLEs, the planner can distribute work across multiple CPU cores:
|
||||
|
||||
```sql
|
||||
-- PostgreSQL may parallelize this across available cores
|
||||
SELECT tle_norad_id(tle),
|
||||
eci_x(sgp4_propagate(tle, now())) AS x_km
|
||||
FROM satellite_catalog;
|
||||
```
|
||||
|
||||
If any function used global state --- even a read-only cache --- PostgreSQL would need to serialize access or copy state between workers. `PARALLEL SAFE` cannot be declared for functions with global mutable state; doing so risks data races and incorrect results.
|
||||
|
||||
### No cross-session contamination
|
||||
|
||||
PostgreSQL backends are long-lived processes that serve multiple sessions. A global variable written by session A persists when session B runs in the same backend. pg_orrery avoids this entirely --- no function call leaves any trace in the process state.
|
||||
|
||||
### Deterministic computation
|
||||
|
||||
Given the same TLE and timestamp, pg_orrery produces the same result regardless of what queries ran before, how many backends are active, or whether the function is running in a parallel worker. There is no path-dependent behavior.
|
||||
|
||||
## SGP4/SDP4 memory model
|
||||
|
||||
The vendored SGP4/SDP4 code has no global mutable state. The propagator state lives entirely in two caller-provided structures:
|
||||
|
||||
| Structure | Size | Contains | Owner |
|
||||
|-----------|------|----------|-------|
|
||||
| `tle_t` | ~200 bytes | Parsed mean elements, identification | Caller (pg_orrery copies from `pg_tle`) |
|
||||
| `params[N_SAT_PARAMS]` | ~736 bytes | Initialized propagator coefficients | Caller (pg_orrery `palloc`s this) |
|
||||
|
||||
The `SGP4_init()` / `SDP4_init()` functions write into the `params` array. The `SGP4()` / `SDP4()` functions read from `params` and `tle_t`, and write position/velocity into caller-provided arrays. No internal state is retained between calls.
|
||||
|
||||
This maps cleanly to PostgreSQL's per-call execution model. There is no object lifecycle to manage, no destructor to call, no persistent state to synchronize.
|
||||
|
||||
## Fixed-size types
|
||||
|
||||
All seven pg_orrery types are fixed-size with `STORAGE = plain`:
|
||||
|
||||
| Type | Size | `ALIGNMENT` | TOAST? |
|
||||
|------|------|-------------|--------|
|
||||
| `tle` | 112 bytes | `double` | No |
|
||||
| `eci_position` | 48 bytes | `double` | No |
|
||||
| `geodetic` | 24 bytes | `double` | No |
|
||||
| `topocentric` | 32 bytes | `double` | No |
|
||||
| `observer` | 24 bytes | `double` | No |
|
||||
| `pass_event` | 48 bytes | `double` | No |
|
||||
| `heliocentric` | 24 bytes | `double` | No |
|
||||
|
||||
### Why fixed-size matters
|
||||
|
||||
**No TOAST overhead.** Variable-length types (varlena) carry a 4-byte header and may be compressed or moved to a secondary TOAST table. Reading a TOASTed value requires a separate heap fetch. Fixed-size types are stored inline in the tuple --- one pointer dereference, no detoasting.
|
||||
|
||||
**Direct pointer access.** `PG_GETARG_POINTER(n)` returns a pointer directly into the tuple data. No copy, no allocation. The function reads the struct in place.
|
||||
|
||||
**Predictable memory layout.** All types use `ALIGNMENT = double` because every struct contains `double` fields. This satisfies the strictest alignment requirement without platform-specific conditionals.
|
||||
|
||||
**Binary I/O.** The `tle_recv()` / `tle_send()` functions implement binary protocol support. The fixed layout means binary transfer is a straight memory copy --- no serialization logic, no endianness concerns beyond what PostgreSQL's binary protocol handles.
|
||||
|
||||
### TLE: 112 bytes vs raw text
|
||||
|
||||
The TLE text format is 138+ bytes (two 69-character lines plus separator). The parsed struct is 112 bytes --- smaller than the text it came from, and it eliminates the ~10x parsing overhead that would be incurred on every propagation call if raw text were stored.
|
||||
|
||||
The text representation can be reconstructed from the parsed elements via the vendored `write_elements_in_tle_format()`. The round-trip is lossless for all fields that affect propagation.
|
||||
|
||||
## Memory usage in practice
|
||||
|
||||
For a typical catalog query propagating 12,000 TLEs:
|
||||
|
||||
| Resource | Per-call | Peak (12K TLEs) |
|
||||
|----------|----------|-----------------|
|
||||
| `params` array | 736 bytes | 736 bytes (reused) |
|
||||
| `tle_t` conversion | 200 bytes (stack) | 200 bytes |
|
||||
| Result `pg_eci` | 48 bytes | 48 bytes (returned, then freed) |
|
||||
| **Total transient** | **~1 KB** | **~1 KB** |
|
||||
|
||||
The 736-byte `params` array is the largest per-call allocation. It is freed before the function returns. At no point does pg_orrery hold allocations proportional to the number of rows being processed --- each row is computed and returned independently.
|
||||
|
||||
<Aside type="caution" title="SRF exception">
|
||||
Set-returning functions hold their context struct for the lifetime of the SRF call. For `predict_passes()` over a 7-day window, this is ~1 KB for the duration of the scan. The context is freed when the SRF completes.
|
||||
</Aside>
|
||||
|
||||
## Error recovery
|
||||
|
||||
When `ereport(ERROR)` fires inside a pg_orrery function, PostgreSQL's error recovery mechanism:
|
||||
|
||||
1. Unwinds the call stack via `longjmp`
|
||||
2. Frees the current memory context (including any `palloc`'d memory)
|
||||
3. Rolls back the current transaction
|
||||
4. Returns an error message to the client
|
||||
|
||||
Because pg_orrery uses only `palloc` and has no global state for v0.1.0/v0.2.0 functions, there is nothing to clean up beyond what PostgreSQL's context system handles automatically. No sockets, no mutex locks, no C++ destructors.
|
||||
|
||||
The v0.3.0 DE reader holds a file descriptor in per-backend static state. This is cleaned up via `on_proc_exit(eph_cleanup, 0)`, registered during `_PG_init()`. If `ereport(ERROR)` fires during a DE function, the file descriptor persists (it will be reused by the next DE call in the same backend) --- it is not leaked, just kept open for the backend's lifetime. The extension is always in a consistent state after error recovery.
|
||||
201
docs/src/content/docs/architecture/observation-pipeline.mdx
Normal file
201
docs/src/content/docs/architecture/observation-pipeline.mdx
Normal file
@ -0,0 +1,201 @@
|
||||
---
|
||||
title: Observation Pipeline
|
||||
sidebar:
|
||||
order: 3
|
||||
---
|
||||
|
||||
import { Aside, Steps, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
When a user calls `planet_observe(5, '40.0N 105.3W 1655m'::observer, now())` to find Jupiter's position in the sky, seven coordinate transformations execute in sequence. Each stage consumes the output of the previous stage and produces input for the next. Understanding this pipeline is a prerequisite for modifying any part of it.
|
||||
|
||||
## The full pipeline
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["VSOP87: Target heliocentric<br/>ecliptic J2000 (AU)"] --> C["Geocentric ecliptic<br/>target - Earth"]
|
||||
B["VSOP87: Earth heliocentric<br/>ecliptic J2000 (AU)"] --> C
|
||||
C --> D["Ecliptic → Equatorial J2000<br/>obliquity rotation"]
|
||||
D --> E["RA/Dec J2000<br/>Cartesian → spherical"]
|
||||
E --> F["Precession J2000 → date<br/>IAU 1976"]
|
||||
F --> G["Sidereal time → hour angle<br/>GMST (Vallado Eq. 3-47)"]
|
||||
G --> H["Equatorial → Horizontal<br/>az/el for observer"]
|
||||
H --> I["pg_topocentric result<br/>(az, el, range, range_rate)"]
|
||||
```
|
||||
|
||||
## Stage-by-stage breakdown
|
||||
|
||||
<Steps>
|
||||
1. **Heliocentric ecliptic position of the target**
|
||||
|
||||
VSOP87 (Bretagnon & Francou, 1988) computes the target planet's position in the heliocentric ecliptic J2000 frame. The output is three Cartesian coordinates in AU.
|
||||
|
||||
VSOP87 is a semi-analytical theory: it expands each coordinate as a sum of trigonometric series with polynomial time arguments. The truncated series used in pg_orrery provides ~1 arcsecond accuracy for the inner planets and ~1-2 arcseconds for the outer planets over the period 2000 BCE to 6000 CE.
|
||||
|
||||
For the Sun, this stage returns $(0, 0, 0)$ --- the Sun is at the origin of heliocentric coordinates. The Sun's apparent position is computed by inverting Earth's heliocentric position.
|
||||
|
||||
For the Moon, this stage is replaced by ELP2000-82B (Chapront-Touze & Chapront, 1988), which computes geocentric ecliptic coordinates directly, skipping stage 2.
|
||||
|
||||
**Code**: `src/vsop87.c:GetVsop87Coor()`, `src/elp82b.c:GetElp82bCoor()`
|
||||
|
||||
2. **Heliocentric ecliptic position of Earth**
|
||||
|
||||
The same VSOP87 call, but for body ID 3 (Earth). This gives Earth's position in the same frame as the target. Without this, there is no way to compute where the target appears from Earth's perspective.
|
||||
|
||||
**Code**: `src/vsop87.c:GetVsop87Coor()` with `body = 2` (VSOP87 uses 0-indexed: Venus=1, Earth=2, Mars=3, etc.)
|
||||
|
||||
3. **Geocentric ecliptic vector**
|
||||
|
||||
Subtract Earth's heliocentric position from the target's:
|
||||
|
||||
$$
|
||||
\vec{r}_\text{geo} = \vec{r}_\text{target} - \vec{r}_\text{Earth}
|
||||
$$
|
||||
|
||||
The result is the target's position as seen from Earth's center, still in the ecliptic J2000 frame. For the Moon, ELP2000-82B provides this directly.
|
||||
|
||||
**Code**: `src/planet_funcs.c:planet_observe()`, lines computing `geo_ecl_au[3]`
|
||||
|
||||
4. **Ecliptic to equatorial rotation**
|
||||
|
||||
The ecliptic and equatorial planes are tilted relative to each other by the obliquity of the ecliptic. At J2000, this angle is:
|
||||
|
||||
$$
|
||||
\varepsilon_0 = 23.4392911\degree = 0.40909280422232897\ \text{rad}
|
||||
$$
|
||||
|
||||
The rotation is around the X-axis (the vernal equinox direction, which is shared between both frames):
|
||||
|
||||
$$
|
||||
\begin{bmatrix} x_\text{equ} \\ y_\text{equ} \\ z_\text{equ} \end{bmatrix}
|
||||
= \begin{bmatrix} 1 & 0 & 0 \\ 0 & \cos\varepsilon_0 & -\sin\varepsilon_0 \\ 0 & \sin\varepsilon_0 & \cos\varepsilon_0 \end{bmatrix}
|
||||
\begin{bmatrix} x_\text{ecl} \\ y_\text{ecl} \\ z_\text{ecl} \end{bmatrix}
|
||||
$$
|
||||
|
||||
This uses the J2000 obliquity, not the obliquity of date, because both VSOP87 and the subsequent precession model are referenced to J2000.
|
||||
|
||||
**Code**: `src/planet_funcs.c:ecliptic_to_equatorial()` (via `astro_math.h`)
|
||||
|
||||
5. **Precession from J2000 to date**
|
||||
|
||||
The equatorial coordinate system itself rotates slowly due to lunisolar and planetary precession. Right ascension and declination at J2000 must be precessed to the epoch of observation.
|
||||
|
||||
pg_orrery uses the IAU 1976 precession model (Lieske et al., 1977), which expresses the three Euler angles $\zeta_A$, $z_A$, and $\theta_A$ as cubic polynomials in centuries from J2000:
|
||||
|
||||
$$
|
||||
\zeta_A = 0\overset{\prime\prime}{.}6406161 \cdot T + 0\overset{\prime\prime}{.}0000839 \cdot T^2 + 0\overset{\prime\prime}{.}0000050 \cdot T^3
|
||||
$$
|
||||
|
||||
The full rotation matrix $R_3(-z_A) \cdot R_2(\theta_A) \cdot R_3(-\zeta_A)$ transforms J2000 equatorial to equatorial of date.
|
||||
|
||||
**Code**: `src/precession.c:precess_j2000_to_date()`
|
||||
|
||||
6. **Sidereal time and hour angle**
|
||||
|
||||
To convert from the celestial sphere to the observer's local sky, we need the observer's relationship to the vernal equinox. This requires two quantities:
|
||||
|
||||
- **GMST** (Greenwich Mean Sidereal Time): the hour angle of the vernal equinox at Greenwich, computed from the Julian date using Vallado Eq. 3-47 (IAU 1982 model).
|
||||
- **LST** (Local Sidereal Time): $\text{LST} = \text{GMST} + \lambda_\text{observer}$, where $\lambda$ is the observer's east longitude in radians.
|
||||
- **Hour angle**: $h = \text{LST} - \alpha$, where $\alpha$ is the right ascension of date from stage 5.
|
||||
|
||||
The GMST formula:
|
||||
|
||||
$$
|
||||
\text{GMST}_\text{sec} = 67310.54841 + (876600 \times 3600 + 8640184.812866) \cdot T + 0.093104 \cdot T^2 - 6.2 \times 10^{-6} \cdot T^3
|
||||
$$
|
||||
|
||||
where $T = (JD - 2451545.0) / 36525.0$. The result in seconds is converted to radians: $\text{GMST}_\text{rad} = \text{GMST}_\text{sec} \times \pi / 43200$.
|
||||
|
||||
**Code**: `src/sidereal_time.c:gmst_from_jd()`
|
||||
|
||||
7. **Equatorial to horizontal**
|
||||
|
||||
Given hour angle $h$, declination $\delta$, and observer latitude $\phi$, compute azimuth $A$ and elevation $a$:
|
||||
|
||||
$$
|
||||
\sin a = \sin\phi \sin\delta + \cos\phi \cos\delta \cos h
|
||||
$$
|
||||
|
||||
$$
|
||||
\tan A = \frac{-\sin h}{\cos\phi \tan\delta - \sin\phi \cos h}
|
||||
$$
|
||||
|
||||
Azimuth is measured from north through east (0 = north, 90 = east, 180 = south, 270 = west).
|
||||
|
||||
The result is packed into a `pg_topocentric` struct with range computed from the geocentric distance ($\text{range}_\text{km} = d_\text{AU} \times 149597870.7$). Range rate is set to 0.0 for planetary observations (velocity computation is not yet implemented for the VSOP87 pipeline).
|
||||
|
||||
**Code**: `src/astro_math.h:observe_from_geocentric()` (shared by `planet_funcs.c`, `moon_funcs.c`, and `de_funcs.c`)
|
||||
</Steps>
|
||||
|
||||
## Pipeline variants
|
||||
|
||||
The seven-stage pipeline applies to planets observed via VSOP87. Other observation targets use modified versions.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Moon (ELP2000-82B)">
|
||||
Stages 1-3 are replaced by a single call to `GetElp82bCoor()`, which returns geocentric ecliptic coordinates directly. The remaining stages (4-7) are identical.
|
||||
|
||||
ELP2000-82B is a semi-analytical lunar theory with ~10 arcsecond accuracy. It accounts for the principal perturbations from the Sun, but not the full set of planetary perturbations included in the DE series.
|
||||
</TabItem>
|
||||
<TabItem label="Planetary moons">
|
||||
Each moon theory (L1.2 for Galilean moons, TASS17 for Saturn, GUST86 for Uranus, MarsSat for Mars) computes the moon's position relative to its parent planet. The pipeline:
|
||||
|
||||
1. Compute parent planet heliocentric position via VSOP87
|
||||
2. Compute moon position relative to parent in parent-equatorial frame
|
||||
3. Transform to heliocentric ecliptic J2000 (parent offset + frame rotation)
|
||||
4. Proceed from stage 3 of the standard pipeline (geocentric ecliptic)
|
||||
</TabItem>
|
||||
<TabItem label="DE ephemeris (v0.3.0)">
|
||||
The `_de()` function variants replace stages 1-2 with JPL DE positions:
|
||||
|
||||
1. DE reader returns target position in ICRS equatorial frame
|
||||
2. DE reader returns Earth position (derived from EMB - Moon/(1+EMRAT))
|
||||
3. Both positions converted from ICRS equatorial to ecliptic J2000 via `equatorial_to_ecliptic()` at the provider boundary
|
||||
4. Proceed from stage 3 of the standard pipeline (geocentric ecliptic)
|
||||
|
||||
Rule 7 of the constant chain of custody requires both target and Earth to come from the same provider. If either DE call fails, both fall back to VSOP87.
|
||||
</TabItem>
|
||||
<TabItem label="Satellites (SGP4)">
|
||||
Satellites use a fundamentally different pipeline. SGP4 outputs TEME (True Equator, Mean Equinox) positions, not heliocentric ecliptic. The satellite pipeline:
|
||||
|
||||
1. SGP4/SDP4 propagation to TEME position/velocity (WGS-72)
|
||||
2. GMST rotation to ECEF
|
||||
3. ECEF to geodetic (WGS-84) or topocentric via SEZ transform
|
||||
</TabItem>
|
||||
<TabItem label="Stars">
|
||||
Stars are effectively at infinite distance. The pipeline:
|
||||
|
||||
1. J2000 catalog coordinates (RA, Dec)
|
||||
2. Precession to date (IAU 1976)
|
||||
3. Sidereal time and hour angle
|
||||
4. Equatorial to horizontal
|
||||
|
||||
No VSOP87 call, no geocentric vector, no distance computation. This makes star observation the fastest pipeline --- 714K observations per second.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Why this pipeline and not another
|
||||
|
||||
Several simplifications are deliberate.
|
||||
|
||||
<Aside type="note" title="No nutation">
|
||||
The pipeline uses precession but not nutation. For 1-arcsecond VSOP87 positions, the ~9-arcsecond nutation correction is below the noise floor of the ephemeris. Adding nutation would increase computation cost without improving practical accuracy.
|
||||
</Aside>
|
||||
|
||||
**No aberration correction.** Annual aberration shifts apparent positions by up to 20 arcseconds, but for observation planning (which quadrant of the sky is Jupiter in tonight?) this is irrelevant. For sub-arcsecond planet positions, pg_orrery v0.3.0 supports [optional DE440/441 ephemeris files](/guides/de-ephemeris/); for apparent position corrections (aberration, light-time), use SPICE or Skyfield.
|
||||
|
||||
**No light-time iteration.** The positions returned are geometric, not apparent. Light-time corrections of a few minutes for the outer planets shift the apparent position by a fraction of an arcsecond at most --- again, below the VSOP87 accuracy floor.
|
||||
|
||||
**No atmospheric refraction.** Refraction near the horizon can shift apparent elevation by half a degree. pg_orrery reports geometric elevation; the user must apply refraction corrections for their local conditions if needed. This is a deliberate choice --- refraction depends on temperature, pressure, and humidity that pg_orrery does not model.
|
||||
|
||||
## Extending the pipeline
|
||||
|
||||
To add a new observation target, identify which stages change:
|
||||
|
||||
| New target | Stages replaced | Stages reused |
|
||||
|-----------|-----------------|---------------|
|
||||
| New moon theory | 1-3 (new theory + parent VSOP87) | 4-7 |
|
||||
| DE ephemeris (v0.3.0) | 1-2 (DE positions for both target and Earth) | 3-7 |
|
||||
| Near-Earth asteroid | 1 (Keplerian propagation) | 2-7 |
|
||||
| Distant star | 1-3 (catalog lookup, no distance) | 5-7 (skip obliquity) |
|
||||
|
||||
The modularity of the pipeline means new targets require implementing only the first few stages. The coordinate transformation machinery from stage 4 onward is shared across all targets.
|
||||
217
docs/src/content/docs/architecture/sgp4-integration.mdx
Normal file
217
docs/src/content/docs/architecture/sgp4-integration.mdx
Normal file
@ -0,0 +1,217 @@
|
||||
---
|
||||
title: SGP4 Integration
|
||||
sidebar:
|
||||
order: 6
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
pg_orrery vendors Bill Gray's `sat_code` library (MIT license, Project Pluto) for SGP4/SDP4 propagation. The relevant source files are vendored into `src/sgp4/` with `.cpp` extensions renamed to `.c` --- the code contains zero C++ features and compiles as pure C99. This page covers why sat_code was chosen, how it integrates with PostgreSQL's build and execution model, and the error handling contract between the two codebases.
|
||||
|
||||
## Why sat_code
|
||||
|
||||
Three SGP4 implementations were evaluated. The choice came down to one question: which library can run inside a PostgreSQL backend without modification?
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="sat_code (chosen)">
|
||||
**Pure C.** Despite upstream's `.cpp` file extensions, the code contains zero C++ features. pg_orrery vendors the files as `.c` and compiles them with `gcc`. The public API in `norad.h` is a flat C function interface: `SGP4_init()`, `SGP4()`, `SDP4_init()`, `SDP4()`, `parse_elements()`, `select_ephemeris()`.
|
||||
|
||||
**No global mutable state.** The propagator state lives in a caller-allocated `double params[N_SAT_PARAMS]` array. This maps directly to PostgreSQL's `palloc`-based memory model.
|
||||
|
||||
**Full SDP4.** Includes deep-space propagation with lunar/solar perturbations for GEO, Molniya, and GPS orbits.
|
||||
|
||||
**MIT license.** Compatible with the PostgreSQL License.
|
||||
|
||||
**Actively maintained.** Used in Bill Gray's Find_Orb production astrometry software.
|
||||
</TabItem>
|
||||
<TabItem label="Vallado reference (rejected)">
|
||||
The canonical implementation from the STR#3 revision paper. Two problems:
|
||||
|
||||
1. Written in C++ with heavy use of global state. The propagator coefficients live in file-scope variables, making it impossible to declare functions `PARALLEL SAFE`.
|
||||
2. License unclear for embedding in a PostgreSQL extension distributed as a shared library.
|
||||
</TabItem>
|
||||
<TabItem label="libsgp4 forks (rejected)">
|
||||
Various GitHub forks, typically C++ class hierarchies assuming an object-per-satellite lifecycle. This conflicts with PostgreSQL's per-call execution model --- you cannot persist C++ objects across function invocations without managing their lifecycle in a memory context callback, adding complexity for no benefit.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Compilation
|
||||
|
||||
sat_code's upstream files use `.cpp` extensions but contain no C++ features --- no classes, templates, namespaces, exceptions, or STL. The vendored copies in `src/sgp4/` are renamed to `.c` and compile with `gcc` alongside the rest of pg_orrery. There is no C/C++ boundary, no `g++`, and no `-lstdc++`.
|
||||
|
||||
```
|
||||
src/*.c --[gcc]--> .o --|
|
||||
src/sgp4/*.c --[gcc]--> .o --|--> pg_orrery.so
|
||||
-lm
|
||||
```
|
||||
|
||||
### Build rules
|
||||
|
||||
```makefile
|
||||
# Vendored SGP4/SDP4 sources (pure C, from Bill Gray's sat_code, MIT license)
|
||||
SGP4_DIR = src/sgp4
|
||||
SGP4_SRCS = $(SGP4_DIR)/sgp4.c $(SGP4_DIR)/sdp4.c \
|
||||
$(SGP4_DIR)/deep.c $(SGP4_DIR)/common.c \
|
||||
$(SGP4_DIR)/basics.c $(SGP4_DIR)/get_el.c \
|
||||
$(SGP4_DIR)/tle_out.c
|
||||
SGP4_OBJS = $(SGP4_SRCS:.c=.o)
|
||||
|
||||
# Include vendored SGP4 headers for our C sources
|
||||
PG_CPPFLAGS = -I$(SGP4_DIR)
|
||||
|
||||
# Pure C — no C++ runtime needed
|
||||
SHLIB_LINK += -lm
|
||||
```
|
||||
|
||||
PGXS handles the `-fPIC` flag and pattern rules for `.c` to `.o` compilation, so the vendored SGP4 files need no special build rules.
|
||||
|
||||
### Header inclusion
|
||||
|
||||
pg_orrery's C files include `norad.h` directly:
|
||||
|
||||
```c
|
||||
#include "norad.h" /* vendored SGP4 public API */
|
||||
#include "types.h" /* pg_orrery types and WGS-72/84 constants */
|
||||
```
|
||||
|
||||
The `PG_CPPFLAGS = -I$(SGP4_DIR)` flag makes `norad.h` available without a path prefix.
|
||||
|
||||
## The SGP4 API surface
|
||||
|
||||
pg_orrery uses a small subset of sat_code's public functions.
|
||||
|
||||
### Initialization
|
||||
|
||||
```c
|
||||
int select_ephemeris(const tle_t *tle);
|
||||
```
|
||||
|
||||
Returns 0 for near-earth (SGP4) or 1 for deep-space (SDP4), based on the orbital period threshold of 225 minutes. Returns -1 if the mean motion or eccentricity is out of range --- an early indicator of an invalid TLE.
|
||||
|
||||
```c
|
||||
void SGP4_init(double *params, const tle_t *tle);
|
||||
void SDP4_init(double *params, const tle_t *tle);
|
||||
```
|
||||
|
||||
Compute the propagator initialization coefficients and store them in the caller-allocated `params` array. This is the expensive step (~5x the cost of a single propagation), so pg_orrery performs it once per TLE and reuses the `params` array for SRF functions that propagate the same TLE to multiple times.
|
||||
|
||||
### Propagation
|
||||
|
||||
```c
|
||||
int SGP4(double tsince, const tle_t *tle, const double *params,
|
||||
double *pos, double *vel);
|
||||
int SDP4(double tsince, const tle_t *tle, const double *params,
|
||||
double *pos, double *vel);
|
||||
```
|
||||
|
||||
Propagate to `tsince` minutes from epoch. Write position (km) and velocity (km/min) into caller-provided arrays. Return 0 on success or a negative error code.
|
||||
|
||||
<Aside type="note" title="Velocity units">
|
||||
sat_code outputs velocity in km/min. pg_orrery converts to km/s at the boundary --- exactly once, in `sgp4_funcs.c` when populating the `pg_eci` struct. The conversion is `vel[i] / 60.0`. All downstream pg_orrery functions work in km/s.
|
||||
</Aside>
|
||||
|
||||
### TLE parsing
|
||||
|
||||
```c
|
||||
int parse_elements(const char *line1, const char *line2, tle_t *tle);
|
||||
```
|
||||
|
||||
Parse two-line element text into a `tle_t` struct. Returns 0 on success. pg_orrery calls this in `tle_in()` to validate input at storage time.
|
||||
|
||||
```c
|
||||
void write_elements_in_tle_format(char *obuff, const tle_t *tle);
|
||||
```
|
||||
|
||||
Reconstruct text from parsed elements. Used in `tle_out()` for display.
|
||||
|
||||
## TLE struct conversion
|
||||
|
||||
pg_orrery stores TLEs in its own `pg_tle` struct (112 bytes, designed for PostgreSQL tuple storage). sat_code uses `tle_t` (a larger struct with additional fields for its own purposes). The conversion between them is a field-by-field copy with no unit conversion --- both use radians, radians/minute, and Julian dates.
|
||||
|
||||
```c
|
||||
static void
|
||||
pg_tle_to_sat_code(const pg_tle *src, tle_t *dst)
|
||||
{
|
||||
memset(dst, 0, sizeof(tle_t));
|
||||
dst->epoch = src->epoch;
|
||||
dst->xincl = src->inclination;
|
||||
dst->xnodeo = src->raan;
|
||||
dst->eo = src->eccentricity;
|
||||
dst->omegao = src->arg_perigee;
|
||||
dst->xmo = src->mean_anomaly;
|
||||
dst->xno = src->mean_motion;
|
||||
dst->xndt2o = src->mean_motion_dot;
|
||||
dst->xndd6o = src->mean_motion_ddot;
|
||||
dst->bstar = src->bstar;
|
||||
/* ... identification fields ... */
|
||||
}
|
||||
```
|
||||
|
||||
This conversion is duplicated in `sgp4_funcs.c`, `coord_funcs.c`, and `pass_funcs.c`. Each file contains its own static copy. The duplication is intentional:
|
||||
|
||||
1. Each translation unit is self-contained --- no hidden coupling through shared internal functions.
|
||||
2. The functions are small (under 20 lines). Binary size increase is negligible.
|
||||
3. The compiler can inline them within each translation unit.
|
||||
4. If the helpers ever need to diverge (e.g., `pass_funcs.c` working in km/min while `coord_funcs.c` works in km/s), they can do so independently.
|
||||
|
||||
## Error codes
|
||||
|
||||
sat_code returns integer error codes from `SGP4()` and `SDP4()`. pg_orrery classifies them by physical meaning and responds accordingly.
|
||||
|
||||
| Code | sat_code constant | Physical meaning | pg_orrery response |
|
||||
|------|-------------------|------------------|-------------------|
|
||||
| 0 | --- | Normal propagation | Return result |
|
||||
| -1 | `SXPX_ERR_NEARLY_PARABOLIC` | Eccentricity $\geq 1$ | `ereport(ERROR)` |
|
||||
| -2 | `SXPX_ERR_NEGATIVE_MAJOR_AXIS` | Orbit has decayed | `ereport(ERROR)` |
|
||||
| -3 | `SXPX_WARN_ORBIT_WITHIN_EARTH` | Entire orbit below surface | `ereport(NOTICE)`, return result |
|
||||
| -4 | `SXPX_WARN_PERIGEE_WITHIN_EARTH` | Perigee below surface | `ereport(NOTICE)`, return result |
|
||||
| -5 | `SXPX_ERR_NEGATIVE_XN` | Negative mean motion | `ereport(ERROR)` |
|
||||
| -6 | `SXPX_ERR_CONVERGENCE_FAIL` | Kepler equation diverged | `ereport(ERROR)` |
|
||||
|
||||
### The warning/error distinction
|
||||
|
||||
Codes -3 and -4 are warnings, not errors. A satellite with perigee within Earth is plausible during reentry or shortly after launch --- the state vector is still mathematically valid. The `NOTICE` tells the user the situation is unusual; the result is still returned.
|
||||
|
||||
Codes -1, -2, -5, and -6 indicate the propagator model has broken down. The output position would be meaningless. These raise `ereport(ERROR)`, which aborts the current query.
|
||||
|
||||
### Context-dependent handling
|
||||
|
||||
The error response changes based on the calling context:
|
||||
|
||||
| Context | Fatal error (-1, -2, -5, -6) | Warning (-3, -4) |
|
||||
|---------|------------------------------|-------------------|
|
||||
| Direct propagation (`sgp4_propagate`) | `ereport(ERROR)` --- abort query | `ereport(NOTICE)` --- return result |
|
||||
| Safe propagation (`sgp4_propagate_safe`) | Return `NULL` | `ereport(NOTICE)` --- return result |
|
||||
| Pass prediction (`elevation_at_jd`) | Return $-\pi$ elevation --- continue scan | Ignore --- return elevation |
|
||||
| SRF series (`sgp4_propagate_series`) | `ereport(ERROR)` --- abort series | `ereport(NOTICE)` --- return result |
|
||||
|
||||
The pass prediction context is the most interesting. A TLE valid for part of a search window should not abort the entire pass search. Returning $-\pi$ radians (well below any physical horizon) causes the coarse scan to treat the time point as "satellite below horizon" and continue looking for passes at other times.
|
||||
|
||||
## Build integration
|
||||
|
||||
sat_code is vendored into `src/sgp4/` --- the minimal set of source files needed for SGP4/SDP4 propagation, committed directly into the pg_orrery repository. A `PROVENANCE.md` file in that directory records the upstream repository, the exact commit hash, and every modification made during vendoring.
|
||||
|
||||
This approach provides:
|
||||
|
||||
- **Pinned version.** The vendored commit is recorded in `src/sgp4/PROVENANCE.md`. Upstream changes do not affect pg_orrery until the files are explicitly re-vendored.
|
||||
- **Clear provenance.** `PROVENANCE.md` documents the upstream repository (github.com/Bill-Gray/sat_code), commit hash, the `.cpp` to `.c` rename rationale, and a line-by-line list of every modification.
|
||||
- **No submodule complexity.** Cloning the repository gets a complete, buildable tree. No `git submodule update --init` step, no risk of missing submodule state.
|
||||
- **Pure C build.** Renaming `.cpp` to `.c` eliminates the `g++` and `-lstdc++` dependencies. The entire extension compiles with a single C compiler.
|
||||
|
||||
### Vendored files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `sgp4.c` | SGP4 near-earth propagator |
|
||||
| `sdp4.c` | SDP4 deep-space propagator |
|
||||
| `deep.c` | Lunar/solar perturbation routines for SDP4 |
|
||||
| `common.c` | Shared initialization code for SGP4/SDP4 |
|
||||
| `basics.c` | Utility functions (angle normalization, etc.) |
|
||||
| `get_el.c` | TLE parsing (`parse_elements()`) |
|
||||
| `tle_out.c` | TLE text reconstruction |
|
||||
| `norad.h` | Public API declarations, `tle_t` struct, constants |
|
||||
| `norad_in.h` | Internal constants (WGS-72 values) |
|
||||
| `PROVENANCE.md` | Upstream commit, modifications, verification notes |
|
||||
| `LICENSE` | MIT license from upstream |
|
||||
|
||||
Other sat_code files (obs_eph.cpp, sat_id.cpp, etc.) are not vendored. pg_orrery uses sat_code strictly as a propagation library, not as a satellite identification or observation planning tool.
|
||||
168
docs/src/content/docs/architecture/theory-to-code.mdx
Normal file
168
docs/src/content/docs/architecture/theory-to-code.mdx
Normal file
@ -0,0 +1,168 @@
|
||||
---
|
||||
title: Theory-to-Code Mapping
|
||||
sidebar:
|
||||
order: 4
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Every equation in pg_orrery traces to a published, peer-reviewed source. This page provides the complete mapping between the celestial mechanics literature and the source files that implement each theory.
|
||||
|
||||
If a constant, algorithm, or formula appears in the code without a citation, that is a defect to be corrected.
|
||||
|
||||
## SGP4/SDP4 propagation
|
||||
|
||||
The core satellite propagation theory, implemented by Bill Gray's sat_code library (vendored into src/sgp4/).
|
||||
|
||||
| Theory | Source Paper | What it computes | Code location |
|
||||
|--------|-------------|------------------|---------------|
|
||||
| Mean element recovery | Brouwer (1959) | Original mean motion $n_0'$ and semi-major axis $a_0'$ from input TLE, removing secular $J_2$ perturbations | `src/sgp4/common.c:sxpall_common_init()` |
|
||||
| Secular perturbations | Lane & Cranford (1969); Hoots & Roehrich STR#3 | Secular rates of $M$, $\omega$, and $\Omega$ due to $J_2$, $J_4$ | `src/sgp4/common.c:sxpx_common_init()` |
|
||||
| Atmospheric drag | Hoots & Roehrich STR#3 | $B^*$ formulation of drag; $C_1$, $C_2$, $C_4$ coefficients; perigee-dependent $s$ parameter | `src/sgp4/common.c:sxpx_common_init()`; `src/sgp4/sgp4.c:SGP4_init()` |
|
||||
| Short-period perturbations | Lane & Cranford (1969); Brouwer (1959) | Oscillatory corrections to radius, argument of latitude, node, and inclination | `src/sgp4/common.c:sxpx_posn_vel()` |
|
||||
| Kepler equation | Classical | Newton-Raphson with second-order correction, bounded first step | `src/sgp4/common.c:sxpx_posn_vel()` |
|
||||
| Deep-space resonance | Hujsak (1979) | Lunar/solar gravitational perturbations; geopotential resonance for 12-hour and 24-hour orbits | `src/sgp4/deep.c:Deep_dpinit()`, `Deep_dpsec()`, `Deep_dpper()` |
|
||||
| Near-earth propagation | Hoots & Roehrich STR#3 | SGP4 main loop: secular + short-period + drag terms | `src/sgp4/sgp4.c:SGP4()` |
|
||||
| Deep-space propagation | Hoots & Roehrich STR#3 | SDP4: SGP4 core + deep-space secular/periodic corrections | `src/sgp4/sdp4.c:SDP4()` |
|
||||
| Near/deep selection | Hoots & Roehrich STR#3 | Period threshold: 225 minutes ($n < 2\pi/225$ rad/min) | `src/sgp4/norad.h:select_ephemeris()` |
|
||||
|
||||
### Primary reference
|
||||
|
||||
Hoots, F. R. & Roehrich, R. L. (1980). "Models for Propagation of NORAD Element Sets." Spacetrack Report No. 3, Aerospace Defense Command, Peterson AFB.
|
||||
|
||||
This is the canonical SGP4/SDP4 reference. All subsequent implementations, including Vallado's 2006 revision, trace back to this report.
|
||||
|
||||
## Coordinate transforms
|
||||
|
||||
| Theory | Source | What it computes | Code location |
|
||||
|--------|--------|------------------|---------------|
|
||||
| GMST | Vallado (2013) Eq. 3-47; IAU 1982 | Greenwich Mean Sidereal Time from Julian date | `src/sidereal_time.c:gmst_from_jd()` |
|
||||
| TEME to ECEF | Vallado (2013) | Z-axis rotation by $-\text{GMST}$; velocity cross-product correction | `src/coord_funcs.c:teme_to_ecef()` |
|
||||
| Geodetic from ECEF | Bowring (1976) | Iterative latitude from ECEF Cartesian on WGS-84 | `src/coord_funcs.c:ecef_to_geodetic()` |
|
||||
| Topocentric transform | Standard SEZ | ECEF range vector rotated to South-East-Zenith; azimuth from north | `src/coord_funcs.c:ecef_to_topocentric()` |
|
||||
| Observer to ECEF | Geodesy standard | WGS-84 ellipsoid surface point to Cartesian | `src/coord_funcs.c:observer_to_ecef()` |
|
||||
| Range rate | Dot product | Projection of relative velocity onto line-of-sight unit vector | `src/coord_funcs.c:eci_to_topocentric()` |
|
||||
| Semi-major axis from $n$ | Kepler's third law | $a = (k_e / n)^{2/3}$ in earth radii | `src/tle_type.c:tle_perigee()` |
|
||||
| IAU 1976 precession | Lieske et al. (1977) | Three Euler angles $\zeta_A$, $z_A$, $\theta_A$ for precession from J2000 to date | `src/precession.c:precess_j2000_to_date()` |
|
||||
| Ecliptic to equatorial | IAU | X-axis rotation by obliquity $\varepsilon_0 = 23.4392911\degree$ | `src/astro_math.h:ecliptic_to_equatorial()` |
|
||||
| Equatorial to ecliptic | IAU | Inverse obliquity rotation (for DE ICRS→ecliptic) | `src/astro_math.h:equatorial_to_ecliptic()` |
|
||||
| Observation pipeline | Standard | Geocentric ecliptic → equatorial → precess → topocentric | `src/astro_math.h:observe_from_geocentric()` |
|
||||
|
||||
### Primary references
|
||||
|
||||
- Vallado, D. A. (2013). *Fundamentals of Astrodynamics and Applications*, 4th ed. Microcosm Press.
|
||||
- Lieske, J. H. et al. (1977). "Expressions for the Precession Quantities Based upon the IAU (1976) System of Astronomical Constants." *Astronomy & Astrophysics*, 58, 1-16.
|
||||
- Bowring, B. R. (1976). "Transformation from Spatial to Geographical Coordinates." *Survey Review*, 23, 323-327.
|
||||
|
||||
## Planetary ephemerides
|
||||
|
||||
| Theory | Source | Bodies | Accuracy | Code location |
|
||||
|--------|--------|--------|----------|---------------|
|
||||
| VSOP87 | Bretagnon & Francou (1988) | Mercury through Neptune | ~1 arcsecond | `src/vsop87.c` |
|
||||
| ELP2000-82B | Chapront-Touze & Chapront (1988) | Moon | ~10 arcseconds | `src/elp82b.c` |
|
||||
| JPL DE440/441 (optional) | JPL (2021) | All bodies + Moon | ~0.1 milliarcsecond | `src/de_reader.c`, `src/eph_provider.c` |
|
||||
|
||||
### VSOP87
|
||||
|
||||
Bretagnon, P. & Francou, G. (1988). "Planetary Theories in Rectangular and Spherical Variables. VSOP87 Solutions." *Astronomy & Astrophysics*, 202, 309-315.
|
||||
|
||||
pg_orrery uses the VSOP87 rectangular ecliptic J2000 variant. The truncated coefficient tables provide full accuracy within the validity range of the theory (roughly 4000 BCE to 8000 CE for the inner planets, with degradation for the outer planets beyond $\pm$2000 years from J2000).
|
||||
|
||||
### ELP2000-82B
|
||||
|
||||
Chapront-Touze, M. & Chapront, J. (1988). "ELP 2000-85: A Semi-Analytical Lunar Ephemeris Adequate for Historical Times." *Astronomy & Astrophysics*, 190, 342-352.
|
||||
|
||||
The 82B revision is the version implemented. It provides geocentric ecliptic coordinates for the Moon, accounting for the principal perturbations from the Sun but not the complete set of planetary perturbations available in modern lunar ephemerides like DE421.
|
||||
|
||||
## Planetary moon theories
|
||||
|
||||
| Theory | Source | Moons | Accuracy | Code location |
|
||||
|--------|--------|-------|----------|---------------|
|
||||
| L1.2 | Lieske (1998) | Io, Europa, Ganymede, Callisto | ~1 arcsecond | `src/l12.c` |
|
||||
| TASS17 | Vienne & Duriez (1995) | Mimas through Iapetus | ~1-5 arcseconds | `src/tass17.c` |
|
||||
| GUST86 | Laskar & Jacobson (1987) | Miranda through Oberon | ~5-10 arcseconds | `src/gust86.c` |
|
||||
| MarsSat | Jacobson (2010) | Phobos, Deimos | ~1 arcsecond | `src/marssat.c` |
|
||||
|
||||
### References
|
||||
|
||||
- Lieske, J. H. (1998). "Galilean Satellites of Jupiter." *Astronomy & Astrophysics Supplement Series*, 129, 205-217.
|
||||
- Vienne, A. & Duriez, L. (1995). "TASS1.7: An Analytical Theory of the Motion of the Main Satellites of Saturn." *Astronomy & Astrophysics*, 297, 588-605.
|
||||
- Laskar, J. & Jacobson, R. A. (1987). "GUST86: An Analytical Ephemeris of the Uranian Satellites." *Astronomy & Astrophysics*, 188, 212-224.
|
||||
- Jacobson, R. A. (2010). "The Orbits and Masses of the Martian Satellites and the Libration of Phobos." *Astronomical Journal*, 139, 668-679.
|
||||
|
||||
## Transfer orbits
|
||||
|
||||
| Theory | Source | What it computes | Code location |
|
||||
|--------|--------|------------------|---------------|
|
||||
| Lambert solver | Izzo (2015) | Transfer velocity vectors given two positions and time of flight | `src/lambert.c` |
|
||||
| Keplerian propagation | Classical | Two-body elliptic/hyperbolic orbit from elements | `src/elliptic_to_rectangular.c` |
|
||||
|
||||
### References
|
||||
|
||||
- Izzo, D. (2015). "Revisiting Lambert's Problem." *Celestial Mechanics and Dynamical Astronomy*, 121, 1-15.
|
||||
|
||||
The Izzo solver uses Householder iterations for fast convergence and handles both short-way and long-way transfers. pg_orrery uses the prograde (short-way) solution by default.
|
||||
|
||||
## Radio emission
|
||||
|
||||
| Theory | Source | What it computes | Code location |
|
||||
|--------|--------|------------------|---------------|
|
||||
| Carr source regions | Carr et al. (1983) | Jupiter-Io decametric burst probability from CML and Io phase | `src/radio_funcs.c` |
|
||||
| CML computation | Standard | Jupiter System III Central Meridian Longitude | `src/radio_funcs.c` |
|
||||
|
||||
### Reference
|
||||
|
||||
Carr, T. D. et al. (1983). "Phenomenology of Magnetospheric Radio Emissions." *Physics of the Jovian Magnetosphere*, Cambridge University Press, 226-284.
|
||||
|
||||
## Vallado reference vectors
|
||||
|
||||
The Vallado 518 test vectors are the definitive verification dataset for SGP4 implementations. Each row specifies a NORAD ID, minutes since epoch, and expected position/velocity in TEME.
|
||||
|
||||
<Aside type="tip" title="Verification standard">
|
||||
All 518 vectors must match to machine epsilon ($\sim 10^{-8}$ km position, $\sim 10^{-11}$ km/s velocity) before any other development proceeds. The test file lives at `test/data/vallado_518.csv`.
|
||||
</Aside>
|
||||
|
||||
Sample from the verification suite:
|
||||
|
||||
```
|
||||
# NORAD 00005 (Vanguard 1) - LEO, low eccentricity
|
||||
# Minutes: 0.00 Expected X: 7022.465290 Y: -1400.082967 Z: 0.039550
|
||||
# Minutes: 360.00 Expected X: -7154.031380 Y: -3783.176825 Z: -2073.655980
|
||||
|
||||
# NORAD 29238 (GPS BIIR-11) - MEO, near-circular
|
||||
# Minutes: 0.00 Expected X: -22503.132440 Y: 14513.963880 Z: 180.989390
|
||||
|
||||
# NORAD 28350 (Galaxy 15) - GEO, deep-space SDP4
|
||||
# Minutes: 0.00 Expected X: -33110.816260 Y: 26044.993650 Z: -20.725400
|
||||
```
|
||||
|
||||
These vectors cover the full range of orbit types that pg_orrery handles: LEO (SGP4), MEO (SGP4), GEO (SDP4), high-eccentricity Molniya (SDP4), and deep-space GPS (SDP4). Any implementation that matches all 518 vectors is functionally equivalent to the Vallado reference.
|
||||
|
||||
## Source file index
|
||||
|
||||
A quick reference for finding the implementation of a specific theory.
|
||||
|
||||
| Source file | Theory/Function | Lines (approx) |
|
||||
|-------------|----------------|-----------------|
|
||||
| `src/vsop87.c` | VSOP87 planet positions | ~3000 (coefficient tables) |
|
||||
| `src/elp82b.c` | ELP2000-82B Moon position | ~2000 (coefficient tables) |
|
||||
| `src/l12.c` | L1.2 Galilean moons | ~800 |
|
||||
| `src/tass17.c` | TASS17 Saturn moons | ~1200 |
|
||||
| `src/gust86.c` | GUST86 Uranus moons | ~600 |
|
||||
| `src/marssat.c` | MarsSat Mars moons | ~400 |
|
||||
| `src/precession.c` | IAU 1976 precession | ~60 |
|
||||
| `src/sidereal_time.c` | GMST computation | ~40 |
|
||||
| `src/lambert.c` | Izzo Lambert solver | ~300 |
|
||||
| `src/coord_funcs.c` | Coordinate transforms | ~650 |
|
||||
| `src/pass_funcs.c` | Pass prediction algorithm | ~550 |
|
||||
| `src/gist_tle.c` | GiST altitude-band index | ~400 |
|
||||
| `src/planet_funcs.c` | VSOP87 observation pipeline | ~200 |
|
||||
| `src/de_reader.c` | Clean-room JPL DE binary reader (Chebyshev/Clenshaw) | ~400 |
|
||||
| `src/eph_provider.c` | DE provider dispatch, GUC, lazy init, frame rotation | ~350 |
|
||||
| `src/de_funcs.c` | All `_de()` SQL function implementations | ~650 |
|
||||
| `src/astro_math.h` | Shared math: obliquity rotation, observation pipeline | ~220 |
|
||||
| `src/radio_funcs.c` | Jupiter radio emission | ~200 |
|
||||
| `src/sgp4/sgp4.c` | SGP4 near-earth propagator | ~300 |
|
||||
| `src/sgp4/sdp4.c` | SDP4 deep-space propagator | ~200 |
|
||||
| `src/sgp4/deep.c` | Deep-space perturbations | ~800 |
|
||||
| `src/sgp4/common.c` | Shared SGP4/SDP4 initialization | ~250 |
|
||||
120
docs/src/content/docs/getting-started/installation.mdx
Normal file
120
docs/src/content/docs/getting-started/installation.mdx
Normal file
@ -0,0 +1,120 @@
|
||||
---
|
||||
title: Installation
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Steps, Aside } from "@astrojs/starlight/components";
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Docker (recommended)">
|
||||
The fastest way to get pg_orrery running. The Docker image ships PostgreSQL 17 with pg_orrery pre-compiled.
|
||||
|
||||
<Steps>
|
||||
1. Pull the image:
|
||||
```bash
|
||||
docker pull git.supported.systems/warehack.ing/pg_orrery:pg17
|
||||
```
|
||||
|
||||
2. Start the container:
|
||||
```bash
|
||||
docker run -d --name pg_orrery \
|
||||
-e POSTGRES_PASSWORD=orbit \
|
||||
-p 5499:5432 \
|
||||
git.supported.systems/warehack.ing/pg_orrery:pg17
|
||||
```
|
||||
|
||||
3. Connect and enable the extension:
|
||||
```bash
|
||||
psql -h localhost -p 5499 -U postgres -c "CREATE EXTENSION pg_orrery;"
|
||||
```
|
||||
</Steps>
|
||||
|
||||
<Aside type="tip">
|
||||
Port 5499 avoids conflicts with any existing PostgreSQL on 5432. Adjust as needed.
|
||||
</Aside>
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Build from Source">
|
||||
Requires PostgreSQL 17 development headers and a C toolchain.
|
||||
|
||||
<Steps>
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://git.supported.systems/warehack.ing/pg_orrery.git
|
||||
cd pg_orrery
|
||||
git submodule update --init
|
||||
```
|
||||
|
||||
2. Build and install:
|
||||
```bash
|
||||
make PG_CONFIG=/usr/bin/pg_config
|
||||
sudo make install PG_CONFIG=/usr/bin/pg_config
|
||||
```
|
||||
|
||||
3. Enable in your database:
|
||||
```sql
|
||||
CREATE EXTENSION pg_orrery;
|
||||
```
|
||||
|
||||
4. Verify installation:
|
||||
```sql
|
||||
SELECT planet_observe(5, '40.0N 105.3W 1655m'::observer, now());
|
||||
```
|
||||
</Steps>
|
||||
|
||||
<Aside type="note">
|
||||
The build compiles pure C throughout --- both pg_orrery and the vendored SGP4/SDP4 library in `src/sgp4/`. No C++ compiler or runtime is required.
|
||||
</Aside>
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Docker Compose">
|
||||
For integration with existing PostgreSQL-based applications.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
db:
|
||||
image: git.supported.systems/warehack.ing/pg_orrery:pg17
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-orbit}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-orbit}
|
||||
ports:
|
||||
- "${PGPORT:-5499}:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
```
|
||||
|
||||
Then:
|
||||
```bash
|
||||
docker compose up -d
|
||||
psql -h localhost -p 5499 -U postgres -d orbit -c "CREATE EXTENSION pg_orrery;"
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Running the test suite
|
||||
|
||||
If building from source, the regression tests verify all 68 functions across 12 test suites:
|
||||
|
||||
```bash
|
||||
make installcheck PG_CONFIG=/usr/bin/pg_config
|
||||
```
|
||||
|
||||
This runs the tests listed in the `REGRESS` variable: TLE parsing, SGP4 propagation, coordinate transforms, pass prediction, GiST indexing, convenience functions, star observation, Keplerian propagation, planet observation, moon observation, Lambert transfers, and DE ephemeris.
|
||||
|
||||
## Upgrading
|
||||
|
||||
If you have a previous version installed, upgrade in place:
|
||||
|
||||
```sql
|
||||
-- From v0.1.0 (satellite-only) to v0.2.0 (solar system)
|
||||
ALTER EXTENSION pg_orrery UPDATE TO '0.2.0';
|
||||
|
||||
-- From v0.2.0 to v0.3.0 (DE ephemeris support)
|
||||
ALTER EXTENSION pg_orrery UPDATE TO '0.3.0';
|
||||
```
|
||||
|
||||
Each migration adds new functions while preserving existing data and functions.
|
||||
116
docs/src/content/docs/getting-started/quick-start.mdx
Normal file
116
docs/src/content/docs/getting-started/quick-start.mdx
Normal file
@ -0,0 +1,116 @@
|
||||
---
|
||||
title: Quick Start
|
||||
sidebar:
|
||||
order: 3
|
||||
---
|
||||
|
||||
import { Steps, Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Five queries that show what pg_orrery can do. Each builds on the previous — from a single planet observation to planning an interplanetary trajectory.
|
||||
|
||||
<Aside type="tip">
|
||||
All examples assume you have pg_orrery installed and `CREATE EXTENSION pg_orrery;` has been run. See [Installation](/getting-started/installation/) if you need to set that up first.
|
||||
</Aside>
|
||||
|
||||
<Steps>
|
||||
1. **Where is Jupiter right now?**
|
||||
|
||||
The `observer` type takes geodetic coordinates as a compact string. This observer is in Boulder, Colorado at 1655m elevation.
|
||||
|
||||
```sql
|
||||
SELECT topo_azimuth(t) AS az,
|
||||
topo_elevation(t) AS el,
|
||||
topo_range(t) / 149597870.7 AS distance_au
|
||||
FROM planet_observe(5, '40.0N 105.3W 1655m'::observer, now()) t;
|
||||
```
|
||||
|
||||
Body ID 5 is Jupiter (the VSOP87 convention: 1=Mercury through 8=Neptune). The result gives azimuth and elevation in degrees, plus range in AU.
|
||||
|
||||
2. **What's the entire solar system doing?**
|
||||
|
||||
Use `generate_series` to loop over all 8 planets and compute their heliocentric positions:
|
||||
|
||||
```sql
|
||||
SELECT body_id,
|
||||
CASE body_id
|
||||
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
|
||||
WHEN 3 THEN 'Earth' WHEN 4 THEN 'Mars'
|
||||
WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn'
|
||||
WHEN 7 THEN 'Uranus' WHEN 8 THEN 'Neptune'
|
||||
END AS name,
|
||||
round(helio_distance(planet_heliocentric(body_id, now()))::numeric, 4) AS distance_au
|
||||
FROM generate_series(1, 8) AS body_id;
|
||||
```
|
||||
|
||||
One query, eight planets, heliocentric distances in AU. No loops, no external libraries.
|
||||
|
||||
3. **Predict ISS passes over your location**
|
||||
|
||||
First, define a TLE and observer. Then predict all passes in the next 24 hours above 10 degrees elevation:
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT pass_aos(p) AS rise_time,
|
||||
pass_max_el(p) AS max_elevation,
|
||||
pass_los(p) AS set_time
|
||||
FROM iss, predict_passes(tle, '40.0N 105.3W 1655m'::observer,
|
||||
now(), now() + interval '24 hours', 10.0) p;
|
||||
```
|
||||
|
||||
The `10.0` parameter is the minimum elevation filter in degrees. `predict_passes` returns a set of `pass_event` records with AOS, TCA, and LOS times plus azimuth data.
|
||||
|
||||
4. **When will Jupiter produce radio bursts tonight?**
|
||||
|
||||
Jupiter emits powerful decametric radio bursts when Io is in certain orbital positions relative to Jupiter's Central Meridian Longitude. Predict the best windows:
|
||||
|
||||
```sql
|
||||
SELECT t,
|
||||
round(io_phase_angle(t)::numeric, 1) AS io_phase,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, t)::numeric, 1) AS cml,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
)::numeric, 3) AS burst_prob
|
||||
FROM generate_series(
|
||||
now(),
|
||||
now() + interval '12 hours',
|
||||
interval '10 minutes'
|
||||
) AS t
|
||||
WHERE jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
) > 0.3;
|
||||
```
|
||||
|
||||
This scans the next 12 hours in 10-minute steps and filters for windows where burst probability exceeds 30%. The underlying model uses the Carr et al. (1983) source regions A, B, C, and D.
|
||||
|
||||
5. **Plan an Earth-Mars transfer**
|
||||
|
||||
Use the Lambert solver to find the transfer orbit for a given departure and arrival date:
|
||||
|
||||
```sql
|
||||
SELECT round(c3_departure::numeric, 2) AS c3_depart_km2s2,
|
||||
round(c3_arrival::numeric, 2) AS c3_arrive_km2s2,
|
||||
round(tof_days::numeric, 1) AS flight_days,
|
||||
round(transfer_sma::numeric, 4) AS sma_au
|
||||
FROM lambert_transfer(
|
||||
3, 4, -- Earth to Mars
|
||||
'2028-10-01'::timestamptz, -- departure
|
||||
'2029-06-15'::timestamptz -- arrival
|
||||
);
|
||||
```
|
||||
|
||||
Body IDs 3 (Earth) and 4 (Mars). The result gives departure C3 (the energy you need to leave Earth), arrival C3, time of flight, and the transfer orbit's semi-major axis. For a full pork chop plot, wrap this in a `CROSS JOIN` of departure and arrival date ranges — see the [Interplanetary Trajectories](/guides/interplanetary-trajectories/) guide.
|
||||
</Steps>
|
||||
|
||||
## Next steps
|
||||
|
||||
You've seen the five domains pg_orrery covers. For deeper dives:
|
||||
|
||||
- **[Tracking Satellites](/guides/tracking-satellites/)** — batch observation, conjunction screening, pass prediction workflows
|
||||
- **[Observing the Solar System](/guides/observing-solar-system/)** — "what's up tonight?" queries, rise/set times, conjunctions
|
||||
- **[JPL DE Ephemeris](/guides/de-ephemeris/)** — opt-in sub-milliarcsecond accuracy using JPL DE440/441 files
|
||||
- **[The SQL Advantage](/workflow/sql-advantage/)** — why doing this in SQL changes what's possible
|
||||
64
docs/src/content/docs/getting-started/what-is-pg-orrery.mdx
Normal file
64
docs/src/content/docs/getting-started/what-is-pg-orrery.mdx
Normal file
@ -0,0 +1,64 @@
|
||||
---
|
||||
title: What is pg_orrery?
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
import { Card, CardGrid, Aside } from "@astrojs/starlight/components";
|
||||
|
||||
An orrery is a clockwork model of the solar system — brass gears turning planets in their courses. pg_orrery is the same idea, built from Keplerian parameters and SQL instead of wheelwork. Where a mechanical orrery approximates orbits with gear ratios, a database orrery computes them from the six orbital elements that define each trajectory.
|
||||
|
||||
## The "PostGIS for space" analogy
|
||||
|
||||
PostGIS added spatial awareness to PostgreSQL — suddenly your database understood geometry, distance, and containment. pg_orrery does the same for celestial mechanics. Your database understands orbits, observation geometry, and the relationships between objects in the solar system. You can JOIN orbital computation results with any other table, filter with WHERE clauses, and let PostgreSQL's query planner parallelize the work.
|
||||
|
||||
## What it covers
|
||||
|
||||
| Domain | Theory | Key Functions | Accuracy |
|
||||
|---|---|---|---|
|
||||
| Satellites | SGP4/SDP4 (Brouwer, 1959) | `observe()`, `predict_passes()` | ~1 km (LEO, fresh TLE) |
|
||||
| Planets | VSOP87 (Bretagnon, 1988) | `planet_observe()`, `planet_heliocentric()` | ~1 arcsecond |
|
||||
| Sun | VSOP87 (Earth vector, inverted) | `sun_observe()` | ~1 arcsecond |
|
||||
| Moon | ELP2000-82B (Chapront, 1988) | `moon_observe()` | ~10 arcseconds |
|
||||
| Planetary moons | L1.2, TASS17, GUST86, MarsSat | `galilean_observe()`, etc. | ~1-10 arcseconds |
|
||||
| Stars | J2000 catalog + precession | `star_observe()` | Limited by catalog |
|
||||
| Comets/asteroids | Two-body Keplerian | `kepler_propagate()`, `comet_observe()` | Varies with eccentricity |
|
||||
| Jupiter radio | Carr et al. (1983) sources | `jupiter_burst_probability()` | Empirical probability |
|
||||
| Transfers | Lambert (Izzo, 2015) | `lambert_transfer()`, `lambert_c3()` | Ballistic two-body |
|
||||
| DE ephemeris (optional) | JPL DE440/441 | `planet_observe_de()`, `moon_observe_de()` | ~0.1 milliarcsecond |
|
||||
|
||||
## Who it's for
|
||||
|
||||
<CardGrid>
|
||||
<Card title="Satellite operators" icon="rocket">
|
||||
You already have TLEs in PostgreSQL. Now your database can propagate them,
|
||||
predict passes, and screen for conjunctions without leaving SQL. Batch
|
||||
12,000 observations in 17ms.
|
||||
</Card>
|
||||
<Card title="Amateur astronomers & radio operators" icon="star">
|
||||
Plan observation sessions entirely in SQL. "What planets are above 20
|
||||
degrees tonight?" is a single query. Jupiter radio burst prediction
|
||||
replaces the Windows-only Radio Jupiter Pro.
|
||||
</Card>
|
||||
<Card title="Mission planning enthusiasts" icon="right-caret">
|
||||
Generate pork chop plots for interplanetary transfers as SQL CROSS JOINs.
|
||||
The Lambert solver handles 800,000 solutions per second. Compare transfer
|
||||
energies across launch windows without writing a line of Python.
|
||||
</Card>
|
||||
</CardGrid>
|
||||
|
||||
## What pg_orrery is NOT
|
||||
|
||||
<Aside type="caution" title="Honest limitations">
|
||||
pg_orrery is a computation engine, not a complete application. Understanding what it doesn't do is as important as knowing what it does.
|
||||
</Aside>
|
||||
|
||||
**Not a GUI.** pg_orrery returns numbers. Use Stellarium, GPredict, or STK for visualization. Use any plotting library to render its output.
|
||||
|
||||
**Not sub-arcsecond by default.** The built-in VSOP87 pipeline is accurate to about 1 arcsecond — sufficient for observation planning and visual astronomy. For precision work (dish pointing, occultation timing, astrometry), pg_orrery v0.3.0 supports [optional JPL DE440/441 ephemeris files](/guides/de-ephemeris/) that bring accuracy to ~0.1 milliarcsecond. DE is opt-in and requires a one-time GUC configuration.
|
||||
|
||||
**Not a TLE source.** Bring your own TLEs from Space-Track, CelesTrak, or any other provider. pg_orrery parses and propagates them; it doesn't fetch them.
|
||||
|
||||
**Not a replacement for SPICE.** No BSP kernel support, no light-time iteration, no aberration corrections at the IAU 2000A level. With DE enabled, pg_orrery matches SPICE on raw planet position accuracy — the remaining gap is in apparent-position corrections (aberration, light-time, nutation) that matter for sub-arcsecond apparent coordinates.
|
||||
|
||||
**Not a full mission design tool.** The Lambert solver handles ballistic two-body transfers — no low-thrust trajectories, no gravity assists, no multi-body optimization. For full mission design, use GMAT or poliastro.
|
||||
269
docs/src/content/docs/guides/comets-asteroids.mdx
Normal file
269
docs/src/content/docs/guides/comets-asteroids.mdx
Normal file
@ -0,0 +1,269 @@
|
||||
---
|
||||
title: Comets and Asteroids
|
||||
sidebar:
|
||||
order: 5
|
||||
---
|
||||
|
||||
import { Steps, Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
pg_orrery propagates comets and asteroids using two-body Keplerian mechanics. You provide six classical orbital elements from the Minor Planet Center (MPC) or any other source, and pg_orrery computes the body's heliocentric position at any time. Combined with Earth's position from VSOP87, you can observe the body from any location on Earth.
|
||||
|
||||
## How you do it today
|
||||
|
||||
Tracking comets and asteroids typically involves:
|
||||
|
||||
- **JPL Small-Body Database (SBDB)**: Look up an object, get its orbital elements, request an ephemeris. One object at a time, web-based.
|
||||
- **Find_Orb**: Fit orbits from observations and propagate forward. Powerful but desktop-only, primarily for orbit determination rather than ephemeris computation.
|
||||
- **Skyfield**: Can propagate comets from MPC elements, but you need to load a planetary ephemeris for the Earth's position and write the observation pipeline yourself.
|
||||
- **Minor Planet Center (MPC)**: Publishes orbital elements for over 1.3 million objects. Getting batch ephemerides means downloading elements and running them through your own propagation code.
|
||||
|
||||
The pattern is familiar: download elements, propagate in Python or C, transform to observer coordinates, and import results into your database.
|
||||
|
||||
## What changes with pg_orrery
|
||||
|
||||
Two functions handle comet/asteroid computation:
|
||||
|
||||
| Function | What it does |
|
||||
|---|---|
|
||||
| `kepler_propagate(q, e, i, omega, Omega, T, time)` | Propagates orbital elements to a heliocentric position (AU) |
|
||||
| `comet_observe(q, e, i, omega, Omega, T, ex, ey, ez, observer, time)` | Full observation pipeline: propagate + geocentric transform + topocentric |
|
||||
|
||||
`kepler_propagate()` solves Kepler's equation for elliptic (e < 1), parabolic (e = 1), and hyperbolic (e > 1) orbits. The solver handles all three cases with appropriate numerical methods.
|
||||
|
||||
`comet_observe()` wraps the full chain: propagate the comet's position, subtract Earth's heliocentric position, and transform to horizon coordinates. You supply Earth's position as three floats (ecliptic J2000, AU) because you might want to compute it once and reuse it across many comets.
|
||||
|
||||
The parameters map directly to MPC orbital element format:
|
||||
|
||||
| Parameter | MPC field | Units |
|
||||
|---|---|---|
|
||||
| `q` | Perihelion distance | AU |
|
||||
| `e` | Eccentricity | dimensionless |
|
||||
| `i` | Inclination | degrees |
|
||||
| `omega` | Argument of perihelion | degrees |
|
||||
| `Omega` | Longitude of ascending node | degrees |
|
||||
| `T` | Perihelion time | Julian date |
|
||||
|
||||
## What pg_orrery does not replace
|
||||
|
||||
<Aside type="caution" title="Two-body limitations">
|
||||
Keplerian propagation assumes the body is influenced only by the Sun. Real small bodies experience planetary perturbations that accumulate over time.
|
||||
</Aside>
|
||||
|
||||
- **No perturbations.** Jupiter alone can shift a comet's position by degrees over a few years. Two-body propagation is most accurate near perihelion, within a few months of the elements' epoch.
|
||||
- **No non-gravitational forces.** Comet outgassing produces accelerations not captured by Keplerian mechanics. For long-period comets far from the Sun, this is negligible. For short-period comets near perihelion, it matters.
|
||||
- **No magnitude estimation.** pg_orrery returns position only. Comet brightness depends on heliocentric distance, geocentric distance, and a magnitude slope parameter that varies per comet.
|
||||
- **No orbit determination.** pg_orrery propagates known orbits. It does not fit orbits from observations.
|
||||
|
||||
For MPC elements less than a few months old, two-body propagation is typically accurate to a few arcminutes for asteroids and tens of arcminutes for comets. Fresh elements give better results.
|
||||
|
||||
## Try it
|
||||
|
||||
### Circular orbit sanity check
|
||||
|
||||
A body in a circular orbit at 1 AU with all angles zero should return to its starting position after one year:
|
||||
|
||||
```sql
|
||||
-- At perihelion (T=0), position should be (1, 0, 0) AU
|
||||
SELECT round(helio_x(kepler_propagate(
|
||||
1.0, 0.0, 0.0, 0.0, 0.0,
|
||||
2451545.0, -- J2000.0
|
||||
'2000-01-01 12:00:00+00'))::numeric, 6) AS x,
|
||||
round(helio_y(kepler_propagate(
|
||||
1.0, 0.0, 0.0, 0.0, 0.0,
|
||||
2451545.0,
|
||||
'2000-01-01 12:00:00+00'))::numeric, 6) AS y;
|
||||
```
|
||||
|
||||
At time = perihelion, the position is exactly (q, 0, 0) in the orbital plane. After a quarter orbit (~91 days), it moves to approximately (0, 1, 0).
|
||||
|
||||
### Eccentric elliptic orbit
|
||||
|
||||
An orbit with e=0.5 and q=0.5 AU has a semi-major axis of 1.0 AU and the same period as Earth, but a very different shape:
|
||||
|
||||
```sql
|
||||
-- Position over one orbit
|
||||
SELECT t::date AS date,
|
||||
round(helio_distance(kepler_propagate(
|
||||
0.5, 0.5, 0.0, 0.0, 0.0,
|
||||
2451545.0, t))::numeric, 4) AS dist_au
|
||||
FROM generate_series(
|
||||
'2000-01-01 12:00:00+00'::timestamptz,
|
||||
'2001-01-01 12:00:00+00'::timestamptz,
|
||||
interval '30 days'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
The distance ranges from 0.5 AU (perihelion) to 1.5 AU (aphelion). This is the classic comet behavior: fast and close to the Sun at perihelion, slow and distant at aphelion.
|
||||
|
||||
### Inclined orbit
|
||||
|
||||
Orbital inclination rotates the orbital plane out of the ecliptic:
|
||||
|
||||
```sql
|
||||
-- A polar orbit (i=90 deg) at 1 AU
|
||||
SELECT round(helio_x(kepler_propagate(
|
||||
1.0, 0.0, 90.0, 0.0, 0.0,
|
||||
2451545.0,
|
||||
'2000-01-01 12:00:00+00'))::numeric, 6) AS x,
|
||||
round(helio_z(kepler_propagate(
|
||||
1.0, 0.0, 90.0, 0.0, 0.0,
|
||||
2451545.0,
|
||||
'2000-01-01 12:00:00+00'))::numeric, 6) AS z;
|
||||
```
|
||||
|
||||
At perihelion, the position is still along the node line (x-axis) regardless of inclination. The inclination only shows up when the body moves away from the node.
|
||||
|
||||
### Hyperbolic orbit
|
||||
|
||||
Interstellar objects like 'Oumuamua travel on hyperbolic trajectories (e > 1):
|
||||
|
||||
```sql
|
||||
-- Hyperbolic orbit: e=1.5, q=1.0 AU
|
||||
-- At perihelion
|
||||
SELECT round(helio_x(kepler_propagate(
|
||||
1.0, 1.5, 0.0, 0.0, 0.0,
|
||||
2451545.0,
|
||||
'2000-01-01 12:00:00+00'))::numeric, 6) AS x_at_perihelion;
|
||||
|
||||
-- 6 months later: body is receding rapidly
|
||||
SELECT round(helio_distance(kepler_propagate(
|
||||
1.0, 1.5, 0.0, 0.0, 0.0,
|
||||
2451545.0,
|
||||
'2000-07-01 12:00:00+00'))::numeric, 2) AS dist_6mo;
|
||||
```
|
||||
|
||||
The body approaches from infinity, swings past the Sun at perihelion distance, and departs on a hyperbola.
|
||||
|
||||
### Near-parabolic comet
|
||||
|
||||
Many long-period comets have eccentricities very close to 1.0. pg_orrery handles the parabolic case (e=1.0 exactly) with a dedicated Barker equation solver:
|
||||
|
||||
```sql
|
||||
SELECT round(helio_x(kepler_propagate(
|
||||
1.0, 1.0, 0.0, 0.0, 0.0,
|
||||
2451545.0,
|
||||
'2000-01-01 12:00:00+00'))::numeric, 6) AS x_parabolic;
|
||||
```
|
||||
|
||||
### Track a comet with real MPC elements
|
||||
|
||||
Here is how you would observe a comet using elements from the Minor Planet Center. This example uses hypothetical elements for a Halley-type orbit:
|
||||
|
||||
<Steps>
|
||||
1. **Get Earth's heliocentric position at the observation time:**
|
||||
|
||||
```sql
|
||||
SELECT helio_x(planet_heliocentric(3, '2024-06-15 04:00:00+00')) AS ex,
|
||||
helio_y(planet_heliocentric(3, '2024-06-15 04:00:00+00')) AS ey,
|
||||
helio_z(planet_heliocentric(3, '2024-06-15 04:00:00+00')) AS ez;
|
||||
```
|
||||
|
||||
2. **Observe the comet using all parameters together:**
|
||||
|
||||
```sql
|
||||
WITH earth AS (
|
||||
SELECT planet_heliocentric(3, '2024-06-15 04:00:00+00') AS pos
|
||||
)
|
||||
SELECT round(topo_azimuth(comet_observe(
|
||||
0.587, 0.967, 162.3, 111.3, 58.4, 2446467.4,
|
||||
helio_x(pos), helio_y(pos), helio_z(pos),
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-15 04:00:00+00'))::numeric, 1) AS az,
|
||||
round(topo_elevation(comet_observe(
|
||||
0.587, 0.967, 162.3, 111.3, 58.4, 2446467.4,
|
||||
helio_x(pos), helio_y(pos), helio_z(pos),
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-15 04:00:00+00'))::numeric, 1) AS el,
|
||||
round(topo_range(comet_observe(
|
||||
0.587, 0.967, 162.3, 111.3, 58.4, 2446467.4,
|
||||
helio_x(pos), helio_y(pos), helio_z(pos),
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-15 04:00:00+00'))::numeric, 0) AS range_km
|
||||
FROM earth;
|
||||
```
|
||||
|
||||
The orbital elements are: q=0.587 AU, e=0.967, i=162.3 deg, omega=111.3 deg, Omega=58.4 deg, T=JD 2446467.4.
|
||||
</Steps>
|
||||
|
||||
### Store a comet catalog
|
||||
|
||||
For batch operations, store orbital elements in a table:
|
||||
|
||||
```sql
|
||||
CREATE TABLE comets (
|
||||
designation text PRIMARY KEY,
|
||||
name text,
|
||||
q_au float8 NOT NULL,
|
||||
eccentricity float8 NOT NULL,
|
||||
inclination_deg float8 NOT NULL,
|
||||
arg_peri_deg float8 NOT NULL,
|
||||
long_node_deg float8 NOT NULL,
|
||||
perihelion_jd float8 NOT NULL,
|
||||
epoch_jd float8
|
||||
);
|
||||
|
||||
-- Insert a few examples
|
||||
INSERT INTO comets VALUES
|
||||
('1P', 'Halley', 0.587, 0.967, 162.3, 111.3, 58.4, 2446467.4, 2446480.0),
|
||||
('2P', 'Encke', 0.336, 0.847, 11.8, 186.5, 334.6, 2460585.0, 2460600.0),
|
||||
('67P', 'C-G', 1.243, 0.641, 7.0, 12.8, 50.1, 2457257.0, 2457260.0);
|
||||
```
|
||||
|
||||
### Batch observe all comets
|
||||
|
||||
```sql
|
||||
WITH earth AS (
|
||||
SELECT planet_heliocentric(3, '2024-06-15 04:00:00+00') AS pos
|
||||
)
|
||||
SELECT c.name,
|
||||
round(topo_azimuth(obs)::numeric, 1) AS az,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el,
|
||||
round(topo_range(obs)::numeric, 0) AS range_km
|
||||
FROM comets c, earth,
|
||||
LATERAL comet_observe(
|
||||
c.q_au, c.eccentricity, c.inclination_deg,
|
||||
c.arg_peri_deg, c.long_node_deg, c.perihelion_jd,
|
||||
helio_x(earth.pos), helio_y(earth.pos), helio_z(earth.pos),
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-15 04:00:00+00') obs
|
||||
WHERE topo_elevation(obs) > 0
|
||||
ORDER BY topo_elevation(obs) DESC;
|
||||
```
|
||||
|
||||
This observes every comet in the catalog and filters to those above the horizon. The Earth position is computed once and reused for all comets.
|
||||
|
||||
### Heliocentric distance over time
|
||||
|
||||
Track how a comet's distance from the Sun changes through its orbit:
|
||||
|
||||
```sql
|
||||
SELECT t::date AS date,
|
||||
round(helio_distance(kepler_propagate(
|
||||
0.336, 0.847, 11.8, 186.5, 334.6,
|
||||
2460585.0, t))::numeric, 3) AS encke_dist_au
|
||||
FROM generate_series(
|
||||
'2024-01-01'::timestamptz,
|
||||
'2024-12-31'::timestamptz,
|
||||
interval '15 days'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
Comet Encke (q=0.336 AU, e=0.847) ranges from 0.336 AU at perihelion to about 4.1 AU at aphelion. Its 3.3-year period means it passes through the inner solar system frequently.
|
||||
|
||||
### Verify: distance conservation for circular orbit
|
||||
|
||||
A useful sanity check. A circular orbit should maintain constant heliocentric distance:
|
||||
|
||||
```sql
|
||||
SELECT t::date AS date,
|
||||
round(helio_distance(kepler_propagate(
|
||||
1.0, 0.0, 0.0, 0.0, 0.0,
|
||||
2451545.0, t))::numeric, 6) AS dist_au
|
||||
FROM generate_series(
|
||||
'2000-01-01'::timestamptz,
|
||||
'2001-01-01'::timestamptz,
|
||||
interval '60 days'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
Every row should read 1.000000 AU. If it does, the Kepler solver is working correctly.
|
||||
290
docs/src/content/docs/guides/conjunction-screening.mdx
Normal file
290
docs/src/content/docs/guides/conjunction-screening.mdx
Normal file
@ -0,0 +1,290 @@
|
||||
---
|
||||
title: Conjunction Screening
|
||||
sidebar:
|
||||
order: 8
|
||||
---
|
||||
|
||||
import { Steps, Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Conjunction screening identifies pairs of satellites that might approach each other closely enough to pose a collision risk. The brute-force approach -- computing pairwise distances for all objects in the catalog at every time step -- scales as O(n^2) and is impractical for large catalogs. pg_orrery solves this with a GiST index on the `tle` type that enables spatial filtering by altitude band and orbital inclination, reducing the candidate set before running full propagation.
|
||||
|
||||
## How you do it today
|
||||
|
||||
Operational conjunction screening uses several established tools and data sources:
|
||||
|
||||
- **STK/SOCRATES** (AGI): Commercial tool that monitors the catalog and generates close-approach reports. Industry standard for satellite operators. Expensive.
|
||||
- **Space-Track CDMs**: The 18th Space Defense Squadron publishes Conjunction Data Messages (CDMs) for predicted close approaches. Free but requires registration and covers only US-tracked objects.
|
||||
- **CelesTrak SOCRATES**: Dr. Kelso's web-based close-approach listing. Updated regularly, covers the full public catalog. Not queryable; you read reports.
|
||||
- **Python scripts**: Propagate the catalog in a loop, compute pairwise distances, filter by threshold. Works for small catalogs. Does not scale.
|
||||
|
||||
The fundamental challenge: a catalog of 25,000+ tracked objects produces over 300 million unique pairs. Even checking each pair at a single epoch takes significant time. Checking over a 7-day window at 1-minute resolution is computationally prohibitive without pre-filtering.
|
||||
|
||||
## What changes with pg_orrery
|
||||
|
||||
pg_orrery attacks the problem in two stages:
|
||||
|
||||
**Stage 1: GiST index reduces candidates.** The GiST index on the `tle` column stores a 2-D key for each TLE: altitude band (perigee to apogee) and inclination range. The `&&` operator tests whether two TLEs occupy overlapping regions in this 2-D space. Only TLEs that share an altitude shell AND a similar inclination can possibly conjunct. This typically reduces 300 million pairs to a few thousand candidates.
|
||||
|
||||
**Stage 2: Full propagation verifies candidates.** For the remaining candidates, `tle_distance()` computes the actual Euclidean distance between two TLEs at a given time using full SGP4/SDP4 propagation. Step through time at the required resolution and filter to close approaches.
|
||||
|
||||
The two operators:
|
||||
|
||||
| Operator | Type | What it checks |
|
||||
|---|---|---|
|
||||
| `tle && tle` | boolean | Altitude band AND inclination range overlap |
|
||||
| `tle <-> tle` | float8 | Minimum altitude-band separation in km |
|
||||
|
||||
The `&&` operator is used for overlap queries (find all objects in the same shell). The `<->` operator is used for nearest-neighbor queries (find the N closest objects by altitude separation).
|
||||
|
||||
## What pg_orrery does not replace
|
||||
|
||||
<Aside type="caution" title="Screening, not assessment">
|
||||
GiST-based conjunction screening is a coarse filter. It finds candidates that share an orbital shell. It does not determine whether two objects will actually come close at a specific time.
|
||||
</Aside>
|
||||
|
||||
- **Not a probability of collision.** pg_orrery does not compute Pc (probability of collision). It identifies objects in overlapping orbital shells and computes distances at discrete time steps. For Pc calculation, use CARA (Conjunction Assessment Risk Analysis) methods.
|
||||
- **No covariance propagation.** SGP4 does not produce covariance matrices. The distance values have no uncertainty bounds. For operational conjunction assessment, use SP ephemerides with covariance (from CDMs or owner/operator data).
|
||||
- **Altitude-band approximation.** The GiST key uses perigee-to-apogee altitude as a 1-D range and inclination as a second dimension. Two TLEs can share an altitude shell and never approach because their RAANs or phases are far apart. Always follow GiST filtering with full propagation.
|
||||
- **No maneuver planning.** pg_orrery identifies close approaches. It does not compute avoidance maneuvers (delta-v, timing, constraints).
|
||||
|
||||
The workflow is: GiST narrows → `tle_distance()` verifies → operator/analyst decides.
|
||||
|
||||
## Try it
|
||||
|
||||
### Set up a test catalog
|
||||
|
||||
Create a small catalog with satellites at different orbital regimes:
|
||||
|
||||
```sql
|
||||
CREATE TABLE catalog (
|
||||
norad_id integer PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
tle tle NOT NULL
|
||||
);
|
||||
|
||||
-- ISS (LEO, ~400km, inc 51.64 deg)
|
||||
INSERT INTO catalog VALUES (25544, 'ISS',
|
||||
'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001');
|
||||
|
||||
-- Hubble (LEO, ~540km, inc 28.47 deg)
|
||||
INSERT INTO catalog VALUES (20580, 'Hubble',
|
||||
'1 20580U 90037B 24001.50000000 .00000790 00000+0 39573-4 0 9992
|
||||
2 20580 28.4705 61.4398 0002797 317.3115 42.7577 15.09395228 00008');
|
||||
|
||||
-- GPS IIR-M (MEO, ~20200km, inc 55.44 deg)
|
||||
INSERT INTO catalog VALUES (28874, 'GPS-IIR',
|
||||
'1 28874U 05038A 24001.50000000 .00000012 00000+0 00000+0 0 9993
|
||||
2 28874 55.4408 300.3467 0117034 51.6543 309.5420 2.00557079 00006');
|
||||
|
||||
-- Equatorial LEO: same altitude as ISS but inc ~5 deg
|
||||
INSERT INTO catalog VALUES (99901, 'Equatorial-LEO',
|
||||
'1 99901U 24999A 24001.50000000 .00016717 00000-0 10270-3 0 9990
|
||||
2 99901 5.0000 208.9163 0006703 30.1694 61.7520 15.50100486 00001');
|
||||
```
|
||||
|
||||
### Create the GiST index
|
||||
|
||||
```sql
|
||||
CREATE INDEX catalog_orbit_gist ON catalog USING gist (tle);
|
||||
```
|
||||
|
||||
The index builds in milliseconds for a small table. For a full 25,000-object catalog, expect about 200ms.
|
||||
|
||||
### Check orbital parameters
|
||||
|
||||
Before screening, inspect the orbital characteristics of the catalog:
|
||||
|
||||
```sql
|
||||
SELECT name,
|
||||
round(tle_perigee(tle)::numeric, 0) AS perigee_km,
|
||||
round(tle_apogee(tle)::numeric, 0) AS apogee_km,
|
||||
round(tle_inclination(tle)::numeric, 1) AS inc_deg,
|
||||
round(tle_period(tle)::numeric, 1) AS period_min
|
||||
FROM catalog
|
||||
ORDER BY tle_perigee(tle);
|
||||
```
|
||||
|
||||
### Overlap queries with &&
|
||||
|
||||
Find all pairs of satellites in overlapping orbital shells:
|
||||
|
||||
```sql
|
||||
SELECT a.name AS sat_a,
|
||||
b.name AS sat_b,
|
||||
a.tle && b.tle AS overlaps
|
||||
FROM catalog a, catalog b
|
||||
WHERE a.norad_id < b.norad_id
|
||||
ORDER BY a.name, b.name;
|
||||
```
|
||||
|
||||
Key insight: ISS and Equatorial-LEO are at the same altitude but different inclinations. The `&&` operator returns **false** for this pair because the 2-D key requires overlap in BOTH altitude AND inclination. Two objects at the same altitude but in very different orbital planes are unlikely to conjunct.
|
||||
|
||||
### Altitude-band distance with `<->`
|
||||
|
||||
The `<->` operator returns the minimum separation between altitude bands, in km:
|
||||
|
||||
```sql
|
||||
SELECT a.name AS sat_a,
|
||||
b.name AS sat_b,
|
||||
round((a.tle <-> b.tle)::numeric, 0) AS alt_separation_km
|
||||
FROM catalog a, catalog b
|
||||
WHERE a.norad_id < b.norad_id
|
||||
ORDER BY a.tle <-> b.tle;
|
||||
```
|
||||
|
||||
ISS and Equatorial-LEO should show ~0 km separation (same altitude shell). ISS and GPS should show ~19,800 km (vastly different orbits).
|
||||
|
||||
### GiST index scan: find overlapping orbits
|
||||
|
||||
Force the query planner to use the index and find all objects in the same shell as the ISS:
|
||||
|
||||
```sql
|
||||
SET enable_seqscan = off;
|
||||
|
||||
SELECT name
|
||||
FROM catalog
|
||||
WHERE tle && (SELECT tle FROM catalog WHERE norad_id = 25544)
|
||||
ORDER BY name;
|
||||
|
||||
RESET enable_seqscan;
|
||||
```
|
||||
|
||||
This should return only ISS itself (and not Equatorial-LEO, which has a different inclination). The GiST index scan avoids checking every object in the catalog.
|
||||
|
||||
### K-nearest-neighbor by altitude
|
||||
|
||||
Find the 3 closest objects to the ISS by altitude band separation, ordered by distance:
|
||||
|
||||
```sql
|
||||
SET enable_seqscan = off;
|
||||
|
||||
SELECT name,
|
||||
round((tle <-> (SELECT tle FROM catalog WHERE norad_id = 25544))::numeric, 0) AS alt_dist_km
|
||||
FROM catalog
|
||||
WHERE norad_id != 25544
|
||||
ORDER BY tle <-> (SELECT tle FROM catalog WHERE norad_id = 25544)
|
||||
LIMIT 3;
|
||||
|
||||
RESET enable_seqscan;
|
||||
```
|
||||
|
||||
This uses the GiST distance operator for efficient ordering. PostgreSQL's KNN-GiST infrastructure handles this without computing all distances upfront.
|
||||
|
||||
### Self-overlap is always true
|
||||
|
||||
Every TLE overlaps with itself:
|
||||
|
||||
```sql
|
||||
SELECT name,
|
||||
tle && tle AS self_overlap
|
||||
FROM catalog
|
||||
ORDER BY name;
|
||||
```
|
||||
|
||||
All rows should return `true`.
|
||||
|
||||
### Full conjunction screening workflow
|
||||
|
||||
The complete two-stage workflow for a larger catalog:
|
||||
|
||||
<Steps>
|
||||
1. **Build the catalog and index:**
|
||||
|
||||
```sql
|
||||
-- Assuming your catalog table is already populated from CelesTrak or Space-Track
|
||||
CREATE INDEX IF NOT EXISTS catalog_orbit_gist ON catalog USING gist (tle);
|
||||
```
|
||||
|
||||
2. **Stage 1: GiST filter to find candidates for a target satellite:**
|
||||
|
||||
```sql
|
||||
CREATE TEMPORARY TABLE candidates AS
|
||||
SELECT c.norad_id, c.name, c.tle
|
||||
FROM catalog c
|
||||
WHERE c.tle && (SELECT tle FROM catalog WHERE norad_id = 25544)
|
||||
AND c.norad_id != 25544;
|
||||
```
|
||||
|
||||
For the ISS in a 25,000-object catalog, this typically returns a few hundred candidates.
|
||||
|
||||
3. **Stage 2: Time-resolved distance computation:**
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT tle FROM catalog WHERE norad_id = 25544
|
||||
)
|
||||
SELECT c.name,
|
||||
t AS check_time,
|
||||
round(tle_distance(iss.tle, c.tle, t)::numeric, 1) AS dist_km
|
||||
FROM candidates c, iss,
|
||||
generate_series(
|
||||
'2024-01-01 00:00:00+00'::timestamptz,
|
||||
'2024-01-02 00:00:00+00'::timestamptz,
|
||||
interval '1 minute'
|
||||
) AS t
|
||||
WHERE tle_distance(iss.tle, c.tle, t) < 25.0
|
||||
ORDER BY dist_km;
|
||||
```
|
||||
|
||||
This propagates each candidate pair at 1-minute resolution over 24 hours and filters to approaches within 25 km. Only the GiST candidates are checked, not the full catalog.
|
||||
|
||||
4. **Review results and take action.**
|
||||
|
||||
The output lists object name, time of closest approach, and distance. An analyst or automated system decides whether to issue a CDM, plan a maneuver, or accept the risk.
|
||||
</Steps>
|
||||
|
||||
### Screening multiple target satellites
|
||||
|
||||
Extend the workflow to screen for conjunctions between any pair of objects in a subset:
|
||||
|
||||
```sql
|
||||
-- All pairs in the LEO catalog (tle_perigee < 2000 km) that share an orbital shell
|
||||
SELECT a.name AS sat_a,
|
||||
b.name AS sat_b,
|
||||
round((a.tle <-> b.tle)::numeric, 0) AS alt_sep_km,
|
||||
round(tle_distance(a.tle, b.tle, '2024-01-01 12:00:00+00')::numeric, 0) AS actual_dist_km
|
||||
FROM catalog a, catalog b
|
||||
WHERE a.norad_id < b.norad_id
|
||||
AND a.tle && b.tle
|
||||
AND tle_perigee(a.tle) < 2000
|
||||
AND tle_perigee(b.tle) < 2000
|
||||
ORDER BY actual_dist_km;
|
||||
```
|
||||
|
||||
<Aside type="tip" title="Performance scaling">
|
||||
The GiST index is the key to scaling. Without it, screening a 25,000-object catalog for all-vs-all conjunctions means 300 million pair evaluations. With GiST, the `&&` operator reduces this to tens of thousands of candidate pairs. The `tle_distance()` computation on candidates is then feasible even at fine time resolution.
|
||||
</Aside>
|
||||
|
||||
### Monitoring over time
|
||||
|
||||
Run a conjunction check at regular intervals and store results for trend analysis:
|
||||
|
||||
```sql
|
||||
CREATE TABLE conjunction_events (
|
||||
id serial PRIMARY KEY,
|
||||
sat_a integer NOT NULL,
|
||||
sat_b integer NOT NULL,
|
||||
event_time timestamptz NOT NULL,
|
||||
dist_km float8 NOT NULL,
|
||||
checked_at timestamptz DEFAULT now()
|
||||
);
|
||||
|
||||
-- Periodic screening job (run daily or as needed)
|
||||
INSERT INTO conjunction_events (sat_a, sat_b, event_time, dist_km)
|
||||
WITH iss AS (
|
||||
SELECT norad_id, tle FROM catalog WHERE norad_id = 25544
|
||||
)
|
||||
SELECT iss.norad_id, c.norad_id, t, tle_distance(iss.tle, c.tle, t)
|
||||
FROM catalog c, iss,
|
||||
generate_series(
|
||||
now(),
|
||||
now() + interval '7 days',
|
||||
interval '5 minutes'
|
||||
) AS t
|
||||
WHERE c.tle && iss.tle
|
||||
AND c.norad_id != iss.norad_id
|
||||
AND tle_distance(iss.tle, c.tle, t) < 50.0;
|
||||
```
|
||||
|
||||
This builds a history of close approaches that you can query, trend, and alert on. The GiST filter ensures it runs efficiently even against a full catalog.
|
||||
167
docs/src/content/docs/guides/de-ephemeris.mdx
Normal file
167
docs/src/content/docs/guides/de-ephemeris.mdx
Normal file
@ -0,0 +1,167 @@
|
||||
---
|
||||
title: JPL DE Ephemeris
|
||||
sidebar:
|
||||
order: 9
|
||||
---
|
||||
|
||||
import { Steps, Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
pg_orrery v0.3.0 adds optional support for JPL Development Ephemeris files (DE440/DE441), bringing positional accuracy from VSOP87's ~1 arcsecond to DE441's ~0.1 milliarcsecond. The upgrade path is designed so that nothing changes unless you opt in.
|
||||
|
||||
## When you need DE
|
||||
|
||||
VSOP87 is sufficient for most purposes --- visual observation planning, rise/set times, conjunction identification, telescope pointing at optical wavelengths. You need DE when:
|
||||
|
||||
- **GHz dish pointing.** A 10m dish at 10 GHz has a ~0.1 degree beamwidth. VSOP87's 1 arcsecond error is well within the beam, but systematic campaigns benefit from the tighter DE error budget.
|
||||
- **Precision astrometry.** Comparing CCD field positions against computed planet positions at the sub-arcsecond level.
|
||||
- **Occultation timing.** Predicting when a planet or asteroid will occult a star requires milliarcsecond precision in the planet's geocentric position.
|
||||
- **Interplanetary mission design.** Lambert transfer C3 values are sensitive to planet position accuracy. DE441 eliminates VSOP87 as an error source.
|
||||
|
||||
<Aside type="tip" title="Zero-change default">
|
||||
If you don't configure a DE file, all `_de()` functions silently fall back to VSOP87/ELP2000-82B. You get identical results to the non-DE variants, with no performance penalty. The extension ships with VSOP87 compiled in --- DE is strictly opt-in.
|
||||
</Aside>
|
||||
|
||||
## Setup
|
||||
|
||||
<Steps>
|
||||
1. **Obtain a DE file.** Download DE441 (~3.1 GB, covers -13200 to +17191) or DE440 (~115 MB, covers 1550 to 2650) from JPL:
|
||||
|
||||
```bash
|
||||
# DE440 (smaller, covers 1550-2650, sufficient for most uses)
|
||||
curl -O https://ssd.jpl.nasa.gov/ftp/eph/planets/Linux/de440/linux_p1550p2650.440
|
||||
|
||||
# DE441 (full range, large file)
|
||||
curl -O https://ssd.jpl.nasa.gov/ftp/eph/planets/Linux/de441/linux_m13000p17000.441
|
||||
```
|
||||
|
||||
2. **Place the file** where PostgreSQL can read it:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/lib/postgres/pg_orrery
|
||||
sudo cp linux_p1550p2650.440 /var/lib/postgres/pg_orrery/de440.bin
|
||||
sudo chown postgres:postgres /var/lib/postgres/pg_orrery/de440.bin
|
||||
```
|
||||
|
||||
3. **Set the GUC** (requires superuser):
|
||||
|
||||
```sql
|
||||
ALTER SYSTEM SET pg_orrery.ephemeris_path = '/var/lib/postgres/pg_orrery/de440.bin';
|
||||
SELECT pg_reload_conf();
|
||||
```
|
||||
|
||||
4. **Verify:**
|
||||
|
||||
```sql
|
||||
SELECT * FROM pg_orrery_ephemeris_info();
|
||||
```
|
||||
|
||||
You should see `provider = 'JPL_DE'` with the file's JD range and version.
|
||||
</Steps>
|
||||
|
||||
## Using DE functions
|
||||
|
||||
Every VSOP87 function has a `_de()` counterpart with the same signature:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="VSOP87 (existing)">
|
||||
```sql
|
||||
-- Always VSOP87, always IMMUTABLE
|
||||
SELECT topo_azimuth(planet_observe(4, '40.0N 105.3W 1655m'::observer, now())) AS mars_az;
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="DE (new)">
|
||||
```sql
|
||||
-- Uses DE if configured, falls back to VSOP87 otherwise. STABLE.
|
||||
SELECT topo_azimuth(planet_observe_de(4, '40.0N 105.3W 1655m'::observer, now())) AS mars_az;
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `_de()` variants share the same parameter types, return types, and body ID conventions. You can swap between them by adding or removing `_de` from the function name.
|
||||
|
||||
### All planets with DE positions
|
||||
|
||||
```sql
|
||||
SELECT body_id,
|
||||
CASE body_id
|
||||
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
|
||||
WHEN 4 THEN 'Mars' WHEN 5 THEN 'Jupiter'
|
||||
WHEN 6 THEN 'Saturn' WHEN 7 THEN 'Uranus'
|
||||
WHEN 8 THEN 'Neptune'
|
||||
END AS planet,
|
||||
round(topo_azimuth(planet_observe_de(body_id, '40.0N 105.3W 1655m'::observer, now()))::numeric, 4) AS az,
|
||||
round(topo_elevation(planet_observe_de(body_id, '40.0N 105.3W 1655m'::observer, now()))::numeric, 4) AS el
|
||||
FROM unnest(ARRAY[1,2,4,5,6,7,8]) AS body_id
|
||||
WHERE topo_elevation(planet_observe_de(body_id, '40.0N 105.3W 1655m'::observer, now())) > 0
|
||||
ORDER BY el DESC;
|
||||
```
|
||||
|
||||
### Moon via DE
|
||||
|
||||
```sql
|
||||
SELECT round(topo_azimuth(moon_observe_de('40.0N 105.3W 1655m'::observer, now()))::numeric, 2) AS az,
|
||||
round(topo_elevation(moon_observe_de('40.0N 105.3W 1655m'::observer, now()))::numeric, 2) AS el,
|
||||
round(topo_range(moon_observe_de('40.0N 105.3W 1655m'::observer, now()))::numeric, 0) AS range_km;
|
||||
```
|
||||
|
||||
### Lambert transfers with DE positions
|
||||
|
||||
```sql
|
||||
-- Earth-Mars transfer with DE-quality planet positions
|
||||
SELECT * FROM lambert_transfer_de(3, 4, '2026-05-01 00:00:00+00', '2027-01-15 00:00:00+00');
|
||||
```
|
||||
|
||||
### Cross-validation: DE vs VSOP87
|
||||
|
||||
Compare DE and VSOP87 results to see the sub-arcsecond difference:
|
||||
|
||||
```sql
|
||||
SELECT body_id,
|
||||
round(helio_distance(planet_heliocentric(body_id, '2024-06-21 12:00:00+00'))::numeric, 10) AS vsop87_r,
|
||||
round(helio_distance(planet_heliocentric_de(body_id, '2024-06-21 12:00:00+00'))::numeric, 10) AS de_r,
|
||||
round((helio_distance(planet_heliocentric_de(body_id, '2024-06-21 12:00:00+00'))
|
||||
- helio_distance(planet_heliocentric(body_id, '2024-06-21 12:00:00+00')))::numeric * 149597870.7, 2) AS diff_km
|
||||
FROM generate_series(1, 8) AS body_id;
|
||||
```
|
||||
|
||||
## Accuracy comparison
|
||||
|
||||
| Provider | Theory | Accuracy | Volatility | Data Source |
|
||||
|----------|--------|----------|------------|-------------|
|
||||
| VSOP87 | Fourier series (Bretagnon 1988) | ~1 arcsecond (inner planets), up to 40 arcseconds (Neptune, +/- 4000 yr) | IMMUTABLE | Compiled-in coefficients |
|
||||
| ELP2000-82B | Fourier series (Chapront 1983) | ~2 arcseconds (longitude) | IMMUTABLE | Compiled-in coefficients |
|
||||
| JPL DE440 | Numerical integration | ~0.1 milliarcsecond | STABLE | External binary file (115 MB) |
|
||||
| JPL DE441 | Numerical integration | ~0.1 milliarcsecond | STABLE | External binary file (3.1 GB) |
|
||||
|
||||
<Aside type="note" title="The volatility trade-off">
|
||||
IMMUTABLE functions can be used in expression indexes and are constant-folded during planning. STABLE functions execute once per row per statement. If you have expression indexes on VSOP87 functions (e.g., almanac tables), keep them on the non-DE variants. Use DE functions for queries where accuracy matters more than planning-time optimization.
|
||||
</Aside>
|
||||
|
||||
## Diagnostics
|
||||
|
||||
The `pg_orrery_ephemeris_info()` function reports the current state:
|
||||
|
||||
```sql
|
||||
SELECT * FROM pg_orrery_ephemeris_info();
|
||||
```
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `provider` | text | `'VSOP87'` or `'JPL_DE'` |
|
||||
| `file_path` | text | Path to DE file (empty if VSOP87-only) |
|
||||
| `start_jd` | float8 | First Julian Date in the DE file |
|
||||
| `end_jd` | float8 | Last Julian Date in the DE file |
|
||||
| `version` | int4 | DE version number (440, 441, etc.) |
|
||||
| `au_km` | float8 | AU value from the DE header |
|
||||
|
||||
When no DE file is configured, `provider` returns `'VSOP87'` and the remaining columns are defaults.
|
||||
|
||||
## Fallback behavior
|
||||
|
||||
DE functions never fail silently. The fallback rules:
|
||||
|
||||
1. **No GUC set** (default): Fall back to VSOP87/ELP2000-82B silently. No NOTICE, no overhead.
|
||||
2. **GUC set, file works**: Use DE positions.
|
||||
3. **GUC set, query-time failure** (JD out of range, I/O error): Emit `NOTICE` explaining the fallback, then use VSOP87/ELP2000-82B.
|
||||
|
||||
This means `_de()` functions always return a valid result. They never abort due to DE-specific issues --- at worst, they degrade to VSOP87 accuracy with a NOTICE explaining why.
|
||||
271
docs/src/content/docs/guides/interplanetary-trajectories.mdx
Normal file
271
docs/src/content/docs/guides/interplanetary-trajectories.mdx
Normal file
@ -0,0 +1,271 @@
|
||||
---
|
||||
title: Interplanetary Trajectories
|
||||
sidebar:
|
||||
order: 7
|
||||
---
|
||||
|
||||
import { Steps, Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
pg_orrery includes a Lambert solver for computing ballistic transfer orbits between any two planets. Given a departure body, arrival body, departure time, and arrival time, the solver returns the transfer orbit's energy characteristics: departure C3, arrival C3, v-infinity, time of flight, and transfer semi-major axis. The function processes about 800,000 solutions per second, which means pork chop plots -- the standard visualization for launch window analysis -- become SQL CROSS JOINs.
|
||||
|
||||
## How you do it today
|
||||
|
||||
Interplanetary trajectory design is one of the more specialized areas of orbital mechanics:
|
||||
|
||||
- **GMAT** (General Mission Analysis Tool): NASA's open-source mission design software. Full-featured but steep learning curve. GUI-driven, script-extensible, not designed for batch parameter sweeps.
|
||||
- **NASA Trajectory Browser**: Web-based tool for browsing pre-computed transfer opportunities. Limited to pre-defined targets and time windows.
|
||||
- **poliastro** (Python): Astrodynamics library with Lambert solvers, orbit plotting, and planetary position computation. Good for one-off analysis; batch sweeps require writing loops.
|
||||
- **STK/Astrogator**: Commercial tool with advanced trajectory design. Expensive, steep learning curve.
|
||||
|
||||
For all of these, the workflow is: pick a departure date, pick an arrival date, run the solver, record the result. To build a pork chop plot, you sweep a grid of departure and arrival dates and collect results. In Python, this means nested loops. In GMAT, this means scripted batch runs.
|
||||
|
||||
## What changes with pg_orrery
|
||||
|
||||
Two functions handle the complete Lambert problem:
|
||||
|
||||
| Function | Returns | Use case |
|
||||
|---|---|---|
|
||||
| `lambert_transfer(dep_id, arr_id, dep_time, arr_time)` | RECORD with 6 fields | Full transfer orbit characterization |
|
||||
| `lambert_c3(dep_id, arr_id, dep_time, arr_time)` | float8 | Departure C3 only (for pork chop plots) |
|
||||
|
||||
The `lambert_transfer()` output fields:
|
||||
|
||||
| Field | Units | Meaning |
|
||||
|---|---|---|
|
||||
| `c3_departure` | km^2/s^2 | Launch energy: the kinetic energy per unit mass above escape velocity at departure |
|
||||
| `c3_arrival` | km^2/s^2 | Arrival energy: excess velocity squared at the target planet |
|
||||
| `v_inf_departure` | km/s | Hyperbolic excess speed at departure (sqrt of C3) |
|
||||
| `v_inf_arrival` | km/s | Hyperbolic excess speed at arrival |
|
||||
| `tof_days` | days | Time of flight |
|
||||
| `transfer_sma` | AU | Semi-major axis of the transfer ellipse |
|
||||
|
||||
Body IDs match the VSOP87 convention: 1=Mercury, 2=Venus, 3=Earth, 4=Mars, 5=Jupiter, 6=Saturn, 7=Uranus, 8=Neptune. The departure body is almost always Earth (3), but the solver works for any planet-to-planet combination.
|
||||
|
||||
## What pg_orrery does not replace
|
||||
|
||||
<Aside type="caution" title="Ballistic two-body only">
|
||||
The Lambert solver computes the conic section connecting two positions in a central gravity field. Real interplanetary trajectories involve more physics.
|
||||
</Aside>
|
||||
|
||||
- **No gravity assists.** Voyager, Cassini, and New Horizons all used flyby maneuvers to gain velocity from intermediate planets. The Lambert solver computes direct transfers only.
|
||||
- **No low-thrust trajectories.** Ion drives and solar sails produce continuous thrust. Lambert assumes an instantaneous departure burn and coast to arrival.
|
||||
- **No three-body effects.** The solver uses heliocentric two-body mechanics. Sphere-of-influence transitions, Lagrange point dynamics, and lunar gravity assists are not modeled.
|
||||
- **No launch vehicle constraints.** The solver returns C3, which determines the required launch energy. Mapping C3 to a specific rocket's payload capacity is a separate analysis.
|
||||
- **No aerocapture or entry design.** The arrival C3 determines how much delta-v is needed for orbit insertion, but pg_orrery does not compute the insertion burn itself.
|
||||
|
||||
For mission design beyond first-order feasibility analysis, use GMAT or poliastro with patched-conic or N-body propagation.
|
||||
|
||||
## Try it
|
||||
|
||||
### Earth-Mars transfer
|
||||
|
||||
The classic trajectory design problem. A 2026 departure window:
|
||||
|
||||
```sql
|
||||
SELECT round(c3_departure::numeric, 2) AS c3_dep_km2s2,
|
||||
round(c3_arrival::numeric, 2) AS c3_arr_km2s2,
|
||||
round(v_inf_departure::numeric, 2) AS vinf_dep_kms,
|
||||
round(v_inf_arrival::numeric, 2) AS vinf_arr_kms,
|
||||
round(tof_days::numeric, 0) AS flight_days,
|
||||
round(transfer_sma::numeric, 4) AS sma_au
|
||||
FROM lambert_transfer(
|
||||
3, 4, -- Earth to Mars
|
||||
'2026-05-01 00:00:00+00'::timestamptz, -- departure
|
||||
'2027-01-15 00:00:00+00'::timestamptz -- arrival
|
||||
);
|
||||
```
|
||||
|
||||
For a typical Earth-Mars transfer, expect departure C3 in the 8-20 km^2/s^2 range, flight times of 200-300 days, and a transfer SMA of roughly 1.2-1.5 AU.
|
||||
|
||||
### Earth-Venus transfer
|
||||
|
||||
Venus transfers require less energy than Mars because Venus is closer to the Sun:
|
||||
|
||||
```sql
|
||||
SELECT round(c3_departure::numeric, 2) AS c3_dep,
|
||||
round(tof_days::numeric, 0) AS flight_days,
|
||||
round(transfer_sma::numeric, 4) AS sma_au
|
||||
FROM lambert_transfer(
|
||||
3, 2, -- Earth to Venus
|
||||
'2026-06-01 00:00:00+00'::timestamptz,
|
||||
'2026-10-15 00:00:00+00'::timestamptz
|
||||
);
|
||||
```
|
||||
|
||||
Typical Earth-Venus C3 is 5-15 km^2/s^2 with flight times of 100-200 days.
|
||||
|
||||
### Earth-Jupiter transfer
|
||||
|
||||
A direct ballistic transfer to Jupiter requires significant energy:
|
||||
|
||||
```sql
|
||||
SELECT round(c3_departure::numeric, 0) AS c3_dep,
|
||||
round(tof_days::numeric, 0) AS flight_days
|
||||
FROM lambert_transfer(
|
||||
3, 5, -- Earth to Jupiter
|
||||
'2026-01-01 00:00:00+00'::timestamptz,
|
||||
'2028-06-01 00:00:00+00'::timestamptz
|
||||
);
|
||||
```
|
||||
|
||||
Expect C3 in the 70-100+ km^2/s^2 range. This is why real Jupiter missions use Venus and Earth gravity assists to reduce the required launch energy.
|
||||
|
||||
### Using lambert_c3 for quick comparisons
|
||||
|
||||
When you only need the departure energy and not the full transfer characterization:
|
||||
|
||||
```sql
|
||||
SELECT round(lambert_c3(3, 4,
|
||||
'2026-05-01 00:00:00+00'::timestamptz,
|
||||
'2027-01-15 00:00:00+00'::timestamptz)::numeric, 2) AS c3;
|
||||
```
|
||||
|
||||
`lambert_c3()` returns just the departure C3 as a single float. It is faster than `lambert_transfer()` when you do not need the other output fields.
|
||||
|
||||
### Mini pork chop plot
|
||||
|
||||
A pork chop plot shows departure C3 as a function of departure date and arrival date. This is the fundamental tool for launch window analysis. Generate one in SQL with a CROSS JOIN:
|
||||
|
||||
```sql
|
||||
SELECT dep::date AS departure,
|
||||
arr::date AS arrival,
|
||||
round(lambert_c3(3, 4, dep, arr)::numeric, 1) AS c3
|
||||
FROM generate_series(
|
||||
'2026-04-01'::timestamptz,
|
||||
'2026-06-01'::timestamptz,
|
||||
interval '10 days'
|
||||
) AS dep
|
||||
CROSS JOIN generate_series(
|
||||
'2027-01-01'::timestamptz,
|
||||
'2027-03-01'::timestamptz,
|
||||
interval '10 days'
|
||||
) AS arr
|
||||
ORDER BY c3;
|
||||
```
|
||||
|
||||
The lowest C3 values correspond to the optimal departure/arrival combination. This mini grid has about 40 points; a real pork chop plot uses finer resolution.
|
||||
|
||||
### Full pork chop plot: 150x150 grid
|
||||
|
||||
For a publication-quality pork chop plot, use 1-day resolution over a wide window:
|
||||
|
||||
```sql
|
||||
SELECT dep::date AS departure,
|
||||
arr::date AS arrival,
|
||||
round(lambert_c3(3, 4, dep, arr)::numeric, 2) AS c3
|
||||
FROM generate_series(
|
||||
'2026-01-01'::timestamptz,
|
||||
'2026-06-01'::timestamptz,
|
||||
interval '1 day'
|
||||
) AS dep
|
||||
CROSS JOIN generate_series(
|
||||
'2026-09-01'::timestamptz,
|
||||
'2027-06-01'::timestamptz,
|
||||
interval '1 day'
|
||||
) AS arr;
|
||||
```
|
||||
|
||||
This generates approximately 150 x 270 = 40,500 transfer solutions. At 800,000 solutions per second, the query completes in well under a second. The result is a table you can feed into any contour plot library.
|
||||
|
||||
### Compare transfer windows
|
||||
|
||||
Look at multiple departure windows side by side:
|
||||
|
||||
```sql
|
||||
WITH windows AS (
|
||||
SELECT '2026 window' AS window, '2026-05-01'::timestamptz AS dep, '2027-01-15'::timestamptz AS arr
|
||||
UNION ALL
|
||||
SELECT '2028 window', '2028-10-01', '2029-06-15'
|
||||
UNION ALL
|
||||
SELECT '2031 window', '2031-02-01', '2031-10-01'
|
||||
)
|
||||
SELECT window,
|
||||
dep::date AS departure,
|
||||
arr::date AS arrival,
|
||||
round(c3_departure::numeric, 2) AS c3_dep,
|
||||
round(c3_arrival::numeric, 2) AS c3_arr,
|
||||
round(tof_days::numeric, 0) AS flight_days,
|
||||
round(transfer_sma::numeric, 4) AS sma_au
|
||||
FROM windows,
|
||||
LATERAL lambert_transfer(3, 4, dep, arr);
|
||||
```
|
||||
|
||||
Earth-Mars launch windows repeat approximately every 26 months (the synodic period). Some windows are more favorable than others because Mars's orbit is significantly eccentric.
|
||||
|
||||
### Find the optimal departure date
|
||||
|
||||
Use a fine sweep over departure dates with a fixed arrival date to find the minimum C3:
|
||||
|
||||
```sql
|
||||
SELECT dep::date AS departure,
|
||||
round(lambert_c3(3, 4, dep,
|
||||
'2027-01-15 00:00:00+00'::timestamptz)::numeric, 2) AS c3
|
||||
FROM generate_series(
|
||||
'2026-03-01'::timestamptz,
|
||||
'2026-08-01'::timestamptz,
|
||||
interval '1 day'
|
||||
) AS dep
|
||||
ORDER BY lambert_c3(3, 4, dep,
|
||||
'2027-01-15 00:00:00+00'::timestamptz)
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
The top 10 rows show the departure dates with the lowest launch energy for a fixed arrival on 2027-01-15. In practice, you would sweep both departure and arrival together (the full pork chop plot), but fixing one dimension is useful for understanding the sensitivity.
|
||||
|
||||
### Venus-Mars via Earth (multi-leg comparison)
|
||||
|
||||
While the Lambert solver does not compute multi-leg gravity assists directly, you can compare the energy requirements of each leg independently:
|
||||
|
||||
```sql
|
||||
-- Earth to Venus (leg 1)
|
||||
SELECT 'Earth-Venus' AS leg,
|
||||
round(c3_departure::numeric, 2) AS c3_dep,
|
||||
round(tof_days::numeric, 0) AS flight_days
|
||||
FROM lambert_transfer(3, 2,
|
||||
'2026-06-01'::timestamptz, '2026-10-15'::timestamptz)
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Earth to Mars (direct)
|
||||
SELECT 'Earth-Mars (direct)',
|
||||
round(c3_departure::numeric, 2),
|
||||
round(tof_days::numeric, 0)
|
||||
FROM lambert_transfer(3, 4,
|
||||
'2026-05-01'::timestamptz, '2027-01-15'::timestamptz)
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Earth to Jupiter (direct)
|
||||
SELECT 'Earth-Jupiter (direct)',
|
||||
round(c3_departure::numeric, 0),
|
||||
round(tof_days::numeric, 0)
|
||||
FROM lambert_transfer(3, 5,
|
||||
'2026-01-01'::timestamptz, '2028-06-01'::timestamptz);
|
||||
```
|
||||
|
||||
This shows why gravity assists exist: the direct Earth-Jupiter C3 is many times higher than the Earth-Venus C3. By flying to Venus first and using its gravity to redirect, missions can reach Jupiter with far less launch energy.
|
||||
|
||||
### Sanity checks
|
||||
|
||||
Verify the solver produces physically reasonable results:
|
||||
|
||||
```sql
|
||||
-- C3 should be positive and less than 200 for any Earth-Mars transfer
|
||||
SELECT lambert_c3(3, 4,
|
||||
'2026-05-01'::timestamptz,
|
||||
'2027-01-15'::timestamptz) > 0 AS positive,
|
||||
lambert_c3(3, 4,
|
||||
'2026-05-01'::timestamptz,
|
||||
'2027-01-15'::timestamptz) < 200 AS reasonable;
|
||||
|
||||
-- Transfer SMA should be between Earth and Mars orbits
|
||||
SELECT transfer_sma > 0.8 AS above_venus,
|
||||
transfer_sma < 5.0 AS below_jupiter
|
||||
FROM lambert_transfer(3, 4,
|
||||
'2026-05-01'::timestamptz,
|
||||
'2027-01-15'::timestamptz);
|
||||
```
|
||||
|
||||
<Aside type="note" title="Error handling">
|
||||
The solver raises an error for degenerate cases: same body for departure and arrival, or arrival before departure. `lambert_c3()` returns NULL if the solver fails to converge for extreme geometries.
|
||||
</Aside>
|
||||
279
docs/src/content/docs/guides/jupiter-radio-bursts.mdx
Normal file
279
docs/src/content/docs/guides/jupiter-radio-bursts.mdx
Normal file
@ -0,0 +1,279 @@
|
||||
---
|
||||
title: Jupiter Radio Bursts
|
||||
sidebar:
|
||||
order: 6
|
||||
---
|
||||
|
||||
import { Steps, Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Jupiter is the strongest radio source in the solar system after the Sun. Its decametric emissions (roughly 15-40 MHz) occur when Io passes through specific orbital positions relative to Jupiter's rotating magnetic field. pg_orrery computes the two geometry parameters that govern these bursts -- Io phase angle and Jupiter Central Meridian Longitude -- and maps them to an empirical burst probability using the Carr et al. (1983) source regions.
|
||||
|
||||
This is the feature built for the Radio JOVE community. There are 500-1000 active Radio JOVE operators worldwide, and until pg_orrery, the standard prediction tool was Radio Jupiter Pro -- a Windows-only desktop application. Batch prediction, calendar generation, and integration with observation scheduling are now possible in SQL.
|
||||
|
||||
## How you do it today
|
||||
|
||||
Jupiter radio observation planning has relied on a small set of tools:
|
||||
|
||||
- **Radio Jupiter Pro** (Windows): The standard tool. Shows a real-time display of Io phase, CML, and burst probability. Single-observer, single-time, no batch output. Windows-only.
|
||||
- **Manual CML/Io-phase charts**: Published charts (Carr, Desch, Alexander 1983) show which CML-Io phase combinations produce bursts. Observers print the chart and overlay their observing window by hand.
|
||||
- **Radio-SkyPipe** and **SkyPipe II**: Recording software that can trigger on signal, but prediction is separate.
|
||||
- **Spreadsheets**: Some operators maintain Excel sheets that compute CML from Jupiter's rotation rate. Error-prone, per-session.
|
||||
|
||||
The problem: planning an observation campaign over weeks or months means running Radio Jupiter Pro repeatedly for each night, eyeballing the probability, and writing down the good windows. There is no way to generate a calendar of optimal observation windows in one operation.
|
||||
|
||||
## What changes with pg_orrery
|
||||
|
||||
Three functions cover the complete Jupiter radio prediction pipeline:
|
||||
|
||||
| Function | Returns | What it computes |
|
||||
|---|---|---|
|
||||
| `io_phase_angle(time)` | degrees [0, 360) | Io's orbital position. 0 = superior conjunction (behind Jupiter). |
|
||||
| `jupiter_cml(observer, time)` | degrees [0, 360) | Central Meridian Longitude, System III (1965.0). Light-time corrected. |
|
||||
| `jupiter_burst_probability(io_phase, cml)` | 0.0 to 1.0 | Empirical probability based on Carr source regions. |
|
||||
|
||||
The probability function encodes four source regions from the Carr et al. (1983) model:
|
||||
|
||||
| Source | CML Range | Io Phase Range | Probability | Description |
|
||||
|---|---|---|---|---|
|
||||
| **A** | 200-260 | 195-265 | 0.8 | Strongest. Io-related, occurs when Io is at superior conjunction. |
|
||||
| **B** | 100-200 | 60-150 | 0.5 | Io-related, occurs when Io is ~90 degrees ahead of Jupiter. |
|
||||
| **C** | 300-20 | 220-310 | 0.3 | Weaker. Non-Io component, occurs at specific CML ranges. |
|
||||
| **D** | 350-60 | 80-140 | 0.2 | Weakest of the four. Non-Io related. |
|
||||
|
||||
Outside these regions, the probability is 0.0. Overlapping regions combine to the higher probability.
|
||||
|
||||
## What pg_orrery does not replace
|
||||
|
||||
<Aside type="caution" title="Empirical model">
|
||||
The burst probability is a statistical average from decades of observations. Individual bursts are stochastic -- a high probability window can produce nothing, and occasional bursts appear outside predicted windows.
|
||||
</Aside>
|
||||
|
||||
- **No signal detection.** pg_orrery predicts when bursts are likely, not whether one is occurring. Use Radio-SkyPipe or SDR software for actual signal capture.
|
||||
- **No frequency prediction.** The model predicts occurrence probability, not the specific frequency structure (L-bursts vs. S-bursts) or intensity.
|
||||
- **No RFI assessment.** Local radio interference is often the biggest obstacle to Jupiter observation. pg_orrery does not model your local RF environment.
|
||||
- **No receiver pointing.** At 20 MHz, most receivers use fixed dipole antennas. Pointing is not an issue, but Jupiter must be above the horizon. Combine with `planet_observe(5, ...)` to check elevation.
|
||||
|
||||
## Try it
|
||||
|
||||
### Check current conditions
|
||||
|
||||
What are the Io phase and CML right now?
|
||||
|
||||
```sql
|
||||
SELECT round(io_phase_angle(now())::numeric, 1) AS io_phase,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, now())::numeric, 1) AS cml,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(now()),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, now())
|
||||
)::numeric, 3) AS burst_prob;
|
||||
```
|
||||
|
||||
### Best burst windows tonight
|
||||
|
||||
Scan the next 12 hours in 10-minute steps and find windows where burst probability exceeds 30%:
|
||||
|
||||
```sql
|
||||
SELECT t,
|
||||
round(io_phase_angle(t)::numeric, 1) AS io_phase,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, t)::numeric, 1) AS cml,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
)::numeric, 3) AS prob
|
||||
FROM generate_series(
|
||||
now(),
|
||||
now() + interval '12 hours',
|
||||
interval '10 minutes'
|
||||
) AS t
|
||||
WHERE jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
) > 0.3
|
||||
ORDER BY t;
|
||||
```
|
||||
|
||||
<Aside type="tip" title="Also check Jupiter elevation">
|
||||
A high burst probability means nothing if Jupiter is below the horizon. Combine this with a `planet_observe(5, ...)` check:
|
||||
|
||||
```sql
|
||||
AND topo_elevation(planet_observe(5,
|
||||
'40.0N 105.3W 1655m'::observer, t)) > 10
|
||||
```
|
||||
</Aside>
|
||||
|
||||
### Best windows tonight with horizon check
|
||||
|
||||
The complete query: burst probability above threshold AND Jupiter above the horizon:
|
||||
|
||||
```sql
|
||||
SELECT t,
|
||||
round(io_phase_angle(t)::numeric, 1) AS io_phase,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, t)::numeric, 1) AS cml,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
)::numeric, 3) AS prob,
|
||||
round(topo_elevation(planet_observe(5,
|
||||
'40.0N 105.3W 1655m'::observer, t))::numeric, 1) AS jupiter_el
|
||||
FROM generate_series(
|
||||
'2024-03-15 00:00:00+00'::timestamptz,
|
||||
'2024-03-15 12:00:00+00'::timestamptz,
|
||||
interval '10 minutes'
|
||||
) AS t
|
||||
WHERE jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
) > 0.0
|
||||
AND topo_elevation(planet_observe(5,
|
||||
'40.0N 105.3W 1655m'::observer, t)) > 10
|
||||
ORDER BY prob DESC, t;
|
||||
```
|
||||
|
||||
### 30-day observation calendar
|
||||
|
||||
Generate a calendar of the best observation windows over an entire month:
|
||||
|
||||
```sql
|
||||
WITH windows AS (
|
||||
SELECT t,
|
||||
io_phase_angle(t) AS io_phase,
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t) AS cml,
|
||||
jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
) AS prob,
|
||||
topo_elevation(planet_observe(5,
|
||||
'40.0N 105.3W 1655m'::observer, t)) AS jupiter_el
|
||||
FROM generate_series(
|
||||
'2024-03-01 00:00:00+00'::timestamptz,
|
||||
'2024-03-31 00:00:00+00'::timestamptz,
|
||||
interval '10 minutes'
|
||||
) AS t
|
||||
)
|
||||
SELECT t::date AS date,
|
||||
t::time AS utc_time,
|
||||
round(io_phase::numeric, 1) AS io_phase,
|
||||
round(cml::numeric, 1) AS cml,
|
||||
round(prob::numeric, 2) AS prob,
|
||||
round(jupiter_el::numeric, 1) AS jup_el
|
||||
FROM windows
|
||||
WHERE prob >= 0.5
|
||||
AND jupiter_el > 15
|
||||
ORDER BY date, utc_time;
|
||||
```
|
||||
|
||||
This finds every 10-minute window in March 2024 where burst probability is at least 50% and Jupiter is more than 15 degrees above the horizon. The result is a printable observation calendar.
|
||||
|
||||
### Identify Carr source regions
|
||||
|
||||
Determine which source region is responsible for a given prediction:
|
||||
|
||||
```sql
|
||||
WITH sources AS (
|
||||
SELECT 'Source A' AS region, 200.0 AS cml_lo, 260.0 AS cml_hi,
|
||||
195.0 AS io_lo, 265.0 AS io_hi, 0.8 AS prob
|
||||
UNION ALL SELECT 'Source B', 100.0, 200.0, 60.0, 150.0, 0.5
|
||||
UNION ALL SELECT 'Source C', 300.0, 380.0, 220.0, 310.0, 0.3
|
||||
UNION ALL SELECT 'Source D', 350.0, 420.0, 80.0, 140.0, 0.2
|
||||
)
|
||||
SELECT t,
|
||||
round(io_phase_angle(t)::numeric, 1) AS io,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, t)::numeric, 1) AS cml,
|
||||
s.region,
|
||||
s.prob
|
||||
FROM generate_series(
|
||||
'2024-03-15 00:00:00+00'::timestamptz,
|
||||
'2024-03-15 12:00:00+00'::timestamptz,
|
||||
interval '15 minutes'
|
||||
) AS t
|
||||
CROSS JOIN sources s
|
||||
WHERE io_phase_angle(t) BETWEEN s.io_lo AND s.io_hi
|
||||
AND (jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
BETWEEN s.cml_lo AND LEAST(s.cml_hi, 360.0)
|
||||
OR jupiter_cml('40.0N 105.3W 1655m'::observer, t) + 360.0
|
||||
BETWEEN s.cml_lo AND s.cml_hi)
|
||||
ORDER BY t, s.prob DESC;
|
||||
```
|
||||
|
||||
<Aside type="note" title="CML wrapping">
|
||||
Source C and D straddle the 360/0 degree boundary. The query handles this by checking both the unwrapped CML and CML+360.
|
||||
</Aside>
|
||||
|
||||
### Io orbital phase rate
|
||||
|
||||
Io completes an orbit in about 1.77 days. Watch the phase angle advance over a full orbit:
|
||||
|
||||
```sql
|
||||
SELECT t,
|
||||
round(io_phase_angle(t)::numeric, 1) AS io_phase
|
||||
FROM generate_series(
|
||||
'2024-03-15 00:00:00+00'::timestamptz,
|
||||
'2024-03-16 18:00:00+00'::timestamptz,
|
||||
interval '2 hours'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
The phase should advance roughly 203 degrees per day (360 / 1.77).
|
||||
|
||||
### Jupiter CML rotation
|
||||
|
||||
Jupiter's System III rotation period is 9h 55m 29.7s. Watch the CML cycle through 360 degrees:
|
||||
|
||||
```sql
|
||||
SELECT t,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, t)::numeric, 1) AS cml
|
||||
FROM generate_series(
|
||||
'2024-03-15 00:00:00+00'::timestamptz,
|
||||
'2024-03-15 10:00:00+00'::timestamptz,
|
||||
interval '30 minutes'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
The CML completes one full rotation in just under 10 hours, meaning the same magnetic field geometry repeats roughly 2.4 times per day. This is why Jupiter radio observation windows can occur multiple times per night.
|
||||
|
||||
### Probability heatmap data
|
||||
|
||||
Generate the data for a CML vs. Io-phase probability plot (the classic Carr diagram):
|
||||
|
||||
```sql
|
||||
SELECT io_phase,
|
||||
cml,
|
||||
jupiter_burst_probability(io_phase, cml) AS prob
|
||||
FROM generate_series(0, 355, 5) AS io_phase,
|
||||
generate_series(0, 355, 5) AS cml
|
||||
WHERE jupiter_burst_probability(io_phase, cml) > 0;
|
||||
```
|
||||
|
||||
This produces a 72x72 grid (5-degree resolution) of probability values, showing exactly the four Carr source regions. The output can be fed to any heatmap visualization tool.
|
||||
|
||||
### Multi-observer comparison
|
||||
|
||||
Compare burst windows for operators at different longitudes. The CML depends on observer position because of light-time correction:
|
||||
|
||||
```sql
|
||||
WITH observers(name, obs) AS (VALUES
|
||||
('Boulder, CO', '40.0N 105.3W 1655m'::observer),
|
||||
('Gainesville, FL', '29.6N 82.3W 30m'::observer),
|
||||
('Paris, FR', '48.9N 2.3E 75m'::observer)
|
||||
)
|
||||
SELECT o.name,
|
||||
t,
|
||||
round(jupiter_cml(o.obs, t)::numeric, 1) AS cml,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml(o.obs, t)
|
||||
)::numeric, 3) AS prob
|
||||
FROM observers o,
|
||||
generate_series(
|
||||
'2024-03-15 02:00:00+00'::timestamptz,
|
||||
'2024-03-15 06:00:00+00'::timestamptz,
|
||||
interval '30 minutes'
|
||||
) AS t
|
||||
WHERE jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml(o.obs, t)
|
||||
) > 0.3
|
||||
ORDER BY t, o.name;
|
||||
```
|
||||
|
||||
The Io phase is the same for all observers (it depends only on time), but the CML varies slightly due to light-time differences. For observers on the same continent, the difference is negligible. Comparing North America to Europe shows a measurable shift.
|
||||
260
docs/src/content/docs/guides/observing-solar-system.mdx
Normal file
260
docs/src/content/docs/guides/observing-solar-system.mdx
Normal file
@ -0,0 +1,260 @@
|
||||
---
|
||||
title: Observing the Solar System
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
import { Steps, Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
pg_orrery computes positions for all eight planets (VSOP87), the Sun, and the Moon (ELP2000-82B). Every observation returns the same `topocentric` type: azimuth, elevation, range, and range rate from a given observer at a given time. The solar system becomes queryable with standard SQL.
|
||||
|
||||
## How you do it today
|
||||
|
||||
Knowing where planets are involves one of a few approaches:
|
||||
|
||||
- **Stellarium** gives you a beautiful real-time sky view. You scrub time, click objects, read coordinates. Not scriptable, not batch-queryable.
|
||||
- **JPL Horizons** computes high-precision ephemerides via web form or API. Accurate to milliarcseconds. One object per request, rate-limited.
|
||||
- **Skyfield** (Python) loads JPL DE441 ephemerides and computes positions with sub-arcsecond accuracy. Excellent for one-off scripts; batch processing over large time ranges or many observers means writing loops.
|
||||
- **Astropy** provides coordinate frames, time systems, and ERFA wrappers. Powerful, but computing "what's above the horizon right now" requires assembling several components.
|
||||
|
||||
All of these produce results that live outside your database. If you want to correlate planet positions with weather data, observation logs, or satellite passes, you export, import, and join.
|
||||
|
||||
## What changes with pg_orrery
|
||||
|
||||
All planets, the Sun, and the Moon are available as SQL function calls. The functions take an observer and a timestamp, and return topocentric coordinates. You can sweep all eight planets, generate time series, filter by elevation, and join with other tables in the same query.
|
||||
|
||||
Key functions:
|
||||
|
||||
| Function | What it computes |
|
||||
|---|---|
|
||||
| `planet_observe(body_id, observer, time)` | Topocentric az/el/range for a planet |
|
||||
| `planet_heliocentric(body_id, time)` | Heliocentric ecliptic J2000 position (AU) |
|
||||
| `sun_observe(observer, time)` | Topocentric Sun position |
|
||||
| `moon_observe(observer, time)` | Topocentric Moon position (ELP2000-82B) |
|
||||
|
||||
Body IDs follow the VSOP87 convention: 1=Mercury, 2=Venus, 3=Earth, 4=Mars, 5=Jupiter, 6=Saturn, 7=Uranus, 8=Neptune. Body 0 returns the Sun at the heliocentric origin (all zeros).
|
||||
|
||||
## What pg_orrery does not replace
|
||||
|
||||
<Aside type="caution" title="Accuracy trade-offs">
|
||||
VSOP87 and ELP2000-82B are analytic theories. They trade the last bits of precision for computational speed and zero external data dependencies.
|
||||
</Aside>
|
||||
|
||||
- **VSOP87 accuracy is about 1 arcsecond.** JPL DE441 (used by Skyfield and SPICE) achieves 0.001 arcsecond. For visual observation planning, 1 arcsecond is more than sufficient. For precision astrometry or GHz dish pointing, pg_orrery v0.3.0 supports [optional DE440/441 ephemeris files](/guides/de-ephemeris/) with sub-milliarcsecond accuracy.
|
||||
- **ELP2000-82B accuracy is about 10 arcseconds** for the Moon. Good enough for knowing when the Moon is up, what phase it is in, and whether it will interfere with observations. Not sufficient for occultation timing.
|
||||
- **No light-time iteration.** pg_orrery computes geometric positions, not apparent positions. The difference matters at the milliarcsecond level.
|
||||
- **No atmospheric refraction.** Objects near the horizon appear slightly higher than their geometric position. pg_orrery does not apply refraction corrections.
|
||||
|
||||
## Try it
|
||||
|
||||
### Where is Jupiter right now?
|
||||
|
||||
The simplest possible observation query:
|
||||
|
||||
```sql
|
||||
SELECT topo_azimuth(t) AS azimuth,
|
||||
topo_elevation(t) AS elevation,
|
||||
topo_range(t) / 149597870.7 AS distance_au
|
||||
FROM planet_observe(5, '40.0N 105.3W 1655m'::observer, now()) t;
|
||||
```
|
||||
|
||||
Body ID 5 is Jupiter. The range comes back in km; dividing by 149,597,870.7 converts to AU.
|
||||
|
||||
### What is up tonight?
|
||||
|
||||
Sweep all eight planets plus the Sun and Moon. Filter to objects above the horizon:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Planets only">
|
||||
```sql
|
||||
SELECT CASE body_id
|
||||
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
|
||||
WHEN 3 THEN 'Earth' WHEN 4 THEN 'Mars'
|
||||
WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn'
|
||||
WHEN 7 THEN 'Uranus' WHEN 8 THEN 'Neptune'
|
||||
END AS planet,
|
||||
round(topo_azimuth(obs)::numeric, 1) AS az,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el,
|
||||
round((topo_range(obs) / 149597870.7)::numeric, 3) AS dist_au
|
||||
FROM generate_series(1, 8) AS body_id,
|
||||
LATERAL planet_observe(body_id, '40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-21 04:00:00+00') obs
|
||||
WHERE body_id != 3 -- cannot observe Earth from Earth
|
||||
AND topo_elevation(obs) > 0
|
||||
ORDER BY topo_elevation(obs) DESC;
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Planets + Sun + Moon">
|
||||
```sql
|
||||
-- Combine planets, Sun, and Moon into one result set
|
||||
WITH observations AS (
|
||||
-- All planets except Earth
|
||||
SELECT CASE body_id
|
||||
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
|
||||
WHEN 4 THEN 'Mars' WHEN 5 THEN 'Jupiter'
|
||||
WHEN 6 THEN 'Saturn' WHEN 7 THEN 'Uranus'
|
||||
WHEN 8 THEN 'Neptune'
|
||||
END AS name,
|
||||
planet_observe(body_id,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-21 04:00:00+00') AS obs
|
||||
FROM generate_series(1, 8) AS body_id
|
||||
WHERE body_id != 3
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT 'Sun',
|
||||
sun_observe('40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-21 04:00:00+00')
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT 'Moon',
|
||||
moon_observe('40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-21 04:00:00+00')
|
||||
)
|
||||
SELECT name,
|
||||
round(topo_azimuth(obs)::numeric, 1) AS az,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el,
|
||||
round((topo_range(obs) / 149597870.7)::numeric, 4) AS dist_au
|
||||
FROM observations
|
||||
WHERE topo_elevation(obs) > 0
|
||||
ORDER BY topo_elevation(obs) DESC;
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Solar system status: heliocentric distances
|
||||
|
||||
See where every planet is relative to the Sun:
|
||||
|
||||
```sql
|
||||
SELECT body_id,
|
||||
CASE body_id
|
||||
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
|
||||
WHEN 3 THEN 'Earth' WHEN 4 THEN 'Mars'
|
||||
WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn'
|
||||
WHEN 7 THEN 'Uranus' WHEN 8 THEN 'Neptune'
|
||||
END AS planet,
|
||||
round(helio_distance(planet_heliocentric(body_id, now()))::numeric, 4) AS dist_au,
|
||||
round(helio_x(planet_heliocentric(body_id, now()))::numeric, 4) AS x_au,
|
||||
round(helio_y(planet_heliocentric(body_id, now()))::numeric, 4) AS y_au,
|
||||
round(helio_z(planet_heliocentric(body_id, now()))::numeric, 4) AS z_au
|
||||
FROM generate_series(1, 8) AS body_id;
|
||||
```
|
||||
|
||||
The heliocentric coordinates are in the ecliptic J2000 frame. X points toward the vernal equinox, Z toward the north ecliptic pole.
|
||||
|
||||
### Planet elevation over one night
|
||||
|
||||
Track Jupiter's elevation from sunset to sunrise:
|
||||
|
||||
```sql
|
||||
SELECT t,
|
||||
round(topo_elevation(
|
||||
planet_observe(5, '40.0N 105.3W 1655m'::observer, t)
|
||||
)::numeric, 1) AS jupiter_el,
|
||||
round(topo_azimuth(
|
||||
planet_observe(5, '40.0N 105.3W 1655m'::observer, t)
|
||||
)::numeric, 1) AS jupiter_az
|
||||
FROM generate_series(
|
||||
'2024-06-21 02:00:00+00'::timestamptz, -- ~8pm MDT
|
||||
'2024-06-21 12:00:00+00'::timestamptz, -- ~6am MDT
|
||||
interval '30 minutes'
|
||||
) AS t
|
||||
WHERE topo_elevation(
|
||||
planet_observe(5, '40.0N 105.3W 1655m'::observer, t)
|
||||
) > 0;
|
||||
```
|
||||
|
||||
This produces a time series of Jupiter's position through the night, filtered to only the hours it is above the horizon. Replace body ID 5 with any other planet.
|
||||
|
||||
### Sun position through the day
|
||||
|
||||
Useful for solar panel analysis, sunrise/sunset approximation, or photography planning:
|
||||
|
||||
```sql
|
||||
SELECT t,
|
||||
round(topo_azimuth(sun_observe('40.0N 105.3W 1655m'::observer, t))::numeric, 1) AS az,
|
||||
round(topo_elevation(sun_observe('40.0N 105.3W 1655m'::observer, t))::numeric, 1) AS el
|
||||
FROM generate_series(
|
||||
'2024-06-21 12:00:00+00'::timestamptz, -- ~6am MDT
|
||||
'2024-06-22 03:00:00+00'::timestamptz, -- ~9pm MDT
|
||||
interval '15 minutes'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
At the summer solstice from Boulder, the Sun reaches about 73 degrees elevation at local noon, rising in the northeast and setting in the northwest.
|
||||
|
||||
### Moon range check
|
||||
|
||||
The Moon's distance varies between about 356,000 km (perigee) and 407,000 km (apogee):
|
||||
|
||||
```sql
|
||||
SELECT t::date AS date,
|
||||
round(topo_range(moon_observe('40.0N 105.3W 1655m'::observer, t))::numeric, 0) AS range_km
|
||||
FROM generate_series(
|
||||
'2024-01-01'::timestamptz,
|
||||
'2024-02-01'::timestamptz,
|
||||
interval '1 day'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
### Multi-observer comparison
|
||||
|
||||
Compare planet visibility from two different locations:
|
||||
|
||||
```sql
|
||||
WITH observers AS (
|
||||
SELECT 'Boulder, CO' AS location, '40.0N 105.3W 1655m'::observer AS obs
|
||||
UNION ALL
|
||||
SELECT 'Sydney, AU', '33.9S 151.2E 58m'::observer
|
||||
)
|
||||
SELECT o.location,
|
||||
CASE body_id
|
||||
WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn'
|
||||
END AS planet,
|
||||
round(topo_elevation(
|
||||
planet_observe(body_id, o.obs, '2024-06-21 10:00:00+00')
|
||||
)::numeric, 1) AS elevation
|
||||
FROM observers o,
|
||||
generate_series(5, 6) AS body_id
|
||||
ORDER BY o.location, body_id;
|
||||
```
|
||||
|
||||
### Earth heliocentric sanity check
|
||||
|
||||
Earth's distance from the Sun should be about 0.983 AU at perihelion (early January) and 1.017 AU at aphelion (early July):
|
||||
|
||||
```sql
|
||||
SELECT 'perihelion' AS point,
|
||||
round(helio_distance(
|
||||
planet_heliocentric(3, '2024-01-03 12:00:00+00')
|
||||
)::numeric, 4) AS earth_au
|
||||
UNION ALL
|
||||
SELECT 'aphelion',
|
||||
round(helio_distance(
|
||||
planet_heliocentric(3, '2024-07-05 12:00:00+00')
|
||||
)::numeric, 4);
|
||||
```
|
||||
|
||||
This is a useful sanity check when verifying the extension is installed correctly.
|
||||
|
||||
### Solar-terrestrial geometry
|
||||
|
||||
When does the Sun cross specific elevation thresholds? Find solar noon and the elevation at specific times:
|
||||
|
||||
```sql
|
||||
-- Sample the Sun every minute around local noon to find peak elevation
|
||||
SELECT t,
|
||||
round(topo_elevation(sun_observe('40.0N 105.3W 1655m'::observer, t))::numeric, 2) AS el
|
||||
FROM generate_series(
|
||||
'2024-06-21 17:30:00+00'::timestamptz,
|
||||
'2024-06-21 18:30:00+00'::timestamptz,
|
||||
interval '1 minute'
|
||||
) AS t
|
||||
ORDER BY topo_elevation(sun_observe('40.0N 105.3W 1655m'::observer, t)) DESC
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
The highest elevation reading approximates solar noon. For Boulder at the summer solstice, expect about 73 degrees.
|
||||
282
docs/src/content/docs/guides/planetary-moons.mdx
Normal file
282
docs/src/content/docs/guides/planetary-moons.mdx
Normal file
@ -0,0 +1,282 @@
|
||||
---
|
||||
title: Planetary Moons
|
||||
sidebar:
|
||||
order: 3
|
||||
---
|
||||
|
||||
import { Steps, Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
pg_orrery computes positions for 19 planetary moons across four systems: the four Galilean moons of Jupiter, eight moons of Saturn, five moons of Uranus, and two moons of Mars. Each uses a dedicated analytic theory optimized for that system.
|
||||
|
||||
## How you do it today
|
||||
|
||||
Observing planetary moons usually means one of:
|
||||
|
||||
- **Stellarium**: Renders moon positions graphically. Good for identifying which moon is which at the eyepiece. Not scriptable.
|
||||
- **JPL Horizons**: Computes precise ephemerides for any solar system body, including all natural satellites. One query per moon, rate-limited web API.
|
||||
- **Skyfield**: Can load JPL satellite ephemeris kernels (BSP files) for high-precision moon positions. Requires downloading and managing kernel files.
|
||||
- **IMCCE**: Provides specialized ephemeris services for natural satellites. Web-based, per-body queries.
|
||||
|
||||
The common limitation: getting positions for many moons at many times means many separate requests or script iterations. Comparing moon positions across systems (all of Jupiter's moons vs. all of Saturn's) requires stitching results together outside the ephemeris tool.
|
||||
|
||||
## What changes with pg_orrery
|
||||
|
||||
Four observation functions cover all 19 moons:
|
||||
|
||||
| Function | Theory | Moons | Accuracy |
|
||||
|---|---|---|---|
|
||||
| `galilean_observe(body_id, observer, time)` | L1.2 (Lieske, 1998) | 4 (Io, Europa, Ganymede, Callisto) | ~1 arcsecond |
|
||||
| `saturn_moon_observe(body_id, observer, time)` | TASS 1.7 (Vienne & Duriez, 1995) | 8 (Mimas through Hyperion) | ~1-5 arcseconds |
|
||||
| `uranus_moon_observe(body_id, observer, time)` | GUST86 (Laskar & Jacobson, 1987) | 5 (Miranda through Oberon) | ~2-10 arcseconds |
|
||||
| `mars_moon_observe(body_id, observer, time)` | MarsSat (Jacobson, 2010) | 2 (Phobos, Deimos) | ~1-5 arcseconds |
|
||||
|
||||
All functions return the same `topocentric` type. Every moon is identified by a system-specific body ID (integer).
|
||||
|
||||
## Body ID reference
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Jupiter (Galilean)">
|
||||
| ID | Moon | Orbital Period | Notes |
|
||||
|---|---|---|---|
|
||||
| 0 | Io | 1.77 days | Volcanic, drives Jupiter radio bursts |
|
||||
| 1 | Europa | 3.55 days | Subsurface ocean candidate |
|
||||
| 2 | Ganymede | 7.15 days | Largest moon in the solar system |
|
||||
| 3 | Callisto | 16.69 days | Most heavily cratered body |
|
||||
</TabItem>
|
||||
<TabItem label="Saturn">
|
||||
| ID | Moon | Orbital Period | Notes |
|
||||
|---|---|---|---|
|
||||
| 0 | Mimas | 0.94 days | "Death Star" crater |
|
||||
| 1 | Enceladus | 1.37 days | Cryovolcanic plumes |
|
||||
| 2 | Tethys | 1.89 days | Odysseus crater |
|
||||
| 3 | Dione | 2.74 days | Ice cliffs |
|
||||
| 4 | Rhea | 4.52 days | Second-largest Saturn moon |
|
||||
| 5 | Titan | 15.95 days | Thick atmosphere, hydrocarbon lakes |
|
||||
| 6 | Iapetus | 79.32 days | Two-tone coloring |
|
||||
| 7 | Hyperion | 21.28 days | Chaotic rotation |
|
||||
</TabItem>
|
||||
<TabItem label="Uranus">
|
||||
| ID | Moon | Orbital Period | Notes |
|
||||
|---|---|---|---|
|
||||
| 0 | Miranda | 1.41 days | Extreme surface features |
|
||||
| 1 | Ariel | 2.52 days | Youngest surface |
|
||||
| 2 | Umbriel | 4.14 days | Dark, heavily cratered |
|
||||
| 3 | Titania | 8.71 days | Largest Uranus moon |
|
||||
| 4 | Oberon | 13.46 days | Most distant major moon |
|
||||
</TabItem>
|
||||
<TabItem label="Mars">
|
||||
| ID | Moon | Orbital Period | Notes |
|
||||
|---|---|---|---|
|
||||
| 0 | Phobos | 0.32 days | Slowly spiraling inward |
|
||||
| 1 | Deimos | 1.26 days | Slowly receding |
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## What pg_orrery does not replace
|
||||
|
||||
<Aside type="caution" title="Theory limitations">
|
||||
Each analytic theory has a valid time range and accuracy envelope. The theories embedded in pg_orrery are designed for current-epoch observation planning, not historical or far-future ephemeris work.
|
||||
</Aside>
|
||||
|
||||
- **Not sub-arcsecond.** The analytic theories produce positions accurate to a few arcseconds at best. For astrometric reduction or spacecraft navigation, use JPL ephemerides via SPICE or Skyfield.
|
||||
- **No mutual events.** pg_orrery does not predict eclipses, occultations, or transits between moons. Use IMCCE's MULTISAT service for mutual event predictions.
|
||||
- **No libration or physical ephemerides.** The functions return topocentric position only — no rotation state, no sub-observer longitude, no apparent disk size.
|
||||
- **19 moons, not hundreds.** Only the major moons with well-characterized analytic theories are included. Irregular satellites, small inner moons, and ring-embedded moonlets are not covered.
|
||||
|
||||
## Try it
|
||||
|
||||
### Observe all four Galilean moons
|
||||
|
||||
```sql
|
||||
SELECT CASE moon_id
|
||||
WHEN 0 THEN 'Io' WHEN 1 THEN 'Europa'
|
||||
WHEN 2 THEN 'Ganymede' WHEN 3 THEN 'Callisto'
|
||||
END AS moon,
|
||||
round(topo_azimuth(obs)::numeric, 1) AS az,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el,
|
||||
round(topo_range(obs)::numeric, 0) AS range_km
|
||||
FROM generate_series(0, 3) AS moon_id,
|
||||
LATERAL galilean_observe(moon_id,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-03-15 03:00:00+00') obs;
|
||||
```
|
||||
|
||||
The range values should cluster near Jupiter's range (about 4-6 AU or 600-900 million km), since the Galilean moons orbit within 0.013 AU of Jupiter.
|
||||
|
||||
### Compare Galilean moon ranges to Jupiter
|
||||
|
||||
Verify that the moons are near their parent planet:
|
||||
|
||||
```sql
|
||||
SELECT 'Jupiter' AS body,
|
||||
round(topo_range(planet_observe(5, '40.0N 105.3W 1655m'::observer,
|
||||
'2024-03-15 03:00:00+00'))::numeric, -4) AS range_km
|
||||
UNION ALL
|
||||
SELECT CASE moon_id
|
||||
WHEN 0 THEN 'Io' WHEN 1 THEN 'Europa'
|
||||
WHEN 2 THEN 'Ganymede' WHEN 3 THEN 'Callisto'
|
||||
END,
|
||||
round(topo_range(galilean_observe(moon_id,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-03-15 03:00:00+00'))::numeric, -4)
|
||||
FROM generate_series(0, 3) AS moon_id;
|
||||
```
|
||||
|
||||
The moon ranges should differ from Jupiter's by at most a few million km. Io orbits closest; Callisto, the farthest Galilean moon, sits about 1.9 million km from Jupiter.
|
||||
|
||||
### All eight Saturn moons
|
||||
|
||||
```sql
|
||||
SELECT CASE moon_id
|
||||
WHEN 0 THEN 'Mimas' WHEN 1 THEN 'Enceladus'
|
||||
WHEN 2 THEN 'Tethys' WHEN 3 THEN 'Dione'
|
||||
WHEN 4 THEN 'Rhea' WHEN 5 THEN 'Titan'
|
||||
WHEN 6 THEN 'Iapetus' WHEN 7 THEN 'Hyperion'
|
||||
END AS moon,
|
||||
round(topo_azimuth(obs)::numeric, 1) AS az,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el,
|
||||
round(topo_range(obs)::numeric, 0) AS range_km
|
||||
FROM generate_series(0, 7) AS moon_id,
|
||||
LATERAL saturn_moon_observe(moon_id,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-15 03:00:00+00') obs;
|
||||
```
|
||||
|
||||
### Uranus moons
|
||||
|
||||
The five major Uranian moons are faint targets, but their positions are still useful for planning deep imaging sessions:
|
||||
|
||||
```sql
|
||||
SELECT CASE moon_id
|
||||
WHEN 0 THEN 'Miranda' WHEN 1 THEN 'Ariel'
|
||||
WHEN 2 THEN 'Umbriel' WHEN 3 THEN 'Titania'
|
||||
WHEN 4 THEN 'Oberon'
|
||||
END AS moon,
|
||||
round(topo_azimuth(obs)::numeric, 1) AS az,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el,
|
||||
round(topo_range(obs)::numeric, 0) AS range_km
|
||||
FROM generate_series(0, 4) AS moon_id,
|
||||
LATERAL uranus_moon_observe(moon_id,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-15 03:00:00+00') obs;
|
||||
```
|
||||
|
||||
### Mars moons
|
||||
|
||||
Phobos and Deimos are challenging visual targets due to Mars glare, but their positions are computed at every epoch:
|
||||
|
||||
```sql
|
||||
SELECT CASE moon_id
|
||||
WHEN 0 THEN 'Phobos'
|
||||
WHEN 1 THEN 'Deimos'
|
||||
END AS moon,
|
||||
round(topo_azimuth(obs)::numeric, 1) AS az,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el,
|
||||
round(topo_range(obs)::numeric, 0) AS range_km
|
||||
FROM generate_series(0, 1) AS moon_id,
|
||||
LATERAL mars_moon_observe(moon_id,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-01-15 06:00:00+00') obs;
|
||||
```
|
||||
|
||||
### All 19 moons at once
|
||||
|
||||
A single query that observes every supported moon in the solar system:
|
||||
|
||||
```sql
|
||||
WITH all_moons AS (
|
||||
-- Galilean moons (Jupiter)
|
||||
SELECT 'Jupiter' AS parent,
|
||||
CASE id WHEN 0 THEN 'Io' WHEN 1 THEN 'Europa'
|
||||
WHEN 2 THEN 'Ganymede' WHEN 3 THEN 'Callisto'
|
||||
END AS moon,
|
||||
galilean_observe(id, '40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-15 03:00:00+00') AS obs
|
||||
FROM generate_series(0, 3) AS id
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Saturn moons
|
||||
SELECT 'Saturn',
|
||||
CASE id WHEN 0 THEN 'Mimas' WHEN 1 THEN 'Enceladus'
|
||||
WHEN 2 THEN 'Tethys' WHEN 3 THEN 'Dione'
|
||||
WHEN 4 THEN 'Rhea' WHEN 5 THEN 'Titan'
|
||||
WHEN 6 THEN 'Iapetus' WHEN 7 THEN 'Hyperion'
|
||||
END,
|
||||
saturn_moon_observe(id, '40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-15 03:00:00+00')
|
||||
FROM generate_series(0, 7) AS id
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Uranus moons
|
||||
SELECT 'Uranus',
|
||||
CASE id WHEN 0 THEN 'Miranda' WHEN 1 THEN 'Ariel'
|
||||
WHEN 2 THEN 'Umbriel' WHEN 3 THEN 'Titania'
|
||||
WHEN 4 THEN 'Oberon'
|
||||
END,
|
||||
uranus_moon_observe(id, '40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-15 03:00:00+00')
|
||||
FROM generate_series(0, 4) AS id
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Mars moons
|
||||
SELECT 'Mars',
|
||||
CASE id WHEN 0 THEN 'Phobos' WHEN 1 THEN 'Deimos' END,
|
||||
mars_moon_observe(id, '40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-15 03:00:00+00')
|
||||
FROM generate_series(0, 1) AS id
|
||||
)
|
||||
SELECT parent,
|
||||
moon,
|
||||
round(topo_azimuth(obs)::numeric, 1) AS az,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el,
|
||||
round((topo_range(obs) / 149597870.7)::numeric, 3) AS dist_au
|
||||
FROM all_moons
|
||||
WHERE topo_elevation(obs) > 0
|
||||
ORDER BY parent, moon;
|
||||
```
|
||||
|
||||
This returns every visible moon from Boulder at the specified time. 19 moons, 19 function calls, one result set.
|
||||
|
||||
### Track Galilean moon positions over time
|
||||
|
||||
Watch Io complete part of its 1.77-day orbit around Jupiter:
|
||||
|
||||
```sql
|
||||
SELECT t,
|
||||
round(topo_range(galilean_observe(0, '40.0N 105.3W 1655m'::observer, t))::numeric, 0) AS io_range_km,
|
||||
round(topo_range(planet_observe(5, '40.0N 105.3W 1655m'::observer, t))::numeric, 0) AS jupiter_range_km,
|
||||
round((topo_range(galilean_observe(0, '40.0N 105.3W 1655m'::observer, t))
|
||||
- topo_range(planet_observe(5, '40.0N 105.3W 1655m'::observer, t)))::numeric, 0) AS separation_km
|
||||
FROM generate_series(
|
||||
'2024-03-15 00:00:00+00'::timestamptz,
|
||||
'2024-03-16 18:00:00+00'::timestamptz,
|
||||
interval '2 hours'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
The `separation_km` column shows Io oscillating between being closer and farther than Jupiter as seen from Earth — the projection of its orbit along the line of sight.
|
||||
|
||||
### Titan observation windows
|
||||
|
||||
Find when Titan is above the horizon over a week:
|
||||
|
||||
```sql
|
||||
SELECT t::date AS date,
|
||||
round(topo_elevation(
|
||||
saturn_moon_observe(5, '40.0N 105.3W 1655m'::observer, t)
|
||||
)::numeric, 1) AS titan_el
|
||||
FROM generate_series(
|
||||
'2024-06-15 00:00:00+00'::timestamptz,
|
||||
'2024-06-22 00:00:00+00'::timestamptz,
|
||||
interval '1 hour'
|
||||
) AS t
|
||||
WHERE topo_elevation(
|
||||
saturn_moon_observe(5, '40.0N 105.3W 1655m'::observer, t)
|
||||
) > 10
|
||||
ORDER BY t;
|
||||
```
|
||||
|
||||
Titan (body ID 5 in the Saturn system) is the only moon in the solar system with a thick atmosphere, making it a frequent target for amateur imaging.
|
||||
256
docs/src/content/docs/guides/star-catalogs.mdx
Normal file
256
docs/src/content/docs/guides/star-catalogs.mdx
Normal file
@ -0,0 +1,256 @@
|
||||
---
|
||||
title: Star Catalogs
|
||||
sidebar:
|
||||
order: 4
|
||||
---
|
||||
|
||||
import { Steps, Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
pg_orrery computes topocentric positions for any star given its J2000 right ascension and declination. Feed it a star catalog table and you can observe hundreds of thousands of stars in a single query. The function applies IAU 1976 precession to bring J2000 coordinates to the observation epoch, then transforms to horizon coordinates for a given observer.
|
||||
|
||||
## How you do it today
|
||||
|
||||
Computing where a star appears in the sky involves:
|
||||
|
||||
- **Stellarium**: Type a star name, get its current position. Not queryable, not batchable.
|
||||
- **Astropy + catalog**: Load the Hipparcos or Tycho-2 catalog, apply precession/nutation/aberration, transform to alt-az. Accurate, but per-object Python calls.
|
||||
- **Skyfield**: Wraps the Hipparcos catalog with high-precision coordinate transforms. Clean API, but processing a full catalog means iterating over rows.
|
||||
- **SIMBAD/VizieR**: Query astronomical databases for catalog data. Returns J2000 coordinates; you still need to transform to local horizon coordinates yourself.
|
||||
|
||||
The bottleneck is the same as with planets: the computation happens outside your database. If your observation log, scheduling system, or data pipeline lives in PostgreSQL, you export catalog data, compute positions externally, and import the results.
|
||||
|
||||
## What changes with pg_orrery
|
||||
|
||||
`star_observe()` takes J2000 RA (in hours) and Dec (in degrees), an observer, and a time. It returns a `topocentric` with azimuth, elevation, and zero range (stars are treated as infinitely distant). The function applies IAU 1976 precession and the standard equatorial-to-horizontal transform.
|
||||
|
||||
`star_observe_safe()` does the same but returns NULL for invalid inputs (RA outside 0-24 hours, Dec outside +/-90 degrees). Use it for batch queries over catalog tables that might contain bad rows.
|
||||
|
||||
The key performance characteristic: star observation processes at about 714,000 observations per second. A 100,000-star catalog can be fully observed from any location at any time in under 150ms.
|
||||
|
||||
## What pg_orrery does not replace
|
||||
|
||||
<Aside type="caution" title="Precision boundaries">
|
||||
Star observation in pg_orrery uses IAU 1976 precession only. The missing corrections are small for observation planning but significant for precision astrometry.
|
||||
</Aside>
|
||||
|
||||
- **No nutation.** IAU 1976 precession alone introduces errors up to ~10 arcseconds over a few decades. For visual observation planning, this is negligible. For sub-arcsecond work, use SOFA/ERFA routines.
|
||||
- **No proper motion.** Barnard's Star moves 10 arcseconds/year. pg_orrery treats catalog coordinates as fixed. If your catalog includes proper motion columns, you can pre-apply the correction in SQL before calling `star_observe()`.
|
||||
- **No aberration.** Annual aberration displaces star positions by up to ~20 arcseconds. This matters for precision pointing but not for finding stars at the eyepiece.
|
||||
- **No parallax.** Stellar parallax is at most ~0.8 arcseconds (Proxima Centauri). Not a concern for observation planning.
|
||||
- **Range is zero.** Stars are treated as infinitely far. The `topo_range()` accessor returns 0 for star observations.
|
||||
|
||||
## Try it
|
||||
|
||||
### Observe well-known stars
|
||||
|
||||
The bright navigational stars and their J2000 coordinates:
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Individual stars">
|
||||
```sql
|
||||
-- Polaris: RA 2h 31m 49s = 2.530303h, Dec +89.2641 deg
|
||||
SELECT 'Polaris' AS star,
|
||||
round(topo_azimuth(star_observe(
|
||||
2.530303, 89.2641,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
now()))::numeric, 1) AS az,
|
||||
round(topo_elevation(star_observe(
|
||||
2.530303, 89.2641,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
now()))::numeric, 1) AS el;
|
||||
```
|
||||
|
||||
From Boulder (latitude ~40 N), Polaris should be at roughly 40 degrees elevation, near due north (azimuth ~0/360).
|
||||
</TabItem>
|
||||
<TabItem label="Multiple stars">
|
||||
```sql
|
||||
-- Observe several bright stars at once
|
||||
WITH stars(name, ra_h, dec_deg) AS (VALUES
|
||||
('Polaris', 2.530303, 89.2641),
|
||||
('Sirius', 6.752478, -16.7161),
|
||||
('Vega', 18.615650, 38.7837),
|
||||
('Betelgeuse', 5.919529, 7.4070),
|
||||
('Rigel', 5.242299, -8.2016),
|
||||
('Arcturus', 14.261027, 19.1824),
|
||||
('Capella', 5.278155, 46.0076),
|
||||
('Procyon', 7.655033, 5.2250)
|
||||
)
|
||||
SELECT name,
|
||||
round(topo_azimuth(obs)::numeric, 1) AS az,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el
|
||||
FROM stars,
|
||||
LATERAL star_observe(ra_h, dec_deg,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-01-15 03:00:00+00') obs
|
||||
WHERE topo_elevation(obs) > 0
|
||||
ORDER BY topo_elevation(obs) DESC;
|
||||
```
|
||||
|
||||
This observes eight bright stars and filters to those above the horizon. The `LATERAL` keyword lets PostgreSQL call `star_observe()` once per star.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Build a star catalog table
|
||||
|
||||
For batch operations, store catalog data in a table. Here is a minimal schema using Hipparcos-style data:
|
||||
|
||||
```sql
|
||||
CREATE TABLE star_catalog (
|
||||
hip_id integer PRIMARY KEY,
|
||||
name text,
|
||||
ra_hours float8 NOT NULL,
|
||||
dec_deg float8 NOT NULL,
|
||||
vmag float8, -- visual magnitude
|
||||
spectral text
|
||||
);
|
||||
|
||||
-- Insert a few bright stars for demonstration
|
||||
INSERT INTO star_catalog VALUES
|
||||
(11767, 'Polaris', 2.530303, 89.2641, 1.98, 'F7Ib'),
|
||||
(32349, 'Sirius', 6.752478, -16.7161, -1.46, 'A1V'),
|
||||
(91262, 'Vega', 18.615650, 38.7837, 0.03, 'A0V'),
|
||||
(27989, 'Betelgeuse', 5.919529, 7.4070, 0.42, 'M1Ia'),
|
||||
(24436, 'Rigel', 5.242299, -8.2016, 0.13, 'B8Ia'),
|
||||
(69673, 'Arcturus', 14.261027, 19.1824, -0.05, 'K1III'),
|
||||
(24608, 'Capella', 5.278155, 46.0076, 0.08, 'G8III'),
|
||||
(37279, 'Procyon', 7.655033, 5.2250, 0.34, 'F5IV'),
|
||||
(7588, 'Achernar', 1.628556, -57.2367, 0.46, 'B3V'),
|
||||
(80763, 'Antares', 16.490128, -26.4320, 0.96, 'M1Ib');
|
||||
```
|
||||
|
||||
### Batch observe the catalog
|
||||
|
||||
Observe every star in the catalog from a given location and time:
|
||||
|
||||
```sql
|
||||
SELECT name,
|
||||
vmag,
|
||||
round(topo_azimuth(obs)::numeric, 1) AS az,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el
|
||||
FROM star_catalog,
|
||||
LATERAL star_observe_safe(ra_hours, dec_deg,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-01-15 03:00:00+00') obs
|
||||
WHERE obs IS NOT NULL
|
||||
AND topo_elevation(obs) > 0
|
||||
ORDER BY vmag;
|
||||
```
|
||||
|
||||
`star_observe_safe()` returns NULL if the catalog contains invalid coordinates, so the query runs cleanly over the full table. The `WHERE obs IS NOT NULL` clause filters those out.
|
||||
|
||||
### What is visible tonight, brighter than magnitude 2?
|
||||
|
||||
```sql
|
||||
SELECT name,
|
||||
vmag,
|
||||
spectral,
|
||||
round(topo_azimuth(obs)::numeric, 1) AS az,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el
|
||||
FROM star_catalog,
|
||||
LATERAL star_observe_safe(ra_hours, dec_deg,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-07-15 04:00:00+00') obs
|
||||
WHERE obs IS NOT NULL
|
||||
AND topo_elevation(obs) > 10
|
||||
AND vmag < 2.0
|
||||
ORDER BY vmag;
|
||||
```
|
||||
|
||||
This finds all bright stars above 10 degrees elevation from Boulder on a July evening. Replace the time and observer for your own conditions.
|
||||
|
||||
### Track a star through the night
|
||||
|
||||
Watch Vega rise, culminate, and set:
|
||||
|
||||
```sql
|
||||
SELECT t,
|
||||
round(topo_azimuth(star_observe(
|
||||
18.615650, 38.7837,
|
||||
'40.0N 105.3W 1655m'::observer, t
|
||||
))::numeric, 1) AS az,
|
||||
round(topo_elevation(star_observe(
|
||||
18.615650, 38.7837,
|
||||
'40.0N 105.3W 1655m'::observer, t
|
||||
))::numeric, 1) AS el
|
||||
FROM generate_series(
|
||||
'2024-07-15 02:00:00+00'::timestamptz,
|
||||
'2024-07-15 12:00:00+00'::timestamptz,
|
||||
interval '30 minutes'
|
||||
) AS t
|
||||
WHERE topo_elevation(star_observe(
|
||||
18.615650, 38.7837,
|
||||
'40.0N 105.3W 1655m'::observer, t
|
||||
)) > 0;
|
||||
```
|
||||
|
||||
Vega culminates at nearly 89 degrees elevation from Boulder — it passes almost directly overhead on summer nights.
|
||||
|
||||
### Precession demonstration
|
||||
|
||||
The same star at J2000.0 epoch vs. 25 years later. IAU 1976 precession shifts the apparent position:
|
||||
|
||||
```sql
|
||||
SELECT 'J2000.0 epoch' AS epoch,
|
||||
round(topo_elevation(star_observe(
|
||||
2.530303, 89.2641,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2000-01-01 12:00:00+00'
|
||||
))::numeric, 2) AS polaris_el
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT '2025 epoch',
|
||||
round(topo_elevation(star_observe(
|
||||
2.530303, 89.2641,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2025-06-15 04:00:00+00'
|
||||
))::numeric, 2);
|
||||
```
|
||||
|
||||
The elevation changes by a fraction of a degree over 25 years. This is precession in action: the Earth's rotational axis slowly traces a circle in space.
|
||||
|
||||
### Cross-match with observation logs
|
||||
|
||||
If you keep an observation log in PostgreSQL, you can join it with star positions:
|
||||
|
||||
```sql
|
||||
-- Hypothetical observation log table
|
||||
CREATE TABLE obs_log (
|
||||
id serial PRIMARY KEY,
|
||||
target_hip integer REFERENCES star_catalog(hip_id),
|
||||
obs_time timestamptz NOT NULL,
|
||||
observer observer NOT NULL,
|
||||
notes text
|
||||
);
|
||||
|
||||
-- What was the elevation of each target at the time of observation?
|
||||
SELECT l.obs_time,
|
||||
s.name,
|
||||
round(topo_elevation(
|
||||
star_observe(s.ra_hours, s.dec_deg, l.observer, l.obs_time)
|
||||
)::numeric, 1) AS actual_el,
|
||||
l.notes
|
||||
FROM obs_log l
|
||||
JOIN star_catalog s ON s.hip_id = l.target_hip
|
||||
ORDER BY l.obs_time;
|
||||
```
|
||||
|
||||
This retroactively computes the sky position of every logged target at the time it was observed. Useful for data quality checks — an observation logged at 5 degrees elevation might be suspect.
|
||||
|
||||
### Full catalog performance
|
||||
|
||||
With a full Hipparcos catalog loaded (118,218 stars), a full-catalog observation runs at about 714,000 stars per second:
|
||||
|
||||
```sql
|
||||
-- Time a full catalog sweep (for benchmarking)
|
||||
EXPLAIN ANALYZE
|
||||
SELECT count(*)
|
||||
FROM star_catalog,
|
||||
LATERAL star_observe_safe(ra_hours, dec_deg,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
now()) obs
|
||||
WHERE obs IS NOT NULL
|
||||
AND topo_elevation(obs) > 0;
|
||||
```
|
||||
|
||||
The exact throughput depends on hardware, but the function is `PARALLEL SAFE`, so PostgreSQL will distribute the work across available cores on large catalogs.
|
||||
324
docs/src/content/docs/guides/tracking-satellites.mdx
Normal file
324
docs/src/content/docs/guides/tracking-satellites.mdx
Normal file
@ -0,0 +1,324 @@
|
||||
---
|
||||
title: Tracking Satellites
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
import { Steps, Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Satellite tracking is the domain pg_orrery was originally built for. The core idea: instead of propagating TLEs one at a time in Python and then writing results to your database, move the propagation into the database itself. The satellite catalog becomes a live, queryable model of near-Earth space.
|
||||
|
||||
## How you do it today
|
||||
|
||||
Most satellite tracking workflows follow the same pattern:
|
||||
|
||||
1. **Download TLEs** from Space-Track or CelesTrak into a file or database table.
|
||||
2. **Propagate** each TLE in Python (python-sgp4, Skyfield) or C++ (libsgp4) to get position/velocity at a given time.
|
||||
3. **Transform** ECI coordinates to observer-relative look angles (azimuth, elevation, range).
|
||||
4. **Predict passes** by stepping through time and finding horizon crossings.
|
||||
5. **Screen for conjunctions** by computing pairwise distances between objects.
|
||||
|
||||
Tools like GPredict handle this with a GUI. Skyfield wraps python-sgp4 with a clean API. CelesTrak's GP data service provides pre-propagated state vectors. Each tool handles one satellite, one observer, one time at a time.
|
||||
|
||||
The bottleneck shows up when you need to process the catalog. Propagating 12,000 TLEs for a single epoch in Python takes seconds. Joining the results against a frequency database or an owner table requires exporting to CSV, loading into a database, and running the join. Pass prediction for a constellation of 100+ satellites means nested loops. Conjunction screening for the full catalog means O(n^2) pairwise comparisons.
|
||||
|
||||
## What changes with pg_orrery
|
||||
|
||||
pg_orrery implements SGP4/SDP4 (Brouwer, 1959; Hoots & Roehrich, 1980) as native PostgreSQL functions. The `tle` type stores parsed mean elements directly in a column. Propagation, observation, and pass prediction are SQL function calls that operate on that column.
|
||||
|
||||
What this means in practice:
|
||||
|
||||
- **Batch observation** of the entire catalog is a single `SELECT`. PostgreSQL parallelizes across cores.
|
||||
- **Joining** satellite positions with metadata (owner, frequency, purpose) is a standard SQL `JOIN`.
|
||||
- **Pass prediction** over a time window for many satellites uses `LATERAL JOIN` with `predict_passes()`.
|
||||
- **Conjunction screening** uses a GiST index on the `tle` column, reducing O(n^2) comparisons to index scans.
|
||||
|
||||
The `observe_safe()` function returns NULL instead of raising an error when a TLE has decayed or diverged. This keeps batch queries running even when the catalog contains stale elements.
|
||||
|
||||
## What pg_orrery does not replace
|
||||
|
||||
<Aside type="caution" title="Know the boundaries">
|
||||
pg_orrery propagates TLEs and computes look angles. It does not replace the full satellite operations stack.
|
||||
</Aside>
|
||||
|
||||
- **No real-time GUI.** GPredict and STK provide map displays, polar plots, and Doppler displays. pg_orrery returns numbers. Use any visualization tool to render its output.
|
||||
- **No rotator control.** Hamlib drives antenna rotators. pg_orrery computes the azimuth and elevation values Hamlib would consume, but it has no hardware interface.
|
||||
- **No TLE fetching.** Bring your own TLEs from Space-Track, CelesTrak, or any provider. pg_orrery parses and propagates them.
|
||||
- **No orbit determination.** pg_orrery propagates existing TLEs. It does not fit orbits from observations.
|
||||
- **No high-precision propagation.** SGP4/SDP4 accuracy degrades with TLE age. For operational conjunction assessment, use SP ephemerides or owner/operator-provided state vectors. pg_orrery's GiST screening finds candidates; you verify with better data.
|
||||
|
||||
## Try it
|
||||
|
||||
### Set up a satellite catalog
|
||||
|
||||
Create a table that stores TLEs alongside metadata. This mirrors what you would have if you ingest CelesTrak data:
|
||||
|
||||
```sql
|
||||
CREATE TABLE satellites (
|
||||
norad_id integer PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
tle tle NOT NULL,
|
||||
owner text,
|
||||
purpose text
|
||||
);
|
||||
|
||||
-- ISS
|
||||
INSERT INTO satellites VALUES (
|
||||
25544, 'ISS (ZARYA)',
|
||||
'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001',
|
||||
'ISS', 'Space Station'
|
||||
);
|
||||
|
||||
-- Hubble Space Telescope
|
||||
INSERT INTO satellites VALUES (
|
||||
20580, 'HST',
|
||||
'1 20580U 90037B 24001.50000000 .00000790 00000+0 39573-4 0 9992
|
||||
2 20580 28.4705 61.4398 0002797 317.3115 42.7577 15.09395228 00008',
|
||||
'NASA', 'Telescope'
|
||||
);
|
||||
|
||||
-- GPS IIR-M
|
||||
INSERT INTO satellites VALUES (
|
||||
28874, 'GPS BIIR-3 (PRN 29)',
|
||||
'1 28874U 05038A 24001.50000000 .00000012 00000+0 00000+0 0 9993
|
||||
2 28874 55.4408 300.3467 0117034 51.6543 309.5420 2.00557079 00006',
|
||||
'USSF', 'Navigation'
|
||||
);
|
||||
```
|
||||
|
||||
### Batch observation
|
||||
|
||||
Observe every satellite in the catalog from a single observer at a single time:
|
||||
|
||||
```sql
|
||||
SELECT s.name,
|
||||
round(topo_azimuth(obs)::numeric, 1) AS az,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el,
|
||||
round(topo_range(obs)::numeric, 0) AS range_km
|
||||
FROM satellites s,
|
||||
observe_safe(s.tle,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-01-01 12:00:00+00') obs
|
||||
WHERE topo_elevation(obs) > 0
|
||||
ORDER BY topo_elevation(obs) DESC;
|
||||
```
|
||||
|
||||
`observe_safe()` returns NULL for decayed or invalid TLEs, so the query runs cleanly over the full catalog. The `WHERE` clause filters to satellites above the horizon. With 12,000 TLEs, this completes in about 17ms.
|
||||
|
||||
### Join with metadata
|
||||
|
||||
The power of doing this in SQL: you can join satellite positions with any other table. Suppose you have a frequency allocation table:
|
||||
|
||||
```sql
|
||||
-- Hypothetical frequency table
|
||||
CREATE TABLE sat_frequencies (
|
||||
norad_id integer REFERENCES satellites(norad_id),
|
||||
downlink_mhz float8,
|
||||
mode text
|
||||
);
|
||||
|
||||
-- Which satellites transmitting on 2m are visible right now?
|
||||
SELECT s.name,
|
||||
f.downlink_mhz,
|
||||
f.mode,
|
||||
round(topo_elevation(obs)::numeric, 1) AS el
|
||||
FROM satellites s
|
||||
JOIN sat_frequencies f USING (norad_id),
|
||||
observe_safe(s.tle,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
now()) obs
|
||||
WHERE f.downlink_mhz BETWEEN 144.0 AND 148.0
|
||||
AND topo_elevation(obs) > 10
|
||||
ORDER BY topo_elevation(obs) DESC;
|
||||
```
|
||||
|
||||
This is a query you cannot write with python-sgp4 alone. It combines orbital propagation with database operations in a single statement.
|
||||
|
||||
### Pass prediction
|
||||
|
||||
Predict passes for a single satellite over the next 24 hours:
|
||||
|
||||
```sql
|
||||
SELECT pass_aos_time(p) AS rise,
|
||||
round(pass_max_elevation(p)::numeric, 1) AS max_el,
|
||||
pass_max_el_time(p) AS culmination,
|
||||
pass_los_time(p) AS set,
|
||||
round(pass_aos_azimuth(p)::numeric, 0) AS rise_az,
|
||||
round(pass_los_azimuth(p)::numeric, 0) AS set_az,
|
||||
pass_duration(p) AS duration
|
||||
FROM satellites s,
|
||||
predict_passes(s.tle,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
now(),
|
||||
now() + interval '24 hours',
|
||||
10.0) p
|
||||
WHERE s.norad_id = 25544;
|
||||
```
|
||||
|
||||
The `10.0` parameter filters to passes with maximum elevation above 10 degrees. Lower the threshold to see more passes; raise it to find only the high ones worth tracking.
|
||||
|
||||
### Predict passes for many satellites
|
||||
|
||||
Use `LATERAL JOIN` to predict passes for every satellite in a subset:
|
||||
|
||||
```sql
|
||||
SELECT s.name,
|
||||
pass_aos_time(p) AS rise,
|
||||
round(pass_max_elevation(p)::numeric, 1) AS max_el,
|
||||
pass_duration(p) AS duration
|
||||
FROM satellites s,
|
||||
LATERAL predict_passes(s.tle,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
now(),
|
||||
now() + interval '24 hours',
|
||||
20.0) p
|
||||
WHERE s.purpose = 'Space Station'
|
||||
ORDER BY pass_aos_time(p);
|
||||
```
|
||||
|
||||
This finds all passes above 20 degrees for every space station in the catalog. The `LATERAL` keyword lets PostgreSQL call `predict_passes()` once per row of the outer query.
|
||||
|
||||
### Ground tracks
|
||||
|
||||
Trace the ISS ground track over one orbit (approximately 93 minutes):
|
||||
|
||||
```sql
|
||||
SELECT t,
|
||||
round(lat::numeric, 2) AS latitude,
|
||||
round(lon::numeric, 2) AS longitude,
|
||||
round(alt::numeric, 0) AS altitude_km
|
||||
FROM satellites s,
|
||||
ground_track(s.tle,
|
||||
'2024-01-01 12:00:00+00',
|
||||
'2024-01-01 13:33:00+00',
|
||||
interval '1 minute')
|
||||
WHERE s.norad_id = 25544;
|
||||
```
|
||||
|
||||
The output is a set of (time, lat, lon, alt) rows ready to plot on a map or export to GeoJSON.
|
||||
|
||||
### Subsatellite point
|
||||
|
||||
The subsatellite point is the nadir location directly below the satellite:
|
||||
|
||||
```sql
|
||||
SELECT geodetic_lat(subsatellite_point(s.tle, now())) AS lat,
|
||||
geodetic_lon(subsatellite_point(s.tle, now())) AS lon,
|
||||
geodetic_alt(subsatellite_point(s.tle, now())) AS alt_km
|
||||
FROM satellites s
|
||||
WHERE s.norad_id = 25544;
|
||||
```
|
||||
|
||||
### Distance between satellites
|
||||
|
||||
Compute the Euclidean distance between any two TLEs at a given time:
|
||||
|
||||
```sql
|
||||
SELECT round(tle_distance(a.tle, b.tle, '2024-01-01 12:00:00+00')::numeric, 0) AS dist_km
|
||||
FROM satellites a, satellites b
|
||||
WHERE a.norad_id = 25544 -- ISS
|
||||
AND b.norad_id = 20580; -- Hubble
|
||||
```
|
||||
|
||||
### Conjunction screening with GiST
|
||||
|
||||
The GiST index on the `tle` column enables fast spatial filtering by altitude band and inclination. This is the foundation for conjunction screening:
|
||||
|
||||
<Steps>
|
||||
1. **Create the index:**
|
||||
|
||||
```sql
|
||||
CREATE INDEX satellites_orbit_idx ON satellites USING gist (tle);
|
||||
```
|
||||
|
||||
The index stores a 2-D key for each TLE: altitude band (perigee to apogee) and inclination range. Building the index over the full catalog takes about 200ms.
|
||||
|
||||
2. **Find satellites in overlapping orbital shells:**
|
||||
|
||||
The `&&` operator tests whether two TLEs occupy overlapping altitude bands AND inclination ranges. This is a necessary (not sufficient) condition for conjunction.
|
||||
|
||||
```sql
|
||||
-- Find all satellites in the same orbital shell as the ISS
|
||||
SELECT b.name,
|
||||
round(tle_perigee(b.tle)::numeric, 0) AS perigee_km,
|
||||
round(tle_apogee(b.tle)::numeric, 0) AS apogee_km,
|
||||
round(tle_inclination(b.tle)::numeric, 1) AS inc_deg
|
||||
FROM satellites a, satellites b
|
||||
WHERE a.norad_id = 25544
|
||||
AND a.norad_id != b.norad_id
|
||||
AND a.tle && b.tle
|
||||
ORDER BY tle_perigee(b.tle);
|
||||
```
|
||||
|
||||
This query uses the GiST index to avoid scanning the full catalog. Only satellites whose altitude band overlaps the ISS and whose inclination is similar are returned.
|
||||
|
||||
3. **Nearest-neighbor by altitude separation:**
|
||||
|
||||
The `<->` operator returns the minimum altitude-band separation in km between two TLEs. Combined with GiST, it supports efficient K-nearest-neighbor queries:
|
||||
|
||||
```sql
|
||||
-- Find the 10 satellites with the closest altitude band to the ISS
|
||||
SELECT b.name,
|
||||
round((a.tle <-> b.tle)::numeric, 0) AS alt_separation_km
|
||||
FROM satellites a, satellites b
|
||||
WHERE a.norad_id = 25544
|
||||
AND a.norad_id != b.norad_id
|
||||
ORDER BY a.tle <-> b.tle
|
||||
LIMIT 10;
|
||||
```
|
||||
|
||||
4. **Full conjunction check on candidates:**
|
||||
|
||||
The GiST filter narrows the catalog to a handful of candidates. Then verify with actual propagation:
|
||||
|
||||
```sql
|
||||
-- Step 1: GiST narrows to candidates (fast)
|
||||
-- Step 2: Compute actual distance at each time step (precise)
|
||||
WITH candidates AS (
|
||||
SELECT b.norad_id, b.name, b.tle
|
||||
FROM satellites a, satellites b
|
||||
WHERE a.norad_id = 25544
|
||||
AND a.norad_id != b.norad_id
|
||||
AND a.tle && b.tle
|
||||
),
|
||||
iss AS (
|
||||
SELECT tle FROM satellites WHERE norad_id = 25544
|
||||
)
|
||||
SELECT c.name,
|
||||
t AS check_time,
|
||||
round(tle_distance(iss.tle, c.tle, t)::numeric, 1) AS dist_km
|
||||
FROM candidates c, iss,
|
||||
generate_series(
|
||||
'2024-01-01 00:00:00+00'::timestamptz,
|
||||
'2024-01-02 00:00:00+00'::timestamptz,
|
||||
interval '5 minutes') t
|
||||
WHERE tle_distance(iss.tle, c.tle, t) < 50.0
|
||||
ORDER BY dist_km;
|
||||
```
|
||||
|
||||
The GiST filter reduces a 12,000-object catalog to a few dozen candidates. The time-stepping check then finds the actual close approaches.
|
||||
</Steps>
|
||||
|
||||
<Aside type="tip" title="GiST is a coarse filter">
|
||||
The `&&` operator checks altitude band and inclination overlap, not instantaneous distance. Two satellites can share an orbital shell and never come close because their RAANs or phases differ. Always follow GiST filtering with full propagation for actual conjunction assessment.
|
||||
</Aside>
|
||||
|
||||
### TLE metadata accessors
|
||||
|
||||
Every TLE exposes its orbital elements as accessor functions:
|
||||
|
||||
```sql
|
||||
SELECT tle_norad_id(tle) AS norad_id,
|
||||
tle_intl_desig(tle) AS cospar_id,
|
||||
round(tle_inclination(tle)::numeric, 2) AS inc_deg,
|
||||
round(tle_eccentricity(tle)::numeric, 6) AS ecc,
|
||||
round(tle_perigee(tle)::numeric, 0) AS perigee_km,
|
||||
round(tle_apogee(tle)::numeric, 0) AS apogee_km,
|
||||
round(tle_period(tle)::numeric, 1) AS period_min,
|
||||
round(tle_age(tle, now())::numeric, 1) AS age_days
|
||||
FROM satellites
|
||||
ORDER BY tle_perigee(tle);
|
||||
```
|
||||
|
||||
The `tle_age()` function returns how many days old the TLE is relative to a given time. Fresh TLEs (age < 3 days) give the best propagation accuracy.
|
||||
71
docs/src/content/docs/index.mdx
Normal file
71
docs/src/content/docs/index.mdx
Normal file
@ -0,0 +1,71 @@
|
||||
---
|
||||
title: pg_orrery Documentation
|
||||
description: Solar system computation for PostgreSQL
|
||||
template: splash
|
||||
hero:
|
||||
tagline: "It's not rocket science. (It's celestial mechanics. But now it's just SQL.)"
|
||||
image:
|
||||
file: ../../assets/hero-elephant-orbit.svg
|
||||
alt: PostgreSQL elephant orbiting a planet
|
||||
actions:
|
||||
- text: Get Started
|
||||
link: /getting-started/what-is-pg-orrery/
|
||||
icon: right-arrow
|
||||
variant: primary
|
||||
- text: What's Different
|
||||
link: /workflow/sql-advantage/
|
||||
icon: open-book
|
||||
---
|
||||
|
||||
import { Card, CardGrid, LinkCard } from "@astrojs/starlight/components";
|
||||
|
||||
## What can pg_orrery do?
|
||||
|
||||
<CardGrid>
|
||||
<Card title="Track anything in orbit" icon="rocket">
|
||||
SGP4/SDP4 propagation over 12,000 TLEs in 17ms. GiST-indexed conjunction
|
||||
screening. Pass prediction with AOS/TCA/LOS. Ground tracks, subsatellite
|
||||
points, and topocentric observation — all as SQL functions.
|
||||
</Card>
|
||||
<Card title="Observe the solar system" icon="sun">
|
||||
Eight planets via VSOP87 (built-in) or optional JPL DE440/441 (~0.1
|
||||
milliarcsecond). The Sun, the Moon via ELP2000-82B, 19 planetary moons
|
||||
across Jupiter, Saturn, Uranus, and Mars. Stars from J2000 catalog
|
||||
coordinates. Comets and asteroids from Keplerian elements.
|
||||
</Card>
|
||||
<Card title="Predict radio bursts" icon="star">
|
||||
Jupiter-Io decametric emission probability from Carr source regions.
|
||||
Io orbital phase, Jupiter Central Meridian Longitude (System III), and
|
||||
burst probability — batch-computed over any time range with generate_series.
|
||||
</Card>
|
||||
<Card title="Plan trajectories" icon="right-caret">
|
||||
Lambert solver for interplanetary transfers between any two planets.
|
||||
Pork chop plots as SQL CROSS JOINs — 22,500 transfer solutions in
|
||||
8.3 seconds. Departure C3, arrival C3, time of flight, transfer SMA.
|
||||
</Card>
|
||||
</CardGrid>
|
||||
|
||||
## Explore the docs
|
||||
|
||||
<CardGrid>
|
||||
<LinkCard
|
||||
title="Quick Start"
|
||||
description="Five SQL queries from 'Where is Jupiter?' to planning an Earth-Mars transfer"
|
||||
href="/getting-started/quick-start/"
|
||||
/>
|
||||
<LinkCard
|
||||
title="Guides"
|
||||
description="Domain-specific walkthroughs for satellites, planets, moons, stars, comets, radio, and trajectories"
|
||||
href="/guides/tracking-satellites/"
|
||||
/>
|
||||
<LinkCard
|
||||
title="Workflow Translation"
|
||||
description="Side-by-side comparisons: how you do it today vs. how pg_orrery changes the game"
|
||||
href="/workflow/from-skyfield/"
|
||||
/>
|
||||
<LinkCard
|
||||
title="Architecture"
|
||||
description="Design principles, constant chain of custody, and the observation pipeline"
|
||||
href="/architecture/design-principles/"
|
||||
/>
|
||||
</CardGrid>
|
||||
268
docs/src/content/docs/performance/benchmarks.mdx
Normal file
268
docs/src/content/docs/performance/benchmarks.mdx
Normal file
@ -0,0 +1,268 @@
|
||||
---
|
||||
title: Benchmarks
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Measured performance numbers for pg_orrery's core operations. Every number on this page was produced by running the listed SQL query against a live PostgreSQL 17 instance with a single backend, no parallel workers, and no connection pooling overhead.
|
||||
|
||||
<Aside type="note" title="Methodology">
|
||||
All benchmarks use PostgreSQL's `EXPLAIN (ANALYZE, BUFFERS)` for timing. The numbers are wall-clock execution time for the query, not per-function overhead. Each benchmark was run three times; the reported value is the median. Cold start was avoided by running each query once before measurement.
|
||||
</Aside>
|
||||
|
||||
## Summary table
|
||||
|
||||
| Operation | Count | Time | Rate | Notes |
|
||||
|-----------|-------|------|------|-------|
|
||||
| TLE propagation (SGP4) | 12,000 | 17 ms | 706K/sec | Mixed LEO/MEO/GEO |
|
||||
| Planet observation (VSOP87) | 875 | 57 ms | 15.4K/sec | All 7 non-Earth planets, 125 times each |
|
||||
| Galilean moon observation | 1,000 | 63 ms | 15.9K/sec | L1.2 + VSOP87 pipeline |
|
||||
| Saturn moon observation | 800 | 53 ms | 15.1K/sec | TASS17 + VSOP87 |
|
||||
| Star observation | 500 | 0.7 ms | 714K/sec | Precession + az/el only |
|
||||
| Lambert transfer solve | 100 | 0.1 ms | 800K/sec | Single-rev prograde |
|
||||
| Pork chop plot (150 x 150) | 22,500 | 8.3 s | 2.7K/sec | Full VSOP87 + Lambert pipeline |
|
||||
|
||||
**Conditions:** PostgreSQL 17.2, single backend, no parallel workers, Intel Xeon E-2286G @ 4.0 GHz, 64 GB ECC DDR4-2666. Extension compiled with GCC 14.2, `-O2`.
|
||||
|
||||
## TLE propagation
|
||||
|
||||
The fundamental operation: given a TLE and a timestamp, compute the TEME position and velocity.
|
||||
|
||||
```sql
|
||||
-- Benchmark: propagate 12,000 TLEs to a single epoch
|
||||
EXPLAIN (ANALYZE, BUFFERS)
|
||||
SELECT sgp4_propagate(tle, '2024-06-15 12:00:00+00'::timestamptz)
|
||||
FROM satellite_catalog;
|
||||
```
|
||||
|
||||
**12,000 TLEs in 17 ms --- 706,000 propagations per second.**
|
||||
|
||||
This rate includes the full SGP4/SDP4 pipeline: struct conversion, `select_ephemeris()`, initialization, propagation, velocity unit conversion (km/min to km/s), and result allocation. The catalog contains a mix of LEO, MEO, and GEO objects, so both SGP4 and SDP4 codepaths are exercised.
|
||||
|
||||
### What limits the rate
|
||||
|
||||
SGP4 propagation is compute-bound, dominated by trigonometric function evaluations in the short-period perturbation corrections. The `params` array (736 bytes) fits in L1 cache. The bottleneck is not memory access but `sin()` / `cos()` calls in the inner loop.
|
||||
|
||||
### Scaling with parallel workers
|
||||
|
||||
When PostgreSQL allocates parallel workers, throughput scales near-linearly because all functions are `PARALLEL SAFE` with zero shared state:
|
||||
|
||||
```sql
|
||||
-- Force parallel execution (for benchmarking only)
|
||||
SET max_parallel_workers_per_gather = 4;
|
||||
SET parallel_tuple_cost = 0;
|
||||
|
||||
EXPLAIN (ANALYZE)
|
||||
SELECT sgp4_propagate(tle, now())
|
||||
FROM satellite_catalog;
|
||||
```
|
||||
|
||||
With 4 workers on a 6-core machine, expect 2.5--3.5x throughput improvement. The sub-linear scaling is due to tuple redistribution overhead, not contention.
|
||||
|
||||
## Planet observation
|
||||
|
||||
The full observation pipeline: VSOP87 for the target, VSOP87 for Earth, geocentric ecliptic, obliquity rotation, precession, sidereal time, and az/el.
|
||||
|
||||
```sql
|
||||
-- Benchmark: observe all 7 non-Earth planets at 125 times each
|
||||
EXPLAIN (ANALYZE)
|
||||
SELECT planet_observe(body_id, '40.0N 105.3W 1655m'::observer, t)
|
||||
FROM generate_series(1, 8) AS body_id,
|
||||
generate_series(
|
||||
'2024-01-01'::timestamptz,
|
||||
'2024-01-01'::timestamptz + interval '125 hours',
|
||||
interval '1 hour'
|
||||
) AS t
|
||||
WHERE body_id != 3; -- skip Earth (observer is on Earth)
|
||||
```
|
||||
|
||||
**875 observations in 57 ms --- 15,400 observations per second.**
|
||||
|
||||
VSOP87 is ~45x slower than SGP4 per call because it evaluates large trigonometric series (hundreds of terms per coordinate). The Earth position is computed twice per observation (once for the target's geocentric position, once for the observer's sidereal time), but the Earth VSOP87 call is cached internally per Julian date.
|
||||
|
||||
### Per-planet breakdown
|
||||
|
||||
The outer planets (Jupiter through Neptune) are slightly faster than the inner planets because their VSOP87 series have fewer significant terms at the truncation level pg_orrery uses.
|
||||
|
||||
## Galilean moon observation
|
||||
|
||||
L1.2 theory for the moon position, plus VSOP87 for Jupiter (parent planet) and Earth.
|
||||
|
||||
```sql
|
||||
-- Benchmark: observe all 4 Galilean moons at 250 times each
|
||||
EXPLAIN (ANALYZE)
|
||||
SELECT galilean_observe(moon_id, '40.0N 105.3W 1655m'::observer, t)
|
||||
FROM generate_series(1, 4) AS moon_id,
|
||||
generate_series(
|
||||
'2024-01-01'::timestamptz,
|
||||
'2024-01-01'::timestamptz + interval '250 hours',
|
||||
interval '1 hour'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
**1,000 observations in 63 ms --- 15,900 per second.**
|
||||
|
||||
The per-call cost is slightly higher than a single planet observation because the pipeline includes the moon theory (L1.2) plus the parent planet VSOP87 call plus the standard observation pipeline.
|
||||
|
||||
## Saturn moon observation
|
||||
|
||||
TASS17 theory, plus VSOP87 for Saturn.
|
||||
|
||||
```sql
|
||||
-- Benchmark: observe 8 Saturn moons at 100 times each
|
||||
EXPLAIN (ANALYZE)
|
||||
SELECT saturn_moon_observe(moon_id, '40.0N 105.3W 1655m'::observer, t)
|
||||
FROM generate_series(1, 8) AS moon_id,
|
||||
generate_series(
|
||||
'2024-01-01'::timestamptz,
|
||||
'2024-01-01'::timestamptz + interval '100 hours',
|
||||
interval '1 hour'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
**800 observations in 53 ms --- 15,100 per second.**
|
||||
|
||||
TASS17 is comparable in complexity to L1.2. The rate difference from Galilean moon observation is within measurement noise.
|
||||
|
||||
## Star observation
|
||||
|
||||
Stars use the simplest pipeline: catalog coordinates (RA/Dec J2000), precession to date, sidereal time, and az/el. No ephemeris computation.
|
||||
|
||||
```sql
|
||||
-- Benchmark: observe 500 stars
|
||||
EXPLAIN (ANALYZE)
|
||||
SELECT star_observe(ra_j2000, dec_j2000, '40.0N 105.3W 1655m'::observer, now())
|
||||
FROM star_catalog
|
||||
LIMIT 500;
|
||||
```
|
||||
|
||||
**500 observations in 0.7 ms --- 714,000 per second.**
|
||||
|
||||
This is nearly as fast as SGP4 propagation because the only computation is matrix multiplication (precession) and a trigonometric transform (az/el). No series evaluation, no iteration.
|
||||
|
||||
## Lambert transfer
|
||||
|
||||
A single Lambert solve: given two planet positions and a time of flight, find the transfer orbit.
|
||||
|
||||
```sql
|
||||
-- Benchmark: 100 Lambert solves with varying TOF
|
||||
EXPLAIN (ANALYZE)
|
||||
SELECT lambert_transfer(3, 4, dep, dep + tof * interval '1 day')
|
||||
FROM generate_series(1, 100) AS tof,
|
||||
(SELECT '2028-10-01'::timestamptz AS dep) d;
|
||||
```
|
||||
|
||||
**100 solves in 0.1 ms --- 800,000 per second.**
|
||||
|
||||
The Lambert solver itself (Izzo's Householder iteration) converges in 3--5 iterations for typical interplanetary transfers. The dominant cost per call is the two VSOP87 evaluations (departure and arrival planet positions), not the solver.
|
||||
|
||||
## Pork chop plot
|
||||
|
||||
The flagship benchmark: a full 150 x 150 grid of departure and arrival dates for an Earth-Mars transfer, each cell requiring two VSOP87 calls plus a Lambert solve.
|
||||
|
||||
```sql
|
||||
-- Benchmark: 150x150 pork chop plot, Earth to Mars
|
||||
EXPLAIN (ANALYZE)
|
||||
SELECT dep_date, arr_date, c3_departure, c3_arrival, tof_days
|
||||
FROM generate_series(
|
||||
'2028-08-01'::timestamptz,
|
||||
'2028-08-01'::timestamptz + interval '150 days',
|
||||
interval '1 day'
|
||||
) AS dep_date
|
||||
CROSS JOIN generate_series(
|
||||
'2029-02-01'::timestamptz,
|
||||
'2029-02-01'::timestamptz + interval '150 days',
|
||||
interval '1 day'
|
||||
) AS arr_date
|
||||
CROSS JOIN LATERAL lambert_transfer(3, 4, dep_date, arr_date) t
|
||||
WHERE t IS NOT NULL;
|
||||
```
|
||||
|
||||
**22,500 transfer solutions in 8.3 seconds --- 2,700 per second.**
|
||||
|
||||
Each cell requires:
|
||||
- 2 VSOP87 evaluations (Earth and Mars at departure)
|
||||
- 2 VSOP87 evaluations (Earth and Mars at arrival, for velocity computation)
|
||||
- 1 Lambert solve
|
||||
- 2 velocity difference computations (departure and arrival $C_3$)
|
||||
|
||||
The per-cell cost is dominated by the four VSOP87 calls. Cells where arrival precedes departure or the time of flight is too short for convergence return NULL and are filtered by the WHERE clause.
|
||||
|
||||
### Parallelization
|
||||
|
||||
This is where `PARALLEL SAFE` pays off most. A 150 x 150 pork chop plot with 4 parallel workers:
|
||||
|
||||
```sql
|
||||
SET max_parallel_workers_per_gather = 4;
|
||||
```
|
||||
|
||||
Expected speedup: 2.5--3x, bringing the total under 3 seconds for 22,500 solutions.
|
||||
|
||||
## Pass prediction
|
||||
|
||||
Pass prediction is harder to benchmark in isolation because it is a search algorithm, not a fixed-cost computation. The number of propagation calls depends on the orbit and search window.
|
||||
|
||||
```sql
|
||||
-- Benchmark: ISS passes over 7 days, minimum 10 degrees
|
||||
EXPLAIN (ANALYZE)
|
||||
SELECT *
|
||||
FROM predict_passes(
|
||||
iss_tle,
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-15'::timestamptz,
|
||||
'2024-06-22'::timestamptz,
|
||||
10.0
|
||||
);
|
||||
```
|
||||
|
||||
A 7-day window at 30-second coarse scan resolution requires ~20,160 propagation calls for the coarse scan, plus bisection and ternary search calls for each pass found. Typical ISS result: 25--35 passes found in ~40 ms.
|
||||
|
||||
## Reproducing these benchmarks
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Requirements">
|
||||
- PostgreSQL 17 with pg_orrery installed
|
||||
- A satellite catalog table with ~12,000 TLEs (available from CelesTrak)
|
||||
- A star catalog table (any subset of Hipparcos or Yale BSC)
|
||||
- No concurrent queries during measurement
|
||||
- `shared_buffers` and `work_mem` at default or higher
|
||||
</TabItem>
|
||||
<TabItem label="Setup">
|
||||
```sql
|
||||
CREATE EXTENSION pg_orrery;
|
||||
|
||||
-- Load a TLE catalog
|
||||
CREATE TABLE satellite_catalog (tle tle);
|
||||
-- (COPY from CelesTrak bulk TLE file)
|
||||
|
||||
-- Verify catalog size
|
||||
SELECT count(*) FROM satellite_catalog;
|
||||
-- Expected: ~12,000 rows
|
||||
|
||||
-- Disable parallel workers for baseline measurement
|
||||
SET max_parallel_workers_per_gather = 0;
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Measurement">
|
||||
```sql
|
||||
-- Run each benchmark query three times
|
||||
-- Discard the first run (cold start)
|
||||
-- Report the median of runs 2 and 3
|
||||
|
||||
-- Example:
|
||||
EXPLAIN (ANALYZE, BUFFERS, TIMING)
|
||||
SELECT sgp4_propagate(tle, now())
|
||||
FROM satellite_catalog;
|
||||
```
|
||||
|
||||
Use `EXPLAIN (ANALYZE)` rather than client-side timing to exclude network latency and result serialization overhead. The `Execution Time` line in the EXPLAIN output is the number to report.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## What these numbers mean
|
||||
|
||||
The benchmarks demonstrate that pg_orrery's computation cost is low enough to treat orbital mechanics as a SQL primitive. Propagating an entire satellite catalog takes less time than a typical index scan on a moderately-sized table. Planet observation is fast enough to generate ephemeris tables with `generate_series`. Pork chop plots are feasible as interactive queries rather than batch jobs.
|
||||
|
||||
The numbers also show where the bottlenecks are: VSOP87 series evaluation dominates everything except star observation and raw SGP4 propagation. If a future optimization effort targets one component, it should be the VSOP87 evaluation loop.
|
||||
170
docs/src/content/docs/reference/body-ids.mdx
Normal file
170
docs/src/content/docs/reference/body-ids.mdx
Normal file
@ -0,0 +1,170 @@
|
||||
---
|
||||
title: "Body ID Reference"
|
||||
sidebar:
|
||||
order: 9
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Complete mapping of integer body identifiers used across all pg_orrery functions. Each function family uses its own ID space; the tables below document which IDs are valid for which functions.
|
||||
|
||||
---
|
||||
|
||||
## Planet IDs
|
||||
|
||||
Used by `planet_heliocentric`, `planet_observe`, and `lambert_transfer` / `lambert_c3`.
|
||||
|
||||
| ID | Body | Valid for `planet_heliocentric` | Valid for `planet_observe` | Valid for `lambert_*` |
|
||||
|----|------|:----:|:----:|:----:|
|
||||
| 0 | Sun | Yes (returns origin) | No | No |
|
||||
| 1 | Mercury | Yes | Yes | Yes |
|
||||
| 2 | Venus | Yes | Yes | Yes |
|
||||
| 3 | Earth | Yes | No | Yes |
|
||||
| 4 | Mars | Yes | Yes | Yes |
|
||||
| 5 | Jupiter | Yes | Yes | Yes |
|
||||
| 6 | Saturn | Yes | Yes | Yes |
|
||||
| 7 | Uranus | Yes | Yes | Yes |
|
||||
| 8 | Neptune | Yes | Yes | Yes |
|
||||
|
||||
<Aside type="note">
|
||||
Body ID 0 (Sun) in `planet_heliocentric` returns the origin (0, 0, 0) by definition. Use `sun_observe` for topocentric Sun observation. Body ID 3 (Earth) cannot be used with `planet_observe` because observing Earth from Earth is undefined; use `planet_heliocentric(3, t)` to get Earth's heliocentric position.
|
||||
</Aside>
|
||||
|
||||
### Conventions
|
||||
|
||||
The planet ID numbering follows the VSOP87 convention:
|
||||
- IDs 1-8 map to the eight planets in order from the Sun
|
||||
- No ID is assigned to Pluto (VSOP87 does not include it)
|
||||
- The Sun is included as ID 0 for completeness in heliocentric queries
|
||||
|
||||
```sql
|
||||
-- Quick lookup: all planet IDs with names
|
||||
SELECT body_id,
|
||||
CASE body_id
|
||||
WHEN 0 THEN 'Sun' WHEN 1 THEN 'Mercury'
|
||||
WHEN 2 THEN 'Venus' WHEN 3 THEN 'Earth'
|
||||
WHEN 4 THEN 'Mars' WHEN 5 THEN 'Jupiter'
|
||||
WHEN 6 THEN 'Saturn' WHEN 7 THEN 'Uranus'
|
||||
WHEN 8 THEN 'Neptune'
|
||||
END AS name
|
||||
FROM generate_series(0, 8) AS body_id;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Galilean Moon IDs
|
||||
|
||||
Used by `galilean_observe`. Numbered in order of distance from Jupiter, following the Lieske L1.2 convention.
|
||||
|
||||
| ID | Moon | Discoverer | Orbital Period | Semi-major Axis |
|
||||
|----|------|------------|----------------|-----------------|
|
||||
| 0 | Io | Galileo (1610) | 1.769 days | 421,700 km |
|
||||
| 1 | Europa | Galileo (1610) | 3.551 days | 671,034 km |
|
||||
| 2 | Ganymede | Galileo (1610) | 7.155 days | 1,070,412 km |
|
||||
| 3 | Callisto | Galileo (1610) | 16.689 days | 1,882,709 km |
|
||||
|
||||
```sql
|
||||
-- All Galilean moon names and IDs
|
||||
SELECT moon_id,
|
||||
CASE moon_id
|
||||
WHEN 0 THEN 'Io' WHEN 1 THEN 'Europa'
|
||||
WHEN 2 THEN 'Ganymede' WHEN 3 THEN 'Callisto'
|
||||
END AS name
|
||||
FROM generate_series(0, 3) AS moon_id;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Saturn Moon IDs
|
||||
|
||||
Used by `saturn_moon_observe`. Numbered in order of distance from Saturn, following the TASS17 convention.
|
||||
|
||||
| ID | Moon | Discoverer | Orbital Period | Semi-major Axis |
|
||||
|----|------|------------|----------------|-----------------|
|
||||
| 0 | Mimas | Herschel (1789) | 0.942 days | 185,539 km |
|
||||
| 1 | Enceladus | Herschel (1789) | 1.370 days | 238,042 km |
|
||||
| 2 | Tethys | Cassini (1684) | 1.888 days | 294,619 km |
|
||||
| 3 | Dione | Cassini (1684) | 2.737 days | 377,396 km |
|
||||
| 4 | Rhea | Cassini (1672) | 4.518 days | 527,108 km |
|
||||
| 5 | Titan | Huygens (1655) | 15.945 days | 1,221,870 km |
|
||||
| 6 | Iapetus | Cassini (1671) | 79.322 days | 3,560,820 km |
|
||||
| 7 | Hyperion | Bond & Lassell (1848) | 21.277 days | 1,481,010 km |
|
||||
|
||||
<Aside type="note">
|
||||
The moon IDs are ordered by distance from Saturn, not by discovery date or ID 7 (Hyperion) being after ID 6 (Iapetus) despite Hyperion orbiting closer. This follows the TASS17 convention where the inner six moons (Mimas through Titan) are modeled as a coupled system, and the outer two (Iapetus, Hyperion) are treated with additional perturbation terms.
|
||||
</Aside>
|
||||
|
||||
```sql
|
||||
SELECT moon_id,
|
||||
CASE moon_id
|
||||
WHEN 0 THEN 'Mimas' WHEN 1 THEN 'Enceladus'
|
||||
WHEN 2 THEN 'Tethys' WHEN 3 THEN 'Dione'
|
||||
WHEN 4 THEN 'Rhea' WHEN 5 THEN 'Titan'
|
||||
WHEN 6 THEN 'Iapetus' WHEN 7 THEN 'Hyperion'
|
||||
END AS name
|
||||
FROM generate_series(0, 7) AS moon_id;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Uranus Moon IDs
|
||||
|
||||
Used by `uranus_moon_observe`. Numbered in order of distance from Uranus, following the GUST86 convention.
|
||||
|
||||
| ID | Moon | Discoverer | Orbital Period | Semi-major Axis |
|
||||
|----|------|------------|----------------|-----------------|
|
||||
| 0 | Miranda | Kuiper (1948) | 1.413 days | 129,390 km |
|
||||
| 1 | Ariel | Lassell (1851) | 2.520 days | 190,900 km |
|
||||
| 2 | Umbriel | Lassell (1851) | 4.144 days | 266,000 km |
|
||||
| 3 | Titania | Herschel (1787) | 8.706 days | 435,910 km |
|
||||
| 4 | Oberon | Herschel (1787) | 13.463 days | 583,520 km |
|
||||
|
||||
```sql
|
||||
SELECT moon_id,
|
||||
CASE moon_id
|
||||
WHEN 0 THEN 'Miranda' WHEN 1 THEN 'Ariel'
|
||||
WHEN 2 THEN 'Umbriel' WHEN 3 THEN 'Titania'
|
||||
WHEN 4 THEN 'Oberon'
|
||||
END AS name
|
||||
FROM generate_series(0, 4) AS moon_id;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mars Moon IDs
|
||||
|
||||
Used by `mars_moon_observe`. Numbered in order of distance from Mars, following the MarsSat convention.
|
||||
|
||||
| ID | Moon | Discoverer | Orbital Period | Semi-major Axis |
|
||||
|----|------|------------|----------------|-----------------|
|
||||
| 0 | Phobos | Hall (1877) | 0.319 days (7h 39m) | 9,376 km |
|
||||
| 1 | Deimos | Hall (1877) | 1.263 days | 23,463 km |
|
||||
|
||||
<Aside type="note">
|
||||
Phobos orbits closer to its parent planet than any other known moon in the solar system, completing more than three orbits per Martian day. Its orbital period (7h 39m) is shorter than Mars's rotation period (24h 37m), so it rises in the west and sets in the east as seen from the Martian surface.
|
||||
</Aside>
|
||||
|
||||
```sql
|
||||
SELECT moon_id,
|
||||
CASE moon_id
|
||||
WHEN 0 THEN 'Phobos'
|
||||
WHEN 1 THEN 'Deimos'
|
||||
END AS name
|
||||
FROM generate_series(0, 1) AS moon_id;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
Total: **8 planets + 19 moons = 27 solar system bodies** computable from SQL.
|
||||
|
||||
| Function | ID Range | Count | Theory |
|
||||
|----------|----------|-------|--------|
|
||||
| `planet_heliocentric` | 0-8 | 9 | VSOP87 |
|
||||
| `planet_observe` | 1-2, 4-8 | 7 | VSOP87 |
|
||||
| `galilean_observe` | 0-3 | 4 | Lieske L1.2 |
|
||||
| `saturn_moon_observe` | 0-7 | 8 | TASS17 |
|
||||
| `uranus_moon_observe` | 0-4 | 5 | GUST86 |
|
||||
| `mars_moon_observe` | 0-1 | 2 | MarsSat |
|
||||
| `lambert_transfer` / `lambert_c3` | 1-8 | 8 | VSOP87 + Lambert |
|
||||
224
docs/src/content/docs/reference/constants-accuracy.mdx
Normal file
224
docs/src/content/docs/reference/constants-accuracy.mdx
Normal file
@ -0,0 +1,224 @@
|
||||
---
|
||||
title: "Constants & Accuracy"
|
||||
sidebar:
|
||||
order: 10
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Physical constants, astronomical constants, and accuracy bounds for every computational theory used in pg_orrery. VSOP87/ELP2000-82B constants are compiled from their original sources and embedded at compile time. Optional JPL DE440/441 ephemeris support (v0.3.0+) reads constants from an external binary file.
|
||||
|
||||
---
|
||||
|
||||
## Physical Constants
|
||||
|
||||
### WGS-72 (SGP4/SDP4 Only)
|
||||
|
||||
The SGP4/SDP4 propagator uses WGS-72 constants internally. This matches the reference frame in which TLEs are generated. Using WGS-84 with SGP4 would introduce systematic errors.
|
||||
|
||||
| Constant | Symbol | Value | Unit |
|
||||
|----------|--------|-------|------|
|
||||
| Gravitational parameter | mu | 398600.8 | km^3/s^2 |
|
||||
| Equatorial radius | ae | 6378.135 | km |
|
||||
| J2 | J2 | 0.001082616 | -- |
|
||||
| J3 | J3 | -0.00000253881 | -- |
|
||||
| J4 | J4 | -0.00000165597 | -- |
|
||||
|
||||
<Aside type="caution">
|
||||
These WGS-72 values are **only** used inside the SGP4/SDP4 propagator. All coordinate output (geodetic, topocentric) uses WGS-84. Mixing WGS-72 and WGS-84 constants in the same computation is a common source of systematic error in satellite tracking software; pg_orrery handles this boundary correctly.
|
||||
</Aside>
|
||||
|
||||
### WGS-84 (Coordinate Output)
|
||||
|
||||
All geodetic and topocentric coordinate conversions use WGS-84 constants.
|
||||
|
||||
| Constant | Symbol | Value | Unit |
|
||||
|----------|--------|-------|------|
|
||||
| Semi-major axis (equatorial radius) | a | 6378.137 | km |
|
||||
| Flattening | f | 1/298.257223563 | -- |
|
||||
| Semi-minor axis (polar radius) | b | 6356.752314245 | km |
|
||||
| First eccentricity squared | e^2 | 0.00669437999014 | -- |
|
||||
|
||||
---
|
||||
|
||||
## Astronomical Constants
|
||||
|
||||
| Constant | Symbol | Value | Unit | Source |
|
||||
|----------|--------|-------|------|--------|
|
||||
| Astronomical Unit | AU | 149597870.7 | km | IAU 2012 |
|
||||
| Obliquity of the ecliptic at J2000 | epsilon | 23.4392911 | degrees | IAU 1976 |
|
||||
| Gaussian gravitational constant | k | 0.01720209895 | AU^(3/2) / (day * Msun^(1/2)) | IAU 1976 |
|
||||
| Julian century | -- | 36525.0 | days | -- |
|
||||
| J2000 epoch | -- | 2451545.0 | JD | 2000-01-01T12:00:00 TT |
|
||||
| Speed of light | c | 299792.458 | km/s | IAU 2012 |
|
||||
| Earth rotation rate | omega_e | 7.292115e-5 | rad/s | WGS-84 |
|
||||
|
||||
### Jupiter-Specific Constants
|
||||
|
||||
| Constant | Value | Unit | Source |
|
||||
|----------|-------|------|--------|
|
||||
| System III rotation period | 9h 55m 29.711s | -- | IAU 1965 |
|
||||
| System III rotation rate | 870.536 | deg/day | Derived |
|
||||
|
||||
---
|
||||
|
||||
## Theory Accuracy Bounds
|
||||
|
||||
Each computational theory in pg_orrery has well-characterized accuracy limits. The bounds below are drawn from the original theory publications and validated against JPL ephemerides where possible.
|
||||
|
||||
### SGP4/SDP4 (Satellite Propagation)
|
||||
|
||||
| Orbit Class | Typical Error at Epoch | Error Growth Rate | Source |
|
||||
|-------------|----------------------|-------------------|--------|
|
||||
| LEO (< 2000 km) | < 1 km at epoch | 1-3 km/day | Vallado et al., 2006 |
|
||||
| MEO (2000-35786 km) | < 5 km at epoch | 5-10 km/day | Vallado et al., 2006 |
|
||||
| GEO (~35786 km) | < 10 km at epoch | 10-50 km/day | Vallado et al., 2006 |
|
||||
| HEO (Molniya-type) | < 10 km at epoch | Highly variable | Vallado et al., 2006 |
|
||||
|
||||
<Aside type="note">
|
||||
These errors assume the TLE is freshly generated. TLE age is the dominant error source. The `tle_age` accessor function reports how old a TLE is relative to the current time. For LEO conjunction screening, TLEs older than 3 days should be treated with caution.
|
||||
</Aside>
|
||||
|
||||
**Valid epoch range:** TLEs are typically valid for +/- 7 days from epoch for LEO, +/- 14 days for GEO. Beyond this, errors grow rapidly and propagation may fail outright (returning a fatal error code).
|
||||
|
||||
**Deep space selection:** The SGP4/SDP4 algorithm switch is based on orbital period. Orbits with period >= 225 minutes (~3.75 hours, corresponding to an altitude of roughly 5,900 km for circular orbits) use the SDP4 deep-space model, which includes lunar and solar perturbation terms.
|
||||
|
||||
### VSOP87 (Planetary Positions)
|
||||
|
||||
Position accuracy relative to JPL DE405 ephemeris:
|
||||
|
||||
| Planet | Max Error (within +/- 2000 yr of J2000) | Max Error (within +/- 4000 yr of J2000) |
|
||||
|--------|------------------------------------------|------------------------------------------|
|
||||
| Mercury | 0.6" | 1" |
|
||||
| Venus | 0.3" | 2" |
|
||||
| Earth-Moon barycenter | 0.4" | 2" |
|
||||
| Mars | 0.8" | 4" |
|
||||
| Jupiter | 0.3" | 7" |
|
||||
| Saturn | 0.4" | 10" |
|
||||
| Uranus | 0.2" | 20" |
|
||||
| Neptune | 0.3" | 40" |
|
||||
|
||||
<Aside type="tip">
|
||||
For dates within a few centuries of J2000, VSOP87 is accurate to sub-arcsecond levels for all planets. For most observational purposes (telescope pointing, rise/set times, conjunction identification), this exceeds requirements.
|
||||
</Aside>
|
||||
|
||||
**Valid epoch range:** The series coefficients are fitted over the interval J2000 +/- 4000 years. Outside this range, accuracy degrades unpredictably. For observational planning (present-day queries), accuracy is well within 1 arcsecond.
|
||||
|
||||
### JPL DE440/441 (Optional, v0.3.0+)
|
||||
|
||||
When configured via the `pg_orrery.ephemeris_path` GUC, DE ephemeris positions are available through the `_de()` function variants.
|
||||
|
||||
| Property | DE440 | DE441 |
|
||||
|----------|-------|-------|
|
||||
| Accuracy | ~0.1 milliarcsecond | ~0.1 milliarcsecond |
|
||||
| Valid range | 1550 to 2650 | -13200 to +17191 |
|
||||
| File size | ~115 MB | ~3.1 GB |
|
||||
| Method | Numerical integration | Numerical integration |
|
||||
|
||||
Accuracy relative to VLBI observations:
|
||||
|
||||
| Body | Position Accuracy | Notes |
|
||||
|------|-------------------|-------|
|
||||
| Inner planets (Mercury-Mars) | < 0.3 milliarcsecond | Constrained by radar ranging |
|
||||
| Jupiter, Saturn | < 1 milliarcsecond | Constrained by spacecraft tracking + CCD astrometry |
|
||||
| Uranus, Neptune | < 10 milliarcsecond | Limited by ground-based astrometry |
|
||||
| Moon | ~1 meter | Constrained by Lunar Laser Ranging |
|
||||
|
||||
<Aside type="note">
|
||||
DE positions are in the ICRS equatorial frame. pg_orrery applies the equatorial-to-ecliptic rotation at the provider boundary before feeding positions into the observation pipeline. Both target and Earth positions always come from the same provider (Rule 7 of the constant chain of custody).
|
||||
</Aside>
|
||||
|
||||
**DE440 vs DE441:** DE440 is recommended for present-day and near-future work. It covers 1550-2650 and produces identical results to DE441 within that range, but is ~27x smaller. DE441's extended range (-13200 to +17191) is needed only for paleoclimate studies, historical astronomy, or very long-term solar system dynamics.
|
||||
|
||||
### ELP2000-82B (Lunar Position)
|
||||
|
||||
| Quantity | Accuracy |
|
||||
|----------|----------|
|
||||
| Geocentric longitude | < 2" for dates within +/- 500 years of J2000 |
|
||||
| Geocentric latitude | < 1" |
|
||||
| Distance | < 0.5 km |
|
||||
|
||||
**Valid epoch range:** Nominally +/- 4000 years from J2000, though accuracy degrades beyond +/- 500 years. For present-day lunar observations, the error is well under 1 arcsecond and 1 km in distance.
|
||||
|
||||
### Lieske L1.2 (Galilean Moons)
|
||||
|
||||
| Quantity | Accuracy |
|
||||
|----------|----------|
|
||||
| Position relative to Jupiter | < 500 km (typical), < 1000 km (worst case) |
|
||||
| Differential positions (moon-to-moon) | < 200 km |
|
||||
|
||||
**Valid epoch range:** Fitted to observations spanning 1891-2000. Accuracy degrades outside this range. For present-day observations and near-term predictions, the theory is reliable.
|
||||
|
||||
### TASS17 (Saturn Moons)
|
||||
|
||||
| Quantity | Accuracy |
|
||||
|----------|----------|
|
||||
| Position relative to Saturn | < 500 km (inner moons), < 2000 km (Hyperion) |
|
||||
| Titan position | < 300 km |
|
||||
|
||||
**Notes:** Hyperion has the largest uncertainty due to its chaotic rotation and irregular orbital perturbations. Titan, as the most massive moon, is the best-determined.
|
||||
|
||||
**Valid epoch range:** Fitted to observations spanning 1886-1985. The theory uses secular terms that limit extrapolation.
|
||||
|
||||
### GUST86 (Uranus Moons)
|
||||
|
||||
| Quantity | Accuracy |
|
||||
|----------|----------|
|
||||
| Position relative to Uranus | < 1000 km (Titania, Oberon), < 2000 km (Miranda) |
|
||||
|
||||
**Notes:** The Uranian system was significantly improved by Voyager 2 encounter data (1986). Pre-Voyager observations constrain secular rates; Voyager data constrains short-period terms.
|
||||
|
||||
**Valid epoch range:** Most reliable within +/- 50 years of the Voyager encounter (1986). Present-day accuracy is good.
|
||||
|
||||
### MarsSat (Mars Moons)
|
||||
|
||||
| Quantity | Accuracy |
|
||||
|----------|----------|
|
||||
| Phobos position relative to Mars | < 10 km |
|
||||
| Deimos position relative to Mars | < 20 km |
|
||||
|
||||
**Notes:** The Mars moon theories benefit from spacecraft tracking data (Viking, Mars Express). Phobos is better determined than Deimos due to more frequent close encounters with Mars orbiters.
|
||||
|
||||
### Kepler Propagation (Comets & Asteroids)
|
||||
|
||||
| Orbit Type | Limitation |
|
||||
|------------|------------|
|
||||
| Elliptic (e < 1) | Two-body only. No planetary perturbations. Error grows with distance from perihelion epoch. |
|
||||
| Parabolic (e = 1) | Barker's equation. Exact for the two-body case. |
|
||||
| Hyperbolic (e > 1) | Two-body only. Valid for interstellar objects near perihelion. |
|
||||
|
||||
<Aside type="caution">
|
||||
Keplerian propagation ignores gravitational perturbations from planets, non-gravitational forces (outgassing, radiation pressure), and relativistic effects. For long-period comets far from perihelion, the two-body approximation is reasonable. For short-period comets with close planetary encounters (e.g., Jupiter-family comets), errors accumulate over time. Use fresh osculating elements.
|
||||
</Aside>
|
||||
|
||||
### Lambert Solver (Transfer Orbits)
|
||||
|
||||
| Quantity | Notes |
|
||||
|----------|-------|
|
||||
| Transfer orbit accuracy | Exact for the two-body (patched conic) approximation |
|
||||
| Planet position accuracy | Limited by VSOP87 (sub-arcsecond for present-day) |
|
||||
| C3 accuracy | Departure C3 values are typically within 0.1 km^2/s^2 of JPL trajectory tools for well-posed transfers |
|
||||
|
||||
**Limitations:** The Lambert solver assumes patched conic trajectories (two-body between planets). It does not account for:
|
||||
- Gravity assists
|
||||
- Solar radiation pressure
|
||||
- Finite thrust arcs
|
||||
- N-body perturbations during the transfer
|
||||
|
||||
For preliminary mission design and pork chop plot generation, these limitations are standard and expected.
|
||||
|
||||
---
|
||||
|
||||
## Reference Publications
|
||||
|
||||
| Theory | Publication |
|
||||
|--------|-------------|
|
||||
| SGP4/SDP4 | Vallado, D.A., Crawford, P., Hujsak, R., Kelso, T.S. "Revisiting Spacetrack Report #3." AIAA 2006-6753, 2006. |
|
||||
| VSOP87 | Bretagnon, P., Francou, G. "Planetary theories in rectangular and spherical variables. VSOP87 solutions." Astronomy and Astrophysics, 202, 309-315, 1988. |
|
||||
| ELP2000-82B | Chapront-Touze, M., Chapront, J. "The lunar ephemeris ELP-2000." Astronomy and Astrophysics, 124, 50-62, 1983. |
|
||||
| Lieske L1.2 | Lieske, J.H. "Galilean satellites of Jupiter." Astronomy and Astrophysics Supplement Series, 129, 205-217, 1998. |
|
||||
| TASS17 | Vienne, A., Duriez, L. "TASS1.7: An ephemeris generator for the major satellites of Saturn." Astronomy and Astrophysics, 297, 588-605, 1995. |
|
||||
| GUST86 | Laskar, J., Jacobson, R.A. "GUST86: An analytical ephemeris of the Uranian satellites." Astronomy and Astrophysics, 188, 212-224, 1987. |
|
||||
| MarsSat | Jacobson, R.A. "The orbits and masses of the Martian satellites and the libration of Phobos." The Astronomical Journal, 139, 668-679, 2010. |
|
||||
| Carr source regions | Carr, T.D., Desch, M.D., Alexander, J.K. "Phenomenology of magnetospheric radio emissions." In Physics of the Jovian Magnetosphere, Cambridge Univ. Press, 1983. |
|
||||
| Lambert solver | Battin, R.H. "An Introduction to the Mathematics and Methods of Astrodynamics." AIAA Education Series, Revised Edition, 1999. |
|
||||
339
docs/src/content/docs/reference/functions-de.mdx
Normal file
339
docs/src/content/docs/reference/functions-de.mdx
Normal file
@ -0,0 +1,339 @@
|
||||
---
|
||||
title: "Functions: DE Ephemeris"
|
||||
sidebar:
|
||||
order: 8
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Optional high-precision function variants that use JPL DE440/441 ephemeris files when configured via the `pg_orrery.ephemeris_path` GUC. Each function mirrors an existing VSOP87/ELP2000-82B counterpart with identical parameters and return types.
|
||||
|
||||
All DE functions are `STABLE STRICT PARALLEL SAFE` (except `pg_orrery_ephemeris_info` which is `STABLE PARALLEL SAFE`, not STRICT). When DE is unavailable, they fall back to their VSOP87/ELP2000-82B equivalents.
|
||||
|
||||
See the [DE Ephemeris guide](/guides/de-ephemeris/) for setup and configuration.
|
||||
|
||||
---
|
||||
|
||||
## planet_heliocentric_de
|
||||
|
||||
Computes the heliocentric ecliptic J2000 position of a planet using DE positions when available, falling back to VSOP87.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
planet_heliocentric_de(body_id int4, t timestamptz) → heliocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `body_id` | `int4` | Planet identifier: 0 (Sun), 1-8 (Mercury through Neptune) |
|
||||
| `t` | `timestamptz` | Evaluation time |
|
||||
|
||||
### Returns
|
||||
|
||||
A `heliocentric` position in AU (ecliptic J2000 frame). Identical return type to `planet_heliocentric()`.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Compare DE vs VSOP87 heliocentric distances
|
||||
SELECT body_id,
|
||||
round(helio_distance(planet_heliocentric(body_id, '2024-06-21 12:00:00+00'))::numeric, 10) AS vsop87,
|
||||
round(helio_distance(planet_heliocentric_de(body_id, '2024-06-21 12:00:00+00'))::numeric, 10) AS de
|
||||
FROM generate_series(1, 8) AS body_id;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## planet_observe_de
|
||||
|
||||
Computes the topocentric position of a planet using DE ephemeris for both the target and Earth positions.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
planet_observe_de(body_id int4, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `body_id` | `int4` | Planet identifier (1-8, excluding 3/Earth) |
|
||||
| `obs` | `observer` | Observer location on Earth |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
<Aside type="caution">
|
||||
Body IDs 0 (Sun) and 3 (Earth) are not valid. Use `sun_observe_de()` for the Sun. Observing Earth from Earth raises an error.
|
||||
</Aside>
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range (km), and range rate (km/s).
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Mars position from Boulder using DE
|
||||
SELECT round(topo_azimuth(t)::numeric, 4) AS az,
|
||||
round(topo_elevation(t)::numeric, 4) AS el,
|
||||
round(topo_range(t)::numeric, 0) AS range_km
|
||||
FROM planet_observe_de(4, '40.0N 105.3W 1655m'::observer, now()) AS t;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## sun_observe_de
|
||||
|
||||
Computes the topocentric position of the Sun using DE positions.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
sun_observe_de(obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `obs` | `observer` | Observer location on Earth |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range (km), and range rate (km/s).
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
SELECT round(topo_azimuth(t)::numeric, 2) AS az,
|
||||
round(topo_elevation(t)::numeric, 2) AS el
|
||||
FROM sun_observe_de('40.0N 105.3W 1655m'::observer, now()) AS t;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## moon_observe_de
|
||||
|
||||
Computes the topocentric position of the Moon using DE positions. Falls back to ELP2000-82B when DE is unavailable.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
moon_observe_de(obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `obs` | `observer` | Observer location on Earth |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range (km), and range rate (km/s).
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
SELECT round(topo_azimuth(t)::numeric, 2) AS az,
|
||||
round(topo_elevation(t)::numeric, 2) AS el,
|
||||
round(topo_range(t)::numeric, 0) AS range_km
|
||||
FROM moon_observe_de('40.0N 105.3W 1655m'::observer, now()) AS t;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## lambert_transfer_de
|
||||
|
||||
Computes a Lambert transfer orbit using DE-precision planet positions.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
lambert_transfer_de(
|
||||
dep_body_id int4,
|
||||
arr_body_id int4,
|
||||
dep_time timestamptz,
|
||||
arr_time timestamptz
|
||||
) → RECORD(c3_departure float8, c3_arrival float8, v_inf_dep float8, v_inf_arr float8, tof_days float8)
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `dep_body_id` | `int4` | Departure planet (1-8) |
|
||||
| `arr_body_id` | `int4` | Arrival planet (1-8, different from departure) |
|
||||
| `dep_time` | `timestamptz` | Departure epoch |
|
||||
| `arr_time` | `timestamptz` | Arrival epoch (must be after departure) |
|
||||
|
||||
### Returns
|
||||
|
||||
Transfer orbit parameters including departure and arrival C3 (km^2/s^2), v-infinity magnitudes, and time of flight.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Earth-Mars transfer window with DE positions
|
||||
SELECT round(c3_departure::numeric, 2) AS c3_dep,
|
||||
round(c3_arrival::numeric, 2) AS c3_arr,
|
||||
round(tof_days::numeric, 1) AS tof
|
||||
FROM lambert_transfer_de(3, 4, '2026-05-01 00:00:00+00', '2027-01-15 00:00:00+00');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## lambert_c3_de
|
||||
|
||||
Returns only the departure C3 (km^2/s^2) from a Lambert transfer computation using DE positions. A convenience wrapper around `lambert_transfer_de()`.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
lambert_c3_de(dep_body_id int4, arr_body_id int4, dep_time timestamptz, arr_time timestamptz) → float8
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Pork chop grid with DE accuracy
|
||||
SELECT dep::date AS departure,
|
||||
arr::date AS arrival,
|
||||
round(lambert_c3_de(3, 4, dep, arr)::numeric, 2) AS c3
|
||||
FROM generate_series('2026-04-01'::timestamptz, '2026-08-01'::timestamptz, '14 days') dep,
|
||||
generate_series('2026-12-01'::timestamptz, '2027-04-01'::timestamptz, '14 days') arr
|
||||
WHERE arr > dep + interval '90 days';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## galilean_observe_de
|
||||
|
||||
Computes the topocentric position of a Galilean moon of Jupiter. Uses DE for Jupiter's heliocentric position and L1.2 theory for the moon's offset.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
galilean_observe_de(moon_id int4, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `moon_id` | `int4` | 0=Io, 1=Europa, 2=Ganymede, 3=Callisto |
|
||||
| `obs` | `observer` | Observer location |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- All four Galilean moons with DE-precision Jupiter
|
||||
SELECT moon_id,
|
||||
CASE moon_id WHEN 0 THEN 'Io' WHEN 1 THEN 'Europa'
|
||||
WHEN 2 THEN 'Ganymede' WHEN 3 THEN 'Callisto' END AS moon,
|
||||
round(topo_elevation(galilean_observe_de(moon_id, '40.0N 105.3W 1655m'::observer, now()))::numeric, 2) AS el
|
||||
FROM generate_series(0, 3) AS moon_id;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## saturn_moon_observe_de
|
||||
|
||||
Computes the topocentric position of a Saturn moon. Uses DE for Saturn's heliocentric position and TASS17 theory for the moon's offset.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
saturn_moon_observe_de(moon_id int4, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `moon_id` | `int4` | 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion |
|
||||
| `obs` | `observer` | Observer location |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
---
|
||||
|
||||
## uranus_moon_observe_de
|
||||
|
||||
Computes the topocentric position of a Uranus moon. Uses DE for Uranus's heliocentric position and GUST86 theory for the moon's offset.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
uranus_moon_observe_de(moon_id int4, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `moon_id` | `int4` | 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon |
|
||||
| `obs` | `observer` | Observer location |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
---
|
||||
|
||||
## mars_moon_observe_de
|
||||
|
||||
Computes the topocentric position of a Mars moon. Uses DE for Mars's heliocentric position and MarsSat theory for the moon's offset.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
mars_moon_observe_de(moon_id int4, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `moon_id` | `int4` | 0=Phobos, 1=Deimos |
|
||||
| `obs` | `observer` | Observer location |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
---
|
||||
|
||||
## pg_orrery_ephemeris_info
|
||||
|
||||
Returns diagnostic information about the current ephemeris provider.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
pg_orrery_ephemeris_info() → RECORD(provider text, file_path text, start_jd float8, end_jd float8, version int4, au_km float8)
|
||||
```
|
||||
|
||||
<Aside type="note">
|
||||
This function is `STABLE PARALLEL SAFE` but **not** `STRICT` --- it takes no arguments and always returns a row.
|
||||
</Aside>
|
||||
|
||||
### Returns
|
||||
|
||||
| Column | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `provider` | `text` | `'VSOP87'` or `'JPL_DE'` |
|
||||
| `file_path` | `text` | Path to the DE file (empty string if no DE) |
|
||||
| `start_jd` | `float8` | First Julian Date covered by the DE file |
|
||||
| `end_jd` | `float8` | Last Julian Date covered by the DE file |
|
||||
| `version` | `int4` | DE version number (440, 441, etc.) |
|
||||
| `au_km` | `float8` | Astronomical Unit in km from the DE header |
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Check current provider
|
||||
SELECT (pg_orrery_ephemeris_info()).provider;
|
||||
|
||||
-- Full diagnostic
|
||||
SELECT * FROM pg_orrery_ephemeris_info();
|
||||
```
|
||||
256
docs/src/content/docs/reference/functions-moons.mdx
Normal file
256
docs/src/content/docs/reference/functions-moons.mdx
Normal file
@ -0,0 +1,256 @@
|
||||
---
|
||||
title: "Functions: Moons"
|
||||
sidebar:
|
||||
order: 4
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Functions for observing the natural satellites of Jupiter, Saturn, Uranus, and Mars. Each moon family uses a dedicated analytical theory for computing satellite positions relative to the parent planet, which is then transformed to Earth-based topocentric coordinates.
|
||||
|
||||
---
|
||||
|
||||
## galilean_observe
|
||||
|
||||
Computes the topocentric position of a Galilean moon of Jupiter as seen from an Earth-based observer. Uses the Lieske L1.2 theory (Lieske, 1998).
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
galilean_observe(moon_id int4, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `moon_id` | `int4` | Galilean moon identifier (see table below) |
|
||||
| `obs` | `observer` | Observer location on Earth |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
#### Moon IDs
|
||||
|
||||
| ID | Moon | Orbital Period |
|
||||
|----|------|----------------|
|
||||
| 0 | Io | 1.769 days |
|
||||
| 1 | Europa | 3.551 days |
|
||||
| 2 | Ganymede | 7.155 days |
|
||||
| 3 | Callisto | 16.689 days |
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range (km), and range rate (km/s).
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Current positions of all four Galilean moons
|
||||
SELECT moon_id,
|
||||
CASE moon_id
|
||||
WHEN 0 THEN 'Io' WHEN 1 THEN 'Europa'
|
||||
WHEN 2 THEN 'Ganymede' WHEN 3 THEN 'Callisto'
|
||||
END AS moon,
|
||||
round(topo_azimuth(t)::numeric, 4) AS az,
|
||||
round(topo_elevation(t)::numeric, 4) AS el,
|
||||
round(topo_range(t)::numeric, 0) AS range_km
|
||||
FROM generate_series(0, 3) AS moon_id,
|
||||
galilean_observe(moon_id, '40.0N 105.3W 1655m'::observer, now()) AS t;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Track Io's position over one orbital period (1.769 days)
|
||||
SELECT t,
|
||||
round(topo_azimuth(pos)::numeric, 4) AS az,
|
||||
round(topo_elevation(pos)::numeric, 4) AS el
|
||||
FROM generate_series(
|
||||
now(), now() + interval '1.769 days', interval '15 minutes'
|
||||
) AS t,
|
||||
galilean_observe(0, '40.0N 105.3W 1655m'::observer, t) AS pos;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## saturn_moon_observe
|
||||
|
||||
Computes the topocentric position of a moon of Saturn as seen from an Earth-based observer. Uses the TASS17 theory (Vienne & Duriez, 1995) for the eight major Saturnian moons.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
saturn_moon_observe(moon_id int4, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `moon_id` | `int4` | Saturn moon identifier (see table below) |
|
||||
| `obs` | `observer` | Observer location on Earth |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
#### Moon IDs
|
||||
|
||||
| ID | Moon | Orbital Period | Approx. Magnitude |
|
||||
|----|------|----------------|-------------------|
|
||||
| 0 | Mimas | 0.942 days | +12.9 |
|
||||
| 1 | Enceladus | 1.370 days | +11.7 |
|
||||
| 2 | Tethys | 1.888 days | +10.2 |
|
||||
| 3 | Dione | 2.737 days | +10.4 |
|
||||
| 4 | Rhea | 4.518 days | +9.7 |
|
||||
| 5 | Titan | 15.945 days | +8.3 |
|
||||
| 6 | Iapetus | 79.322 days | +10.2-11.9 |
|
||||
| 7 | Hyperion | 21.277 days | +14.2 |
|
||||
|
||||
<Aside type="note">
|
||||
Iapetus has a large brightness variation due to its two-toned surface — the leading hemisphere is much darker than the trailing hemisphere. The magnitude range reflects this dichotomy.
|
||||
</Aside>
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range (km), and range rate (km/s).
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- All eight Saturn moons' current positions
|
||||
SELECT moon_id,
|
||||
CASE moon_id
|
||||
WHEN 0 THEN 'Mimas' WHEN 1 THEN 'Enceladus'
|
||||
WHEN 2 THEN 'Tethys' WHEN 3 THEN 'Dione'
|
||||
WHEN 4 THEN 'Rhea' WHEN 5 THEN 'Titan'
|
||||
WHEN 6 THEN 'Iapetus' WHEN 7 THEN 'Hyperion'
|
||||
END AS moon,
|
||||
round(topo_azimuth(t)::numeric, 4) AS az,
|
||||
round(topo_elevation(t)::numeric, 4) AS el,
|
||||
round(topo_range(t)::numeric, 0) AS range_km
|
||||
FROM generate_series(0, 7) AS moon_id,
|
||||
saturn_moon_observe(moon_id, '40.0N 105.3W 1655m'::observer, now()) AS t;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Track Titan over one orbital period
|
||||
SELECT t,
|
||||
round(topo_azimuth(pos)::numeric, 4) AS az,
|
||||
round(topo_elevation(pos)::numeric, 4) AS el
|
||||
FROM generate_series(
|
||||
now(), now() + interval '15.945 days', interval '1 hour'
|
||||
) AS t,
|
||||
saturn_moon_observe(5, '40.0N 105.3W 1655m'::observer, t) AS pos;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## uranus_moon_observe
|
||||
|
||||
Computes the topocentric position of a moon of Uranus as seen from an Earth-based observer. Uses the GUST86 theory (Laskar & Jacobson, 1987).
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
uranus_moon_observe(moon_id int4, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `moon_id` | `int4` | Uranus moon identifier (see table below) |
|
||||
| `obs` | `observer` | Observer location on Earth |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
#### Moon IDs
|
||||
|
||||
| ID | Moon | Orbital Period | Approx. Magnitude |
|
||||
|----|------|----------------|-------------------|
|
||||
| 0 | Miranda | 1.413 days | +16.5 |
|
||||
| 1 | Ariel | 2.520 days | +14.2 |
|
||||
| 2 | Umbriel | 4.144 days | +14.8 |
|
||||
| 3 | Titania | 8.706 days | +13.9 |
|
||||
| 4 | Oberon | 13.463 days | +14.1 |
|
||||
|
||||
<Aside type="caution">
|
||||
The Uranian moons are faint. Miranda at magnitude +16.5 requires a large aperture telescope. Titania and Oberon are the most accessible at around magnitude +14.
|
||||
</Aside>
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range (km), and range rate (km/s).
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- All five Uranus moons' current positions
|
||||
SELECT moon_id,
|
||||
CASE moon_id
|
||||
WHEN 0 THEN 'Miranda' WHEN 1 THEN 'Ariel'
|
||||
WHEN 2 THEN 'Umbriel' WHEN 3 THEN 'Titania'
|
||||
WHEN 4 THEN 'Oberon'
|
||||
END AS moon,
|
||||
round(topo_azimuth(t)::numeric, 4) AS az,
|
||||
round(topo_elevation(t)::numeric, 4) AS el,
|
||||
round(topo_range(t)::numeric, 0) AS range_km
|
||||
FROM generate_series(0, 4) AS moon_id,
|
||||
uranus_moon_observe(moon_id, '40.0N 105.3W 1655m'::observer, now()) AS t;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## mars_moon_observe
|
||||
|
||||
Computes the topocentric position of a moon of Mars as seen from an Earth-based observer. Uses the MarsSat analytical theory.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
mars_moon_observe(moon_id int4, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `moon_id` | `int4` | Mars moon identifier (see table below) |
|
||||
| `obs` | `observer` | Observer location on Earth |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
#### Moon IDs
|
||||
|
||||
| ID | Moon | Orbital Period | Approx. Magnitude |
|
||||
|----|------|----------------|-------------------|
|
||||
| 0 | Phobos | 0.319 days (7.66 hours) | +11.4 |
|
||||
| 1 | Deimos | 1.263 days | +12.5 |
|
||||
|
||||
<Aside type="note">
|
||||
Phobos and Deimos are extremely close to Mars and are typically overwhelmed by Mars's glare. Observations are best attempted near Mars opposition when the planet is closest to Earth and the moons have maximum angular separation.
|
||||
</Aside>
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range (km), and range rate (km/s).
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Phobos and Deimos positions from Boulder
|
||||
SELECT moon_id,
|
||||
CASE moon_id
|
||||
WHEN 0 THEN 'Phobos'
|
||||
WHEN 1 THEN 'Deimos'
|
||||
END AS moon,
|
||||
round(topo_azimuth(t)::numeric, 4) AS az,
|
||||
round(topo_elevation(t)::numeric, 4) AS el,
|
||||
round(topo_range(t)::numeric, 0) AS range_km
|
||||
FROM generate_series(0, 1) AS moon_id,
|
||||
mars_moon_observe(moon_id, '40.0N 105.3W 1655m'::observer, now()) AS t;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Track Phobos through one full orbit (~7.66 hours)
|
||||
SELECT t,
|
||||
round(topo_azimuth(pos)::numeric, 4) AS az,
|
||||
round(topo_elevation(pos)::numeric, 4) AS el
|
||||
FROM generate_series(
|
||||
now(), now() + interval '7.66 hours', interval '5 minutes'
|
||||
) AS t,
|
||||
mars_moon_observe(0, '40.0N 105.3W 1655m'::observer, t) AS pos;
|
||||
```
|
||||
193
docs/src/content/docs/reference/functions-radio.mdx
Normal file
193
docs/src/content/docs/reference/functions-radio.mdx
Normal file
@ -0,0 +1,193 @@
|
||||
---
|
||||
title: "Functions: Radio"
|
||||
sidebar:
|
||||
order: 6
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Functions for predicting Jupiter decametric radio emissions. Jupiter is the strongest radio source in the solar system after the Sun, producing bursts in the 10-40 MHz range driven by the interaction between Io and Jupiter's magnetosphere. These functions compute the geometric parameters needed to predict when bursts are likely.
|
||||
|
||||
---
|
||||
|
||||
## io_phase_angle
|
||||
|
||||
Computes the orbital phase angle of Io relative to Jupiter's superior conjunction as seen from Earth. The phase angle determines the position of Io in its orbit as projected against Jupiter's disk, which is one of two parameters needed for burst prediction.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
io_phase_angle(t timestamptz) → float8
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `t` | `timestamptz` | Evaluation time |
|
||||
|
||||
### Returns
|
||||
|
||||
Io's orbital phase angle in degrees, range [0, 360).
|
||||
|
||||
- **0** = superior conjunction (Io behind Jupiter, as seen from Earth)
|
||||
- **90** = eastern elongation (Io east of Jupiter)
|
||||
- **180** = inferior conjunction (Io between Earth and Jupiter)
|
||||
- **270** = western elongation (Io west of Jupiter)
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Current Io phase angle
|
||||
SELECT round(io_phase_angle(now())::numeric, 1) AS io_phase;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Io phase over the next 24 hours at 30-minute intervals
|
||||
SELECT t,
|
||||
round(io_phase_angle(t)::numeric, 1) AS io_phase
|
||||
FROM generate_series(now(), now() + interval '24 hours', interval '30 minutes') AS t;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## jupiter_cml
|
||||
|
||||
Computes Jupiter's Central Meridian Longitude (CML) in System III (1965.0) as seen from an Earth-based observer. System III is tied to Jupiter's magnetic field rotation (period = 9h 55m 29.711s) and is the standard reference for radio astronomy.
|
||||
|
||||
The result is corrected for light travel time between Jupiter and the observer.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
jupiter_cml(obs observer, t timestamptz) → float8
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `obs` | `observer` | Observer location on Earth |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
### Returns
|
||||
|
||||
Central Meridian Longitude in degrees, range [0, 360). This is the longitude of the Jovian meridian facing the observer at the given time, in System III coordinates.
|
||||
|
||||
<Aside type="note">
|
||||
The CML depends on the observer's position on Earth because it is corrected for light travel time. The difference between two observers on opposite sides of Earth is small (order of 0.01 degrees) but is included for correctness.
|
||||
</Aside>
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Current Jupiter CML from Boulder
|
||||
SELECT round(jupiter_cml('40.0N 105.3W 1655m'::observer, now())::numeric, 1) AS cml;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- CML sweep over one Jupiter rotation (~9h 55m)
|
||||
SELECT t,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, t)::numeric, 1) AS cml
|
||||
FROM generate_series(
|
||||
now(),
|
||||
now() + interval '9 hours 55 minutes',
|
||||
interval '10 minutes'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## jupiter_burst_probability
|
||||
|
||||
Computes the probability of detecting a Jupiter decametric radio burst given the current Io phase angle and Jupiter CML. Based on the Carr, Desch & Alexander (1983) source region model.
|
||||
|
||||
The function evaluates whether the Io phase and CML fall within one of the known emission source regions and returns a probability between 0 and 1.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
jupiter_burst_probability(io_phase float8, cml float8) → float8
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Unit | Description |
|
||||
|-----------|------|------|-------------|
|
||||
| `io_phase` | `float8` | degrees | Io orbital phase angle (output of `io_phase_angle`) |
|
||||
| `cml` | `float8` | degrees | Jupiter CML System III (output of `jupiter_cml`) |
|
||||
|
||||
### Returns
|
||||
|
||||
Burst probability as a value from 0.0 to 1.0.
|
||||
|
||||
### Source Regions
|
||||
|
||||
The Carr model identifies four primary Io-related source regions in the Io phase vs. CML parameter space:
|
||||
|
||||
| Source | Io Phase Range | CML Range | Description |
|
||||
|--------|----------------|-----------|-------------|
|
||||
| **Io-A** | 195-265 | 200-290 | Strongest Io-related source. Io near western elongation, CML in the 200-290 range. |
|
||||
| **Io-B** | 75-105 | 95-195 | Second strongest. Io near eastern elongation, CML roughly opposite to Io-A. |
|
||||
| **Io-C** | 195-265 | 290-10 | Weaker Io-related source. Same Io phase as Io-A but different CML range. |
|
||||
| **Io-D** | 75-105 | 0-95 | Weakest of the four. Same Io phase as Io-B but CML shifted. |
|
||||
|
||||
<Aside type="tip">
|
||||
Non-Io emissions also occur (sources A, B, C without Io dependency) but are weaker and less predictable. The probability returned by this function reflects the combined Io-dependent likelihood.
|
||||
</Aside>
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Current burst probability
|
||||
SELECT round(
|
||||
jupiter_burst_probability(
|
||||
io_phase_angle(now()),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, now())
|
||||
)::numeric, 3
|
||||
) AS burst_prob;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Find high-probability windows tonight
|
||||
SELECT t,
|
||||
round(io_phase_angle(t)::numeric, 1) AS io_phase,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, t)::numeric, 1) AS cml,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
)::numeric, 3) AS probability
|
||||
FROM generate_series(
|
||||
'2024-06-15 02:00:00+00',
|
||||
'2024-06-15 10:00:00+00',
|
||||
interval '5 minutes'
|
||||
) AS t
|
||||
WHERE jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
) > 0.2
|
||||
ORDER BY probability DESC;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Full radio observing plan: combine burst probability with Jupiter visibility
|
||||
SELECT t,
|
||||
round(topo_elevation(jup)::numeric, 1) AS jupiter_el,
|
||||
round(io_phase_angle(t)::numeric, 1) AS io_phase,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, t)::numeric, 1) AS cml,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
)::numeric, 3) AS burst_prob
|
||||
FROM generate_series(
|
||||
'2024-06-15 02:00:00+00',
|
||||
'2024-06-15 10:00:00+00',
|
||||
interval '10 minutes'
|
||||
) AS t,
|
||||
planet_observe(5, '40.0N 105.3W 1655m'::observer, t) AS jup
|
||||
WHERE topo_elevation(jup) > 10
|
||||
AND jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
) > 0.1;
|
||||
```
|
||||
529
docs/src/content/docs/reference/functions-satellite.mdx
Normal file
529
docs/src/content/docs/reference/functions-satellite.mdx
Normal file
@ -0,0 +1,529 @@
|
||||
---
|
||||
title: "Functions: Satellite"
|
||||
sidebar:
|
||||
order: 2
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Functions for propagating TLEs, converting coordinate frames, computing ground tracks, and predicting satellite passes. These form the core satellite tracking pipeline in pg_orrery.
|
||||
|
||||
---
|
||||
|
||||
## sgp4_propagate
|
||||
|
||||
Propagates a TLE to a given time using the SGP4 (near-earth) or SDP4 (deep-space) algorithm. The algorithm is selected automatically based on orbital period: elements with a period >= 225 minutes use SDP4.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
sgp4_propagate(tle tle, t timestamptz) → eci_position
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `tle` | `tle` | Two-Line Element set to propagate |
|
||||
| `t` | `timestamptz` | Target epoch for propagation |
|
||||
|
||||
### Returns
|
||||
|
||||
An `eci_position` in the TEME reference frame. Position in km, velocity in km/s.
|
||||
|
||||
### Errors
|
||||
|
||||
Raises an exception if SGP4/SDP4 returns a fatal error code (e.g., satellite decay, eccentricity out of range, mean motion near zero). Use `sgp4_propagate_safe` if you need NULL-on-error behavior.
|
||||
|
||||
<Aside type="caution">
|
||||
SGP4 accuracy degrades as you propagate further from the TLE epoch. For LEO satellites, errors grow roughly 1-3 km per day. Keep TLEs fresh.
|
||||
</Aside>
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT eci_x(pos) AS x_km,
|
||||
eci_y(pos) AS y_km,
|
||||
eci_z(pos) AS z_km,
|
||||
eci_speed(pos) AS speed_kms
|
||||
FROM iss, sgp4_propagate(tle, '2024-01-02 12:00:00+00') AS pos;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## sgp4_propagate_safe
|
||||
|
||||
Identical to `sgp4_propagate`, but returns NULL instead of raising an exception on propagation errors. This is the batch-safe variant for processing large TLE catalogs where some elements may be stale or invalid.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
sgp4_propagate_safe(tle tle, t timestamptz) → eci_position
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `tle` | `tle` | Two-Line Element set to propagate |
|
||||
| `t` | `timestamptz` | Target epoch for propagation |
|
||||
|
||||
### Returns
|
||||
|
||||
An `eci_position`, or NULL if propagation fails.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Propagate an entire catalog, skipping failed elements
|
||||
SELECT norad_id,
|
||||
eci_x(pos) AS x_km,
|
||||
eci_y(pos) AS y_km,
|
||||
eci_z(pos) AS z_km
|
||||
FROM satellite_catalog,
|
||||
sgp4_propagate_safe(tle, now()) AS pos
|
||||
WHERE pos IS NOT NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## sgp4_propagate_series
|
||||
|
||||
Generates a time series of TEME ECI positions for a single TLE over a time range. Returns one row per time step. This is significantly faster than calling `sgp4_propagate` inside a `generate_series` because the SGP4 initializer runs once.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
sgp4_propagate_series(
|
||||
tle tle,
|
||||
start_time timestamptz,
|
||||
end_time timestamptz,
|
||||
step interval
|
||||
) → TABLE(t timestamptz, x float8, y float8, z float8, vx float8, vy float8, vz float8)
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `tle` | `tle` | Two-Line Element set |
|
||||
| `start_time` | `timestamptz` | Start of the time range |
|
||||
| `end_time` | `timestamptz` | End of the time range (inclusive if aligned to step) |
|
||||
| `step` | `interval` | Time between samples |
|
||||
|
||||
### Returns
|
||||
|
||||
A set of rows with timestamp and TEME position/velocity components. Position in km, velocity in km/s.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT t, x, y, z, vx, vy, vz
|
||||
FROM iss,
|
||||
sgp4_propagate_series(tle,
|
||||
'2024-01-02 00:00:00+00',
|
||||
'2024-01-02 01:00:00+00',
|
||||
interval '1 minute');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## tle_distance
|
||||
|
||||
Computes the Euclidean distance between two satellites at a given time. Both TLEs are propagated to the target time and the 3D distance between their TEME positions is returned.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
tle_distance(a tle, b tle, t timestamptz) → float8
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `a` | `tle` | First satellite |
|
||||
| `b` | `tle` | Second satellite |
|
||||
| `t` | `timestamptz` | Evaluation time |
|
||||
|
||||
### Returns
|
||||
|
||||
Distance in kilometers. Raises an exception if either TLE fails to propagate.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Distance between two satellites at a specific time
|
||||
SELECT tle_distance(sat_a.tle, sat_b.tle, '2024-06-15 12:00:00+00') AS dist_km
|
||||
FROM satellite_catalog sat_a, satellite_catalog sat_b
|
||||
WHERE sat_a.norad_id = 25544 -- ISS
|
||||
AND sat_b.norad_id = 48274; -- CSS (Tianhe)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## eci_to_geodetic
|
||||
|
||||
Converts a TEME ECI position to WGS-84 geodetic coordinates. The timestamp is required to compute the Earth's rotation angle (Greenwich Apparent Sidereal Time).
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
eci_to_geodetic(pos eci_position, t timestamptz) → geodetic
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `pos` | `eci_position` | TEME ECI position |
|
||||
| `t` | `timestamptz` | Time of the position (for sidereal time computation) |
|
||||
|
||||
### Returns
|
||||
|
||||
A `geodetic` with WGS-84 latitude, longitude, and altitude.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT geo_lat(g) AS lat,
|
||||
geo_lon(g) AS lon,
|
||||
geo_alt(g) AS alt_km
|
||||
FROM iss,
|
||||
eci_to_geodetic(sgp4_propagate(tle, now()), now()) AS g;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## eci_to_topocentric
|
||||
|
||||
Converts a TEME ECI position to topocentric (observer-relative) coordinates. Computes azimuth, elevation, slant range, and range rate.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
eci_to_topocentric(pos eci_position, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `pos` | `eci_position` | TEME ECI position and velocity |
|
||||
| `obs` | `observer` | Observer location |
|
||||
| `t` | `timestamptz` | Time of the position (for sidereal time computation) |
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range, and range rate.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT topo_azimuth(tc) AS az,
|
||||
topo_elevation(tc) AS el,
|
||||
topo_range(tc) AS range_km,
|
||||
topo_range_rate(tc) AS range_rate_kms
|
||||
FROM iss,
|
||||
eci_to_topocentric(
|
||||
sgp4_propagate(tle, now()),
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
now()
|
||||
) AS tc;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## subsatellite_point
|
||||
|
||||
Returns the nadir (directly below the satellite) point on the WGS-84 ellipsoid for a given TLE at a given time. This is a convenience function equivalent to propagating and then converting to geodetic, but with altitude set to the satellite altitude.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
subsatellite_point(tle tle, t timestamptz) → geodetic
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `tle` | `tle` | Satellite TLE |
|
||||
| `t` | `timestamptz` | Evaluation time |
|
||||
|
||||
### Returns
|
||||
|
||||
A `geodetic` with the latitude, longitude, and altitude of the satellite above the WGS-84 ellipsoid.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT geo_lat(sp) AS nadir_lat,
|
||||
geo_lon(sp) AS nadir_lon,
|
||||
geo_alt(sp) AS altitude_km
|
||||
FROM iss, subsatellite_point(tle, now()) AS sp;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ground_track
|
||||
|
||||
Generates a time series of subsatellite points (nadir ground track) for a satellite over a time range. Each row contains the timestamp, latitude, longitude, and altitude.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
ground_track(
|
||||
tle tle,
|
||||
start_time timestamptz,
|
||||
end_time timestamptz,
|
||||
step interval
|
||||
) → TABLE(t timestamptz, lat float8, lon float8, alt float8)
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `tle` | `tle` | Satellite TLE |
|
||||
| `start_time` | `timestamptz` | Start of the time range |
|
||||
| `end_time` | `timestamptz` | End of the time range |
|
||||
| `step` | `interval` | Time between samples |
|
||||
|
||||
### Returns
|
||||
|
||||
A set of rows with timestamp, latitude (degrees), longitude (degrees), and altitude (km).
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- ISS ground track for one orbit (~92 minutes) at 30-second resolution
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT t, lat, lon, alt
|
||||
FROM iss, ground_track(tle, now(), now() + interval '92 minutes', interval '30 seconds');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## observe
|
||||
|
||||
Propagates a TLE and computes topocentric look angles in a single call. Equivalent to `eci_to_topocentric(sgp4_propagate(tle, t), obs, t)`, but avoids the intermediate allocation.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
observe(tle tle, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `tle` | `tle` | Satellite TLE |
|
||||
| `obs` | `observer` | Observer location |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range, and range rate.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT topo_azimuth(o) AS az,
|
||||
topo_elevation(o) AS el,
|
||||
topo_range(o) AS range_km
|
||||
FROM iss, observe(tle, '40.0N 105.3W 1655m'::observer, now()) AS o;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## observe_safe
|
||||
|
||||
Identical to `observe`, but returns NULL instead of raising an exception on propagation errors. Use this when processing large TLE catalogs in batch.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
observe_safe(tle tle, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `tle` | `tle` | Satellite TLE |
|
||||
| `obs` | `observer` | Observer location |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric`, or NULL if propagation fails.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Find all satellites above the horizon right now, skipping stale TLEs
|
||||
SELECT norad_id,
|
||||
topo_azimuth(o) AS az,
|
||||
topo_elevation(o) AS el
|
||||
FROM satellite_catalog,
|
||||
observe_safe(tle, '40.0N 105.3W 1655m'::observer, now()) AS o
|
||||
WHERE o IS NOT NULL
|
||||
AND topo_elevation(o) > 0
|
||||
ORDER BY topo_elevation(o) DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## next_pass
|
||||
|
||||
Finds the next satellite pass over an observer location. Searches forward from the given start time up to 7 days.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
next_pass(tle tle, obs observer, start timestamptz) → pass_event
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `tle` | `tle` | Satellite TLE |
|
||||
| `obs` | `observer` | Observer location |
|
||||
| `start` | `timestamptz` | Time to begin searching from |
|
||||
|
||||
### Returns
|
||||
|
||||
A `pass_event` with AOS, maximum elevation, LOS, azimuths, and duration. Returns NULL if no pass is found within 7 days (possible for equatorial observers looking for high-inclination satellites, or vice versa).
|
||||
|
||||
<Aside type="tip">
|
||||
For finding multiple passes, use `predict_passes` instead. `next_pass` is optimized for the single-pass case (e.g., "when is the ISS visible next?").
|
||||
</Aside>
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT pass_aos_time(p) AS rise,
|
||||
pass_max_elevation(p) AS max_el,
|
||||
pass_los_time(p) AS set,
|
||||
pass_duration(p) AS duration
|
||||
FROM iss, next_pass(tle, '40.0N 105.3W 1655m'::observer, now()) AS p;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## predict_passes
|
||||
|
||||
Finds all satellite passes over an observer within a time window, optionally filtered by minimum elevation. Returns a set of `pass_event` records.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
predict_passes(
|
||||
tle tle,
|
||||
obs observer,
|
||||
start_time timestamptz,
|
||||
end_time timestamptz,
|
||||
min_el float8 DEFAULT 0.0
|
||||
) → SETOF pass_event
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `tle` | `tle` | | Satellite TLE |
|
||||
| `obs` | `observer` | | Observer location |
|
||||
| `start_time` | `timestamptz` | | Start of the search window |
|
||||
| `end_time` | `timestamptz` | | End of the search window |
|
||||
| `min_el` | `float8` | `0.0` | Minimum peak elevation in degrees. Passes whose maximum elevation is below this threshold are excluded. |
|
||||
|
||||
### Returns
|
||||
|
||||
A set of `pass_event` records, ordered by AOS time.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- All ISS passes above 20 degrees in the next 3 days
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT pass_aos_time(p) AS rise,
|
||||
pass_max_elevation(p) AS max_el,
|
||||
pass_aos_azimuth(p) AS rise_az,
|
||||
pass_los_azimuth(p) AS set_az,
|
||||
pass_duration(p) AS dur
|
||||
FROM iss,
|
||||
predict_passes(tle, '40.0N 105.3W 1655m'::observer,
|
||||
now(), now() + interval '3 days', 20.0) AS p;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## pass_visible
|
||||
|
||||
Returns true if at least one satellite pass occurs over the observer during the given time window.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
pass_visible(tle tle, obs observer, start_time timestamptz, end_time timestamptz) → boolean
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `tle` | `tle` | Satellite TLE |
|
||||
| `obs` | `observer` | Observer location |
|
||||
| `start_time` | `timestamptz` | Start of the search window |
|
||||
| `end_time` | `timestamptz` | End of the search window |
|
||||
|
||||
### Returns
|
||||
|
||||
`true` if any pass (elevation > 0) occurs in the window; `false` otherwise. This is faster than `predict_passes` when you only need a yes/no answer because it stops searching after the first pass is found.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Which satellites from the catalog pass over Boulder tonight?
|
||||
SELECT norad_id, name
|
||||
FROM satellite_catalog
|
||||
WHERE pass_visible(tle, '40.0N 105.3W 1655m'::observer,
|
||||
'2024-06-15 02:00:00+00', '2024-06-15 10:00:00+00');
|
||||
```
|
||||
230
docs/src/content/docs/reference/functions-solar-system.mdx
Normal file
230
docs/src/content/docs/reference/functions-solar-system.mdx
Normal file
@ -0,0 +1,230 @@
|
||||
---
|
||||
title: "Functions: Solar System"
|
||||
sidebar:
|
||||
order: 3
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Functions for computing planetary positions, observing the Sun, Moon, and planets from an Earth-based observer. Planetary positions use the VSOP87 theory (Bretagnon & Francou, 1988). Lunar position uses ELP2000-82B (Chapront-Touze & Chapront, 1983). All functions are `IMMUTABLE STRICT PARALLEL SAFE`.
|
||||
|
||||
For higher precision, v0.3.0 adds optional `_de()` variants that use JPL DE440/441 ephemeris files. See [Functions: DE Ephemeris](/reference/functions-de/).
|
||||
|
||||
---
|
||||
|
||||
## planet_heliocentric
|
||||
|
||||
Computes the heliocentric ecliptic J2000 position of a solar system body using VSOP87 series C (heliocentric, ecliptic, rectangular). Returns position in Astronomical Units.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
planet_heliocentric(body_id int4, t timestamptz) → heliocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `body_id` | `int4` | Planet identifier (see table below) |
|
||||
| `t` | `timestamptz` | Evaluation time |
|
||||
|
||||
#### Body IDs
|
||||
|
||||
| ID | Body |
|
||||
|----|------|
|
||||
| 0 | Sun (returns origin: 0, 0, 0) |
|
||||
| 1 | Mercury |
|
||||
| 2 | Venus |
|
||||
| 3 | Earth |
|
||||
| 4 | Mars |
|
||||
| 5 | Jupiter |
|
||||
| 6 | Saturn |
|
||||
| 7 | Uranus |
|
||||
| 8 | Neptune |
|
||||
|
||||
### Returns
|
||||
|
||||
A `heliocentric` position in AU (ecliptic J2000 frame). For `body_id = 0` (Sun), all components are zero.
|
||||
|
||||
<Aside type="note">
|
||||
VSOP87 is a truncated Fourier series. Accuracy varies by planet and epoch. For the inner planets within +/- 4000 years of J2000, positional accuracy is sub-arcsecond. For the outer planets, accuracy is a few arcseconds. See [Constants & Accuracy](/reference/constants-accuracy/) for detailed bounds.
|
||||
</Aside>
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Distance of each planet from the Sun
|
||||
SELECT body_id,
|
||||
CASE body_id
|
||||
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
|
||||
WHEN 3 THEN 'Earth' WHEN 4 THEN 'Mars'
|
||||
WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn'
|
||||
WHEN 7 THEN 'Uranus' WHEN 8 THEN 'Neptune'
|
||||
END AS planet,
|
||||
round(helio_distance(planet_heliocentric(body_id, now()))::numeric, 6) AS dist_au
|
||||
FROM generate_series(1, 8) AS body_id;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Earth's position over one year at weekly intervals
|
||||
SELECT t,
|
||||
helio_x(h) AS x_au,
|
||||
helio_y(h) AS y_au,
|
||||
helio_z(h) AS z_au
|
||||
FROM generate_series(
|
||||
'2024-01-01'::timestamptz,
|
||||
'2025-01-01'::timestamptz,
|
||||
interval '7 days'
|
||||
) AS t,
|
||||
planet_heliocentric(3, t) AS h;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## planet_observe
|
||||
|
||||
Computes the topocentric position of a planet as seen from an Earth-based observer. Internally computes the heliocentric positions of both Earth and the target planet, applies geometric transformation to geocentric, then converts to topocentric coordinates.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
planet_observe(body_id int4, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `body_id` | `int4` | Planet identifier (1-8, same as `planet_heliocentric` excluding 0 and 3) |
|
||||
| `obs` | `observer` | Observer location on Earth |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
<Aside type="caution">
|
||||
`body_id` 0 (Sun) and 3 (Earth) are not valid for `planet_observe`. Use `sun_observe` for the Sun. Observing Earth from Earth is not defined.
|
||||
</Aside>
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range (km), and range rate (km/s).
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Where is Mars tonight from Greenwich?
|
||||
SELECT topo_azimuth(t) AS az_deg,
|
||||
topo_elevation(t) AS el_deg,
|
||||
topo_range(t) / 149597870.7 AS dist_au
|
||||
FROM planet_observe(4, '51.4769N 0.0005W 11m'::observer, now()) AS t;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- All planets' current positions from Boulder
|
||||
SELECT body_id,
|
||||
CASE body_id
|
||||
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
|
||||
WHEN 4 THEN 'Mars' WHEN 5 THEN 'Jupiter'
|
||||
WHEN 6 THEN 'Saturn' WHEN 7 THEN 'Uranus'
|
||||
WHEN 8 THEN 'Neptune'
|
||||
END AS planet,
|
||||
round(topo_azimuth(t)::numeric, 2) AS az,
|
||||
round(topo_elevation(t)::numeric, 2) AS el
|
||||
FROM unnest(ARRAY[1,2,4,5,6,7,8]) AS body_id,
|
||||
planet_observe(body_id, '40.0N 105.3W 1655m'::observer, now()) AS t
|
||||
ORDER BY topo_elevation(t) DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## sun_observe
|
||||
|
||||
Computes the topocentric position of the Sun from an Earth-based observer.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
sun_observe(obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `obs` | `observer` | Observer location on Earth |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range (km), and range rate (km/s).
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Sun position right now
|
||||
SELECT topo_azimuth(t) AS az,
|
||||
topo_elevation(t) AS el,
|
||||
topo_range(t) / 149597870.7 AS dist_au
|
||||
FROM sun_observe('40.0N 105.3W 1655m'::observer, now()) AS t;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Find today's solar noon (maximum elevation)
|
||||
SELECT t,
|
||||
round(topo_elevation(s)::numeric, 2) AS el
|
||||
FROM generate_series(
|
||||
now()::date::timestamptz,
|
||||
now()::date::timestamptz + interval '24 hours',
|
||||
interval '1 minute'
|
||||
) AS t,
|
||||
sun_observe('40.0N 105.3W 1655m'::observer, t) AS s
|
||||
ORDER BY topo_elevation(s) DESC
|
||||
LIMIT 1;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## moon_observe
|
||||
|
||||
Computes the topocentric position of the Moon from an Earth-based observer. Uses the ELP2000-82B lunar theory (Chapront-Touze & Chapront, 1983).
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
moon_observe(obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `obs` | `observer` | Observer location on Earth |
|
||||
| `t` | `timestamptz` | Observation time |
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range (km), and range rate (km/s). The Moon's range is typically 356,500 to 406,700 km.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Current Moon position and distance
|
||||
SELECT topo_azimuth(t) AS az,
|
||||
topo_elevation(t) AS el,
|
||||
topo_range(t) AS range_km
|
||||
FROM moon_observe('40.0N 105.3W 1655m'::observer, now()) AS t;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Moon's path across the sky tonight at 5-minute intervals
|
||||
SELECT t,
|
||||
round(topo_azimuth(m)::numeric, 1) AS az,
|
||||
round(topo_elevation(m)::numeric, 1) AS el,
|
||||
round(topo_range(m)::numeric, 0) AS range_km
|
||||
FROM generate_series(
|
||||
'2024-06-15 02:00:00+00',
|
||||
'2024-06-15 10:00:00+00',
|
||||
interval '5 minutes'
|
||||
) AS t,
|
||||
moon_observe('40.0N 105.3W 1655m'::observer, t) AS m
|
||||
WHERE topo_elevation(m) > 0;
|
||||
```
|
||||
257
docs/src/content/docs/reference/functions-stars-comets.mdx
Normal file
257
docs/src/content/docs/reference/functions-stars-comets.mdx
Normal file
@ -0,0 +1,257 @@
|
||||
---
|
||||
title: "Functions: Stars & Comets"
|
||||
sidebar:
|
||||
order: 5
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Functions for computing topocentric positions of stars from catalog coordinates, propagating comets and asteroids on Keplerian orbits, and observing them from Earth.
|
||||
|
||||
---
|
||||
|
||||
## star_observe
|
||||
|
||||
Converts a J2000 equatorial position (right ascension and declination) to topocentric coordinates for an Earth-based observer. Applies sidereal time rotation and horizon transformation. Stars are treated as being at infinite distance, so `topo_range` is always 0.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
star_observe(ra_hours float8, dec_deg float8, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Unit | Description |
|
||||
|-----------|------|------|-------------|
|
||||
| `ra_hours` | `float8` | hours | Right Ascension in J2000 equatorial frame (0-24) |
|
||||
| `dec_deg` | `float8` | degrees | Declination in J2000 equatorial frame (-90 to +90) |
|
||||
| `obs` | `observer` | | Observer location on Earth |
|
||||
| `t` | `timestamptz` | | Observation time |
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth and elevation in degrees. `topo_range` is 0 (infinite distance). `topo_range_rate` is 0.
|
||||
|
||||
<Aside type="note">
|
||||
This function does not account for proper motion, parallax, aberration, or atmospheric refraction. For stars with significant proper motion (e.g., Barnard's Star), the J2000 coordinates should be corrected externally before calling this function.
|
||||
</Aside>
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Where is Sirius (RA 6h 45m 8.9s, Dec -16d 42m 58s)?
|
||||
SELECT topo_azimuth(t) AS az,
|
||||
topo_elevation(t) AS el
|
||||
FROM star_observe(6.7525, -16.7161, '40.0N 105.3W 1655m'::observer, now()) AS t;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Which bright stars are above the horizon right now?
|
||||
-- (Assumes a star_catalog table with ra_hours, dec_deg, magnitude columns)
|
||||
SELECT name, magnitude,
|
||||
round(topo_azimuth(t)::numeric, 1) AS az,
|
||||
round(topo_elevation(t)::numeric, 1) AS el
|
||||
FROM star_catalog,
|
||||
star_observe(ra_hours, dec_deg, '40.0N 105.3W 1655m'::observer, now()) AS t
|
||||
WHERE magnitude < 2.0
|
||||
AND topo_elevation(t) > 0
|
||||
ORDER BY magnitude;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## star_observe_safe
|
||||
|
||||
Identical to `star_observe`, but returns NULL instead of raising an exception on invalid inputs (e.g., RA outside 0-24, Dec outside -90 to +90).
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
star_observe_safe(ra_hours float8, dec_deg float8, obs observer, t timestamptz) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Unit | Description |
|
||||
|-----------|------|------|-------------|
|
||||
| `ra_hours` | `float8` | hours | Right Ascension (0-24) |
|
||||
| `dec_deg` | `float8` | degrees | Declination (-90 to +90) |
|
||||
| `obs` | `observer` | | Observer location |
|
||||
| `t` | `timestamptz` | | Observation time |
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric`, or NULL if the input coordinates are invalid.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Batch-process a catalog, skipping any rows with bad coordinates
|
||||
SELECT catalog_id, name,
|
||||
topo_azimuth(t) AS az,
|
||||
topo_elevation(t) AS el
|
||||
FROM star_catalog,
|
||||
star_observe_safe(ra_hours, dec_deg, '40.0N 105.3W 1655m'::observer, now()) AS t
|
||||
WHERE t IS NOT NULL
|
||||
AND topo_elevation(t) > 10;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## kepler_propagate
|
||||
|
||||
Propagates an object on a Keplerian orbit (two-body problem) to a given time. Returns the heliocentric ecliptic J2000 position in AU. Handles elliptic (e < 1), parabolic (e = 1), and hyperbolic (e > 1) orbits.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
kepler_propagate(
|
||||
q_au float8,
|
||||
ecc float8,
|
||||
inc_deg float8,
|
||||
arg_peri_deg float8,
|
||||
long_node_deg float8,
|
||||
perihelion_jd float8,
|
||||
t timestamptz
|
||||
) → heliocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Unit | Description |
|
||||
|-----------|------|------|-------------|
|
||||
| `q_au` | `float8` | AU | Perihelion distance |
|
||||
| `ecc` | `float8` | | Eccentricity. 0 < e < 1 for elliptic, e = 1 for parabolic, e > 1 for hyperbolic |
|
||||
| `inc_deg` | `float8` | degrees | Orbital inclination |
|
||||
| `arg_peri_deg` | `float8` | degrees | Argument of perihelion |
|
||||
| `long_node_deg` | `float8` | degrees | Longitude of ascending node |
|
||||
| `perihelion_jd` | `float8` | JD | Time of perihelion passage as Julian Date |
|
||||
| `t` | `timestamptz` | | Evaluation time |
|
||||
|
||||
### Returns
|
||||
|
||||
A `heliocentric` position in AU (ecliptic J2000 frame).
|
||||
|
||||
<Aside type="tip">
|
||||
Orbital elements for comets and asteroids are available from the [IAU Minor Planet Center](https://www.minorplanetcenter.net/iau/Ephemerides/Comets/Soft06Cmt.txt) and [JPL Small-Body Database](https://ssd.jpl.nasa.gov/tools/sbdb_query.html). The parameter names match the MPC format.
|
||||
</Aside>
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Propagate Comet Halley (1P/Halley)
|
||||
-- q=0.586 AU, e=0.967, i=162.3, w=111.3, node=58.4, T=2446467.4 (1986 Feb 9)
|
||||
SELECT helio_x(h) AS x_au,
|
||||
helio_y(h) AS y_au,
|
||||
helio_z(h) AS z_au,
|
||||
helio_distance(h) AS r_au
|
||||
FROM kepler_propagate(
|
||||
0.586, -- perihelion distance
|
||||
0.967, -- eccentricity
|
||||
162.3, -- inclination
|
||||
111.3, -- argument of perihelion
|
||||
58.4, -- longitude of ascending node
|
||||
2446467.4, -- perihelion Julian Date
|
||||
now()
|
||||
) AS h;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Track a near-parabolic comet over 6 months
|
||||
SELECT t,
|
||||
helio_distance(h) AS r_au
|
||||
FROM generate_series(
|
||||
'2024-01-01'::timestamptz,
|
||||
'2024-07-01'::timestamptz,
|
||||
interval '1 day'
|
||||
) AS t,
|
||||
kepler_propagate(1.01, 0.9995, 45.0, 130.0, 210.0, 2460400.5, t) AS h;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## comet_observe
|
||||
|
||||
Computes the topocentric position of a comet or asteroid as seen from an Earth-based observer. This function combines Keplerian propagation with the Earth's heliocentric position to produce observer-relative coordinates.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
comet_observe(
|
||||
q_au float8,
|
||||
ecc float8,
|
||||
inc_deg float8,
|
||||
arg_peri_deg float8,
|
||||
long_node_deg float8,
|
||||
perihelion_jd float8,
|
||||
earth_x float8,
|
||||
earth_y float8,
|
||||
earth_z float8,
|
||||
obs observer,
|
||||
t timestamptz
|
||||
) → topocentric
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Unit | Description |
|
||||
|-----------|------|------|-------------|
|
||||
| `q_au` | `float8` | AU | Perihelion distance |
|
||||
| `ecc` | `float8` | | Eccentricity |
|
||||
| `inc_deg` | `float8` | degrees | Orbital inclination |
|
||||
| `arg_peri_deg` | `float8` | degrees | Argument of perihelion |
|
||||
| `long_node_deg` | `float8` | degrees | Longitude of ascending node |
|
||||
| `perihelion_jd` | `float8` | JD | Time of perihelion passage as Julian Date |
|
||||
| `earth_x` | `float8` | AU | Earth's heliocentric X (ecliptic J2000) |
|
||||
| `earth_y` | `float8` | AU | Earth's heliocentric Y (ecliptic J2000) |
|
||||
| `earth_z` | `float8` | AU | Earth's heliocentric Z (ecliptic J2000) |
|
||||
| `obs` | `observer` | | Observer location on Earth |
|
||||
| `t` | `timestamptz` | | Observation time |
|
||||
|
||||
<Aside type="note">
|
||||
The Earth position parameters (`earth_x`, `earth_y`, `earth_z`) should be obtained from `planet_heliocentric(3, t)`. They are passed explicitly rather than computed internally so that when observing multiple comets at the same time, you compute Earth's position once.
|
||||
</Aside>
|
||||
|
||||
### Returns
|
||||
|
||||
A `topocentric` with azimuth, elevation, range (km), and range rate (km/s).
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Observe Comet Halley from Boulder
|
||||
WITH earth AS (
|
||||
SELECT planet_heliocentric(3, now()) AS h
|
||||
)
|
||||
SELECT topo_azimuth(c) AS az,
|
||||
topo_elevation(c) AS el,
|
||||
topo_range(c) / 149597870.7 AS dist_au
|
||||
FROM earth,
|
||||
comet_observe(
|
||||
0.586, 0.967, 162.3, 111.3, 58.4, 2446467.4,
|
||||
helio_x(earth.h), helio_y(earth.h), helio_z(earth.h),
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
now()
|
||||
) AS c;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Batch-observe all comets from a catalog
|
||||
WITH earth AS (
|
||||
SELECT planet_heliocentric(3, now()) AS h
|
||||
)
|
||||
SELECT name,
|
||||
round(topo_azimuth(c)::numeric, 2) AS az,
|
||||
round(topo_elevation(c)::numeric, 2) AS el,
|
||||
round((topo_range(c) / 149597870.7)::numeric, 4) AS dist_au
|
||||
FROM comet_catalog, earth,
|
||||
comet_observe(
|
||||
q_au, ecc, inc_deg, arg_peri_deg, long_node_deg, perihelion_jd,
|
||||
helio_x(earth.h), helio_y(earth.h), helio_z(earth.h),
|
||||
'40.0N 105.3W 1655m'::observer,
|
||||
now()
|
||||
) AS c
|
||||
WHERE topo_elevation(c) > 0
|
||||
ORDER BY topo_range(c);
|
||||
```
|
||||
152
docs/src/content/docs/reference/functions-transfers.mdx
Normal file
152
docs/src/content/docs/reference/functions-transfers.mdx
Normal file
@ -0,0 +1,152 @@
|
||||
---
|
||||
title: "Functions: Transfers"
|
||||
sidebar:
|
||||
order: 7
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
Functions for computing interplanetary transfer orbits using the Lambert problem solver. Given a departure body, arrival body, departure time, and arrival time, the solver finds the transfer orbit that connects the two positions.
|
||||
|
||||
---
|
||||
|
||||
## lambert_transfer
|
||||
|
||||
Solves the Lambert problem for a transfer between two planets and returns the full solution record. The solver computes heliocentric positions of both bodies at the departure and arrival times using VSOP87, then finds the conic section connecting them.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
lambert_transfer(
|
||||
dep_body int4,
|
||||
arr_body int4,
|
||||
dep_time timestamptz,
|
||||
arr_time timestamptz
|
||||
) → RECORD(
|
||||
c3_departure float8,
|
||||
c3_arrival float8,
|
||||
v_inf_departure float8,
|
||||
v_inf_arrival float8,
|
||||
tof_days float8,
|
||||
transfer_sma float8
|
||||
)
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `dep_body` | `int4` | Departure planet body ID (1-8). See [Body ID Reference](/reference/body-ids/). |
|
||||
| `arr_body` | `int4` | Arrival planet body ID (1-8) |
|
||||
| `dep_time` | `timestamptz` | Departure epoch |
|
||||
| `arr_time` | `timestamptz` | Arrival epoch. Must be after `dep_time`. |
|
||||
|
||||
### Returns
|
||||
|
||||
A record with the following fields:
|
||||
|
||||
| Field | Type | Unit | Description |
|
||||
|-------|------|------|-------------|
|
||||
| `c3_departure` | `float8` | km^2/s^2 | Departure characteristic energy. The square of the hyperbolic excess velocity at the departure planet. A smaller C3 requires less launch energy. |
|
||||
| `c3_arrival` | `float8` | km^2/s^2 | Arrival characteristic energy. The energy that must be shed for orbit insertion at the arrival planet. |
|
||||
| `v_inf_departure` | `float8` | km/s | Hyperbolic excess velocity at departure (= sqrt(C3)). |
|
||||
| `v_inf_arrival` | `float8` | km/s | Hyperbolic excess velocity at arrival. |
|
||||
| `tof_days` | `float8` | days | Time of flight from departure to arrival. |
|
||||
| `transfer_sma` | `float8` | AU | Semi-major axis of the transfer orbit. Negative for hyperbolic transfers. |
|
||||
|
||||
<Aside type="caution">
|
||||
The solver finds the short-way (Type I) transfer. For departure and arrival times that would require a long-way (Type II, > 180 degree transfer angle) solution, the solver may return NULL or produce a degenerate result. The departure time must be strictly before the arrival time.
|
||||
</Aside>
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Earth to Mars transfer for a 2028 opportunity
|
||||
SELECT round(c3_departure::numeric, 2) AS c3_depart,
|
||||
round(c3_arrival::numeric, 2) AS c3_arrive,
|
||||
round(v_inf_departure::numeric, 3) AS v_inf_dep,
|
||||
round(v_inf_arrival::numeric, 3) AS v_inf_arr,
|
||||
round(tof_days::numeric, 1) AS flight_days,
|
||||
round(transfer_sma::numeric, 4) AS sma_au
|
||||
FROM lambert_transfer(3, 4,
|
||||
'2028-10-01'::timestamptz,
|
||||
'2029-06-15'::timestamptz);
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Pork chop plot: scan departure and arrival dates for Earth-Mars
|
||||
-- 150 departure dates x 150 arrival dates = 22,500 solutions
|
||||
SELECT dep, arr,
|
||||
round(c3_departure::numeric, 2) AS c3
|
||||
FROM generate_series('2028-08-01'::timestamptz, '2029-01-28'::timestamptz, interval '1 day') AS dep
|
||||
CROSS JOIN generate_series('2029-03-01'::timestamptz, '2029-07-28'::timestamptz, interval '1 day') AS arr,
|
||||
LATERAL lambert_transfer(3, 4, dep, arr) AS lt
|
||||
WHERE c3_departure IS NOT NULL
|
||||
AND c3_departure < 50
|
||||
ORDER BY c3_departure;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Compare transfer windows for all outer planets from Earth
|
||||
SELECT arr_body,
|
||||
CASE arr_body
|
||||
WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn'
|
||||
WHEN 7 THEN 'Uranus' WHEN 8 THEN 'Neptune'
|
||||
END AS target,
|
||||
round(c3_departure::numeric, 2) AS c3,
|
||||
round(tof_days::numeric, 0) AS flight_days
|
||||
FROM unnest(ARRAY[5,6,7,8]) AS arr_body,
|
||||
lambert_transfer(3, arr_body,
|
||||
'2030-01-01'::timestamptz,
|
||||
'2030-01-01'::timestamptz + interval '2 years') AS lt;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## lambert_c3
|
||||
|
||||
A convenience function that solves the Lambert problem and returns only the departure C3 value. Returns NULL on solver failure (e.g., degenerate geometry, transfer angle near 0 or 180 degrees). This is the function to use for generating pork chop plots where only departure energy matters.
|
||||
|
||||
### Signature
|
||||
|
||||
```sql
|
||||
lambert_c3(dep_body int4, arr_body int4, dep_time timestamptz, arr_time timestamptz) → float8
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `dep_body` | `int4` | Departure planet body ID (1-8) |
|
||||
| `arr_body` | `int4` | Arrival planet body ID (1-8) |
|
||||
| `dep_time` | `timestamptz` | Departure epoch |
|
||||
| `arr_time` | `timestamptz` | Arrival epoch |
|
||||
|
||||
### Returns
|
||||
|
||||
Departure C3 in km^2/s^2, or NULL if the solver fails.
|
||||
|
||||
### Example
|
||||
|
||||
```sql
|
||||
-- Quick C3 check for a specific transfer
|
||||
SELECT round(lambert_c3(3, 4,
|
||||
'2028-10-15'::timestamptz,
|
||||
'2029-05-01'::timestamptz
|
||||
)::numeric, 2) AS c3_km2s2;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Dense pork chop plot using the scalar function
|
||||
-- Faster than lambert_transfer when you only need C3
|
||||
SELECT dep, arr,
|
||||
round(lambert_c3(3, 4, dep, arr)::numeric, 2) AS c3
|
||||
FROM generate_series('2028-08-01'::timestamptz, '2029-01-28'::timestamptz, interval '1 day') AS dep
|
||||
CROSS JOIN generate_series('2029-03-01'::timestamptz, '2029-07-28'::timestamptz, interval '1 day') AS arr
|
||||
WHERE lambert_c3(3, 4, dep, arr) IS NOT NULL
|
||||
AND lambert_c3(3, 4, dep, arr) < 30;
|
||||
```
|
||||
|
||||
<Aside type="tip">
|
||||
For pork chop plots, filtering with `WHERE c3 < threshold` eliminates the unphysical fringes at short and long transfer times. A typical threshold for Earth-Mars is 15-25 km^2/s^2; for Earth-Jupiter, 80-120 km^2/s^2.
|
||||
</Aside>
|
||||
190
docs/src/content/docs/reference/operators-gist.mdx
Normal file
190
docs/src/content/docs/reference/operators-gist.mdx
Normal file
@ -0,0 +1,190 @@
|
||||
---
|
||||
title: "Operators & GiST Index"
|
||||
sidebar:
|
||||
order: 8
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
pg_orrery defines two operators on the `tle` type and a GiST operator class that enables indexed conjunction screening over large satellite catalogs. The operators work on the orbital altitude band and inclination range extracted from TLE elements, providing a fast necessary-condition filter for proximity analysis.
|
||||
|
||||
---
|
||||
|
||||
## Operators
|
||||
|
||||
### && (Overlap)
|
||||
|
||||
Tests whether two TLEs have overlapping orbital envelopes. The envelopes are defined by the altitude band (perigee to apogee) and inclination range. Overlap is a necessary but not sufficient condition for a conjunction: two satellites whose altitude bands and inclination ranges do not overlap can never come close to each other.
|
||||
|
||||
#### Signature
|
||||
|
||||
```sql
|
||||
tle && tle → boolean
|
||||
```
|
||||
|
||||
#### Description
|
||||
|
||||
Returns `true` if both of the following conditions hold:
|
||||
1. The altitude bands overlap (one satellite's perigee is below the other's apogee, and vice versa)
|
||||
2. The inclination ranges are compatible (the orbits could geometrically intersect)
|
||||
|
||||
Returns `false` if the orbits are guaranteed to never intersect based on these geometric bounds.
|
||||
|
||||
<Aside type="note">
|
||||
This operator is conservative: it may return `true` for satellite pairs that never actually approach each other (false positive), but it will never return `false` for a pair that does (no false negatives). Use `tle_distance` for precise distance computation on the pairs that pass this filter.
|
||||
</Aside>
|
||||
|
||||
#### Example
|
||||
|
||||
```sql
|
||||
-- Find all satellites whose orbits could potentially intersect with the ISS
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT norad_id, name
|
||||
FROM satellite_catalog, iss
|
||||
WHERE satellite_catalog.tle && iss.tle;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `<->` (Distance)
|
||||
|
||||
Computes the minimum separation between the altitude bands of two TLEs, in kilometers. If the altitude bands overlap, returns 0.
|
||||
|
||||
#### Signature
|
||||
|
||||
```sql
|
||||
tle <-> tle → float8
|
||||
```
|
||||
|
||||
#### Description
|
||||
|
||||
This is an altitude-only metric. It computes:
|
||||
- `max(0, perigee_a - apogee_b)` and `max(0, perigee_b - apogee_a)`
|
||||
- Returns the minimum of these two values
|
||||
|
||||
The result is the minimum possible radial separation. A result of 0 means the altitude bands overlap (but the satellites may still be far apart in along-track or cross-track distance).
|
||||
|
||||
#### Example
|
||||
|
||||
```sql
|
||||
-- Altitude band separation between ISS and a GEO satellite
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
),
|
||||
geo AS (
|
||||
SELECT '1 28884U 05041A 24001.50000000 -.00000089 00000-0 00000-0 0 9997
|
||||
2 28884 0.0153 93.0424 0001699 138.1498 336.5718 1.00271128 67481'::tle AS tle
|
||||
)
|
||||
SELECT round((iss.tle <-> geo.tle)::numeric, 1) AS separation_km
|
||||
FROM iss, geo;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Order catalog by altitude proximity to a target satellite
|
||||
WITH target AS (
|
||||
SELECT tle FROM satellite_catalog WHERE norad_id = 25544
|
||||
)
|
||||
SELECT norad_id, name,
|
||||
round((satellite_catalog.tle <-> target.tle)::numeric, 1) AS alt_sep_km
|
||||
FROM satellite_catalog, target
|
||||
ORDER BY satellite_catalog.tle <-> target.tle
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GiST Operator Class: tle_ops
|
||||
|
||||
The `tle_ops` operator class enables GiST indexing on `tle` columns. With this index in place, the `&&` (overlap) and `<->` (distance) operators use index scans instead of sequential scans, making conjunction screening over large catalogs practical.
|
||||
|
||||
### Creating the Index
|
||||
|
||||
```sql
|
||||
CREATE INDEX idx_tle_gist ON satellite_catalog USING gist (tle tle_ops);
|
||||
```
|
||||
|
||||
### What Gets Indexed
|
||||
|
||||
The GiST index stores a bounding representation of each TLE's orbital envelope:
|
||||
- **Altitude band:** perigee altitude to apogee altitude (km, above WGS-72)
|
||||
- **Inclination range:** orbital inclination (degrees)
|
||||
|
||||
These are extracted from the TLE's mean motion and eccentricity at index creation time. The index does not store time-varying quantities; it represents the geometric envelope of the orbit as defined by the current osculating elements.
|
||||
|
||||
### Index-Accelerated Queries
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Overlap scan">
|
||||
```sql
|
||||
-- Find all catalog objects that could intersect with the ISS orbit
|
||||
-- Uses the GiST index to avoid a full catalog scan
|
||||
WITH iss AS (
|
||||
SELECT tle FROM satellite_catalog WHERE norad_id = 25544
|
||||
)
|
||||
SELECT c.norad_id, c.name
|
||||
FROM satellite_catalog c, iss
|
||||
WHERE c.tle && iss.tle
|
||||
AND c.norad_id != 25544;
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="kNN by altitude">
|
||||
```sql
|
||||
-- Find the 10 satellites with the closest altitude bands to the ISS
|
||||
-- The <-> operator supports GiST ordering (ORDER BY ... <-> ...)
|
||||
WITH iss AS (
|
||||
SELECT tle FROM satellite_catalog WHERE norad_id = 25544
|
||||
)
|
||||
SELECT c.norad_id, c.name,
|
||||
round((c.tle <-> iss.tle)::numeric, 1) AS alt_sep_km
|
||||
FROM satellite_catalog c, iss
|
||||
WHERE c.norad_id != 25544
|
||||
ORDER BY c.tle <-> iss.tle
|
||||
LIMIT 10;
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Two-stage screening">
|
||||
```sql
|
||||
-- Full conjunction screening pipeline:
|
||||
-- Stage 1: GiST index filters by orbital envelope overlap
|
||||
-- Stage 2: Precise distance computation on surviving pairs
|
||||
WITH iss AS (
|
||||
SELECT tle FROM satellite_catalog WHERE norad_id = 25544
|
||||
),
|
||||
candidates AS (
|
||||
SELECT c.norad_id, c.name, c.tle
|
||||
FROM satellite_catalog c, iss
|
||||
WHERE c.tle && iss.tle
|
||||
AND c.norad_id != 25544
|
||||
)
|
||||
SELECT norad_id, name,
|
||||
round(tle_distance(candidates.tle, iss.tle, now())::numeric, 1) AS dist_km
|
||||
FROM candidates, iss
|
||||
WHERE tle_distance(candidates.tle, iss.tle, now()) < 100
|
||||
ORDER BY dist_km;
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Performance
|
||||
|
||||
Without the GiST index, the `&&` operator requires a sequential scan of the entire catalog (O(n) per query). With the index, overlap queries run in O(log n) time. For a catalog of 12,000 active TLEs, this reduces conjunction screening from seconds to milliseconds.
|
||||
|
||||
<Aside type="tip">
|
||||
The GiST index is most valuable for large catalogs (thousands of TLEs). For small catalogs (< 100 TLEs), sequential scans may be faster than the index overhead. PostgreSQL's query planner handles this decision automatically.
|
||||
</Aside>
|
||||
|
||||
### Index Maintenance
|
||||
|
||||
The GiST index is maintained automatically by PostgreSQL on `INSERT`, `UPDATE`, and `DELETE`. When TLEs are refreshed (e.g., daily catalog updates), the index is updated in place. No manual `REINDEX` is needed under normal operation.
|
||||
|
||||
If you perform a bulk catalog replacement (truncate + reload), run `REINDEX` after loading:
|
||||
|
||||
```sql
|
||||
TRUNCATE satellite_catalog;
|
||||
COPY satellite_catalog FROM '/path/to/catalog.csv' WITH (FORMAT csv);
|
||||
REINDEX INDEX idx_tle_gist;
|
||||
```
|
||||
269
docs/src/content/docs/reference/types.mdx
Normal file
269
docs/src/content/docs/reference/types.mdx
Normal file
@ -0,0 +1,269 @@
|
||||
---
|
||||
title: Types
|
||||
sidebar:
|
||||
order: 1
|
||||
---
|
||||
|
||||
import { Aside, Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
pg_orrery defines seven composite types that represent the core data structures of orbital mechanics. Each type has a fixed on-disk size, a text I/O format for readability, and accessor functions for extracting individual fields.
|
||||
|
||||
## tle
|
||||
|
||||
**Size:** 112 bytes
|
||||
|
||||
A Two-Line Element set, the standard orbital element format maintained by NORAD and distributed by CelesTrak and Space-Track. Internally stores all SGP4/SDP4-relevant fields in parsed, double-precision form.
|
||||
|
||||
### Input Format
|
||||
|
||||
Standard two-line TLE text. Both lines are concatenated into a single-quoted string with a newline separator:
|
||||
|
||||
```sql
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle;
|
||||
```
|
||||
|
||||
<Aside type="caution">
|
||||
The TLE parser validates checksums on both lines. Malformed or truncated lines will raise a parse error. Trailing whitespace is tolerated; leading whitespace is not.
|
||||
</Aside>
|
||||
|
||||
### Constructor
|
||||
|
||||
| Function | Signature | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `tle_from_lines` | `tle_from_lines(line1 text, line2 text) → tle` | Constructs a TLE from two separate line strings. Useful when lines are stored in separate columns. |
|
||||
|
||||
```sql
|
||||
SELECT tle_from_lines(
|
||||
'1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025',
|
||||
'2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'
|
||||
);
|
||||
```
|
||||
|
||||
### Accessor Functions
|
||||
|
||||
| Function | Return Type | Description |
|
||||
|----------|-------------|-------------|
|
||||
| `tle_norad_id(tle)` | `int4` | NORAD catalog number |
|
||||
| `tle_intl_desig(tle)` | `text` | International designator (e.g. `98067A`) |
|
||||
| `tle_epoch(tle)` | `timestamptz` | Epoch as a PostgreSQL timestamp |
|
||||
| `tle_inclination(tle)` | `float8` | Inclination in degrees |
|
||||
| `tle_raan(tle)` | `float8` | Right Ascension of Ascending Node in degrees |
|
||||
| `tle_eccentricity(tle)` | `float8` | Eccentricity (dimensionless) |
|
||||
| `tle_arg_perigee(tle)` | `float8` | Argument of perigee in degrees |
|
||||
| `tle_mean_anomaly(tle)` | `float8` | Mean anomaly in degrees |
|
||||
| `tle_mean_motion(tle)` | `float8` | Mean motion in revolutions/day |
|
||||
| `tle_bstar(tle)` | `float8` | B* drag coefficient (1/earth-radii) |
|
||||
| `tle_period(tle)` | `float8` | Orbital period in minutes |
|
||||
| `tle_perigee(tle)` | `float8` | Perigee altitude in km (above WGS-72 ellipsoid) |
|
||||
| `tle_apogee(tle)` | `float8` | Apogee altitude in km (above WGS-72 ellipsoid) |
|
||||
| `tle_age(tle)` | `interval` | Age of the TLE relative to `now()` |
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT tle_norad_id(tle) AS norad_id,
|
||||
tle_inclination(tle) AS inc_deg,
|
||||
tle_eccentricity(tle) AS ecc,
|
||||
tle_period(tle) AS period_min,
|
||||
tle_perigee(tle) AS perigee_km,
|
||||
tle_apogee(tle) AS apogee_km,
|
||||
tle_epoch(tle) AS epoch,
|
||||
tle_age(tle) AS age
|
||||
FROM iss;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## eci_position
|
||||
|
||||
**Size:** 48 bytes
|
||||
|
||||
Earth-Centered Inertial position and velocity in the True Equator Mean Equinox (TEME) reference frame. Position components are in kilometers; velocity components are in km/s. This is the native output frame of the SGP4/SDP4 propagator.
|
||||
|
||||
### Accessor Functions
|
||||
|
||||
| Function | Return Type | Unit | Description |
|
||||
|----------|-------------|------|-------------|
|
||||
| `eci_x(eci_position)` | `float8` | km | X position (TEME) |
|
||||
| `eci_y(eci_position)` | `float8` | km | Y position (TEME) |
|
||||
| `eci_z(eci_position)` | `float8` | km | Z position (TEME) |
|
||||
| `eci_vx(eci_position)` | `float8` | km/s | X velocity (TEME) |
|
||||
| `eci_vy(eci_position)` | `float8` | km/s | Y velocity (TEME) |
|
||||
| `eci_vz(eci_position)` | `float8` | km/s | Z velocity (TEME) |
|
||||
| `eci_speed(eci_position)` | `float8` | km/s | Magnitude of velocity vector |
|
||||
| `eci_altitude(eci_position)` | `float8` | km | Geocentric altitude (distance from Earth center minus WGS-84 equatorial radius) |
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT eci_x(pos) AS x_km,
|
||||
eci_y(pos) AS y_km,
|
||||
eci_z(pos) AS z_km,
|
||||
eci_speed(pos) AS speed_kms,
|
||||
eci_altitude(pos) AS alt_km
|
||||
FROM iss, sgp4_propagate(tle, now()) AS pos;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## geodetic
|
||||
|
||||
**Size:** 24 bytes
|
||||
|
||||
WGS-84 geodetic coordinates: latitude, longitude, and altitude above the reference ellipsoid.
|
||||
|
||||
### Accessor Functions
|
||||
|
||||
| Function | Return Type | Unit | Description |
|
||||
|----------|-------------|------|-------------|
|
||||
| `geo_lat(geodetic)` | `float8` | degrees | Geodetic latitude (-90 to +90, north positive) |
|
||||
| `geo_lon(geodetic)` | `float8` | degrees | Geodetic longitude (-180 to +180, east positive) |
|
||||
| `geo_alt(geodetic)` | `float8` | km | Altitude above WGS-84 ellipsoid |
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT geo_lat(g) AS lat,
|
||||
geo_lon(g) AS lon,
|
||||
geo_alt(g) AS alt_km
|
||||
FROM iss, subsatellite_point(tle, now()) AS g;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## topocentric
|
||||
|
||||
**Size:** 32 bytes
|
||||
|
||||
Observer-relative coordinates: azimuth, elevation, slant range, and range rate. This is the output of all `*_observe` functions.
|
||||
|
||||
### Accessor Functions
|
||||
|
||||
| Function | Return Type | Unit | Description |
|
||||
|----------|-------------|------|-------------|
|
||||
| `topo_azimuth(topocentric)` | `float8` | degrees | Azimuth measured clockwise from true north (0-360) |
|
||||
| `topo_elevation(topocentric)` | `float8` | degrees | Elevation above the local horizon (-90 to +90) |
|
||||
| `topo_range(topocentric)` | `float8` | km | Slant range from observer to target |
|
||||
| `topo_range_rate(topocentric)` | `float8` | km/s | Rate of change of range. Positive = receding from observer. |
|
||||
|
||||
<Aside type="note">
|
||||
For `star_observe`, `topo_range` is always 0 because stars are treated as being at infinite distance. For solar system bodies, the range is the true geometric distance at the observation time.
|
||||
</Aside>
|
||||
|
||||
```sql
|
||||
-- Where is Saturn from Boulder right now?
|
||||
SELECT topo_azimuth(t) AS az,
|
||||
topo_elevation(t) AS el,
|
||||
topo_range(t) AS range_km
|
||||
FROM planet_observe(6, '40.0N 105.3W 1655m'::observer, now()) AS t;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## observer
|
||||
|
||||
**Size:** 24 bytes
|
||||
|
||||
An observer's geodetic location on Earth. Used as input to all topocentric observation functions.
|
||||
|
||||
### Input Format
|
||||
|
||||
A compact string encoding latitude, longitude, and optional altitude:
|
||||
|
||||
```
|
||||
'40.0N 105.3W 1655m'
|
||||
```
|
||||
|
||||
- Latitude: decimal degrees followed by `N` or `S`
|
||||
- Longitude: decimal degrees followed by `E` or `W`
|
||||
- Altitude: meters followed by `m` (optional, defaults to 0)
|
||||
|
||||
```sql
|
||||
SELECT '40.0N 105.3W 1655m'::observer; -- Boulder, CO
|
||||
SELECT '51.4769N 0.0005W 11m'::observer; -- Greenwich Observatory
|
||||
SELECT '35.6762N 139.6503E 40m'::observer; -- Tokyo
|
||||
SELECT '33.9S 18.5E 0m'::observer; -- Cape Town
|
||||
```
|
||||
|
||||
### Constructor
|
||||
|
||||
| Function | Signature | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `observer_from_geodetic` | `observer_from_geodetic(lat float8, lon float8, alt_m float8 DEFAULT 0) → observer` | Construct from numeric lat/lon (degrees) and altitude (meters). Latitude: north positive. Longitude: east positive. |
|
||||
|
||||
```sql
|
||||
-- These are equivalent:
|
||||
SELECT '40.0N 105.3W 1655m'::observer;
|
||||
SELECT observer_from_geodetic(40.0, -105.3, 1655);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## pass_event
|
||||
|
||||
**Size:** 48 bytes
|
||||
|
||||
A satellite pass over an observer location, with AOS (Acquisition of Signal), maximum elevation, and LOS (Loss of Signal) timestamps plus geometry.
|
||||
|
||||
### Accessor Functions
|
||||
|
||||
| Function | Return Type | Unit | Description |
|
||||
|----------|-------------|------|-------------|
|
||||
| `pass_aos_time(pass_event)` | `timestamptz` | | Time the satellite rises above the horizon (or minimum elevation threshold) |
|
||||
| `pass_max_el_time(pass_event)` | `timestamptz` | | Time of maximum elevation (closest approach) |
|
||||
| `pass_los_time(pass_event)` | `timestamptz` | | Time the satellite sets below the horizon (or minimum elevation threshold) |
|
||||
| `pass_max_elevation(pass_event)` | `float8` | degrees | Peak elevation during the pass |
|
||||
| `pass_aos_azimuth(pass_event)` | `float8` | degrees | Azimuth at AOS |
|
||||
| `pass_los_azimuth(pass_event)` | `float8` | degrees | Azimuth at LOS |
|
||||
| `pass_duration(pass_event)` | `interval` | | Duration from AOS to LOS |
|
||||
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT pass_aos_time(p) AS rise,
|
||||
pass_max_el_time(p) AS culmination,
|
||||
pass_max_elevation(p) AS max_el,
|
||||
pass_los_time(p) AS set,
|
||||
pass_aos_azimuth(p) AS rise_az,
|
||||
pass_los_azimuth(p) AS set_az,
|
||||
pass_duration(p) AS dur
|
||||
FROM iss, predict_passes(tle, '40.0N 105.3W 1655m'::observer,
|
||||
now(), now() + interval '24 hours', 10.0) AS p;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## heliocentric
|
||||
|
||||
**Size:** 24 bytes
|
||||
|
||||
Heliocentric position in the ecliptic plane of J2000.0, measured in Astronomical Units (AU). This is the output of `planet_heliocentric` and `kepler_propagate`.
|
||||
|
||||
### Accessor Functions
|
||||
|
||||
| Function | Return Type | Unit | Description |
|
||||
|----------|-------------|------|-------------|
|
||||
| `helio_x(heliocentric)` | `float8` | AU | X position (ecliptic J2000) |
|
||||
| `helio_y(heliocentric)` | `float8` | AU | Y position (ecliptic J2000) |
|
||||
| `helio_z(heliocentric)` | `float8` | AU | Z position (ecliptic J2000) |
|
||||
| `helio_distance(heliocentric)` | `float8` | AU | Distance from the Sun (vector magnitude) |
|
||||
|
||||
```sql
|
||||
-- Heliocentric positions of all eight planets
|
||||
SELECT body_id,
|
||||
helio_x(h) AS x_au,
|
||||
helio_y(h) AS y_au,
|
||||
helio_z(h) AS z_au,
|
||||
helio_distance(h) AS dist_au
|
||||
FROM generate_series(1, 8) AS body_id,
|
||||
planet_heliocentric(body_id, now()) AS h;
|
||||
```
|
||||
281
docs/src/content/docs/workflow/from-gmat.mdx
Normal file
281
docs/src/content/docs/workflow/from-gmat.mdx
Normal file
@ -0,0 +1,281 @@
|
||||
---
|
||||
title: From GMAT to SQL
|
||||
sidebar:
|
||||
order: 3
|
||||
description: Comparing GMAT's mission design workflow with pg_orrery's SQL approach for Lambert transfer analysis.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside, Steps } from "@astrojs/starlight/components";
|
||||
|
||||
GMAT (General Mission Analysis Tool) is NASA's open-source mission design software. It handles everything from preliminary trajectory design to full mission planning with low-thrust propulsion, gravity assists, and multi-body optimization. It's powerful, free, and built for professional mission designers.
|
||||
|
||||
pg_orrery is not a mission design tool. It solves one specific problem from GMAT's domain — Lambert transfers between planets — and makes it available as a SQL function. This page is about that narrow overlap, and why doing Lambert analysis in SQL is sometimes a better fit than launching a full mission design environment.
|
||||
|
||||
## The Lambert problem
|
||||
|
||||
Given two positions in space and a time of flight, find the orbit that connects them. This is the starting point for interplanetary trajectory design: "If I leave Earth on this date and arrive at Mars on that date, what does the transfer orbit look like?"
|
||||
|
||||
The solution gives you departure C3 (the energy needed to escape Earth's gravity), arrival C3 (the energy you arrive with at the target), and the transfer orbit parameters. A pork chop plot is a grid of these solutions across a range of departure and arrival dates — the visual tool mission designers use to identify launch windows.
|
||||
|
||||
## Setting up a Lambert transfer
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="GMAT">
|
||||
GMAT uses a scripting language with a GUI. A minimal Lambert-style analysis
|
||||
(technically a "targeting" problem in GMAT) requires understanding several
|
||||
concepts specific to the tool.
|
||||
|
||||
```
|
||||
% GMAT script for Earth-Mars transfer targeting
|
||||
% This is simplified — real GMAT scripts are longer
|
||||
|
||||
Create Spacecraft sc;
|
||||
sc.DateFormat = UTCGregorian;
|
||||
sc.Epoch = '01 Oct 2028 00:00:00.000';
|
||||
sc.CoordinateSystem = EarthMJ2000Eq;
|
||||
sc.DisplayStateType = Cartesian;
|
||||
|
||||
Create ForceModel DeepSpace;
|
||||
DeepSpace.CentralBody = Sun;
|
||||
DeepSpace.PointMasses = {Sun, Earth, Mars};
|
||||
|
||||
Create Propagator DeepSpaceProp;
|
||||
DeepSpaceProp.FM = DeepSpace;
|
||||
DeepSpaceProp.Type = PrinceDormand78;
|
||||
DeepSpaceProp.InitialStepSize = 86400;
|
||||
|
||||
Create ImpulsiveBurn TOI;
|
||||
TOI.CoordinateSystem = EarthMJ2000Eq;
|
||||
|
||||
Create ImpulsiveBurn MOI;
|
||||
MOI.CoordinateSystem = MarsMJ2000Eq;
|
||||
|
||||
Create DifferentialCorrector DC;
|
||||
|
||||
Create OrbitView SolarSystemView;
|
||||
SolarSystemView.Add = {sc, Earth, Mars, Sun};
|
||||
|
||||
BeginMissionSequence;
|
||||
Target DC;
|
||||
Vary DC(TOI.Element1 = 3.0, {Perturbation = 0.01, ...});
|
||||
Vary DC(TOI.Element2 = 0.0, {Perturbation = 0.01, ...});
|
||||
Vary DC(TOI.Element3 = 0.5, {Perturbation = 0.01, ...});
|
||||
Maneuver TOI(sc);
|
||||
Propagate DeepSpaceProp(sc) {sc.Mars.Periapsis};
|
||||
Achieve DC(sc.Mars.RMAG = 3500, {Tolerance = 0.1});
|
||||
Achieve DC(sc.ElapsedDays = 258, {Tolerance = 0.1});
|
||||
EndTarget;
|
||||
```
|
||||
|
||||
This is the compressed version. The GMAT tutorial for interplanetary transfers
|
||||
runs about 50 pages. You configure:
|
||||
- Spacecraft objects with initial state vectors
|
||||
- Force models with point masses and perturbations
|
||||
- Propagators with numerical integration settings
|
||||
- Impulsive burn objects
|
||||
- A differential corrector (the targeting engine)
|
||||
- Visualization objects
|
||||
|
||||
Then you write a targeting sequence that varies the burn parameters
|
||||
until the spacecraft reaches the desired conditions at arrival.
|
||||
|
||||
For a **single transfer**, this gives you a precise, multi-body solution
|
||||
with accurate planetary perturbations. That's genuinely valuable for
|
||||
mission design.
|
||||
|
||||
For a **survey** across hundreds of departure/arrival date combinations
|
||||
to find the optimal launch window? You'd need to script a loop over
|
||||
the targeting sequence, handle convergence failures, and manage output
|
||||
parsing.
|
||||
</TabItem>
|
||||
<TabItem label="pg_orrery (SQL)">
|
||||
```sql
|
||||
-- Single Earth-Mars transfer
|
||||
SELECT round(c3_departure::numeric, 2) AS c3_depart_km2s2,
|
||||
round(c3_arrival::numeric, 2) AS c3_arrive_km2s2,
|
||||
round(v_inf_departure::numeric, 3) AS v_inf_depart_kms,
|
||||
round(v_inf_arrival::numeric, 3) AS v_inf_arrive_kms,
|
||||
round(tof_days::numeric, 1) AS flight_days,
|
||||
round(transfer_sma::numeric, 4) AS sma_au
|
||||
FROM lambert_transfer(
|
||||
3, 4, -- Earth to Mars
|
||||
'2028-10-01'::timestamptz, -- departure
|
||||
'2029-06-15'::timestamptz -- arrival
|
||||
);
|
||||
```
|
||||
|
||||
Five lines. The function handles:
|
||||
- Computing Earth and Mars heliocentric positions at the given dates (VSOP87)
|
||||
- Solving the Lambert boundary value problem (Izzo algorithm)
|
||||
- Returning C3, v-infinity, time of flight, and transfer SMA
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Pork chop plots
|
||||
|
||||
This is where the SQL approach really differentiates itself. A pork chop plot surveys transfer energy across a grid of departure and arrival dates. In mission design, this is how you find launch windows.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="GMAT">
|
||||
GMAT doesn't have a built-in pork chop plot generator. You would:
|
||||
|
||||
1. Write a GMAT script with parameterized departure/arrival epochs
|
||||
2. Create an outer loop (in GMAT script or an external driver — Python, MATLAB)
|
||||
that iterates over your date grid
|
||||
3. For each combination, run the targeting sequence
|
||||
4. Handle convergence failures (some date combinations don't produce
|
||||
valid solutions with the chosen initial guess)
|
||||
5. Collect output from GMAT's report files
|
||||
6. Parse and aggregate into a matrix
|
||||
7. Plot externally (GMAT's plotting is limited for contour-type visualization)
|
||||
|
||||
This is doable — mission design teams do it — but it's a multi-hour workflow
|
||||
for setup and execution, and convergence issues require manual attention.
|
||||
|
||||
Alternatively, most teams use a dedicated Lambert solver in Python or MATLAB
|
||||
(not GMAT) for pork chop plots, and only bring GMAT in for detailed
|
||||
trajectory refinement once the launch window is identified.
|
||||
</TabItem>
|
||||
<TabItem label="pg_orrery (SQL)">
|
||||
```sql
|
||||
-- Full pork chop plot: Earth to Mars, 2028-2029 window
|
||||
-- 150 departure dates x 150 arrival dates = 22,500 transfer solutions
|
||||
SELECT dep::date AS departure,
|
||||
arr::date AS arrival,
|
||||
round(c3_departure::numeric, 2) AS c3_km2s2,
|
||||
round(tof_days::numeric, 0) AS tof
|
||||
FROM generate_series(
|
||||
'2028-06-01'::timestamptz,
|
||||
'2029-01-01'::timestamptz,
|
||||
interval '1.4 days'
|
||||
) AS dep,
|
||||
generate_series(
|
||||
'2029-02-01'::timestamptz,
|
||||
'2029-10-01'::timestamptz,
|
||||
interval '1.6 days'
|
||||
) AS arr,
|
||||
LATERAL lambert_transfer(3, 4, dep, arr) AS xfer
|
||||
WHERE tof_days > 90
|
||||
AND c3_departure < 50; -- Filter unreasonable solutions
|
||||
```
|
||||
|
||||
22,500 Lambert solves. About 8.3 seconds on commodity hardware. Export to CSV:
|
||||
|
||||
```sql
|
||||
COPY (
|
||||
SELECT dep::date, arr::date,
|
||||
round(c3_departure::numeric, 2) AS c3
|
||||
FROM generate_series(
|
||||
'2028-06-01'::timestamptz, '2029-01-01'::timestamptz,
|
||||
interval '1.4 days') AS dep,
|
||||
generate_series(
|
||||
'2029-02-01'::timestamptz, '2029-10-01'::timestamptz,
|
||||
interval '1.6 days') AS arr,
|
||||
LATERAL lambert_transfer(3, 4, dep, arr) AS xfer
|
||||
WHERE tof_days > 90
|
||||
) TO '/tmp/porkchop.csv' WITH CSV HEADER;
|
||||
```
|
||||
|
||||
Then plot with gnuplot, matplotlib, or any contour plotting tool.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Multi-planet survey
|
||||
|
||||
Which planets have favorable transfer windows from Earth in a given year? This kind of broad survey is natural in SQL but tedious in GMAT.
|
||||
|
||||
```sql
|
||||
-- Survey all inner/outer planet transfers from Earth, 2028-2030
|
||||
-- Which targets have the lowest departure C3 in each year?
|
||||
SELECT target_id,
|
||||
CASE target_id
|
||||
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
|
||||
WHEN 4 THEN 'Mars' WHEN 5 THEN 'Jupiter'
|
||||
WHEN 6 THEN 'Saturn'
|
||||
END AS target,
|
||||
dep::date AS best_departure,
|
||||
arr::date AS best_arrival,
|
||||
round(min_c3::numeric, 2) AS min_c3_km2s2,
|
||||
round(tof::numeric, 0) AS flight_days
|
||||
FROM (
|
||||
SELECT target_id, dep, arr,
|
||||
c3_departure AS min_c3,
|
||||
tof_days AS tof,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY target_id
|
||||
ORDER BY c3_departure
|
||||
) AS rn
|
||||
FROM generate_series(1, 6) AS target_id,
|
||||
generate_series(
|
||||
'2028-01-01'::timestamptz,
|
||||
'2030-12-01'::timestamptz,
|
||||
interval '10 days'
|
||||
) AS dep,
|
||||
generate_series(
|
||||
'2028-06-01'::timestamptz,
|
||||
'2033-01-01'::timestamptz,
|
||||
interval '10 days'
|
||||
) AS arr,
|
||||
LATERAL lambert_transfer(3, target_id, dep, arr) AS xfer
|
||||
WHERE target_id != 3 -- Not Earth-to-Earth
|
||||
AND tof_days BETWEEN 60 AND 1500
|
||||
AND c3_departure < 100
|
||||
) sub
|
||||
WHERE rn = 1
|
||||
ORDER BY min_c3;
|
||||
```
|
||||
|
||||
This scans a wide grid and finds the single best (lowest C3) departure/arrival combination for each target planet. It's a brute-force approach — not how a mission designer would work, but useful for exploration and screening.
|
||||
|
||||
## Where GMAT wins
|
||||
|
||||
<Aside type="note" title="GMAT is a different class of tool">
|
||||
pg_orrery's Lambert solver is a screening tool. GMAT is a mission design environment. The comparison is between a calculator and a workshop.
|
||||
</Aside>
|
||||
|
||||
**Multi-body dynamics.** GMAT propagates trajectories with full N-body gravitational perturbations — Sun, planets, moons, solar radiation pressure, atmospheric drag. pg_orrery's Lambert solver is strictly two-body (Sun + spacecraft).
|
||||
|
||||
**Low-thrust trajectories.** Electric propulsion missions (ion engines, Hall thrusters) require continuous thrust modeling. GMAT handles this; pg_orrery does not.
|
||||
|
||||
**Gravity assists.** Flyby trajectories — using a planet's gravity to change direction and speed — are central to many interplanetary missions. GMAT models these with full patched-conic or N-body dynamics. pg_orrery solves point-to-point transfers only.
|
||||
|
||||
**Mission sequence optimization.** GMAT's differential corrector and optimizer can target complex mission constraints: orbital insertion parameters, flyby altitudes, fuel budgets. pg_orrery returns raw transfer parameters without optimization.
|
||||
|
||||
**Attitude modeling.** GMAT tracks spacecraft orientation — important for solar panel pointing, antenna alignment, and sensor geometry. pg_orrery has no concept of spacecraft attitude.
|
||||
|
||||
**Visualization.** GMAT includes 3D trajectory visualization. pg_orrery returns numbers.
|
||||
|
||||
## Where pg_orrery wins
|
||||
|
||||
**Speed of iteration.** Changing a date range or adding a constraint is editing a SQL query. No restarting a GUI, no waiting for script compilation, no managing convergence parameters.
|
||||
|
||||
**Batch computation.** 22,500 Lambert solves in 8.3 seconds, parallelized across cores automatically. GMAT's targeting loop would take orders of magnitude longer for the same grid.
|
||||
|
||||
**Integration with data.** If your satellite catalog, contact schedules, or mission database lives in PostgreSQL, Lambert results join directly with those tables. No file export/import cycle.
|
||||
|
||||
**Accessibility.** GMAT has a steep learning curve — the tutorial for a basic interplanetary transfer is a 50-page document. pg_orrery's Lambert solver requires knowing one SQL function and its six output columns.
|
||||
|
||||
## The intended workflow
|
||||
|
||||
pg_orrery's Lambert solver fits into a specific phase of mission planning:
|
||||
|
||||
<Steps>
|
||||
1. **Screen with pg_orrery.** Generate a pork chop plot across a broad date range. Identify the departure/arrival windows with favorable C3 values. This takes minutes, not hours.
|
||||
|
||||
2. **Refine with GMAT.** Take the promising date ranges from step 1 and set up detailed trajectory design in GMAT. Add perturbations, model the departure and arrival spirals, check flyby opportunities.
|
||||
|
||||
3. **Iterate.** If the refined trajectory shifts the window, go back to pg_orrery to explore the neighborhood in SQL. Faster iteration between broad surveys and detailed analysis.
|
||||
</Steps>
|
||||
|
||||
<Aside type="tip" title="Quick C3 comparison">
|
||||
For a fast sanity check without building a full transfer analysis, use `lambert_c3()`:
|
||||
|
||||
```sql
|
||||
-- Just the departure C3, nothing else
|
||||
SELECT round(lambert_c3(3, 4,
|
||||
'2028-10-01'::timestamptz,
|
||||
'2029-06-15'::timestamptz)::numeric, 2) AS c3_km2s2;
|
||||
```
|
||||
|
||||
Useful for scripting quick "is this window even worth investigating?" checks.
|
||||
</Aside>
|
||||
341
docs/src/content/docs/workflow/from-jpl-horizons.mdx
Normal file
341
docs/src/content/docs/workflow/from-jpl-horizons.mdx
Normal file
@ -0,0 +1,341 @@
|
||||
---
|
||||
title: From JPL Horizons to SQL
|
||||
sidebar:
|
||||
order: 2
|
||||
description: Side-by-side comparison of JPL Horizons API workflows and equivalent pg_orrery SQL queries.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside, Steps } from "@astrojs/starlight/components";
|
||||
|
||||
JPL Horizons is the gold standard for solar system ephemeris data. Run by the Solar System Dynamics group at the Jet Propulsion Laboratory, it serves precise positions for every known body — planets, moons, asteroids, comets, spacecraft. You can access it through a web interface, telnet, email, or REST API.
|
||||
|
||||
pg_orrery does not replace Horizons. What it does is move the 95% of queries that don't need sub-milliarcsecond precision from a remote API into your local database — with no rate limits, no network latency, and results that join directly with your other tables.
|
||||
|
||||
## Planet ephemeris query
|
||||
|
||||
The most common Horizons request: "Where is Mars from my location at this time?"
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Horizons (API)">
|
||||
```python
|
||||
import requests
|
||||
|
||||
params = {
|
||||
'format': 'json',
|
||||
'COMMAND': '499', # Mars
|
||||
'OBJ_DATA': 'NO',
|
||||
'MAKE_EPHEM': 'YES',
|
||||
'EPHEM_TYPE': 'OBSERVER',
|
||||
'CENTER': 'coord@399',
|
||||
'COORD_TYPE': 'GEODETIC',
|
||||
'SITE_COORD': '-105.3,40.0,1.655', # lon, lat, alt(km)
|
||||
'START_TIME': '2025-06-15 00:00',
|
||||
'STOP_TIME': '2025-06-15 00:01',
|
||||
'STEP_SIZE': '1',
|
||||
'QUANTITIES': '1,4,20', # Astrometric RA/Dec, Az/El, Range
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
'https://ssd.jpl.nasa.gov/api/horizons.api',
|
||||
params=params
|
||||
)
|
||||
|
||||
data = response.json()
|
||||
# Parse the text block in data['result']
|
||||
# Horizons returns fixed-width text, not structured JSON
|
||||
print(data['result'])
|
||||
```
|
||||
|
||||
The API returns a text block with column headers embedded in the response body.
|
||||
Parsing it requires knowing the column positions or using a library like
|
||||
`astroquery.jplhorizons`. The response format varies depending on which
|
||||
quantities you request.
|
||||
|
||||
**Rate limits:** JPL asks for no more than ~200 heavy queries per hour from a
|
||||
single IP. Automated batch jobs that generate thousands of queries risk being
|
||||
throttled or blocked.
|
||||
</TabItem>
|
||||
<TabItem label="pg_orrery (SQL)">
|
||||
```sql
|
||||
SELECT topo_azimuth(t) AS az,
|
||||
topo_elevation(t) AS el,
|
||||
topo_range(t) AS range_km
|
||||
FROM planet_observe(4, '40.0N 105.3W 1655m'::observer,
|
||||
'2025-06-15 00:00:00+00'::timestamptz) t;
|
||||
```
|
||||
|
||||
Same computation. No network call, no parsing, no rate limits.
|
||||
The result is a typed `topocentric` value — access individual components
|
||||
with `topo_azimuth()`, `topo_elevation()`, `topo_range()`, `topo_range_rate()`.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Batch queries over time ranges
|
||||
|
||||
This is where the workflow difference becomes dramatic. Generating a 24-hour elevation profile at 10-minute resolution means 144 data points.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Horizons (API)">
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Option A: Single request with STEP_SIZE
|
||||
params = {
|
||||
'format': 'json',
|
||||
'COMMAND': '599', # Jupiter
|
||||
'MAKE_EPHEM': 'YES',
|
||||
'EPHEM_TYPE': 'OBSERVER',
|
||||
'CENTER': 'coord@399',
|
||||
'COORD_TYPE': 'GEODETIC',
|
||||
'SITE_COORD': '-105.3,40.0,1.655',
|
||||
'START_TIME': '2025-06-15 00:00',
|
||||
'STOP_TIME': '2025-06-16 00:00',
|
||||
'STEP_SIZE': '10m', # 10-minute intervals
|
||||
'QUANTITIES': '4', # Az/El only
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
'https://ssd.jpl.nasa.gov/api/horizons.api',
|
||||
params=params
|
||||
)
|
||||
|
||||
# Parse 144 lines of fixed-width text
|
||||
# Extract azimuth and elevation from each line
|
||||
lines = response.json()['result'].split('\n')
|
||||
# ... parsing logic ...
|
||||
```
|
||||
|
||||
For a single body and time range, Horizons handles this in one request.
|
||||
But what if you want this for all 8 planets? That's 8 API calls. For
|
||||
5 observers? That's 40. For a full year at 1-hour resolution?
|
||||
You're managing thousands of requests, rate limiting, error handling,
|
||||
and stitching results together.
|
||||
</TabItem>
|
||||
<TabItem label="pg_orrery (SQL)">
|
||||
```sql
|
||||
-- Jupiter elevation over 24 hours, 10-minute steps
|
||||
SELECT t,
|
||||
topo_azimuth(obs) AS az,
|
||||
topo_elevation(obs) AS el
|
||||
FROM generate_series(
|
||||
'2025-06-15 00:00:00+00'::timestamptz,
|
||||
'2025-06-16 00:00:00+00'::timestamptz,
|
||||
interval '10 minutes'
|
||||
) AS t,
|
||||
LATERAL planet_observe(5, '40.0N 105.3W 1655m'::observer, t) AS obs;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- All 8 planets, 5 observers, full year, 1-hour resolution
|
||||
-- = 8 * 5 * 8760 = 350,400 observations
|
||||
SELECT body_id, obs_name, t,
|
||||
topo_elevation(planet_observe(body_id, location, t)) AS el
|
||||
FROM generate_series(1, 8) AS body_id,
|
||||
(VALUES
|
||||
('Boulder', '40.0N 105.3W 1655m'::observer),
|
||||
('London', '51.5N 0.1W 11m'::observer),
|
||||
('Tokyo', '35.7N 139.7E 40m'::observer),
|
||||
('Sydney', '33.9S 151.2E 58m'::observer),
|
||||
('Nairobi', '1.3S 36.8E 1795m'::observer)
|
||||
) AS observers(obs_name, location),
|
||||
generate_series(
|
||||
'2025-01-01'::timestamptz,
|
||||
'2025-12-31'::timestamptz,
|
||||
interval '1 hour'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
350,400 observations. One query. No rate limits. Results land in a table you
|
||||
can index, aggregate, and join.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Moon positions
|
||||
|
||||
Horizons excels at moons — it has ephemerides for every known natural satellite. pg_orrery covers the 19 most-observed moons.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Horizons (API)">
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Galilean moons: Io=501, Europa=502, Ganymede=503, Callisto=504
|
||||
moons = {'Io': '501', 'Europa': '502', 'Ganymede': '503', 'Callisto': '504'}
|
||||
|
||||
for name, code in moons.items():
|
||||
params = {
|
||||
'format': 'json',
|
||||
'COMMAND': code,
|
||||
'MAKE_EPHEM': 'YES',
|
||||
'EPHEM_TYPE': 'OBSERVER',
|
||||
'CENTER': 'coord@399',
|
||||
'COORD_TYPE': 'GEODETIC',
|
||||
'SITE_COORD': '-105.3,40.0,1.655',
|
||||
'START_TIME': '2025-06-15 03:00',
|
||||
'STOP_TIME': '2025-06-15 03:01',
|
||||
'STEP_SIZE': '1',
|
||||
'QUANTITIES': '1,4,20',
|
||||
}
|
||||
response = requests.get(
|
||||
'https://ssd.jpl.nasa.gov/api/horizons.api',
|
||||
params=params
|
||||
)
|
||||
# Parse each response separately...
|
||||
```
|
||||
|
||||
Four separate API calls. To track all four moons over a night of observation
|
||||
at 5-minute intervals (say, 8 hours = 96 steps), that's 4 requests or
|
||||
careful batching.
|
||||
</TabItem>
|
||||
<TabItem label="pg_orrery (SQL)">
|
||||
```sql
|
||||
-- All four Galilean moons, right now
|
||||
SELECT moon_id,
|
||||
CASE moon_id
|
||||
WHEN 0 THEN 'Io' WHEN 1 THEN 'Europa'
|
||||
WHEN 2 THEN 'Ganymede' WHEN 3 THEN 'Callisto'
|
||||
END AS name,
|
||||
topo_azimuth(galilean_observe(moon_id, obs, now())) AS az,
|
||||
topo_elevation(galilean_observe(moon_id, obs, now())) AS el,
|
||||
topo_range(galilean_observe(moon_id, obs, now())) AS range_km
|
||||
FROM generate_series(0, 3) AS moon_id,
|
||||
(VALUES ('40.0N 105.3W 1655m'::observer)) AS o(obs);
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Track all four moons over an 8-hour observation session
|
||||
SELECT t,
|
||||
moon_id,
|
||||
topo_elevation(galilean_observe(moon_id, obs, t)) AS el
|
||||
FROM generate_series(0, 3) AS moon_id,
|
||||
generate_series(
|
||||
'2025-06-15 02:00:00+00'::timestamptz,
|
||||
'2025-06-15 10:00:00+00'::timestamptz,
|
||||
interval '5 minutes'
|
||||
) AS t,
|
||||
(VALUES ('40.0N 105.3W 1655m'::observer)) AS o(obs);
|
||||
```
|
||||
|
||||
384 observations (4 moons times 96 timestamps). One query.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Lambert transfer survey
|
||||
|
||||
This is where the difference is most striking. Horizons doesn't compute transfer orbits directly — you'd use its ephemeris data as input to your own Lambert solver. pg_orrery does both in one step.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Horizons + Python solver">
|
||||
```python
|
||||
from astroquery.jplhorizons import Horizons
|
||||
from poliastro.iod import izzo
|
||||
from astropy import units as u
|
||||
import numpy as np
|
||||
|
||||
# Step 1: Get Earth and Mars positions from Horizons
|
||||
# for each departure/arrival date pair
|
||||
dep_dates = pd.date_range('2028-08-01', '2028-12-01', freq='5D')
|
||||
arr_dates = pd.date_range('2029-04-01', '2029-09-01', freq='5D')
|
||||
|
||||
results = []
|
||||
for dep in dep_dates:
|
||||
# Query Earth heliocentric state at departure
|
||||
earth = Horizons(id='399', location='@sun', epochs=dep.jd)
|
||||
earth_vec = earth.vectors() # API call
|
||||
|
||||
for arr in arr_dates:
|
||||
# Query Mars heliocentric state at arrival
|
||||
mars = Horizons(id='499', location='@sun', epochs=arr.jd)
|
||||
mars_vec = mars.vectors() # API call
|
||||
|
||||
# Solve Lambert problem
|
||||
r1 = [earth_vec['x'][0], earth_vec['y'][0], earth_vec['z'][0]] * u.AU
|
||||
r2 = [mars_vec['x'][0], mars_vec['y'][0], mars_vec['z'][0]] * u.AU
|
||||
tof = (arr - dep).days * u.day
|
||||
|
||||
try:
|
||||
(v1, v2), = izzo.lambert(Sun.k, r1, r2, tof)
|
||||
c3 = (np.linalg.norm(v1.value) ** 2)
|
||||
results.append({'dep': dep, 'arr': arr, 'c3': c3})
|
||||
except:
|
||||
pass
|
||||
|
||||
# For a 25x31 grid, that's 775 departure queries + 775 arrival queries
|
||||
# to Horizons, plus 775 Lambert solves in Python
|
||||
```
|
||||
|
||||
The Horizons queries alone — even with careful batching — take minutes
|
||||
and risk rate limiting. The Lambert solve is the easy part.
|
||||
</TabItem>
|
||||
<TabItem label="pg_orrery (SQL)">
|
||||
```sql
|
||||
-- Full pork chop plot: 25 departure dates x 31 arrival dates = 775 transfers
|
||||
SELECT dep::date AS departure,
|
||||
arr::date AS arrival,
|
||||
round(c3_departure::numeric, 2) AS c3_km2s2,
|
||||
round(tof_days::numeric, 0) AS flight_days
|
||||
FROM generate_series(
|
||||
'2028-08-01'::timestamptz,
|
||||
'2028-12-01'::timestamptz,
|
||||
interval '5 days'
|
||||
) AS dep,
|
||||
generate_series(
|
||||
'2029-04-01'::timestamptz,
|
||||
'2029-09-01'::timestamptz,
|
||||
interval '5 days'
|
||||
) AS arr,
|
||||
LATERAL lambert_transfer(3, 4, dep, arr) AS xfer
|
||||
WHERE tof_days > 90; -- Filter unrealistic short transfers
|
||||
```
|
||||
|
||||
pg_orrery computes the planet positions AND solves Lambert internally.
|
||||
No external API calls. The 775 transfer solutions run in under a second.
|
||||
|
||||
Scale it up to a 150x150 grid (22,500 solutions) and it finishes in
|
||||
about 8.3 seconds.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Where Horizons wins
|
||||
|
||||
<Aside type="note" title="Horizons remains essential for some use cases">
|
||||
JPL Horizons uses DE441 and applies the full suite of apparent-position corrections. For certain applications, it's the right source — but the accuracy gap has narrowed significantly.
|
||||
</Aside>
|
||||
|
||||
**Apparent-position corrections.** Horizons applies light-time iteration, stellar aberration, and gravitational deflection of light. pg_orrery computes geometric positions. The difference is up to ~20 arcseconds for planets — irrelevant for observation planning, but significant for precision astrometry of apparent coordinates.
|
||||
|
||||
**Positional accuracy.** With the built-in VSOP87 pipeline, pg_orrery is accurate to ~1 arcsecond. With [optional DE440/441 support](/guides/de-ephemeris/) (v0.3.0+), pg_orrery reads the same JPL ephemeris data that powers Horizons — matching it at ~0.1 milliarcsecond for geometric positions. The remaining gap is in the apparent-position corrections above, not in the underlying ephemeris.
|
||||
|
||||
**Physical properties.** Horizons can return visual magnitude, angular diameter, phase angle, illuminated fraction, and surface brightness. pg_orrery returns geometric position and range.
|
||||
|
||||
**Topographic corrections.** Horizons accounts for Earth's oblateness and topographic features at the observer's location using precise geodetic models. pg_orrery uses a WGS-84 ellipsoid.
|
||||
|
||||
**Body catalog.** Horizons knows about every numbered asteroid, every known comet, and spacecraft past and present. pg_orrery covers the 8 planets, the Sun, the Moon, 19 planetary moons, and whatever comets/asteroids you define with Keplerian elements.
|
||||
|
||||
## Where pg_orrery wins
|
||||
|
||||
**No network dependency.** pg_orrery runs locally, in your database process. No DNS resolution, no TLS handshake, no API parsing. Useful in air-gapped environments, on aircraft, or when Horizons is down for maintenance.
|
||||
|
||||
**No rate limits.** Horizons is generous but not unlimited. Automated pipelines that generate thousands of queries — pork chop plot surveys, Monte Carlo trajectory analysis, multi-body scheduling — can hit throttling. pg_orrery has no external limits; you're bounded only by your own hardware.
|
||||
|
||||
**Batch everything locally.** The Lambert transfer example above illustrates this best. What takes hundreds of API calls and minutes of wall-clock time in the Horizons workflow is a single query that runs in seconds.
|
||||
|
||||
**Results in your database.** Horizons returns text that you parse and then insert. pg_orrery results are already rows in PostgreSQL — ready to JOIN, index, aggregate, or export.
|
||||
|
||||
**Reproducibility.** A pg_orrery query is deterministic. Given the same inputs, it produces the same output on any PostgreSQL instance with the extension installed. No dependency on the current state of a remote API or the version of its ephemeris files.
|
||||
|
||||
## A practical workflow
|
||||
|
||||
For many projects, the right approach uses both.
|
||||
|
||||
<Steps>
|
||||
1. **Start with VSOP87 (the default).** No configuration needed. For observation planning, rise/set times, and "what's up tonight?" queries, ~1 arcsecond accuracy is more than sufficient.
|
||||
|
||||
2. **Enable DE for precision work.** If you need sub-arcsecond planet positions — dish pointing, occultation timing, precision Lambert transfers — [configure a DE file](/guides/de-ephemeris/) and use the `_de()` function variants. You get Horizons-quality positions locally.
|
||||
|
||||
3. **Use pg_orrery for surveys.** Any time you need positions for many bodies, many timestamps, or many observers — parameter sweeps, scheduling optimization, catalog screening — run it locally. No rate limits, no network dependency.
|
||||
|
||||
4. **Use pg_orrery for integration.** When orbital data needs to join with other database tables — observation logs, equipment schedules, frequency allocations — computing inside PostgreSQL eliminates the ETL step.
|
||||
|
||||
5. **Use Horizons for exotic bodies.** If you need positions for Pluto, numbered asteroids with precise osculating elements, or decommissioned spacecraft, Horizons is the only option.
|
||||
</Steps>
|
||||
275
docs/src/content/docs/workflow/from-radio-jupiter-pro.mdx
Normal file
275
docs/src/content/docs/workflow/from-radio-jupiter-pro.mdx
Normal file
@ -0,0 +1,275 @@
|
||||
---
|
||||
title: From Radio Jupiter Pro to SQL
|
||||
sidebar:
|
||||
order: 4
|
||||
description: Replacing the Windows-only Radio Jupiter Pro desktop app with pg_orrery SQL queries for Jupiter decametric burst prediction.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside, Steps } from "@astrojs/starlight/components";
|
||||
|
||||
Radio Jupiter Pro is a Windows desktop application used by the Radio JOVE community — roughly 500 to 1000 amateur radio astronomers worldwide who monitor Jupiter's decametric radio emissions. It predicts when Jupiter-Io interactions will produce radio bursts detectable from a given location, based on the Io orbital phase angle and Jupiter's Central Meridian Longitude (CML, System III).
|
||||
|
||||
The application works. It has served the community well for years. But it has limitations that SQL can address: it's Windows-only, it doesn't export data for automated scheduling, and batch analysis over long time ranges requires manual date entry.
|
||||
|
||||
## How Jupiter radio burst prediction works
|
||||
|
||||
Jupiter emits powerful radio bursts at frequencies between roughly 15 and 38 MHz. The strongest emissions correlate with the orbital position of Io relative to Jupiter and with Jupiter's rotation (the CML). The Carr et al. (1983) model maps source regions — labeled A, B, C, and D — onto an Io phase vs. CML diagram. When the current Io phase and CML fall within a source region, the probability of detecting a burst is elevated.
|
||||
|
||||
Both Radio Jupiter Pro and pg_orrery use this same underlying model.
|
||||
|
||||
## Checking burst probability right now
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Radio Jupiter Pro">
|
||||
1. Launch the application (Windows only, or via Wine on Linux/Mac)
|
||||
2. Set your geographic coordinates in the preferences
|
||||
3. Set the date and time to the current moment
|
||||
4. Read the Io phase, CML, and source region prediction from the display
|
||||
5. Check whether a source region is active in the CML/Io-phase chart
|
||||
|
||||
There is no programmatic access. The result is on screen — you read it
|
||||
and decide whether to turn on your receiver.
|
||||
</TabItem>
|
||||
<TabItem label="pg_orrery (SQL)">
|
||||
```sql
|
||||
SELECT round(io_phase_angle(now())::numeric, 1) AS io_phase,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, now())::numeric, 1) AS cml,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(now()),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, now())
|
||||
)::numeric, 3) AS burst_prob;
|
||||
```
|
||||
|
||||
One row: Io phase angle (degrees), CML (degrees), burst probability (0 to 1).
|
||||
The probability comes from the same Carr source region model.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Best burst windows tonight
|
||||
|
||||
This is where manual tools hit their limit. "When should I turn on my receiver tonight?" requires scanning hours of time at reasonable intervals.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Radio Jupiter Pro">
|
||||
In Radio Jupiter Pro, you would:
|
||||
|
||||
1. Set the start time to sunset
|
||||
2. Advance time manually (or use the animation feature) in small steps
|
||||
3. Watch the CML/Io-phase indicator to see when it enters a source region
|
||||
4. Note the time and source region on paper or in a spreadsheet
|
||||
5. Repeat until sunrise
|
||||
|
||||
For a single evening, this takes 5 to 10 minutes of clicking. For
|
||||
planning a week of observations, multiply accordingly.
|
||||
</TabItem>
|
||||
<TabItem label="pg_orrery (SQL)">
|
||||
```sql
|
||||
-- Best burst windows tonight (6 PM to 6 AM local, 10-minute steps)
|
||||
-- Only show times when Jupiter is above the horizon AND burst probability > 0.2
|
||||
SELECT t AT TIME ZONE 'America/Denver' AS local_time,
|
||||
round(io_phase_angle(t)::numeric, 1) AS io_phase,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, t)::numeric, 1) AS cml,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
)::numeric, 3) AS prob
|
||||
FROM generate_series(
|
||||
'2025-06-15 00:00:00+00'::timestamptz,
|
||||
'2025-06-15 12:00:00+00'::timestamptz,
|
||||
interval '10 minutes'
|
||||
) AS t
|
||||
WHERE topo_elevation(planet_observe(5, '40.0N 105.3W 1655m'::observer, t)) > 5
|
||||
AND jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
) > 0.2
|
||||
ORDER BY prob DESC;
|
||||
```
|
||||
|
||||
This filters for:
|
||||
- Jupiter above 5 degrees elevation (actually observable)
|
||||
- Burst probability above 20%
|
||||
|
||||
Sorted by probability so the best windows are at the top. The entire scan
|
||||
takes milliseconds.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## 30-day burst calendar
|
||||
|
||||
Planning a month of observations. Which nights have the best windows?
|
||||
|
||||
```sql
|
||||
-- 30-day burst calendar: peak probability each night
|
||||
-- Checks every 15 minutes between 00:00 and 12:00 UTC
|
||||
WITH nightly_scan AS (
|
||||
SELECT t::date AS night,
|
||||
t,
|
||||
jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
) AS prob,
|
||||
topo_elevation(planet_observe(5, '40.0N 105.3W 1655m'::observer, t)) AS jup_el
|
||||
FROM generate_series(
|
||||
now(),
|
||||
now() + interval '30 days',
|
||||
interval '15 minutes'
|
||||
) AS t
|
||||
WHERE topo_elevation(planet_observe(5, '40.0N 105.3W 1655m'::observer, t)) > 5
|
||||
)
|
||||
SELECT night,
|
||||
round(max(prob)::numeric, 3) AS peak_prob,
|
||||
(array_agg(t ORDER BY prob DESC))[1] AS best_time_utc,
|
||||
round(max(jup_el)::numeric, 1) AS max_jupiter_el
|
||||
FROM nightly_scan
|
||||
WHERE prob > 0.1
|
||||
GROUP BY night
|
||||
ORDER BY night;
|
||||
```
|
||||
|
||||
One query scans 30 nights and returns the peak burst probability for each, along with the specific time and Jupiter's elevation at that moment. In Radio Jupiter Pro, this would require manually advancing through 30 separate nights.
|
||||
|
||||
## Correlate with observation logs
|
||||
|
||||
If you maintain a database of past observations, pg_orrery lets you answer questions like "did I actually detect bursts when the model predicted them?"
|
||||
|
||||
```sql
|
||||
-- Compare burst predictions with actual observation results
|
||||
-- Assumes an observation_log table with timestamps and detection flags
|
||||
SELECT o.obs_time,
|
||||
o.detected,
|
||||
o.snr_db,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(o.obs_time),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, o.obs_time)
|
||||
)::numeric, 3) AS predicted_prob,
|
||||
round(io_phase_angle(o.obs_time)::numeric, 1) AS io_phase,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, o.obs_time)::numeric, 1) AS cml
|
||||
FROM observation_log o
|
||||
WHERE o.receiver = 'radio_jove_20m'
|
||||
AND o.obs_time > now() - interval '1 year'
|
||||
ORDER BY o.obs_time;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Detection rate by probability bucket
|
||||
SELECT prob_bucket,
|
||||
count(*) AS total_obs,
|
||||
count(*) FILTER (WHERE detected) AS detections,
|
||||
round(count(*) FILTER (WHERE detected)::numeric / count(*)::numeric, 2) AS det_rate
|
||||
FROM (
|
||||
SELECT o.detected,
|
||||
CASE
|
||||
WHEN jupiter_burst_probability(
|
||||
io_phase_angle(o.obs_time),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, o.obs_time)
|
||||
) < 0.1 THEN '< 10%'
|
||||
WHEN jupiter_burst_probability(
|
||||
io_phase_angle(o.obs_time),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, o.obs_time)
|
||||
) < 0.3 THEN '10-30%'
|
||||
WHEN jupiter_burst_probability(
|
||||
io_phase_angle(o.obs_time),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, o.obs_time)
|
||||
) < 0.5 THEN '30-50%'
|
||||
ELSE '> 50%'
|
||||
END AS prob_bucket
|
||||
FROM observation_log o
|
||||
WHERE o.receiver = 'radio_jove_20m'
|
||||
AND o.obs_time > now() - interval '1 year'
|
||||
) sub
|
||||
GROUP BY prob_bucket
|
||||
ORDER BY prob_bucket;
|
||||
```
|
||||
|
||||
This is the kind of analysis that's impossible with Radio Jupiter Pro — it has no concept of historical data or past observations. The program shows you the current prediction and that's it.
|
||||
|
||||
## Automated scheduling
|
||||
|
||||
For operators who want to automate their receivers, pg_orrery can drive a scheduling system.
|
||||
|
||||
```sql
|
||||
-- Generate tonight's observation schedule
|
||||
-- Schedule 30-minute blocks centered on high-probability windows
|
||||
CREATE MATERIALIZED VIEW tonight_schedule AS
|
||||
WITH windows AS (
|
||||
SELECT t,
|
||||
jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
) AS prob,
|
||||
topo_elevation(planet_observe(5, '40.0N 105.3W 1655m'::observer, t)) AS el
|
||||
FROM generate_series(
|
||||
date_trunc('day', now()) + interval '1 hour',
|
||||
date_trunc('day', now()) + interval '13 hours',
|
||||
interval '5 minutes'
|
||||
) AS t
|
||||
),
|
||||
high_prob AS (
|
||||
SELECT t AS window_center,
|
||||
prob,
|
||||
el
|
||||
FROM windows
|
||||
WHERE prob > 0.3
|
||||
AND el > 10
|
||||
)
|
||||
SELECT window_center - interval '15 minutes' AS rec_start,
|
||||
window_center + interval '15 minutes' AS rec_stop,
|
||||
round(prob::numeric, 3) AS probability,
|
||||
round(el::numeric, 1) AS jupiter_el
|
||||
FROM high_prob
|
||||
ORDER BY window_center;
|
||||
```
|
||||
|
||||
Export with `COPY` to feed into a receiver control script, a cron job, or any scheduling system. Refresh nightly with `REFRESH MATERIALIZED VIEW tonight_schedule`.
|
||||
|
||||
## Io phase and CML time series
|
||||
|
||||
For operators building their own CML/Io-phase diagrams (the standard visualization in Jupiter radio astronomy):
|
||||
|
||||
```sql
|
||||
-- CML vs Io phase over 24 hours, 5-minute resolution
|
||||
-- Export for plotting a CML/Io-phase track
|
||||
COPY (
|
||||
SELECT t,
|
||||
round(io_phase_angle(t)::numeric, 2) AS io_phase,
|
||||
round(jupiter_cml('40.0N 105.3W 1655m'::observer, t)::numeric, 2) AS cml,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
)::numeric, 3) AS prob
|
||||
FROM generate_series(
|
||||
'2025-06-15 00:00:00+00'::timestamptz,
|
||||
'2025-06-16 00:00:00+00'::timestamptz,
|
||||
interval '5 minutes'
|
||||
) AS t
|
||||
) TO '/tmp/cml_io_phase.csv' WITH CSV HEADER;
|
||||
```
|
||||
|
||||
Feed the CSV into gnuplot, matplotlib, or any plotting tool to generate the CML/Io-phase diagram. Overlay the Carr source regions and you have the same visualization Radio Jupiter Pro provides — but with data you can customize, share, and version-control.
|
||||
|
||||
## Where Radio Jupiter Pro wins
|
||||
|
||||
<Aside type="note" title="Radio Jupiter Pro has unique strengths">
|
||||
For casual, visual use, a dedicated GUI application has advantages.
|
||||
</Aside>
|
||||
|
||||
**Visual CML/Io-phase chart.** Radio Jupiter Pro displays the source regions graphically with the current position highlighted. You can see at a glance which source is active and how close the geometry is to a boundary. pg_orrery returns numbers — you build your own visualization.
|
||||
|
||||
**Audio prediction.** Radio Jupiter Pro includes models for the expected spectral characteristics of different source regions. pg_orrery provides geometry and probability only.
|
||||
|
||||
**Integrated display.** Radio Jupiter Pro shows Jupiter's position, the current CML, Io phase, predicted source, and receiver recommendations all in one window. With pg_orrery, you compose the information yourself from separate function calls.
|
||||
|
||||
**Zero setup.** Install the application, enter your coordinates, and it works. pg_orrery requires PostgreSQL, the extension, and SQL knowledge.
|
||||
|
||||
## Where pg_orrery wins
|
||||
|
||||
**Platform independence.** Radio Jupiter Pro is Windows-only. pg_orrery runs on any platform that supports PostgreSQL — Linux, macOS, Windows, containers, cloud instances.
|
||||
|
||||
**Batch analysis.** Scanning 30 days, 90 days, or a full Jovian apparition at arbitrary resolution is a single `generate_series` query. No manual date advancement.
|
||||
|
||||
**Data integration.** Correlating predictions with observation logs, equipment status, weather data, or any other database table is a JOIN. Radio Jupiter Pro has no data export or import capability.
|
||||
|
||||
**Automated scheduling.** pg_orrery results feed directly into scripts, cron jobs, or any scheduling system through standard SQL exports. Radio Jupiter Pro requires a human to read the screen.
|
||||
|
||||
**Reproducibility.** A SQL query is a complete specification. Share it with another JOVE operator and they get the same results for their location by changing the observer coordinates.
|
||||
312
docs/src/content/docs/workflow/from-skyfield.mdx
Normal file
312
docs/src/content/docs/workflow/from-skyfield.mdx
Normal file
@ -0,0 +1,312 @@
|
||||
---
|
||||
title: From Skyfield to SQL
|
||||
sidebar:
|
||||
order: 1
|
||||
description: Side-by-side comparison of Skyfield Python workflows and equivalent pg_orrery SQL queries.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside, Steps } from "@astrojs/starlight/components";
|
||||
|
||||
Skyfield is an excellent Python library for positional astronomy — well-documented, well-tested, and built on the same JPL ephemeris data used by spacecraft navigation teams. If you already use Skyfield, you'll recognize the computations pg_orrery performs. The difference is where they happen.
|
||||
|
||||
This page shows the same tasks done both ways. Not to argue one is better than the other — they make different trade-offs — but to help you decide which fits your workflow.
|
||||
|
||||
## Observing a planet
|
||||
|
||||
The most common starting point: where is Jupiter from my location, right now?
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Skyfield (Python)">
|
||||
```python
|
||||
from skyfield.api import load, Topos
|
||||
|
||||
ts = load.timescale() # downloads finals2000A.all
|
||||
eph = load('de421.bsp') # downloads 17MB BSP file
|
||||
|
||||
observer = Topos('40.0 N', '105.3 W', elevation_m=1655)
|
||||
t = ts.now()
|
||||
|
||||
astrometric = (eph['earth'] + observer).at(t).observe(eph['jupiter barycenter'])
|
||||
alt, az, distance = astrometric.apparent().altaz()
|
||||
|
||||
print(f"Az: {az.degrees:.2f} El: {alt.degrees:.2f} Dist: {distance.au:.4f} AU")
|
||||
```
|
||||
|
||||
Before this runs, Skyfield downloads two files:
|
||||
- `de421.bsp` (17 MB) — JPL planetary ephemeris
|
||||
- `finals2000A.all` (3.5 MB) — Earth orientation parameters
|
||||
|
||||
These files expire. `finals2000A.all` needs refreshing every few months. The BSP file
|
||||
itself is stable, but managing file paths across environments (local dev, CI, production)
|
||||
adds friction.
|
||||
</TabItem>
|
||||
<TabItem label="pg_orrery (SQL)">
|
||||
```sql
|
||||
SELECT topo_azimuth(t) AS az,
|
||||
topo_elevation(t) AS el,
|
||||
topo_range(t) / 149597870.7 AS distance_au
|
||||
FROM planet_observe(5, '40.0N 105.3W 1655m'::observer, now()) t;
|
||||
```
|
||||
|
||||
No files to download. No timescale object. The VSOP87 coefficients are compiled
|
||||
into the extension — they ship with `CREATE EXTENSION pg_orrery` and never expire.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Batch satellite observation
|
||||
|
||||
Observe many satellites at the same timestamp. This is where the architectural difference starts to matter.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Skyfield (Python)">
|
||||
```python
|
||||
from skyfield.api import load, EarthSatellite, Topos
|
||||
|
||||
ts = load.timescale()
|
||||
observer = Topos('40.0 N', '105.3 W', elevation_m=1655)
|
||||
t = ts.now()
|
||||
|
||||
# Load TLE file
|
||||
with open('catalog.tle') as f:
|
||||
lines = f.readlines()
|
||||
|
||||
results = []
|
||||
for i in range(0, len(lines), 3):
|
||||
name = lines[i].strip()
|
||||
sat = EarthSatellite(lines[i+1], lines[i+2], name, ts)
|
||||
topo = (sat - observer).at(t)
|
||||
alt, az, dist = topo.altaz()
|
||||
if alt.degrees > 0:
|
||||
results.append({
|
||||
'name': name,
|
||||
'az': az.degrees,
|
||||
'el': alt.degrees,
|
||||
'range_km': dist.km
|
||||
})
|
||||
|
||||
# Now you have results in a Python list.
|
||||
# To correlate with other data, you need to:
|
||||
# 1. Load that data from your database
|
||||
# 2. Match by satellite name or NORAD ID
|
||||
# 3. Merge in Python (pandas, dict lookup, etc.)
|
||||
```
|
||||
|
||||
The results live in Python memory. If you need to correlate with operator contact
|
||||
schedules, frequency assignments, or historical observation logs that live in
|
||||
PostgreSQL, you have to bridge two systems.
|
||||
</TabItem>
|
||||
<TabItem label="pg_orrery (SQL)">
|
||||
```sql
|
||||
-- TLEs already in your database? Just JOIN.
|
||||
SELECT s.norad_id,
|
||||
s.name,
|
||||
topo_azimuth(observe(s.tle, obs.location, now())) AS az,
|
||||
topo_elevation(observe(s.tle, obs.location, now())) AS el,
|
||||
topo_range(observe(s.tle, obs.location, now())) AS range_km
|
||||
FROM satellites s,
|
||||
(VALUES ('40.0N 105.3W 1655m'::observer)) AS obs(location)
|
||||
WHERE topo_elevation(observe(s.tle, obs.location, now())) > 0;
|
||||
```
|
||||
|
||||
```sql
|
||||
-- Correlate with frequency assignments in the same query
|
||||
SELECT s.norad_id,
|
||||
s.name,
|
||||
f.downlink_mhz,
|
||||
topo_elevation(observe(s.tle, obs.location, now())) AS el
|
||||
FROM satellites s
|
||||
JOIN freq_assignments f ON f.norad_id = s.norad_id,
|
||||
(VALUES ('40.0N 105.3W 1655m'::observer)) AS obs(location)
|
||||
WHERE topo_elevation(observe(s.tle, obs.location, now())) > 10
|
||||
ORDER BY el DESC;
|
||||
```
|
||||
|
||||
The computation and the correlation happen in the same process. No data
|
||||
transfer between Python and PostgreSQL. The query planner can parallelize
|
||||
across cores when scanning large catalogs.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Time series generation
|
||||
|
||||
Generate positions over a time range — for plotting an elevation profile, building a ground track, or analyzing visibility windows.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Skyfield (Python)">
|
||||
```python
|
||||
from skyfield.api import load, Topos
|
||||
import numpy as np
|
||||
|
||||
ts = load.timescale()
|
||||
eph = load('de421.bsp')
|
||||
observer = Topos('40.0 N', '105.3 W', elevation_m=1655)
|
||||
|
||||
# Generate 144 timestamps over 24 hours
|
||||
t0 = ts.now()
|
||||
t1 = ts.tt_jd(t0.tt + 1.0)
|
||||
times = ts.linspace(t0, t1, 144)
|
||||
|
||||
earth_obs = eph['earth'] + observer
|
||||
jupiter = eph['jupiter barycenter']
|
||||
|
||||
elevations = []
|
||||
for t in times:
|
||||
alt, az, dist = earth_obs.at(t).observe(jupiter).apparent().altaz()
|
||||
elevations.append(alt.degrees)
|
||||
|
||||
# Plot with matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
plt.plot(range(len(elevations)), elevations)
|
||||
plt.ylabel('Elevation (degrees)')
|
||||
plt.show()
|
||||
```
|
||||
|
||||
The loop is explicit. For 144 points this is fast, but the pattern doesn't
|
||||
parallelize automatically. For larger sweeps (thousands of satellites, days
|
||||
of 1-minute resolution), you manage the iteration yourself.
|
||||
</TabItem>
|
||||
<TabItem label="pg_orrery (SQL)">
|
||||
```sql
|
||||
SELECT t AS time,
|
||||
topo_azimuth(obs) AS az,
|
||||
topo_elevation(obs) AS el
|
||||
FROM generate_series(
|
||||
now(),
|
||||
now() + interval '24 hours',
|
||||
interval '10 minutes'
|
||||
) AS t,
|
||||
LATERAL planet_observe(5, '40.0N 105.3W 1655m'::observer, t) AS obs;
|
||||
```
|
||||
|
||||
`generate_series` replaces the Python loop. To change the resolution from
|
||||
10 minutes to 1 minute, change one parameter. The same pattern works for
|
||||
any observable — planets, satellites, moons, stars.
|
||||
|
||||
Export for plotting:
|
||||
|
||||
```sql
|
||||
COPY (
|
||||
SELECT t,
|
||||
topo_elevation(planet_observe(5, '40.0N 105.3W 1655m'::observer, t)) AS el
|
||||
FROM generate_series(now(), now() + interval '24 hours', interval '10 minutes') t
|
||||
) TO '/tmp/jupiter_elevation.csv' WITH CSV HEADER;
|
||||
```
|
||||
|
||||
Then plot with whatever tool you prefer — gnuplot, matplotlib, Observable,
|
||||
a spreadsheet. pg_orrery produces data; visualization is a separate concern.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Pass prediction
|
||||
|
||||
Predict when a satellite will be visible from a location. This is where Skyfield and pg_orrery take genuinely different approaches.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Skyfield (Python)">
|
||||
```python
|
||||
from skyfield.api import load, EarthSatellite, Topos
|
||||
|
||||
ts = load.timescale()
|
||||
observer = Topos('40.0 N', '105.3 W', elevation_m=1655)
|
||||
|
||||
line1 = '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025'
|
||||
line2 = '2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'
|
||||
sat = EarthSatellite(line1, line2, 'ISS', ts)
|
||||
|
||||
t0 = ts.now()
|
||||
t1 = ts.tt_jd(t0.tt + 1.0)
|
||||
|
||||
# find_events returns (times, event_types)
|
||||
# event_type: 0=rise, 1=culminate, 2=set
|
||||
times, events = sat.find_events(observer, t0, t1, altitude_degrees=10.0)
|
||||
|
||||
for ti, event in zip(times, events):
|
||||
name = ('rise', 'culminate', 'set')[event]
|
||||
print(f"{ti.utc_iso()} — {name}")
|
||||
```
|
||||
|
||||
Skyfield's `find_events` uses root-finding to locate the exact moments when
|
||||
elevation crosses the threshold. This gives sub-second precision for AOS and
|
||||
LOS times.
|
||||
</TabItem>
|
||||
<TabItem label="pg_orrery (SQL)">
|
||||
```sql
|
||||
WITH iss AS (
|
||||
SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle AS tle
|
||||
)
|
||||
SELECT pass_aos(p) AS rise_time,
|
||||
pass_tca(p) AS max_el_time,
|
||||
pass_max_el(p) AS max_elevation,
|
||||
pass_los(p) AS set_time,
|
||||
pass_aos_az(p) AS rise_azimuth,
|
||||
pass_los_az(p) AS set_azimuth
|
||||
FROM iss,
|
||||
predict_passes(tle, '40.0N 105.3W 1655m'::observer,
|
||||
now(), now() + interval '24 hours', 10.0) p;
|
||||
```
|
||||
|
||||
`predict_passes` returns structured `pass_event` records. Batch prediction
|
||||
across many satellites is a JOIN:
|
||||
|
||||
```sql
|
||||
SELECT s.name,
|
||||
pass_aos(p) AS rise,
|
||||
pass_max_el(p) AS max_el,
|
||||
pass_los(p) AS set
|
||||
FROM satellites s,
|
||||
LATERAL predict_passes(s.tle, '40.0N 105.3W 1655m'::observer,
|
||||
now(), now() + interval '24 hours', 10.0) p
|
||||
WHERE s.constellation = 'Iridium'
|
||||
ORDER BY pass_aos(p);
|
||||
```
|
||||
|
||||
Every Iridium pass in the next 24 hours, filtered by constellation, sorted
|
||||
chronologically. Adding `JOIN schedules` or `JOIN ground_contacts` keeps the
|
||||
correlation inside the database.
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Where Skyfield wins
|
||||
|
||||
<Aside type="note" title="Skyfield has real advantages">
|
||||
pg_orrery does not replace Skyfield for all use cases. Be clear about where the trade-offs fall.
|
||||
</Aside>
|
||||
|
||||
**Apparent-position corrections.** Skyfield uses the full IAU 2000A nutation model, polar motion corrections, delta-T from IERS data, and iterates for light-time and stellar aberration. pg_orrery v0.3.0 can [optionally use DE441](/guides/de-ephemeris/) for the same underlying geometric accuracy (~0.1 milliarcsecond), but Skyfield still applies corrections that pg_orrery does not — corrections that matter for precision apparent-coordinate work like occultation timing or sub-arcsecond astrometry.
|
||||
|
||||
**Rise/set finding.** `find_events()` uses numerical root-finding to pinpoint the exact moment a body crosses an elevation threshold. pg_orrery's `predict_passes` uses a step-and-refine approach that's fast for batches but less precise for individual events.
|
||||
|
||||
**Aberration and light-time.** Skyfield iterates to correct for light travel time and applies stellar aberration. pg_orrery uses geometric positions without light-time iteration — the difference is under 20 arcseconds for planets and irrelevant for satellite tracking, but it matters for some applications.
|
||||
|
||||
**Visualization integration.** Skyfield works directly with matplotlib, numpy, and the rest of the Python scientific stack. pg_orrery produces rows and columns — you export to CSV or JSON and then plot separately.
|
||||
|
||||
**Extensibility.** Skyfield handles arbitrary BSP kernels — Pluto, spacecraft, asteroids with precise ephemerides. pg_orrery's body catalog is fixed at compile time.
|
||||
|
||||
## Where pg_orrery wins
|
||||
|
||||
**No file management.** No BSP kernels, no timescale data files, no expiring Earth orientation parameters. The computation ships with the extension.
|
||||
|
||||
**Batch operations at database speed.** Observing 12,000 satellites in 17ms. 22,500 Lambert solves in 8.3 seconds. These aren't optimized benchmarks — they're `SELECT` statements running on commodity hardware.
|
||||
|
||||
**Data correlation.** The computation happens where your data lives. JOIN orbital results with contact schedules, frequency assignments, observation logs, or any other table. No ETL pipeline between Python and PostgreSQL.
|
||||
|
||||
**Automatic parallelism.** PostgreSQL's query planner distributes PARALLEL SAFE functions across available cores. You don't manage threads or multiprocessing pools.
|
||||
|
||||
**Reproducibility.** A SQL query is a complete, self-contained specification of a computation. No virtual environment, no package versions, no file paths. The same query produces the same result on any PostgreSQL instance with pg_orrery installed.
|
||||
|
||||
## Migrating gradually
|
||||
|
||||
You don't have to choose one or the other. A practical migration path:
|
||||
|
||||
<Steps>
|
||||
1. **Keep Skyfield for apparent-position work.** Anything requiring aberration corrections, polar motion, nutation at IAU 2000A level, or custom BSP kernels stays in Python. For raw geometric position accuracy, pg_orrery with [DE enabled](/guides/de-ephemeris/) matches Skyfield.
|
||||
|
||||
2. **Move batch observation to SQL.** If you're computing positions for hundreds of objects to filter or correlate with database records, pg_orrery eliminates the Python-to-PostgreSQL round trip.
|
||||
|
||||
3. **Move scheduling to SQL.** Pass prediction and visibility windows over time ranges are natural `generate_series` + `predict_passes` queries.
|
||||
|
||||
4. **Move reporting to SQL.** "What was above 20 degrees from each of our 5 observers last night?" is a single query with a CROSS JOIN, not a Python loop over observers and timestamps.
|
||||
|
||||
5. **Enable DE when accuracy matters.** If you find VSOP87's ~1 arcsecond isn't enough for a specific use case, [configure a DE file](/guides/de-ephemeris/) and switch to `_de()` function variants. Same SQL patterns, same parameters — just add `_de` to the function name.
|
||||
</Steps>
|
||||
502
docs/src/content/docs/workflow/sql-advantage.mdx
Normal file
502
docs/src/content/docs/workflow/sql-advantage.mdx
Normal file
@ -0,0 +1,502 @@
|
||||
---
|
||||
title: The SQL Advantage
|
||||
sidebar:
|
||||
order: 5
|
||||
description: SQL patterns that make pg_orrery uniquely powerful — generate_series, CROSS JOIN, PARALLEL SAFE, materialized views, GiST indexes, and more.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside } from "@astrojs/starlight/components";
|
||||
|
||||
The previous pages compared pg_orrery with specific tools — Skyfield, JPL Horizons, GMAT, Radio Jupiter Pro. This page steps back and looks at the thing they all have in common: none of them are SQL.
|
||||
|
||||
That sounds obvious, but the implications are deeper than "you can write queries instead of scripts." SQL brings a set of patterns — compositional, declarative, and optimized by decades of database engine development — that change what's practical to compute.
|
||||
|
||||
Each pattern below includes a concrete pg_orrery example.
|
||||
|
||||
## generate_series: time series without loops
|
||||
|
||||
The most common pattern in orbital computation is "evaluate this function at regular time intervals." In Python, that's a for-loop. In SQL, it's `generate_series`.
|
||||
|
||||
```sql
|
||||
-- Mars elevation every 15 minutes for a week
|
||||
SELECT t,
|
||||
topo_elevation(planet_observe(4, '40.0N 105.3W 1655m'::observer, t)) AS el
|
||||
FROM generate_series(
|
||||
now(),
|
||||
now() + interval '7 days',
|
||||
interval '15 minutes'
|
||||
) AS t;
|
||||
```
|
||||
|
||||
672 data points. One statement. Change the interval to `'1 minute'` and you get 10,080 points — same statement, same structure. The query planner decides how to execute it; you describe what you want.
|
||||
|
||||
This replaces the boilerplate that dominates astronomical computation scripts: initializing time arrays, iterating, collecting results into lists, converting units. Here, the time array is the `generate_series` call, and the computation is inline.
|
||||
|
||||
### Irregular time grids
|
||||
|
||||
`generate_series` handles regular intervals. For irregular grids — say, the timestamps from an observation log — use a subquery or VALUES list:
|
||||
|
||||
```sql
|
||||
-- Compute Jupiter position at each recorded observation time
|
||||
SELECT o.obs_id,
|
||||
o.obs_time,
|
||||
topo_azimuth(planet_observe(5, o.location, o.obs_time)) AS az,
|
||||
topo_elevation(planet_observe(5, o.location, o.obs_time)) AS el
|
||||
FROM observations o
|
||||
WHERE o.target = 'Jupiter'
|
||||
AND o.obs_time > now() - interval '30 days';
|
||||
```
|
||||
|
||||
The function evaluates at whatever timestamps exist in your data. No pre-generating a time grid and interpolating.
|
||||
|
||||
## CROSS JOIN: parameter sweeps
|
||||
|
||||
When you need to evaluate a function across every combination of multiple parameters, SQL's CROSS JOIN (or implicit comma-join) generates the Cartesian product.
|
||||
|
||||
### Pork chop plot
|
||||
|
||||
The canonical example: departure date x arrival date for Lambert transfers.
|
||||
|
||||
```sql
|
||||
SELECT dep::date AS departure,
|
||||
arr::date AS arrival,
|
||||
round(c3_departure::numeric, 2) AS c3_km2s2
|
||||
FROM generate_series(
|
||||
'2028-08-01'::timestamptz, '2029-01-01'::timestamptz,
|
||||
interval '2 days') AS dep,
|
||||
generate_series(
|
||||
'2029-03-01'::timestamptz, '2029-10-01'::timestamptz,
|
||||
interval '2 days') AS arr,
|
||||
LATERAL lambert_transfer(3, 4, dep, arr) AS xfer
|
||||
WHERE tof_days > 90;
|
||||
```
|
||||
|
||||
Two `generate_series` calls, one `LATERAL` function. The database generates every combination, evaluates the Lambert solver for each, and returns the results. No nested loops, no progress bars, no managing iteration state.
|
||||
|
||||
### Multi-observer visibility
|
||||
|
||||
Which observer has the best view of each planet right now?
|
||||
|
||||
```sql
|
||||
SELECT body_id,
|
||||
obs_name,
|
||||
round(topo_elevation(
|
||||
planet_observe(body_id, location, now())
|
||||
)::numeric, 1) AS el
|
||||
FROM generate_series(1, 8) AS body_id,
|
||||
(VALUES
|
||||
('Boulder', '40.0N 105.3W 1655m'::observer),
|
||||
('Mauna Kea', '19.8N 155.5W 4205m'::observer),
|
||||
('Paranal', '24.6S 70.4W 2635m'::observer),
|
||||
('Tenerife', '28.3N 16.5W 2390m'::observer)
|
||||
) AS obs(obs_name, location)
|
||||
WHERE topo_elevation(planet_observe(body_id, location, now())) > 0
|
||||
ORDER BY body_id, el DESC;
|
||||
```
|
||||
|
||||
8 planets times 4 observers = 32 evaluations. Filtered to only above-horizon results. Sorted so the best observer for each planet appears first.
|
||||
|
||||
## JOIN: correlate with anything
|
||||
|
||||
This is the pattern that no standalone computation tool can replicate. When orbital data lives in the same database as your other data, correlation is a JOIN — not an export-import-match pipeline.
|
||||
|
||||
### Satellite visibility with frequency data
|
||||
|
||||
```sql
|
||||
-- Which satellites are visible AND transmitting on frequencies
|
||||
-- our receiver can handle?
|
||||
SELECT s.norad_id,
|
||||
s.name,
|
||||
f.downlink_mhz,
|
||||
f.mode,
|
||||
round(topo_elevation(
|
||||
observe(s.tle, '40.0N 105.3W 1655m'::observer, now())
|
||||
)::numeric, 1) AS el,
|
||||
round(topo_range(
|
||||
observe(s.tle, '40.0N 105.3W 1655m'::observer, now())
|
||||
)::numeric, 0) AS range_km
|
||||
FROM satellites s
|
||||
JOIN freq_assignments f ON f.norad_id = s.norad_id
|
||||
WHERE f.downlink_mhz BETWEEN 144.0 AND 146.0 -- 2m band
|
||||
AND topo_elevation(observe(s.tle, '40.0N 105.3W 1655m'::observer, now())) > 10
|
||||
ORDER BY el DESC;
|
||||
```
|
||||
|
||||
The satellite catalog, frequency database, and orbit propagation all participate in the same query. No intermediate files, no API calls, no data format conversion.
|
||||
|
||||
### Pass prediction with contact scheduling
|
||||
|
||||
```sql
|
||||
-- Predict passes for our constellation and check against existing schedule
|
||||
SELECT s.name,
|
||||
pass_aos(p) AS rise,
|
||||
pass_max_el(p) AS max_el,
|
||||
pass_los(p) AS set,
|
||||
CASE WHEN cs.id IS NOT NULL THEN 'SCHEDULED'
|
||||
ELSE 'AVAILABLE'
|
||||
END AS status
|
||||
FROM satellites s,
|
||||
LATERAL predict_passes(
|
||||
s.tle, '40.0N 105.3W 1655m'::observer,
|
||||
now(), now() + interval '24 hours', 15.0
|
||||
) p
|
||||
LEFT JOIN contact_schedule cs
|
||||
ON cs.norad_id = s.norad_id
|
||||
AND cs.start_time < pass_los(p)
|
||||
AND cs.end_time > pass_aos(p)
|
||||
WHERE s.constellation = 'ORBCOMM'
|
||||
ORDER BY pass_aos(p);
|
||||
```
|
||||
|
||||
Every predicted pass, annotated with whether it overlaps an existing scheduled contact. The LEFT JOIN means unscheduled windows show up as 'AVAILABLE' — these are the gaps you can fill.
|
||||
|
||||
### Burst prediction with weather
|
||||
|
||||
```sql
|
||||
-- Jupiter burst windows, filtered by weather forecast
|
||||
SELECT t AT TIME ZONE 'America/Denver' AS local_time,
|
||||
round(jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
)::numeric, 3) AS burst_prob,
|
||||
w.cloud_cover_pct,
|
||||
w.precipitation_mm
|
||||
FROM generate_series(
|
||||
now(), now() + interval '12 hours', interval '15 minutes'
|
||||
) AS t
|
||||
LEFT JOIN weather_forecast w
|
||||
ON w.station = 'KBDU'
|
||||
AND w.forecast_time = date_trunc('hour', t)
|
||||
WHERE topo_elevation(planet_observe(5, '40.0N 105.3W 1655m'::observer, t)) > 10
|
||||
AND jupiter_burst_probability(
|
||||
io_phase_angle(t),
|
||||
jupiter_cml('40.0N 105.3W 1655m'::observer, t)
|
||||
) > 0.2
|
||||
ORDER BY t;
|
||||
```
|
||||
|
||||
Radio observation is affected by weather differently than optical — rain matters less than ionospheric conditions — but the pattern is the same. Whatever data you have that affects observing decisions, JOIN it in.
|
||||
|
||||
## PARALLEL SAFE: automatic multi-core
|
||||
|
||||
All pg_orrery computation functions are declared `PARALLEL SAFE`. This means PostgreSQL's query planner can distribute work across multiple CPU cores without any explicit threading or multiprocessing code.
|
||||
|
||||
<Aside type="tip" title="When parallelism kicks in">
|
||||
PostgreSQL enables parallel query execution when the estimated cost exceeds `parallel_tuple_cost * min_parallel_table_scan_size`. For pg_orrery, this typically happens when scanning tables with hundreds or thousands of rows, or when `generate_series` produces large result sets. You can check the execution plan with `EXPLAIN ANALYZE`.
|
||||
</Aside>
|
||||
|
||||
```sql
|
||||
-- Observe all 12,000+ satellites in a catalog
|
||||
-- PostgreSQL will parallelize this across available cores
|
||||
EXPLAIN ANALYZE
|
||||
SELECT s.norad_id,
|
||||
topo_elevation(observe(s.tle, '40.0N 105.3W 1655m'::observer, now())) AS el
|
||||
FROM satellites s;
|
||||
```
|
||||
|
||||
The execution plan will show `Parallel Seq Scan` or `Gather` nodes when the planner decides parallelism is worthwhile. You don't request it, configure worker pools, or manage thread safety. The database handles it.
|
||||
|
||||
For explicit control when testing:
|
||||
|
||||
```sql
|
||||
-- Force parallel execution with 4 workers
|
||||
SET max_parallel_workers_per_gather = 4;
|
||||
SET parallel_tuple_cost = 0;
|
||||
SET parallel_setup_cost = 0;
|
||||
|
||||
SELECT s.norad_id,
|
||||
topo_elevation(observe(s.tle, '40.0N 105.3W 1655m'::observer, now())) AS el
|
||||
FROM satellites s
|
||||
WHERE topo_elevation(observe(s.tle, '40.0N 105.3W 1655m'::observer, now())) > 0;
|
||||
```
|
||||
|
||||
## CREATE MATERIALIZED VIEW: cache expensive computations
|
||||
|
||||
Some computations are expensive to run repeatedly — a 30-day burst calendar, a full catalog observation, a pork chop plot survey. Materialized views store the result and let you query it like a table.
|
||||
|
||||
```sql
|
||||
-- Cache tonight's satellite visibility for all observers
|
||||
CREATE MATERIALIZED VIEW tonight_visibility AS
|
||||
SELECT s.norad_id,
|
||||
s.name,
|
||||
obs.obs_name,
|
||||
pass_aos(p) AS rise,
|
||||
pass_max_el(p) AS max_el,
|
||||
pass_los(p) AS set
|
||||
FROM satellites s,
|
||||
(VALUES
|
||||
('Boulder', '40.0N 105.3W 1655m'::observer),
|
||||
('London', '51.5N 0.1W 11m'::observer),
|
||||
('Tokyo', '35.7N 139.7E 40m'::observer)
|
||||
) AS obs(obs_name, location),
|
||||
LATERAL predict_passes(
|
||||
s.tle, obs.location,
|
||||
now(), now() + interval '24 hours', 10.0
|
||||
) p;
|
||||
|
||||
-- Query it instantly
|
||||
SELECT * FROM tonight_visibility
|
||||
WHERE obs_name = 'Boulder'
|
||||
AND max_el > 45
|
||||
ORDER BY rise;
|
||||
|
||||
-- Refresh when TLEs update
|
||||
REFRESH MATERIALIZED VIEW tonight_visibility;
|
||||
```
|
||||
|
||||
The initial computation might take seconds for a large catalog. Subsequent queries against the materialized view are instant — it's just reading a table. Refresh it when the underlying data changes (new TLEs, new day).
|
||||
|
||||
### Concurrent refresh
|
||||
|
||||
For production systems where you don't want to block readers during refresh:
|
||||
|
||||
```sql
|
||||
CREATE UNIQUE INDEX ON tonight_visibility (norad_id, obs_name, rise);
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY tonight_visibility;
|
||||
```
|
||||
|
||||
The `CONCURRENTLY` option requires a unique index but allows queries to continue reading the old data while the refresh runs.
|
||||
|
||||
## COPY TO: export to anything
|
||||
|
||||
pg_orrery produces structured data. Getting it out of PostgreSQL and into other tools is a `COPY` statement.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="CSV">
|
||||
```sql
|
||||
COPY (
|
||||
SELECT t, topo_elevation(planet_observe(5, '40.0N 105.3W 1655m'::observer, t)) AS el
|
||||
FROM generate_series(now(), now() + interval '24 hours', interval '10 minutes') t
|
||||
) TO '/tmp/jupiter_elevation.csv' WITH CSV HEADER;
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="JSON">
|
||||
```sql
|
||||
COPY (
|
||||
SELECT json_build_object(
|
||||
'time', t,
|
||||
'az', topo_azimuth(planet_observe(5, obs, t)),
|
||||
'el', topo_elevation(planet_observe(5, obs, t)),
|
||||
'range_au', topo_range(planet_observe(5, obs, t)) / 149597870.7
|
||||
)
|
||||
FROM generate_series(now(), now() + interval '24 hours', interval '1 hour') t,
|
||||
(VALUES ('40.0N 105.3W 1655m'::observer)) AS o(obs)
|
||||
) TO '/tmp/jupiter.jsonl';
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="psql to stdout">
|
||||
```bash
|
||||
psql -c "
|
||||
COPY (
|
||||
SELECT dep::date, arr::date, round(c3_departure::numeric, 2) AS c3
|
||||
FROM generate_series('2028-08-01'::timestamptz, '2029-01-01'::timestamptz, '5 days') dep,
|
||||
generate_series('2029-03-01'::timestamptz, '2029-10-01'::timestamptz, '5 days') arr,
|
||||
LATERAL lambert_transfer(3, 4, dep, arr) xfer
|
||||
WHERE tof_days > 90
|
||||
) TO STDOUT WITH CSV HEADER
|
||||
" > porkchop.csv
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
CSV feeds into gnuplot, matplotlib, Excel, R, Observable, or any visualization tool. JSON feeds into web applications, APIs, or document databases. The computation stays in PostgreSQL; rendering happens wherever you prefer.
|
||||
|
||||
## GiST INDEX: spatial queries on orbital elements
|
||||
|
||||
pg_orrery's TLE type supports GiST indexing. This enables spatial-style queries over orbital elements — finding satellites that share similar orbits or screening for conjunction risks.
|
||||
|
||||
```sql
|
||||
-- Create a GiST index on the satellite catalog
|
||||
CREATE INDEX idx_satellites_tle ON satellites USING gist (tle);
|
||||
|
||||
-- Find satellites with orbital overlap (similar altitude, inclination, RAAN)
|
||||
SELECT a.name AS sat_a,
|
||||
b.name AS sat_b,
|
||||
a.tle <-> b.tle AS altitude_separation_km
|
||||
FROM satellites a, satellites b
|
||||
WHERE a.norad_id < b.norad_id -- Avoid duplicate pairs
|
||||
AND a.tle && b.tle -- Orbital overlap (GiST accelerated)
|
||||
AND a.tle <-> b.tle < 50 -- Within 50 km altitude
|
||||
ORDER BY a.tle <-> b.tle
|
||||
LIMIT 100;
|
||||
```
|
||||
|
||||
The `&&` operator tests for orbital overlap — whether two TLEs describe orbits in the same region of space. The `<->` operator returns altitude separation in kilometers. Both are accelerated by the GiST index, meaning the database can prune the search space before evaluating expensive propagation.
|
||||
|
||||
For a catalog of 12,000 satellites, a full cross-product would be 72 million pairs. The GiST index reduces this to the pairs that are actually in the same orbital regime.
|
||||
|
||||
### Conjunction screening
|
||||
|
||||
```sql
|
||||
-- Catalog-wide conjunction screening for the next 24 hours
|
||||
-- GiST index pre-filters to nearby orbital regimes
|
||||
SELECT a.norad_id AS sat_a,
|
||||
b.norad_id AS sat_b,
|
||||
a.tle <-> b.tle AS alt_sep_km
|
||||
FROM satellites a, satellites b
|
||||
WHERE a.norad_id < b.norad_id
|
||||
AND a.tle && b.tle
|
||||
AND a.tle <-> b.tle < 10
|
||||
ORDER BY a.tle <-> b.tle;
|
||||
```
|
||||
|
||||
This is a screening filter, not a precision conjunction analysis. It identifies pairs worth investigating further — the ones where orbital elements suggest close approaches. Detailed conjunction assessment would then propagate those specific pairs at high time resolution.
|
||||
|
||||
## Provider switching: accuracy when you need it
|
||||
|
||||
pg_orrery v0.3.0 has two ephemeris providers — the built-in VSOP87 pipeline (~1 arcsecond) and optional [JPL DE440/441](/guides/de-ephemeris/) (~0.1 milliarcsecond). The SQL interface makes switching between them a one-character change.
|
||||
|
||||
```sql
|
||||
-- VSOP87 (built-in, IMMUTABLE, no setup)
|
||||
SELECT topo_elevation(planet_observe(5, '40.0N 105.3W 1655m'::observer, now()));
|
||||
|
||||
-- DE441 (opt-in, STABLE, sub-milliarcsecond)
|
||||
SELECT topo_elevation(planet_observe_de(5, '40.0N 105.3W 1655m'::observer, now()));
|
||||
```
|
||||
|
||||
Same parameters, same return type, same SQL patterns. Add `_de` and you get Horizons-quality positions. Remove it and you get zero-dependency speed.
|
||||
|
||||
The distinction matters at the SQL level because of **volatility**. VSOP87 functions are `IMMUTABLE` — their output depends only on their arguments. PostgreSQL can constant-fold them during planning, use them in expression indexes, and cache results aggressively. DE functions are `STABLE` — they depend on an external file, so the planner evaluates them once per row per statement but can't index on them.
|
||||
|
||||
```sql
|
||||
-- This works: expression index on IMMUTABLE VSOP87 function
|
||||
CREATE INDEX ON almanac (date)
|
||||
WHERE topo_elevation(planet_observe(5, location, date)) > 0;
|
||||
|
||||
-- For DE queries, use a materialized view instead
|
||||
CREATE MATERIALIZED VIEW almanac_de AS
|
||||
SELECT date, topo_elevation(planet_observe_de(5, location, date)) AS el
|
||||
FROM almanac WHERE topo_elevation(planet_observe_de(5, location, date)) > 0;
|
||||
```
|
||||
|
||||
Use VSOP87 for indexes and fast screening. Use DE for final-answer queries where accuracy matters. Both compose with every other SQL pattern on this page.
|
||||
|
||||
## Window functions: tracking changes over time
|
||||
|
||||
SQL window functions let you compute values relative to neighboring rows — previous values, running averages, ranks within groups — without self-joins or subqueries.
|
||||
|
||||
### Range rate changes
|
||||
|
||||
```sql
|
||||
-- Track ISS range and range rate, flagging closest approach
|
||||
WITH iss_track AS (
|
||||
SELECT t,
|
||||
topo_range(observe(iss.tle, '40.0N 105.3W 1655m'::observer, t)) AS range_km,
|
||||
topo_range_rate(observe(iss.tle, '40.0N 105.3W 1655m'::observer, t)) AS range_rate,
|
||||
topo_elevation(observe(iss.tle, '40.0N 105.3W 1655m'::observer, t)) AS el
|
||||
FROM (SELECT '1 25544U 98067A 24001.50000000 .00016717 00000-0 10270-3 0 9025
|
||||
2 25544 51.6400 208.9163 0006703 30.1694 61.7520 15.50100486 00001'::tle) AS iss(tle),
|
||||
generate_series(now(), now() + interval '2 hours', interval '30 seconds') AS t
|
||||
WHERE topo_elevation(observe(iss.tle, '40.0N 105.3W 1655m'::observer, t)) > 0
|
||||
)
|
||||
SELECT t,
|
||||
round(range_km::numeric, 1) AS range_km,
|
||||
round(range_rate::numeric, 3) AS range_rate_kms,
|
||||
round(el::numeric, 1) AS elevation,
|
||||
CASE
|
||||
WHEN range_rate < 0 AND lead(range_rate) OVER (ORDER BY t) >= 0
|
||||
THEN 'CLOSEST APPROACH'
|
||||
ELSE ''
|
||||
END AS event
|
||||
FROM iss_track
|
||||
ORDER BY t;
|
||||
```
|
||||
|
||||
The `lead()` window function looks at the next row's range rate. When range rate crosses from negative to positive, the satellite has passed closest approach. No separate analysis step — it's computed inline.
|
||||
|
||||
### Daily peak elevation
|
||||
|
||||
```sql
|
||||
-- Which planet reaches the highest elevation each night this month?
|
||||
WITH hourly AS (
|
||||
SELECT body_id,
|
||||
t::date AS night,
|
||||
topo_elevation(planet_observe(body_id, '40.0N 105.3W 1655m'::observer, t)) AS el
|
||||
FROM generate_series(1, 8) AS body_id,
|
||||
generate_series(now(), now() + interval '30 days', interval '1 hour') AS t
|
||||
)
|
||||
SELECT night,
|
||||
body_id,
|
||||
CASE body_id
|
||||
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus' WHEN 3 THEN 'Earth'
|
||||
WHEN 4 THEN 'Mars' WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn'
|
||||
WHEN 7 THEN 'Uranus' WHEN 8 THEN 'Neptune'
|
||||
END AS planet,
|
||||
round(max_el::numeric, 1) AS peak_el
|
||||
FROM (
|
||||
SELECT *,
|
||||
max(el) OVER (PARTITION BY body_id, night) AS max_el,
|
||||
ROW_NUMBER() OVER (PARTITION BY night ORDER BY max(el) OVER (PARTITION BY body_id, night) DESC) AS rn
|
||||
FROM hourly
|
||||
WHERE el > 0
|
||||
) sub
|
||||
WHERE rn = 1
|
||||
GROUP BY night, body_id, max_el
|
||||
ORDER BY night;
|
||||
```
|
||||
|
||||
For each night, this finds which planet reaches the highest elevation — useful for deciding what to observe. The window function handles the ranking within each night without a correlated subquery.
|
||||
|
||||
## Composition: building complex queries from simple parts
|
||||
|
||||
The real power of SQL is that these patterns compose. A single query can use `generate_series` for time steps, `CROSS JOIN` for parameter sweeps, `JOIN` for data correlation, window functions for change detection, and `COPY TO` for export — all in one statement.
|
||||
|
||||
```sql
|
||||
-- Complete observation planning query:
|
||||
-- For each of 3 observers, for each visible planet tonight,
|
||||
-- find the 2-hour window with the highest average elevation,
|
||||
-- export to CSV
|
||||
COPY (
|
||||
WITH planet_track AS (
|
||||
SELECT obs_name, body_id, t,
|
||||
topo_elevation(planet_observe(body_id, location, t)) AS el
|
||||
FROM (VALUES
|
||||
('Boulder', '40.0N 105.3W 1655m'::observer),
|
||||
('Mauna Kea','19.8N 155.5W 4205m'::observer),
|
||||
('Paranal', '24.6S 70.4W 2635m'::observer)
|
||||
) AS obs(obs_name, location),
|
||||
generate_series(1, 8) AS body_id,
|
||||
generate_series(
|
||||
date_trunc('day', now()) + interval '1 hour',
|
||||
date_trunc('day', now()) + interval '13 hours',
|
||||
interval '15 minutes'
|
||||
) AS t
|
||||
WHERE topo_elevation(planet_observe(body_id, location, t)) > 10
|
||||
),
|
||||
windowed AS (
|
||||
SELECT obs_name, body_id, t,
|
||||
el,
|
||||
avg(el) OVER (
|
||||
PARTITION BY obs_name, body_id
|
||||
ORDER BY t
|
||||
ROWS BETWEEN 4 PRECEDING AND 4 FOLLOWING
|
||||
) AS rolling_avg_el
|
||||
FROM planet_track
|
||||
)
|
||||
SELECT DISTINCT ON (obs_name, body_id)
|
||||
obs_name,
|
||||
body_id,
|
||||
CASE body_id
|
||||
WHEN 1 THEN 'Mercury' WHEN 2 THEN 'Venus'
|
||||
WHEN 3 THEN 'Earth' WHEN 4 THEN 'Mars'
|
||||
WHEN 5 THEN 'Jupiter' WHEN 6 THEN 'Saturn'
|
||||
WHEN 7 THEN 'Uranus' WHEN 8 THEN 'Neptune'
|
||||
END AS planet,
|
||||
t AS best_window_center,
|
||||
round(rolling_avg_el::numeric, 1) AS avg_el_in_window
|
||||
FROM windowed
|
||||
ORDER BY obs_name, body_id, rolling_avg_el DESC
|
||||
) TO '/tmp/observation_plan.csv' WITH CSV HEADER;
|
||||
```
|
||||
|
||||
This query:
|
||||
1. Generates time steps across the night (`generate_series`)
|
||||
2. Evaluates all 8 planets from 3 observers (`CROSS JOIN`)
|
||||
3. Filters to above-horizon results (`WHERE`)
|
||||
4. Computes a rolling 2-hour average elevation (`window function`)
|
||||
5. Selects the best window for each observer/planet pair (`DISTINCT ON`)
|
||||
6. Exports to CSV (`COPY TO`)
|
||||
|
||||
In a traditional workflow, each of these steps would be a separate script, a separate data file, and a separate tool. In SQL, they compose into a single declarative statement that the database engine optimizes and parallelizes.
|
||||
|
||||
That's the advantage. Not that SQL is a better programming language — it isn't. But for the specific pattern of "evaluate a function over structured parameter spaces and correlate the results with existing data," SQL is exactly the right tool. pg_orrery puts 68 functions inside that tool — from 17ms satellite batch propagation to sub-milliarcsecond DE441 planet positions — and every one of them composes with every SQL pattern on this page.
|
||||
183
docs/src/og-renderer.tsx
Normal file
183
docs/src/og-renderer.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
import React from "react";
|
||||
import type { RenderFunctionInput } from "astro-opengraph-images";
|
||||
|
||||
export async function pgOrreryOgImage({
|
||||
title,
|
||||
description,
|
||||
}: RenderFunctionInput): Promise<React.ReactNode> {
|
||||
return Promise.resolve(
|
||||
<div
|
||||
style={{
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
backgroundColor: "#0a0e17",
|
||||
fontFamily: "Inter, system-ui, sans-serif",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{/* Top accent bar */}
|
||||
<div
|
||||
style={{
|
||||
height: "4px",
|
||||
width: "100%",
|
||||
background: "linear-gradient(to right, #f59e0b, #fbbf24, #f59e0b)",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Decorative orbital rings (top-right) */}
|
||||
<svg
|
||||
width="320"
|
||||
height="320"
|
||||
viewBox="0 0 320 320"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-60px",
|
||||
right: "-40px",
|
||||
opacity: 0.08,
|
||||
}}
|
||||
>
|
||||
<ellipse
|
||||
cx="160"
|
||||
cy="160"
|
||||
rx="140"
|
||||
ry="60"
|
||||
stroke="#f59e0b"
|
||||
stroke-width="2"
|
||||
fill="none"
|
||||
transform="rotate(-20 160 160)"
|
||||
/>
|
||||
<ellipse
|
||||
cx="160"
|
||||
cy="160"
|
||||
rx="120"
|
||||
ry="45"
|
||||
stroke="#fbbf24"
|
||||
stroke-width="1.5"
|
||||
fill="none"
|
||||
transform="rotate(35 160 160)"
|
||||
/>
|
||||
<ellipse
|
||||
cx="160"
|
||||
cy="160"
|
||||
rx="90"
|
||||
ry="35"
|
||||
stroke="#f59e0b"
|
||||
stroke-width="1"
|
||||
fill="none"
|
||||
transform="rotate(-5 160 160)"
|
||||
/>
|
||||
<circle cx="160" cy="160" r="8" fill="#f59e0b" />
|
||||
</svg>
|
||||
|
||||
{/* Content area */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between",
|
||||
flexGrow: 1,
|
||||
padding: "48px 64px",
|
||||
}}
|
||||
>
|
||||
{/* Title + description */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
marginTop: "32px",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 56,
|
||||
fontWeight: 700,
|
||||
color: "#e2e8f0",
|
||||
lineHeight: 1.15,
|
||||
maxWidth: "900px",
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
{description && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: 28,
|
||||
color: "#8896a8",
|
||||
marginTop: "20px",
|
||||
lineHeight: 1.4,
|
||||
maxWidth: "800px",
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "12px",
|
||||
}}
|
||||
>
|
||||
{/* Amber dot */}
|
||||
<div
|
||||
style={{
|
||||
width: "12px",
|
||||
height: "12px",
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#f59e0b",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 24,
|
||||
fontWeight: 700,
|
||||
color: "#e2e8f0",
|
||||
}}
|
||||
>
|
||||
pg_orrery
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: "#556677",
|
||||
marginLeft: "4px",
|
||||
}}
|
||||
>
|
||||
Celestial mechanics for PostgreSQL
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 18,
|
||||
color: "#556677",
|
||||
}}
|
||||
>
|
||||
pg-orrery.warehack.ing
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom accent bar */}
|
||||
<div
|
||||
style={{
|
||||
height: "4px",
|
||||
width: "100%",
|
||||
background: "linear-gradient(to right, #f59e0b, #fbbf24, #f59e0b)",
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
129
docs/src/styles/custom.css
Normal file
129
docs/src/styles/custom.css
Normal file
@ -0,0 +1,129 @@
|
||||
@import "@fontsource/inter/400.css";
|
||||
@import "@fontsource/inter/500.css";
|
||||
@import "@fontsource/inter/600.css";
|
||||
@import "@fontsource/inter/700.css";
|
||||
@import "@fontsource/jetbrains-mono/400.css";
|
||||
@import "@fontsource/jetbrains-mono/500.css";
|
||||
|
||||
/* pg_orrery palette — deep space observation theme */
|
||||
:root {
|
||||
--sl-font: "Inter", system-ui, -apple-system, sans-serif;
|
||||
--sl-font-mono: "JetBrains Mono", "Fira Code", ui-monospace, monospace;
|
||||
|
||||
--sl-color-accent-low: #451a03;
|
||||
--sl-color-accent: #f59e0b;
|
||||
--sl-color-accent-high: #fef3c7;
|
||||
|
||||
--sl-color-white: #e2e8f0;
|
||||
--sl-color-gray-1: #cbd5e1;
|
||||
--sl-color-gray-2: #8896a8;
|
||||
--sl-color-gray-3: #556677;
|
||||
--sl-color-gray-4: #2a3f54;
|
||||
--sl-color-gray-5: #1e2d3d;
|
||||
--sl-color-gray-6: #111827;
|
||||
--sl-color-gray-7: #0a0e17;
|
||||
|
||||
--sl-color-bg-nav: var(--sl-color-gray-6);
|
||||
--sl-color-bg-sidebar: var(--sl-color-gray-7);
|
||||
--sl-color-hairline-light: var(--sl-color-gray-5);
|
||||
--sl-color-hairline-shade: var(--sl-color-gray-4);
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--sl-color-gray-7);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--sl-color-gray-5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--sl-color-gray-4);
|
||||
}
|
||||
|
||||
/* Selection color with amber accent */
|
||||
::selection {
|
||||
background-color: #92400e;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
/* SQL code blocks get amber left-border accent */
|
||||
pre:has(> code.language-sql) {
|
||||
border-left: 3px solid var(--sl-color-accent);
|
||||
}
|
||||
|
||||
/* Code blocks — raised surface */
|
||||
pre {
|
||||
background-color: #111827 !important;
|
||||
border: 1px solid #1e2d3d;
|
||||
}
|
||||
|
||||
/* Sidebar section labels */
|
||||
.sl-sidebar-group summary {
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
/* Hero title gradient */
|
||||
.hero .hero-html h1 {
|
||||
background: linear-gradient(135deg, #f59e0b 0%, #fbbf24 50%, #d97706 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--sl-color-gray-2);
|
||||
border-bottom: 1px solid var(--sl-color-gray-5);
|
||||
}
|
||||
|
||||
td {
|
||||
border-bottom: 1px solid var(--sl-color-gray-5);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
tr:hover td {
|
||||
background-color: #1a2332;
|
||||
}
|
||||
|
||||
/* Aside tweaks */
|
||||
.starlight-aside--note {
|
||||
border-color: var(--sl-color-accent);
|
||||
}
|
||||
|
||||
/* Focus visible */
|
||||
:focus-visible {
|
||||
outline: 2px solid var(--sl-color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Workflow comparison blocks */
|
||||
.workflow-compare {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.workflow-compare {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
48
docs/src/styles/katex-fixes.css
Normal file
48
docs/src/styles/katex-fixes.css
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* KaTeX fixes for Starlight
|
||||
*
|
||||
* Starlight sets `svg { height: auto }` globally which breaks KaTeX's
|
||||
* internal SVG elements (fraction bars, radicals, delimiters). These
|
||||
* elements rely on explicit height values from KaTeX's layout engine.
|
||||
*/
|
||||
|
||||
.katex-html svg {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
.katex-html .vlist svg {
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
/* Wide equations need horizontal scroll */
|
||||
.katex-display {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Ensure KaTeX text is visible against dark background */
|
||||
.katex {
|
||||
color: var(--sl-color-white, #e2e8f0);
|
||||
}
|
||||
|
||||
/* Display-mode equations get breathing room */
|
||||
.katex-display > .katex {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Fix KaTeX newline spacing in aligned environments */
|
||||
.katex .base {
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Ensure fraction lines are visible */
|
||||
.katex .frac-line {
|
||||
border-color: currentColor;
|
||||
}
|
||||
|
||||
/* Inline math shouldn't break across lines */
|
||||
.katex-inline {
|
||||
white-space: nowrap;
|
||||
}
|
||||
3
docs/tsconfig.json
Normal file
3
docs/tsconfig.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict"
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit ff7b98957dfa2979700a482bde9de9542807293e
|
||||
@ -1,4 +0,0 @@
|
||||
comment = 'Orbital mechanics types and functions for PostgreSQL'
|
||||
default_version = '0.1.0'
|
||||
module_pathname = '$libdir/pg_orbit'
|
||||
relocatable = true
|
||||
4
pg_orrery.control
Normal file
4
pg_orrery.control
Normal file
@ -0,0 +1,4 @@
|
||||
comment = 'A database orrery — celestial mechanics types and functions for PostgreSQL'
|
||||
default_version = '0.5.0'
|
||||
module_pathname = '$libdir/pg_orrery'
|
||||
relocatable = true
|
||||
188
sql/pg_orrery--0.1.0--0.2.0.sql
Normal file
188
sql/pg_orrery--0.1.0--0.2.0.sql
Normal file
@ -0,0 +1,188 @@
|
||||
-- pg_orrery 0.1.0 -> 0.2.0 migration
|
||||
--
|
||||
-- Phase 1: Stars, comets, and Keplerian propagation.
|
||||
-- Adds heliocentric type, star observation, and two-body propagation.
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Heliocentric type: ecliptic J2000 position in AU
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE heliocentric;
|
||||
|
||||
CREATE FUNCTION heliocentric_in(cstring) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_out(heliocentric) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_recv(internal) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_send(heliocentric) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE heliocentric (
|
||||
INPUT = heliocentric_in,
|
||||
OUTPUT = heliocentric_out,
|
||||
RECEIVE = heliocentric_recv,
|
||||
SEND = heliocentric_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE heliocentric IS 'Heliocentric position in ecliptic J2000 frame (x, y, z in AU)';
|
||||
|
||||
CREATE FUNCTION helio_x(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_x(heliocentric) IS 'X component in AU (ecliptic J2000, vernal equinox direction)';
|
||||
|
||||
CREATE FUNCTION helio_y(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_y(heliocentric) IS 'Y component in AU (ecliptic J2000)';
|
||||
|
||||
CREATE FUNCTION helio_z(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_z(heliocentric) IS 'Z component in AU (ecliptic J2000, north ecliptic pole)';
|
||||
|
||||
CREATE FUNCTION helio_distance(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_distance(heliocentric) IS 'Heliocentric distance in AU';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Star observation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION star_observe(float8, float8, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION star_observe(float8, float8, observer, timestamptz) IS
|
||||
'Observe a star from (ra_hours J2000, dec_degrees J2000, observer, time). Returns topocentric az/el. Range is 0 (infinite distance).';
|
||||
|
||||
CREATE FUNCTION star_observe_safe(float8, float8, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION star_observe_safe(float8, float8, observer, timestamptz) IS
|
||||
'Like star_observe but returns NULL on invalid inputs. For batch queries over star catalogs.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Keplerian propagation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION kepler_propagate(
|
||||
q_au float8, eccentricity float8,
|
||||
inclination_deg float8, arg_perihelion_deg float8,
|
||||
long_asc_node_deg float8, perihelion_jd float8,
|
||||
t timestamptz
|
||||
) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION kepler_propagate(float8, float8, float8, float8, float8, float8, timestamptz) IS
|
||||
'Two-body Keplerian propagation from classical orbital elements. Returns heliocentric ecliptic J2000 position in AU. Handles elliptic, parabolic, and hyperbolic orbits.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Comet observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION comet_observe(
|
||||
q_au float8, eccentricity float8,
|
||||
inclination_deg float8, arg_perihelion_deg float8,
|
||||
long_asc_node_deg float8, perihelion_jd float8,
|
||||
earth_x_au float8, earth_y_au float8, earth_z_au float8,
|
||||
obs observer, t timestamptz
|
||||
) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION comet_observe(float8, float8, float8, float8, float8, float8, float8, float8, float8, observer, timestamptz) IS
|
||||
'Observe a comet/asteroid from orbital elements. Requires Earth heliocentric ecliptic J2000 position (AU). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 2: VSOP87 planets, ELP82B Moon, Sun observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION planet_heliocentric(int4, timestamptz) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_heliocentric(int4, timestamptz) IS
|
||||
'VSOP87 heliocentric ecliptic J2000 position (AU). Body IDs: 0=Sun, 1=Mercury, ..., 8=Neptune.';
|
||||
|
||||
CREATE FUNCTION planet_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_observe(int4, observer, timestamptz) IS
|
||||
'Observe a planet from (body_id 1-8, observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
CREATE FUNCTION sun_observe(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_observe(observer, timestamptz) IS
|
||||
'Observe the Sun from (observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
CREATE FUNCTION moon_observe(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_observe(observer, timestamptz) IS
|
||||
'Observe the Moon via ELP2000-82B from (observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 3: Planetary moon observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION galilean_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION galilean_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Galilean moon of Jupiter via L1.2 theory. Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.';
|
||||
|
||||
CREATE FUNCTION saturn_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION saturn_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Saturn moon via TASS 1.7. Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion.';
|
||||
|
||||
CREATE FUNCTION uranus_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION uranus_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Uranus moon via GUST86. Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon.';
|
||||
|
||||
CREATE FUNCTION mars_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION mars_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Mars moon via MarsSat. Body IDs: 0=Phobos, 1=Deimos.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 3: Jupiter decametric radio burst prediction
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION io_phase_angle(timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION io_phase_angle(timestamptz) IS
|
||||
'Io orbital phase angle in degrees [0,360). 0=superior conjunction (behind Jupiter). Standard Radio JOVE convention.';
|
||||
|
||||
CREATE FUNCTION jupiter_cml(observer, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION jupiter_cml(observer, timestamptz) IS
|
||||
'Jupiter Central Meridian Longitude, System III (1965.0), in degrees [0,360). Light-time corrected.';
|
||||
|
||||
CREATE FUNCTION jupiter_burst_probability(float8, float8) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION jupiter_burst_probability(float8, float8) IS
|
||||
'Estimated Jupiter decametric burst probability (0-1) from (io_phase_deg, cml_deg). Based on Carr et al. (1983) source regions A, B, C, D.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 4: Interplanetary transfer orbits (Lambert solver)
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION lambert_transfer(
|
||||
dep_body_id int4, arr_body_id int4,
|
||||
dep_time timestamptz, arr_time timestamptz,
|
||||
OUT c3_departure float8, OUT c3_arrival float8,
|
||||
OUT v_inf_departure float8, OUT v_inf_arrival float8,
|
||||
OUT tof_days float8, OUT transfer_sma float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_transfer(int4, int4, timestamptz, timestamptz) IS
|
||||
'Solve Lambert transfer between two planets. Returns C3 (km^2/s^2), v_infinity (km/s), TOF (days), SMA (AU). Body IDs 1-8.';
|
||||
|
||||
CREATE FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) IS
|
||||
'Departure C3 (km^2/s^2) for a Lambert transfer. Returns NULL if solver fails. For pork chop plots.';
|
||||
@ -1,4 +1,4 @@
|
||||
-- pg_orbit -- Orbital mechanics types and functions for PostgreSQL
|
||||
-- pg_orrery -- Orbital mechanics types and functions for PostgreSQL
|
||||
--
|
||||
-- Types: tle, eci_position, geodetic, topocentric, observer, pass_event
|
||||
-- Provides SGP4/SDP4 propagation, coordinate transforms, pass prediction,
|
||||
86
sql/pg_orrery--0.2.0--0.3.0.sql
Normal file
86
sql/pg_orrery--0.2.0--0.3.0.sql
Normal file
@ -0,0 +1,86 @@
|
||||
-- pg_orrery 0.2.0 -> 0.3.0 migration
|
||||
--
|
||||
-- Adds optional JPL DE440/441 ephemeris functions.
|
||||
-- Existing VSOP87/ELP2000-82B functions are unchanged (still IMMUTABLE).
|
||||
-- New _de() functions are STABLE (depend on external DE binary file).
|
||||
-- When DE is unavailable, _de() functions fall back to VSOP87 silently.
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 5: DE ephemeris functions (optional high-precision)
|
||||
-- ============================================================
|
||||
|
||||
-- Planet observation with DE ephemeris
|
||||
|
||||
CREATE FUNCTION planet_heliocentric_de(int4, timestamptz) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_heliocentric_de(int4, timestamptz) IS
|
||||
'Heliocentric ecliptic J2000 position via JPL DE (sub-arcsecond). Falls back to VSOP87 if DE unavailable. Body IDs: 0=Sun, 1-8=Mercury-Neptune.';
|
||||
|
||||
CREATE FUNCTION planet_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe planet via JPL DE. Falls back to VSOP87. Body IDs: 1-8 (Mercury-Neptune).';
|
||||
|
||||
CREATE FUNCTION sun_observe_de(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_observe_de(observer, timestamptz) IS
|
||||
'Observe Sun via JPL DE. Falls back to VSOP87.';
|
||||
|
||||
CREATE FUNCTION moon_observe_de(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_observe_de(observer, timestamptz) IS
|
||||
'Observe Moon via JPL DE. Falls back to ELP2000-82B.';
|
||||
|
||||
|
||||
-- Lambert transfer with DE positions
|
||||
|
||||
CREATE FUNCTION lambert_transfer_de(
|
||||
dep_body_id int4, arr_body_id int4,
|
||||
dep_time timestamptz, arr_time timestamptz,
|
||||
OUT c3_departure float8, OUT c3_arrival float8,
|
||||
OUT v_inf_departure float8, OUT v_inf_arrival float8,
|
||||
OUT tof_days float8, OUT transfer_sma float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_transfer_de(int4, int4, timestamptz, timestamptz) IS
|
||||
'Lambert transfer via JPL DE positions. Falls back to VSOP87. Body IDs 1-8.';
|
||||
|
||||
CREATE FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) IS
|
||||
'Departure C3 via JPL DE. Falls back to VSOP87. For pork chop plots.';
|
||||
|
||||
|
||||
-- Planetary moon observation with DE parent positions
|
||||
|
||||
CREATE FUNCTION galilean_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION galilean_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Galilean moon with JPL DE parent position. L1.2 moon offsets. Body IDs: 0-3 (Io-Callisto).';
|
||||
|
||||
CREATE FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Saturn moon with JPL DE parent position. TASS 1.7 moon offsets. Body IDs: 0-7 (Mimas-Hyperion).';
|
||||
|
||||
CREATE FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Uranus moon with JPL DE parent position. GUST86 moon offsets. Body IDs: 0-4 (Miranda-Oberon).';
|
||||
|
||||
CREATE FUNCTION mars_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION mars_moon_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Mars moon with JPL DE parent position. MarsSat moon offsets. Body IDs: 0-1 (Phobos-Deimos).';
|
||||
|
||||
|
||||
-- Diagnostic function
|
||||
|
||||
CREATE FUNCTION pg_orrery_ephemeris_info(
|
||||
OUT provider text, OUT file_path text,
|
||||
OUT start_jd float8, OUT end_jd float8,
|
||||
OUT version int4, OUT au_km float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pg_orrery_ephemeris_info() IS
|
||||
'Returns current ephemeris provider status: VSOP87 or JPL_DE with file path, JD range, version, and AU value.';
|
||||
704
sql/pg_orrery--0.2.0.sql
Normal file
704
sql/pg_orrery--0.2.0.sql
Normal file
@ -0,0 +1,704 @@
|
||||
-- pg_orrery -- Orbital mechanics types and functions for PostgreSQL
|
||||
--
|
||||
-- Types: tle, eci_position, geodetic, topocentric, observer, pass_event
|
||||
-- Provides SGP4/SDP4 propagation, coordinate transforms, pass prediction,
|
||||
-- and GiST indexing on altitude bands for conjunction screening.
|
||||
--
|
||||
-- All propagation uses WGS-72 constants (matching TLE mean element fitting).
|
||||
-- Coordinate output uses WGS-84 (matching modern geodetic standards).
|
||||
|
||||
-- ============================================================
|
||||
-- Shell types (forward declarations)
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE tle;
|
||||
CREATE TYPE eci_position;
|
||||
CREATE TYPE geodetic;
|
||||
CREATE TYPE topocentric;
|
||||
CREATE TYPE observer;
|
||||
CREATE TYPE pass_event;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- TLE type: Two-Line Element set
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION tle_in(cstring) RETURNS tle
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION tle_out(tle) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION tle_recv(internal) RETURNS tle
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION tle_send(tle) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE tle (
|
||||
INPUT = tle_in,
|
||||
OUTPUT = tle_out,
|
||||
RECEIVE = tle_recv,
|
||||
SEND = tle_send,
|
||||
INTERNALLENGTH = 112,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE tle IS 'Two-Line Element set — parsed mean orbital elements for SGP4/SDP4 propagation';
|
||||
|
||||
-- TLE accessor functions
|
||||
|
||||
CREATE FUNCTION tle_epoch(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_epoch(tle) IS 'TLE epoch as Julian date (UTC)';
|
||||
|
||||
CREATE FUNCTION tle_norad_id(tle) RETURNS int4
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_norad_id(tle) IS 'NORAD catalog number';
|
||||
|
||||
CREATE FUNCTION tle_inclination(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_inclination(tle) IS 'Orbital inclination in degrees';
|
||||
|
||||
CREATE FUNCTION tle_eccentricity(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_eccentricity(tle) IS 'Orbital eccentricity (dimensionless)';
|
||||
|
||||
CREATE FUNCTION tle_raan(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_raan(tle) IS 'Right ascension of ascending node in degrees';
|
||||
|
||||
CREATE FUNCTION tle_arg_perigee(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_arg_perigee(tle) IS 'Argument of perigee in degrees';
|
||||
|
||||
CREATE FUNCTION tle_mean_anomaly(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_mean_anomaly(tle) IS 'Mean anomaly in degrees';
|
||||
|
||||
CREATE FUNCTION tle_mean_motion(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_mean_motion(tle) IS 'Mean motion in revolutions per day';
|
||||
|
||||
CREATE FUNCTION tle_bstar(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_bstar(tle) IS 'B* drag coefficient (1/earth-radii)';
|
||||
|
||||
CREATE FUNCTION tle_period(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_period(tle) IS 'Orbital period in minutes';
|
||||
|
||||
CREATE FUNCTION tle_age(tle, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_age(tle, timestamptz) IS 'TLE age in days (positive = past epoch, negative = before epoch)';
|
||||
|
||||
CREATE FUNCTION tle_perigee(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_perigee(tle) IS 'Perigee altitude in km above WGS-72 ellipsoid';
|
||||
|
||||
CREATE FUNCTION tle_apogee(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_apogee(tle) IS 'Apogee altitude in km above WGS-72 ellipsoid';
|
||||
|
||||
CREATE FUNCTION tle_intl_desig(tle) RETURNS text
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_intl_desig(tle) IS 'International designator (COSPAR ID)';
|
||||
|
||||
CREATE FUNCTION tle_from_lines(text, text) RETURNS tle
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_lines(text, text) IS
|
||||
'Construct TLE from separate line1/line2 text columns';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- ECI position type: True Equator Mean Equinox (TEME) frame
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION eci_in(cstring) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_out(eci_position) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_recv(internal) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_send(eci_position) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE eci_position (
|
||||
INPUT = eci_in,
|
||||
OUTPUT = eci_out,
|
||||
RECEIVE = eci_recv,
|
||||
SEND = eci_send,
|
||||
INTERNALLENGTH = 48,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE eci_position IS 'Earth-Centered Inertial position and velocity in TEME frame (km, km/s)';
|
||||
|
||||
-- ECI accessor functions
|
||||
|
||||
CREATE FUNCTION eci_x(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_y(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_z(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_vx(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_vy(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_vz(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_speed(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_speed(eci_position) IS 'Velocity magnitude in km/s';
|
||||
|
||||
CREATE FUNCTION eci_altitude(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_altitude(eci_position) IS 'Approximate geocentric altitude in km (radius - WGS72_AE)';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Geodetic type: WGS-84 latitude/longitude/altitude
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION geodetic_in(cstring) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_out(geodetic) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_recv(internal) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_send(geodetic) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE geodetic (
|
||||
INPUT = geodetic_in,
|
||||
OUTPUT = geodetic_out,
|
||||
RECEIVE = geodetic_recv,
|
||||
SEND = geodetic_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE geodetic IS 'Geodetic coordinates on WGS-84 ellipsoid (lat/lon in degrees, altitude in km)';
|
||||
|
||||
CREATE FUNCTION geodetic_lat(geodetic) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_lon(geodetic) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_alt(geodetic) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Topocentric type: observer-relative az/el/range
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION topocentric_in(cstring) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION topocentric_out(topocentric) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION topocentric_recv(internal) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION topocentric_send(topocentric) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE topocentric (
|
||||
INPUT = topocentric_in,
|
||||
OUTPUT = topocentric_out,
|
||||
RECEIVE = topocentric_recv,
|
||||
SEND = topocentric_send,
|
||||
INTERNALLENGTH = 32,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE topocentric IS 'Topocentric coordinates relative to observer (azimuth, elevation, range, range rate)';
|
||||
|
||||
CREATE FUNCTION topo_azimuth(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_azimuth(topocentric) IS 'Azimuth in degrees (0=N, 90=E, 180=S, 270=W)';
|
||||
|
||||
CREATE FUNCTION topo_elevation(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_elevation(topocentric) IS 'Elevation in degrees (0=horizon, 90=zenith)';
|
||||
|
||||
CREATE FUNCTION topo_range(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_range(topocentric) IS 'Slant range in km';
|
||||
|
||||
CREATE FUNCTION topo_range_rate(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_range_rate(topocentric) IS 'Range rate in km/s (positive = receding)';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Observer type: ground station location
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION observer_in(cstring) RETURNS observer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION observer_out(observer) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION observer_recv(internal) RETURNS observer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION observer_send(observer) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE observer (
|
||||
INPUT = observer_in,
|
||||
OUTPUT = observer_out,
|
||||
RECEIVE = observer_recv,
|
||||
SEND = observer_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE observer IS 'Ground observer location (accepts: 40.0N 105.3W 1655m or decimal degrees)';
|
||||
|
||||
CREATE FUNCTION observer_lat(observer) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_lat(observer) IS 'Latitude in degrees (positive = North)';
|
||||
|
||||
CREATE FUNCTION observer_lon(observer) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_lon(observer) IS 'Longitude in degrees (positive = East)';
|
||||
|
||||
CREATE FUNCTION observer_alt(observer) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_alt(observer) IS 'Altitude in meters above WGS-84 ellipsoid';
|
||||
|
||||
CREATE FUNCTION observer_from_geodetic(float8, float8, float8 DEFAULT 0.0) RETURNS observer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_from_geodetic(float8, float8, float8) IS
|
||||
'Construct observer from lat (deg), lon (deg), altitude (meters). Avoids text formatting round-trips.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Pass event type: satellite visibility window
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION pass_event_in(cstring) RETURNS pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION pass_event_out(pass_event) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION pass_event_recv(internal) RETURNS pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION pass_event_send(pass_event) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE pass_event (
|
||||
INPUT = pass_event_in,
|
||||
OUTPUT = pass_event_out,
|
||||
RECEIVE = pass_event_recv,
|
||||
SEND = pass_event_send,
|
||||
INTERNALLENGTH = 48,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE pass_event IS 'Satellite pass event (AOS/MAX/LOS times, max elevation, AOS/LOS azimuths)';
|
||||
|
||||
CREATE FUNCTION pass_aos_time(pass_event) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_aos_time(pass_event) IS 'Acquisition of signal time';
|
||||
|
||||
CREATE FUNCTION pass_max_el_time(pass_event) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_max_el_time(pass_event) IS 'Maximum elevation time';
|
||||
|
||||
CREATE FUNCTION pass_los_time(pass_event) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_los_time(pass_event) IS 'Loss of signal time';
|
||||
|
||||
CREATE FUNCTION pass_max_elevation(pass_event) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_max_elevation(pass_event) IS 'Maximum elevation in degrees';
|
||||
|
||||
CREATE FUNCTION pass_aos_azimuth(pass_event) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_aos_azimuth(pass_event) IS 'AOS azimuth in degrees (0=N)';
|
||||
|
||||
CREATE FUNCTION pass_los_azimuth(pass_event) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_los_azimuth(pass_event) IS 'LOS azimuth in degrees (0=N)';
|
||||
|
||||
CREATE FUNCTION pass_duration(pass_event) RETURNS interval
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_duration(pass_event) IS 'Pass duration (LOS - AOS)';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- SGP4/SDP4 propagation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION sgp4_propagate(tle, timestamptz) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sgp4_propagate(tle, timestamptz) IS
|
||||
'Propagate TLE to a point in time using SGP4 (near-earth) or SDP4 (deep-space). Returns TEME ECI position/velocity.';
|
||||
|
||||
CREATE FUNCTION sgp4_propagate_safe(tle, timestamptz) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sgp4_propagate_safe(tle, timestamptz) IS
|
||||
'Like sgp4_propagate but returns NULL on error instead of raising an exception. For batch queries with potentially invalid TLEs.';
|
||||
|
||||
CREATE FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval)
|
||||
RETURNS TABLE(t timestamptz, x float8, y float8, z float8, vx float8, vy float8, vz float8)
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
|
||||
ROWS 100;
|
||||
COMMENT ON FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval) IS
|
||||
'Propagate TLE over a time range at regular intervals. Returns time series of TEME positions.';
|
||||
|
||||
CREATE FUNCTION tle_distance(tle, tle, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_distance(tle, tle, timestamptz) IS
|
||||
'Euclidean distance in km between two TLEs at a reference time';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Coordinate transform functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION eci_to_geodetic(eci_position, timestamptz) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_to_geodetic(eci_position, timestamptz) IS
|
||||
'Convert TEME ECI position to WGS-84 geodetic coordinates at given time';
|
||||
|
||||
CREATE FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) IS
|
||||
'Convert TEME ECI position to topocentric (az/el/range) relative to observer';
|
||||
|
||||
CREATE FUNCTION subsatellite_point(tle, timestamptz) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION subsatellite_point(tle, timestamptz) IS
|
||||
'Subsatellite (nadir) point on WGS-84 ellipsoid at given time';
|
||||
|
||||
CREATE FUNCTION ground_track(tle, timestamptz, timestamptz, interval)
|
||||
RETURNS TABLE(t timestamptz, lat float8, lon float8, alt float8)
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
|
||||
ROWS 100;
|
||||
COMMENT ON FUNCTION ground_track(tle, timestamptz, timestamptz, interval) IS
|
||||
'Ground track as time series of subsatellite points (lat/lon in degrees, alt in km)';
|
||||
|
||||
CREATE FUNCTION observe(tle, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observe(tle, observer, timestamptz) IS
|
||||
'Propagate TLE and compute observer-relative look angles in one call. Returns topocentric (az/el/range/range_rate).';
|
||||
|
||||
CREATE FUNCTION observe_safe(tle, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observe_safe(tle, observer, timestamptz) IS
|
||||
'Like observe() but returns NULL on propagation error. For batch queries with potentially invalid/decayed TLEs.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Pass prediction functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION next_pass(tle, observer, timestamptz) RETURNS pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION next_pass(tle, observer, timestamptz) IS
|
||||
'Find the next satellite pass over observer (searches up to 7 days ahead)';
|
||||
|
||||
CREATE FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8 DEFAULT 0.0)
|
||||
RETURNS SETOF pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE
|
||||
ROWS 10;
|
||||
COMMENT ON FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8) IS
|
||||
'Predict all satellite passes over observer in time window. Optional min_elevation in degrees.';
|
||||
|
||||
CREATE FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) RETURNS boolean
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) IS
|
||||
'True if any pass occurs over observer in the time window';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- GiST operator support functions
|
||||
-- ============================================================
|
||||
|
||||
-- Overlap operator: do orbital keys overlap in altitude AND inclination?
|
||||
CREATE FUNCTION tle_overlap(tle, tle) RETURNS boolean
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
-- Altitude distance operator (altitude-only, for KNN ordering)
|
||||
CREATE FUNCTION tle_alt_distance(tle, tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE OPERATOR && (
|
||||
LEFTARG = tle,
|
||||
RIGHTARG = tle,
|
||||
FUNCTION = tle_overlap,
|
||||
COMMUTATOR = &&,
|
||||
RESTRICT = areasel,
|
||||
JOIN = areajoinsel
|
||||
);
|
||||
|
||||
COMMENT ON OPERATOR && (tle, tle) IS 'Orbital key overlap (altitude band AND inclination range) — necessary condition for conjunction';
|
||||
|
||||
CREATE OPERATOR <-> (
|
||||
LEFTARG = tle,
|
||||
RIGHTARG = tle,
|
||||
FUNCTION = tle_alt_distance,
|
||||
COMMUTATOR = <->
|
||||
);
|
||||
|
||||
COMMENT ON OPERATOR <-> (tle, tle) IS 'Minimum altitude-band separation in km (0 if overlapping). Altitude-only — does not account for inclination. Use && for 2-D filtering.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- GiST operator class for 2-D orbital indexing (altitude + inclination)
|
||||
-- ============================================================
|
||||
|
||||
-- GiST internal support functions
|
||||
CREATE FUNCTION gist_tle_compress(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_decompress(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_consistent(internal, tle, smallint, oid, internal) RETURNS boolean
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_union(internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_penalty(internal, internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_picksplit(internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_same(internal, internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_distance(internal, tle, smallint, oid, internal) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE OPERATOR CLASS tle_ops
|
||||
DEFAULT FOR TYPE tle USING gist AS
|
||||
OPERATOR 3 && ,
|
||||
OPERATOR 15 <-> (tle, tle) FOR ORDER BY float_ops,
|
||||
FUNCTION 1 gist_tle_consistent(internal, tle, smallint, oid, internal),
|
||||
FUNCTION 2 gist_tle_union(internal, internal),
|
||||
FUNCTION 3 gist_tle_compress(internal),
|
||||
FUNCTION 4 gist_tle_decompress(internal),
|
||||
FUNCTION 5 gist_tle_penalty(internal, internal, internal),
|
||||
FUNCTION 6 gist_tle_picksplit(internal, internal),
|
||||
FUNCTION 7 gist_tle_same(internal, internal, internal),
|
||||
FUNCTION 8 gist_tle_distance(internal, tle, smallint, oid, internal);
|
||||
-- pg_orrery 0.1.0 -> 0.2.0 migration
|
||||
--
|
||||
-- Phase 1: Stars, comets, and Keplerian propagation.
|
||||
-- Adds heliocentric type, star observation, and two-body propagation.
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Heliocentric type: ecliptic J2000 position in AU
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE heliocentric;
|
||||
|
||||
CREATE FUNCTION heliocentric_in(cstring) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_out(heliocentric) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_recv(internal) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_send(heliocentric) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE heliocentric (
|
||||
INPUT = heliocentric_in,
|
||||
OUTPUT = heliocentric_out,
|
||||
RECEIVE = heliocentric_recv,
|
||||
SEND = heliocentric_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE heliocentric IS 'Heliocentric position in ecliptic J2000 frame (x, y, z in AU)';
|
||||
|
||||
CREATE FUNCTION helio_x(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_x(heliocentric) IS 'X component in AU (ecliptic J2000, vernal equinox direction)';
|
||||
|
||||
CREATE FUNCTION helio_y(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_y(heliocentric) IS 'Y component in AU (ecliptic J2000)';
|
||||
|
||||
CREATE FUNCTION helio_z(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_z(heliocentric) IS 'Z component in AU (ecliptic J2000, north ecliptic pole)';
|
||||
|
||||
CREATE FUNCTION helio_distance(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_distance(heliocentric) IS 'Heliocentric distance in AU';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Star observation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION star_observe(float8, float8, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION star_observe(float8, float8, observer, timestamptz) IS
|
||||
'Observe a star from (ra_hours J2000, dec_degrees J2000, observer, time). Returns topocentric az/el. Range is 0 (infinite distance).';
|
||||
|
||||
CREATE FUNCTION star_observe_safe(float8, float8, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION star_observe_safe(float8, float8, observer, timestamptz) IS
|
||||
'Like star_observe but returns NULL on invalid inputs. For batch queries over star catalogs.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Keplerian propagation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION kepler_propagate(
|
||||
q_au float8, eccentricity float8,
|
||||
inclination_deg float8, arg_perihelion_deg float8,
|
||||
long_asc_node_deg float8, perihelion_jd float8,
|
||||
t timestamptz
|
||||
) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION kepler_propagate(float8, float8, float8, float8, float8, float8, timestamptz) IS
|
||||
'Two-body Keplerian propagation from classical orbital elements. Returns heliocentric ecliptic J2000 position in AU. Handles elliptic, parabolic, and hyperbolic orbits.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Comet observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION comet_observe(
|
||||
q_au float8, eccentricity float8,
|
||||
inclination_deg float8, arg_perihelion_deg float8,
|
||||
long_asc_node_deg float8, perihelion_jd float8,
|
||||
earth_x_au float8, earth_y_au float8, earth_z_au float8,
|
||||
obs observer, t timestamptz
|
||||
) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION comet_observe(float8, float8, float8, float8, float8, float8, float8, float8, float8, observer, timestamptz) IS
|
||||
'Observe a comet/asteroid from orbital elements. Requires Earth heliocentric ecliptic J2000 position (AU). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 2: VSOP87 planets, ELP82B Moon, Sun observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION planet_heliocentric(int4, timestamptz) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_heliocentric(int4, timestamptz) IS
|
||||
'VSOP87 heliocentric ecliptic J2000 position (AU). Body IDs: 0=Sun, 1=Mercury, ..., 8=Neptune.';
|
||||
|
||||
CREATE FUNCTION planet_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_observe(int4, observer, timestamptz) IS
|
||||
'Observe a planet from (body_id 1-8, observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
CREATE FUNCTION sun_observe(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_observe(observer, timestamptz) IS
|
||||
'Observe the Sun from (observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
CREATE FUNCTION moon_observe(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_observe(observer, timestamptz) IS
|
||||
'Observe the Moon via ELP2000-82B from (observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 3: Planetary moon observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION galilean_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION galilean_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Galilean moon of Jupiter via L1.2 theory. Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.';
|
||||
|
||||
CREATE FUNCTION saturn_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION saturn_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Saturn moon via TASS 1.7. Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion.';
|
||||
|
||||
CREATE FUNCTION uranus_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION uranus_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Uranus moon via GUST86. Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon.';
|
||||
|
||||
CREATE FUNCTION mars_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION mars_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Mars moon via MarsSat. Body IDs: 0=Phobos, 1=Deimos.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 3: Jupiter decametric radio burst prediction
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION io_phase_angle(timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION io_phase_angle(timestamptz) IS
|
||||
'Io orbital phase angle in degrees [0,360). 0=superior conjunction (behind Jupiter). Standard Radio JOVE convention.';
|
||||
|
||||
CREATE FUNCTION jupiter_cml(observer, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION jupiter_cml(observer, timestamptz) IS
|
||||
'Jupiter Central Meridian Longitude, System III (1965.0), in degrees [0,360). Light-time corrected.';
|
||||
|
||||
CREATE FUNCTION jupiter_burst_probability(float8, float8) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION jupiter_burst_probability(float8, float8) IS
|
||||
'Estimated Jupiter decametric burst probability (0-1) from (io_phase_deg, cml_deg). Based on Carr et al. (1983) source regions A, B, C, D.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 4: Interplanetary transfer orbits (Lambert solver)
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION lambert_transfer(
|
||||
dep_body_id int4, arr_body_id int4,
|
||||
dep_time timestamptz, arr_time timestamptz,
|
||||
OUT c3_departure float8, OUT c3_arrival float8,
|
||||
OUT v_inf_departure float8, OUT v_inf_arrival float8,
|
||||
OUT tof_days float8, OUT transfer_sma float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_transfer(int4, int4, timestamptz, timestamptz) IS
|
||||
'Solve Lambert transfer between two planets. Returns C3 (km^2/s^2), v_infinity (km/s), TOF (days), SMA (AU). Body IDs 1-8.';
|
||||
|
||||
CREATE FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) IS
|
||||
'Departure C3 (km^2/s^2) for a Lambert transfer. Returns NULL if solver fails. For pork chop plots.';
|
||||
64
sql/pg_orrery--0.3.0--0.4.0.sql
Normal file
64
sql/pg_orrery--0.3.0--0.4.0.sql
Normal file
@ -0,0 +1,64 @@
|
||||
-- pg_orrery 0.3.0 -> 0.4.0 migration
|
||||
--
|
||||
-- Adds observation-to-TLE fitting via batch weighted least-squares
|
||||
-- differential correction (Vallado & Crawford 2008, AIAA 2008-6770).
|
||||
-- Uses equinoctial elements internally for singularity-free optimization.
|
||||
-- LAPACK dgelss_() for SVD solve.
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 6: Orbit determination (TLE fitting from observations)
|
||||
-- ============================================================
|
||||
|
||||
-- Fit TLE from ECI position/velocity ephemeris
|
||||
|
||||
CREATE FUNCTION tle_from_eci(
|
||||
positions eci_position[],
|
||||
times timestamptz[],
|
||||
seed tle DEFAULT NULL,
|
||||
fit_bstar boolean DEFAULT false,
|
||||
max_iter int4 DEFAULT 15,
|
||||
OUT fitted_tle tle,
|
||||
OUT iterations int4,
|
||||
OUT rms_final float8,
|
||||
OUT rms_initial float8,
|
||||
OUT status text
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4) IS
|
||||
'Fit a TLE from ECI position/velocity observations via differential correction. Returns fitted TLE, iteration count, RMS residuals, and convergence status. Requires >= 6 observations.';
|
||||
|
||||
-- Fit TLE from topocentric observations (az/el/range)
|
||||
|
||||
CREATE FUNCTION tle_from_topocentric(
|
||||
observations topocentric[],
|
||||
times timestamptz[],
|
||||
obs observer,
|
||||
seed tle DEFAULT NULL,
|
||||
fit_bstar boolean DEFAULT false,
|
||||
max_iter int4 DEFAULT 15,
|
||||
OUT fitted_tle tle,
|
||||
OUT iterations int4,
|
||||
OUT rms_final float8,
|
||||
OUT rms_initial float8,
|
||||
OUT status text
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4) IS
|
||||
'Fit a TLE from topocentric (az/el/range) observations via differential correction. Requires seed TLE and >= 6 observations.';
|
||||
|
||||
-- Per-observation residuals diagnostic
|
||||
|
||||
CREATE FUNCTION tle_fit_residuals(
|
||||
fitted tle,
|
||||
positions eci_position[],
|
||||
times timestamptz[]
|
||||
) RETURNS TABLE (
|
||||
t timestamptz,
|
||||
dx_km float8,
|
||||
dy_km float8,
|
||||
dz_km float8,
|
||||
pos_err_km float8
|
||||
)
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_fit_residuals(tle, eci_position[], timestamptz[]) IS
|
||||
'Compute per-observation position residuals (km) between a TLE and ECI observations. Useful for fit quality assessment.';
|
||||
790
sql/pg_orrery--0.3.0.sql
Normal file
790
sql/pg_orrery--0.3.0.sql
Normal file
@ -0,0 +1,790 @@
|
||||
-- pg_orrery -- Orbital mechanics types and functions for PostgreSQL
|
||||
--
|
||||
-- Types: tle, eci_position, geodetic, topocentric, observer, pass_event
|
||||
-- Provides SGP4/SDP4 propagation, coordinate transforms, pass prediction,
|
||||
-- and GiST indexing on altitude bands for conjunction screening.
|
||||
--
|
||||
-- All propagation uses WGS-72 constants (matching TLE mean element fitting).
|
||||
-- Coordinate output uses WGS-84 (matching modern geodetic standards).
|
||||
|
||||
-- ============================================================
|
||||
-- Shell types (forward declarations)
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE tle;
|
||||
CREATE TYPE eci_position;
|
||||
CREATE TYPE geodetic;
|
||||
CREATE TYPE topocentric;
|
||||
CREATE TYPE observer;
|
||||
CREATE TYPE pass_event;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- TLE type: Two-Line Element set
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION tle_in(cstring) RETURNS tle
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION tle_out(tle) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION tle_recv(internal) RETURNS tle
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION tle_send(tle) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE tle (
|
||||
INPUT = tle_in,
|
||||
OUTPUT = tle_out,
|
||||
RECEIVE = tle_recv,
|
||||
SEND = tle_send,
|
||||
INTERNALLENGTH = 112,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE tle IS 'Two-Line Element set — parsed mean orbital elements for SGP4/SDP4 propagation';
|
||||
|
||||
-- TLE accessor functions
|
||||
|
||||
CREATE FUNCTION tle_epoch(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_epoch(tle) IS 'TLE epoch as Julian date (UTC)';
|
||||
|
||||
CREATE FUNCTION tle_norad_id(tle) RETURNS int4
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_norad_id(tle) IS 'NORAD catalog number';
|
||||
|
||||
CREATE FUNCTION tle_inclination(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_inclination(tle) IS 'Orbital inclination in degrees';
|
||||
|
||||
CREATE FUNCTION tle_eccentricity(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_eccentricity(tle) IS 'Orbital eccentricity (dimensionless)';
|
||||
|
||||
CREATE FUNCTION tle_raan(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_raan(tle) IS 'Right ascension of ascending node in degrees';
|
||||
|
||||
CREATE FUNCTION tle_arg_perigee(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_arg_perigee(tle) IS 'Argument of perigee in degrees';
|
||||
|
||||
CREATE FUNCTION tle_mean_anomaly(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_mean_anomaly(tle) IS 'Mean anomaly in degrees';
|
||||
|
||||
CREATE FUNCTION tle_mean_motion(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_mean_motion(tle) IS 'Mean motion in revolutions per day';
|
||||
|
||||
CREATE FUNCTION tle_bstar(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_bstar(tle) IS 'B* drag coefficient (1/earth-radii)';
|
||||
|
||||
CREATE FUNCTION tle_period(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_period(tle) IS 'Orbital period in minutes';
|
||||
|
||||
CREATE FUNCTION tle_age(tle, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_age(tle, timestamptz) IS 'TLE age in days (positive = past epoch, negative = before epoch)';
|
||||
|
||||
CREATE FUNCTION tle_perigee(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_perigee(tle) IS 'Perigee altitude in km above WGS-72 ellipsoid';
|
||||
|
||||
CREATE FUNCTION tle_apogee(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_apogee(tle) IS 'Apogee altitude in km above WGS-72 ellipsoid';
|
||||
|
||||
CREATE FUNCTION tle_intl_desig(tle) RETURNS text
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_intl_desig(tle) IS 'International designator (COSPAR ID)';
|
||||
|
||||
CREATE FUNCTION tle_from_lines(text, text) RETURNS tle
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_lines(text, text) IS
|
||||
'Construct TLE from separate line1/line2 text columns';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- ECI position type: True Equator Mean Equinox (TEME) frame
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION eci_in(cstring) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_out(eci_position) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_recv(internal) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_send(eci_position) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE eci_position (
|
||||
INPUT = eci_in,
|
||||
OUTPUT = eci_out,
|
||||
RECEIVE = eci_recv,
|
||||
SEND = eci_send,
|
||||
INTERNALLENGTH = 48,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE eci_position IS 'Earth-Centered Inertial position and velocity in TEME frame (km, km/s)';
|
||||
|
||||
-- ECI accessor functions
|
||||
|
||||
CREATE FUNCTION eci_x(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_y(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_z(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_vx(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_vy(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_vz(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_speed(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_speed(eci_position) IS 'Velocity magnitude in km/s';
|
||||
|
||||
CREATE FUNCTION eci_altitude(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_altitude(eci_position) IS 'Approximate geocentric altitude in km (radius - WGS72_AE)';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Geodetic type: WGS-84 latitude/longitude/altitude
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION geodetic_in(cstring) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_out(geodetic) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_recv(internal) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_send(geodetic) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE geodetic (
|
||||
INPUT = geodetic_in,
|
||||
OUTPUT = geodetic_out,
|
||||
RECEIVE = geodetic_recv,
|
||||
SEND = geodetic_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE geodetic IS 'Geodetic coordinates on WGS-84 ellipsoid (lat/lon in degrees, altitude in km)';
|
||||
|
||||
CREATE FUNCTION geodetic_lat(geodetic) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_lon(geodetic) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_alt(geodetic) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Topocentric type: observer-relative az/el/range
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION topocentric_in(cstring) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION topocentric_out(topocentric) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION topocentric_recv(internal) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION topocentric_send(topocentric) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE topocentric (
|
||||
INPUT = topocentric_in,
|
||||
OUTPUT = topocentric_out,
|
||||
RECEIVE = topocentric_recv,
|
||||
SEND = topocentric_send,
|
||||
INTERNALLENGTH = 32,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE topocentric IS 'Topocentric coordinates relative to observer (azimuth, elevation, range, range rate)';
|
||||
|
||||
CREATE FUNCTION topo_azimuth(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_azimuth(topocentric) IS 'Azimuth in degrees (0=N, 90=E, 180=S, 270=W)';
|
||||
|
||||
CREATE FUNCTION topo_elevation(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_elevation(topocentric) IS 'Elevation in degrees (0=horizon, 90=zenith)';
|
||||
|
||||
CREATE FUNCTION topo_range(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_range(topocentric) IS 'Slant range in km';
|
||||
|
||||
CREATE FUNCTION topo_range_rate(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_range_rate(topocentric) IS 'Range rate in km/s (positive = receding)';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Observer type: ground station location
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION observer_in(cstring) RETURNS observer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION observer_out(observer) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION observer_recv(internal) RETURNS observer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION observer_send(observer) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE observer (
|
||||
INPUT = observer_in,
|
||||
OUTPUT = observer_out,
|
||||
RECEIVE = observer_recv,
|
||||
SEND = observer_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE observer IS 'Ground observer location (accepts: 40.0N 105.3W 1655m or decimal degrees)';
|
||||
|
||||
CREATE FUNCTION observer_lat(observer) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_lat(observer) IS 'Latitude in degrees (positive = North)';
|
||||
|
||||
CREATE FUNCTION observer_lon(observer) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_lon(observer) IS 'Longitude in degrees (positive = East)';
|
||||
|
||||
CREATE FUNCTION observer_alt(observer) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_alt(observer) IS 'Altitude in meters above WGS-84 ellipsoid';
|
||||
|
||||
CREATE FUNCTION observer_from_geodetic(float8, float8, float8 DEFAULT 0.0) RETURNS observer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_from_geodetic(float8, float8, float8) IS
|
||||
'Construct observer from lat (deg), lon (deg), altitude (meters). Avoids text formatting round-trips.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Pass event type: satellite visibility window
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION pass_event_in(cstring) RETURNS pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION pass_event_out(pass_event) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION pass_event_recv(internal) RETURNS pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION pass_event_send(pass_event) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE pass_event (
|
||||
INPUT = pass_event_in,
|
||||
OUTPUT = pass_event_out,
|
||||
RECEIVE = pass_event_recv,
|
||||
SEND = pass_event_send,
|
||||
INTERNALLENGTH = 48,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE pass_event IS 'Satellite pass event (AOS/MAX/LOS times, max elevation, AOS/LOS azimuths)';
|
||||
|
||||
CREATE FUNCTION pass_aos_time(pass_event) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_aos_time(pass_event) IS 'Acquisition of signal time';
|
||||
|
||||
CREATE FUNCTION pass_max_el_time(pass_event) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_max_el_time(pass_event) IS 'Maximum elevation time';
|
||||
|
||||
CREATE FUNCTION pass_los_time(pass_event) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_los_time(pass_event) IS 'Loss of signal time';
|
||||
|
||||
CREATE FUNCTION pass_max_elevation(pass_event) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_max_elevation(pass_event) IS 'Maximum elevation in degrees';
|
||||
|
||||
CREATE FUNCTION pass_aos_azimuth(pass_event) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_aos_azimuth(pass_event) IS 'AOS azimuth in degrees (0=N)';
|
||||
|
||||
CREATE FUNCTION pass_los_azimuth(pass_event) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_los_azimuth(pass_event) IS 'LOS azimuth in degrees (0=N)';
|
||||
|
||||
CREATE FUNCTION pass_duration(pass_event) RETURNS interval
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_duration(pass_event) IS 'Pass duration (LOS - AOS)';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- SGP4/SDP4 propagation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION sgp4_propagate(tle, timestamptz) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sgp4_propagate(tle, timestamptz) IS
|
||||
'Propagate TLE to a point in time using SGP4 (near-earth) or SDP4 (deep-space). Returns TEME ECI position/velocity.';
|
||||
|
||||
CREATE FUNCTION sgp4_propagate_safe(tle, timestamptz) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sgp4_propagate_safe(tle, timestamptz) IS
|
||||
'Like sgp4_propagate but returns NULL on error instead of raising an exception. For batch queries with potentially invalid TLEs.';
|
||||
|
||||
CREATE FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval)
|
||||
RETURNS TABLE(t timestamptz, x float8, y float8, z float8, vx float8, vy float8, vz float8)
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
|
||||
ROWS 100;
|
||||
COMMENT ON FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval) IS
|
||||
'Propagate TLE over a time range at regular intervals. Returns time series of TEME positions.';
|
||||
|
||||
CREATE FUNCTION tle_distance(tle, tle, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_distance(tle, tle, timestamptz) IS
|
||||
'Euclidean distance in km between two TLEs at a reference time';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Coordinate transform functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION eci_to_geodetic(eci_position, timestamptz) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_to_geodetic(eci_position, timestamptz) IS
|
||||
'Convert TEME ECI position to WGS-84 geodetic coordinates at given time';
|
||||
|
||||
CREATE FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) IS
|
||||
'Convert TEME ECI position to topocentric (az/el/range) relative to observer';
|
||||
|
||||
CREATE FUNCTION subsatellite_point(tle, timestamptz) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION subsatellite_point(tle, timestamptz) IS
|
||||
'Subsatellite (nadir) point on WGS-84 ellipsoid at given time';
|
||||
|
||||
CREATE FUNCTION ground_track(tle, timestamptz, timestamptz, interval)
|
||||
RETURNS TABLE(t timestamptz, lat float8, lon float8, alt float8)
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
|
||||
ROWS 100;
|
||||
COMMENT ON FUNCTION ground_track(tle, timestamptz, timestamptz, interval) IS
|
||||
'Ground track as time series of subsatellite points (lat/lon in degrees, alt in km)';
|
||||
|
||||
CREATE FUNCTION observe(tle, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observe(tle, observer, timestamptz) IS
|
||||
'Propagate TLE and compute observer-relative look angles in one call. Returns topocentric (az/el/range/range_rate).';
|
||||
|
||||
CREATE FUNCTION observe_safe(tle, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observe_safe(tle, observer, timestamptz) IS
|
||||
'Like observe() but returns NULL on propagation error. For batch queries with potentially invalid/decayed TLEs.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Pass prediction functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION next_pass(tle, observer, timestamptz) RETURNS pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION next_pass(tle, observer, timestamptz) IS
|
||||
'Find the next satellite pass over observer (searches up to 7 days ahead)';
|
||||
|
||||
CREATE FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8 DEFAULT 0.0)
|
||||
RETURNS SETOF pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE
|
||||
ROWS 10;
|
||||
COMMENT ON FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8) IS
|
||||
'Predict all satellite passes over observer in time window. Optional min_elevation in degrees.';
|
||||
|
||||
CREATE FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) RETURNS boolean
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) IS
|
||||
'True if any pass occurs over observer in the time window';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- GiST operator support functions
|
||||
-- ============================================================
|
||||
|
||||
-- Overlap operator: do orbital keys overlap in altitude AND inclination?
|
||||
CREATE FUNCTION tle_overlap(tle, tle) RETURNS boolean
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
-- Altitude distance operator (altitude-only, for KNN ordering)
|
||||
CREATE FUNCTION tle_alt_distance(tle, tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE OPERATOR && (
|
||||
LEFTARG = tle,
|
||||
RIGHTARG = tle,
|
||||
FUNCTION = tle_overlap,
|
||||
COMMUTATOR = &&,
|
||||
RESTRICT = areasel,
|
||||
JOIN = areajoinsel
|
||||
);
|
||||
|
||||
COMMENT ON OPERATOR && (tle, tle) IS 'Orbital key overlap (altitude band AND inclination range) — necessary condition for conjunction';
|
||||
|
||||
CREATE OPERATOR <-> (
|
||||
LEFTARG = tle,
|
||||
RIGHTARG = tle,
|
||||
FUNCTION = tle_alt_distance,
|
||||
COMMUTATOR = <->
|
||||
);
|
||||
|
||||
COMMENT ON OPERATOR <-> (tle, tle) IS 'Minimum altitude-band separation in km (0 if overlapping). Altitude-only — does not account for inclination. Use && for 2-D filtering.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- GiST operator class for 2-D orbital indexing (altitude + inclination)
|
||||
-- ============================================================
|
||||
|
||||
-- GiST internal support functions
|
||||
CREATE FUNCTION gist_tle_compress(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_decompress(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_consistent(internal, tle, smallint, oid, internal) RETURNS boolean
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_union(internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_penalty(internal, internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_picksplit(internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_same(internal, internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_distance(internal, tle, smallint, oid, internal) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE OPERATOR CLASS tle_ops
|
||||
DEFAULT FOR TYPE tle USING gist AS
|
||||
OPERATOR 3 && ,
|
||||
OPERATOR 15 <-> (tle, tle) FOR ORDER BY float_ops,
|
||||
FUNCTION 1 gist_tle_consistent(internal, tle, smallint, oid, internal),
|
||||
FUNCTION 2 gist_tle_union(internal, internal),
|
||||
FUNCTION 3 gist_tle_compress(internal),
|
||||
FUNCTION 4 gist_tle_decompress(internal),
|
||||
FUNCTION 5 gist_tle_penalty(internal, internal, internal),
|
||||
FUNCTION 6 gist_tle_picksplit(internal, internal),
|
||||
FUNCTION 7 gist_tle_same(internal, internal, internal),
|
||||
FUNCTION 8 gist_tle_distance(internal, tle, smallint, oid, internal);
|
||||
-- pg_orrery 0.1.0 -> 0.2.0 migration
|
||||
--
|
||||
-- Phase 1: Stars, comets, and Keplerian propagation.
|
||||
-- Adds heliocentric type, star observation, and two-body propagation.
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Heliocentric type: ecliptic J2000 position in AU
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE heliocentric;
|
||||
|
||||
CREATE FUNCTION heliocentric_in(cstring) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_out(heliocentric) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_recv(internal) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_send(heliocentric) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE heliocentric (
|
||||
INPUT = heliocentric_in,
|
||||
OUTPUT = heliocentric_out,
|
||||
RECEIVE = heliocentric_recv,
|
||||
SEND = heliocentric_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE heliocentric IS 'Heliocentric position in ecliptic J2000 frame (x, y, z in AU)';
|
||||
|
||||
CREATE FUNCTION helio_x(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_x(heliocentric) IS 'X component in AU (ecliptic J2000, vernal equinox direction)';
|
||||
|
||||
CREATE FUNCTION helio_y(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_y(heliocentric) IS 'Y component in AU (ecliptic J2000)';
|
||||
|
||||
CREATE FUNCTION helio_z(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_z(heliocentric) IS 'Z component in AU (ecliptic J2000, north ecliptic pole)';
|
||||
|
||||
CREATE FUNCTION helio_distance(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_distance(heliocentric) IS 'Heliocentric distance in AU';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Star observation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION star_observe(float8, float8, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION star_observe(float8, float8, observer, timestamptz) IS
|
||||
'Observe a star from (ra_hours J2000, dec_degrees J2000, observer, time). Returns topocentric az/el. Range is 0 (infinite distance).';
|
||||
|
||||
CREATE FUNCTION star_observe_safe(float8, float8, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION star_observe_safe(float8, float8, observer, timestamptz) IS
|
||||
'Like star_observe but returns NULL on invalid inputs. For batch queries over star catalogs.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Keplerian propagation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION kepler_propagate(
|
||||
q_au float8, eccentricity float8,
|
||||
inclination_deg float8, arg_perihelion_deg float8,
|
||||
long_asc_node_deg float8, perihelion_jd float8,
|
||||
t timestamptz
|
||||
) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION kepler_propagate(float8, float8, float8, float8, float8, float8, timestamptz) IS
|
||||
'Two-body Keplerian propagation from classical orbital elements. Returns heliocentric ecliptic J2000 position in AU. Handles elliptic, parabolic, and hyperbolic orbits.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Comet observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION comet_observe(
|
||||
q_au float8, eccentricity float8,
|
||||
inclination_deg float8, arg_perihelion_deg float8,
|
||||
long_asc_node_deg float8, perihelion_jd float8,
|
||||
earth_x_au float8, earth_y_au float8, earth_z_au float8,
|
||||
obs observer, t timestamptz
|
||||
) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION comet_observe(float8, float8, float8, float8, float8, float8, float8, float8, float8, observer, timestamptz) IS
|
||||
'Observe a comet/asteroid from orbital elements. Requires Earth heliocentric ecliptic J2000 position (AU). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 2: VSOP87 planets, ELP82B Moon, Sun observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION planet_heliocentric(int4, timestamptz) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_heliocentric(int4, timestamptz) IS
|
||||
'VSOP87 heliocentric ecliptic J2000 position (AU). Body IDs: 0=Sun, 1=Mercury, ..., 8=Neptune.';
|
||||
|
||||
CREATE FUNCTION planet_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_observe(int4, observer, timestamptz) IS
|
||||
'Observe a planet from (body_id 1-8, observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
CREATE FUNCTION sun_observe(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_observe(observer, timestamptz) IS
|
||||
'Observe the Sun from (observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
CREATE FUNCTION moon_observe(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_observe(observer, timestamptz) IS
|
||||
'Observe the Moon via ELP2000-82B from (observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 3: Planetary moon observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION galilean_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION galilean_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Galilean moon of Jupiter via L1.2 theory. Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.';
|
||||
|
||||
CREATE FUNCTION saturn_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION saturn_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Saturn moon via TASS 1.7. Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion.';
|
||||
|
||||
CREATE FUNCTION uranus_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION uranus_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Uranus moon via GUST86. Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon.';
|
||||
|
||||
CREATE FUNCTION mars_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION mars_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Mars moon via MarsSat. Body IDs: 0=Phobos, 1=Deimos.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 3: Jupiter decametric radio burst prediction
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION io_phase_angle(timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION io_phase_angle(timestamptz) IS
|
||||
'Io orbital phase angle in degrees [0,360). 0=superior conjunction (behind Jupiter). Standard Radio JOVE convention.';
|
||||
|
||||
CREATE FUNCTION jupiter_cml(observer, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION jupiter_cml(observer, timestamptz) IS
|
||||
'Jupiter Central Meridian Longitude, System III (1965.0), in degrees [0,360). Light-time corrected.';
|
||||
|
||||
CREATE FUNCTION jupiter_burst_probability(float8, float8) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION jupiter_burst_probability(float8, float8) IS
|
||||
'Estimated Jupiter decametric burst probability (0-1) from (io_phase_deg, cml_deg). Based on Carr et al. (1983) source regions A, B, C, D.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 4: Interplanetary transfer orbits (Lambert solver)
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION lambert_transfer(
|
||||
dep_body_id int4, arr_body_id int4,
|
||||
dep_time timestamptz, arr_time timestamptz,
|
||||
OUT c3_departure float8, OUT c3_arrival float8,
|
||||
OUT v_inf_departure float8, OUT v_inf_arrival float8,
|
||||
OUT tof_days float8, OUT transfer_sma float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_transfer(int4, int4, timestamptz, timestamptz) IS
|
||||
'Solve Lambert transfer between two planets. Returns C3 (km^2/s^2), v_infinity (km/s), TOF (days), SMA (AU). Body IDs 1-8.';
|
||||
|
||||
CREATE FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) IS
|
||||
'Departure C3 (km^2/s^2) for a Lambert transfer. Returns NULL if solver fails. For pork chop plots.';
|
||||
-- pg_orrery 0.2.0 -> 0.3.0 migration
|
||||
--
|
||||
-- Adds optional JPL DE440/441 ephemeris functions.
|
||||
-- Existing VSOP87/ELP2000-82B functions are unchanged (still IMMUTABLE).
|
||||
-- New _de() functions are STABLE (depend on external DE binary file).
|
||||
-- When DE is unavailable, _de() functions fall back to VSOP87 silently.
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 5: DE ephemeris functions (optional high-precision)
|
||||
-- ============================================================
|
||||
|
||||
-- Planet observation with DE ephemeris
|
||||
|
||||
CREATE FUNCTION planet_heliocentric_de(int4, timestamptz) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_heliocentric_de(int4, timestamptz) IS
|
||||
'Heliocentric ecliptic J2000 position via JPL DE (sub-arcsecond). Falls back to VSOP87 if DE unavailable. Body IDs: 0=Sun, 1-8=Mercury-Neptune.';
|
||||
|
||||
CREATE FUNCTION planet_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe planet via JPL DE. Falls back to VSOP87. Body IDs: 1-8 (Mercury-Neptune).';
|
||||
|
||||
CREATE FUNCTION sun_observe_de(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_observe_de(observer, timestamptz) IS
|
||||
'Observe Sun via JPL DE. Falls back to VSOP87.';
|
||||
|
||||
CREATE FUNCTION moon_observe_de(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_observe_de(observer, timestamptz) IS
|
||||
'Observe Moon via JPL DE. Falls back to ELP2000-82B.';
|
||||
|
||||
|
||||
-- Lambert transfer with DE positions
|
||||
|
||||
CREATE FUNCTION lambert_transfer_de(
|
||||
dep_body_id int4, arr_body_id int4,
|
||||
dep_time timestamptz, arr_time timestamptz,
|
||||
OUT c3_departure float8, OUT c3_arrival float8,
|
||||
OUT v_inf_departure float8, OUT v_inf_arrival float8,
|
||||
OUT tof_days float8, OUT transfer_sma float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_transfer_de(int4, int4, timestamptz, timestamptz) IS
|
||||
'Lambert transfer via JPL DE positions. Falls back to VSOP87. Body IDs 1-8.';
|
||||
|
||||
CREATE FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) IS
|
||||
'Departure C3 via JPL DE. Falls back to VSOP87. For pork chop plots.';
|
||||
|
||||
|
||||
-- Planetary moon observation with DE parent positions
|
||||
|
||||
CREATE FUNCTION galilean_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION galilean_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Galilean moon with JPL DE parent position. L1.2 moon offsets. Body IDs: 0-3 (Io-Callisto).';
|
||||
|
||||
CREATE FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Saturn moon with JPL DE parent position. TASS 1.7 moon offsets. Body IDs: 0-7 (Mimas-Hyperion).';
|
||||
|
||||
CREATE FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Uranus moon with JPL DE parent position. GUST86 moon offsets. Body IDs: 0-4 (Miranda-Oberon).';
|
||||
|
||||
CREATE FUNCTION mars_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION mars_moon_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Mars moon with JPL DE parent position. MarsSat moon offsets. Body IDs: 0-1 (Phobos-Deimos).';
|
||||
|
||||
|
||||
-- Diagnostic function
|
||||
|
||||
CREATE FUNCTION pg_orrery_ephemeris_info(
|
||||
OUT provider text, OUT file_path text,
|
||||
OUT start_jd float8, OUT end_jd float8,
|
||||
OUT version int4, OUT au_km float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pg_orrery_ephemeris_info() IS
|
||||
'Returns current ephemeris provider status: VSOP87 or JPL_DE with file path, JD range, version, and AU value.';
|
||||
62
sql/pg_orrery--0.4.0--0.5.0.sql
Normal file
62
sql/pg_orrery--0.4.0--0.5.0.sql
Normal file
@ -0,0 +1,62 @@
|
||||
-- pg_orrery 0.4.0 -> 0.5.0 migration
|
||||
--
|
||||
-- Adds multi-observer support, IOD bootstrap (seed-free fitting),
|
||||
-- and covariance output for uncertainty estimation.
|
||||
--
|
||||
-- Covariance changes the return type of tle_from_eci and
|
||||
-- tle_from_topocentric (5 → 8 OUT params), which requires
|
||||
-- DROP + re-CREATE.
|
||||
|
||||
-- ============================================================
|
||||
-- Drop old 5-column OD functions
|
||||
-- ============================================================
|
||||
|
||||
DROP FUNCTION IF EXISTS tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4);
|
||||
DROP FUNCTION IF EXISTS tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4);
|
||||
|
||||
-- ============================================================
|
||||
-- Re-create with 8-column output (adds covariance)
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION tle_from_eci(
|
||||
positions eci_position[], times timestamptz[],
|
||||
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||
max_iter int4 DEFAULT 15,
|
||||
OUT fitted_tle tle, OUT iterations int4,
|
||||
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4) IS
|
||||
'Fit a TLE from ECI position/velocity observations via differential correction. Returns fitted TLE, iteration count, RMS residuals, convergence status, condition number, and formal covariance matrix.';
|
||||
|
||||
CREATE FUNCTION tle_from_topocentric(
|
||||
observations topocentric[], times timestamptz[],
|
||||
obs observer,
|
||||
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||
max_iter int4 DEFAULT 15,
|
||||
OUT fitted_tle tle, OUT iterations int4,
|
||||
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4) IS
|
||||
'Fit a TLE from topocentric (az/el/range) observations via differential correction. Returns fitted TLE, RMS residuals, convergence status, condition number, and formal covariance matrix.';
|
||||
|
||||
-- ============================================================
|
||||
-- Multi-observer topocentric fitting (new overload)
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION tle_from_topocentric(
|
||||
observations topocentric[], times timestamptz[],
|
||||
observers observer[], observer_ids int4[],
|
||||
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||
max_iter int4 DEFAULT 15,
|
||||
OUT fitted_tle tle, OUT iterations int4,
|
||||
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME', 'tle_from_topocentric_multi'
|
||||
LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer[], int4[], tle, boolean, int4) IS
|
||||
'Fit a TLE from topocentric observations collected by multiple ground stations. observer_ids[i] indexes into observers[]. Returns convergence status, condition number, and formal covariance matrix.';
|
||||
854
sql/pg_orrery--0.4.0.sql
Normal file
854
sql/pg_orrery--0.4.0.sql
Normal file
@ -0,0 +1,854 @@
|
||||
-- pg_orrery -- Orbital mechanics types and functions for PostgreSQL
|
||||
--
|
||||
-- Types: tle, eci_position, geodetic, topocentric, observer, pass_event
|
||||
-- Provides SGP4/SDP4 propagation, coordinate transforms, pass prediction,
|
||||
-- and GiST indexing on altitude bands for conjunction screening.
|
||||
--
|
||||
-- All propagation uses WGS-72 constants (matching TLE mean element fitting).
|
||||
-- Coordinate output uses WGS-84 (matching modern geodetic standards).
|
||||
|
||||
-- ============================================================
|
||||
-- Shell types (forward declarations)
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE tle;
|
||||
CREATE TYPE eci_position;
|
||||
CREATE TYPE geodetic;
|
||||
CREATE TYPE topocentric;
|
||||
CREATE TYPE observer;
|
||||
CREATE TYPE pass_event;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- TLE type: Two-Line Element set
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION tle_in(cstring) RETURNS tle
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION tle_out(tle) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION tle_recv(internal) RETURNS tle
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION tle_send(tle) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE tle (
|
||||
INPUT = tle_in,
|
||||
OUTPUT = tle_out,
|
||||
RECEIVE = tle_recv,
|
||||
SEND = tle_send,
|
||||
INTERNALLENGTH = 112,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE tle IS 'Two-Line Element set — parsed mean orbital elements for SGP4/SDP4 propagation';
|
||||
|
||||
-- TLE accessor functions
|
||||
|
||||
CREATE FUNCTION tle_epoch(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_epoch(tle) IS 'TLE epoch as Julian date (UTC)';
|
||||
|
||||
CREATE FUNCTION tle_norad_id(tle) RETURNS int4
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_norad_id(tle) IS 'NORAD catalog number';
|
||||
|
||||
CREATE FUNCTION tle_inclination(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_inclination(tle) IS 'Orbital inclination in degrees';
|
||||
|
||||
CREATE FUNCTION tle_eccentricity(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_eccentricity(tle) IS 'Orbital eccentricity (dimensionless)';
|
||||
|
||||
CREATE FUNCTION tle_raan(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_raan(tle) IS 'Right ascension of ascending node in degrees';
|
||||
|
||||
CREATE FUNCTION tle_arg_perigee(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_arg_perigee(tle) IS 'Argument of perigee in degrees';
|
||||
|
||||
CREATE FUNCTION tle_mean_anomaly(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_mean_anomaly(tle) IS 'Mean anomaly in degrees';
|
||||
|
||||
CREATE FUNCTION tle_mean_motion(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_mean_motion(tle) IS 'Mean motion in revolutions per day';
|
||||
|
||||
CREATE FUNCTION tle_bstar(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_bstar(tle) IS 'B* drag coefficient (1/earth-radii)';
|
||||
|
||||
CREATE FUNCTION tle_period(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_period(tle) IS 'Orbital period in minutes';
|
||||
|
||||
CREATE FUNCTION tle_age(tle, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_age(tle, timestamptz) IS 'TLE age in days (positive = past epoch, negative = before epoch)';
|
||||
|
||||
CREATE FUNCTION tle_perigee(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_perigee(tle) IS 'Perigee altitude in km above WGS-72 ellipsoid';
|
||||
|
||||
CREATE FUNCTION tle_apogee(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_apogee(tle) IS 'Apogee altitude in km above WGS-72 ellipsoid';
|
||||
|
||||
CREATE FUNCTION tle_intl_desig(tle) RETURNS text
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_intl_desig(tle) IS 'International designator (COSPAR ID)';
|
||||
|
||||
CREATE FUNCTION tle_from_lines(text, text) RETURNS tle
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_lines(text, text) IS
|
||||
'Construct TLE from separate line1/line2 text columns';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- ECI position type: True Equator Mean Equinox (TEME) frame
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION eci_in(cstring) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_out(eci_position) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_recv(internal) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_send(eci_position) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE eci_position (
|
||||
INPUT = eci_in,
|
||||
OUTPUT = eci_out,
|
||||
RECEIVE = eci_recv,
|
||||
SEND = eci_send,
|
||||
INTERNALLENGTH = 48,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE eci_position IS 'Earth-Centered Inertial position and velocity in TEME frame (km, km/s)';
|
||||
|
||||
-- ECI accessor functions
|
||||
|
||||
CREATE FUNCTION eci_x(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_y(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_z(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_vx(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_vy(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_vz(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_speed(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_speed(eci_position) IS 'Velocity magnitude in km/s';
|
||||
|
||||
CREATE FUNCTION eci_altitude(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_altitude(eci_position) IS 'Approximate geocentric altitude in km (radius - WGS72_AE)';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Geodetic type: WGS-84 latitude/longitude/altitude
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION geodetic_in(cstring) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_out(geodetic) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_recv(internal) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_send(geodetic) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE geodetic (
|
||||
INPUT = geodetic_in,
|
||||
OUTPUT = geodetic_out,
|
||||
RECEIVE = geodetic_recv,
|
||||
SEND = geodetic_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE geodetic IS 'Geodetic coordinates on WGS-84 ellipsoid (lat/lon in degrees, altitude in km)';
|
||||
|
||||
CREATE FUNCTION geodetic_lat(geodetic) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_lon(geodetic) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_alt(geodetic) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Topocentric type: observer-relative az/el/range
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION topocentric_in(cstring) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION topocentric_out(topocentric) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION topocentric_recv(internal) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION topocentric_send(topocentric) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE topocentric (
|
||||
INPUT = topocentric_in,
|
||||
OUTPUT = topocentric_out,
|
||||
RECEIVE = topocentric_recv,
|
||||
SEND = topocentric_send,
|
||||
INTERNALLENGTH = 32,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE topocentric IS 'Topocentric coordinates relative to observer (azimuth, elevation, range, range rate)';
|
||||
|
||||
CREATE FUNCTION topo_azimuth(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_azimuth(topocentric) IS 'Azimuth in degrees (0=N, 90=E, 180=S, 270=W)';
|
||||
|
||||
CREATE FUNCTION topo_elevation(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_elevation(topocentric) IS 'Elevation in degrees (0=horizon, 90=zenith)';
|
||||
|
||||
CREATE FUNCTION topo_range(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_range(topocentric) IS 'Slant range in km';
|
||||
|
||||
CREATE FUNCTION topo_range_rate(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_range_rate(topocentric) IS 'Range rate in km/s (positive = receding)';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Observer type: ground station location
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION observer_in(cstring) RETURNS observer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION observer_out(observer) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION observer_recv(internal) RETURNS observer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION observer_send(observer) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE observer (
|
||||
INPUT = observer_in,
|
||||
OUTPUT = observer_out,
|
||||
RECEIVE = observer_recv,
|
||||
SEND = observer_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE observer IS 'Ground observer location (accepts: 40.0N 105.3W 1655m or decimal degrees)';
|
||||
|
||||
CREATE FUNCTION observer_lat(observer) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_lat(observer) IS 'Latitude in degrees (positive = North)';
|
||||
|
||||
CREATE FUNCTION observer_lon(observer) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_lon(observer) IS 'Longitude in degrees (positive = East)';
|
||||
|
||||
CREATE FUNCTION observer_alt(observer) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_alt(observer) IS 'Altitude in meters above WGS-84 ellipsoid';
|
||||
|
||||
CREATE FUNCTION observer_from_geodetic(float8, float8, float8 DEFAULT 0.0) RETURNS observer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_from_geodetic(float8, float8, float8) IS
|
||||
'Construct observer from lat (deg), lon (deg), altitude (meters). Avoids text formatting round-trips.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Pass event type: satellite visibility window
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION pass_event_in(cstring) RETURNS pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION pass_event_out(pass_event) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION pass_event_recv(internal) RETURNS pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION pass_event_send(pass_event) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE pass_event (
|
||||
INPUT = pass_event_in,
|
||||
OUTPUT = pass_event_out,
|
||||
RECEIVE = pass_event_recv,
|
||||
SEND = pass_event_send,
|
||||
INTERNALLENGTH = 48,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE pass_event IS 'Satellite pass event (AOS/MAX/LOS times, max elevation, AOS/LOS azimuths)';
|
||||
|
||||
CREATE FUNCTION pass_aos_time(pass_event) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_aos_time(pass_event) IS 'Acquisition of signal time';
|
||||
|
||||
CREATE FUNCTION pass_max_el_time(pass_event) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_max_el_time(pass_event) IS 'Maximum elevation time';
|
||||
|
||||
CREATE FUNCTION pass_los_time(pass_event) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_los_time(pass_event) IS 'Loss of signal time';
|
||||
|
||||
CREATE FUNCTION pass_max_elevation(pass_event) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_max_elevation(pass_event) IS 'Maximum elevation in degrees';
|
||||
|
||||
CREATE FUNCTION pass_aos_azimuth(pass_event) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_aos_azimuth(pass_event) IS 'AOS azimuth in degrees (0=N)';
|
||||
|
||||
CREATE FUNCTION pass_los_azimuth(pass_event) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_los_azimuth(pass_event) IS 'LOS azimuth in degrees (0=N)';
|
||||
|
||||
CREATE FUNCTION pass_duration(pass_event) RETURNS interval
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_duration(pass_event) IS 'Pass duration (LOS - AOS)';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- SGP4/SDP4 propagation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION sgp4_propagate(tle, timestamptz) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sgp4_propagate(tle, timestamptz) IS
|
||||
'Propagate TLE to a point in time using SGP4 (near-earth) or SDP4 (deep-space). Returns TEME ECI position/velocity.';
|
||||
|
||||
CREATE FUNCTION sgp4_propagate_safe(tle, timestamptz) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sgp4_propagate_safe(tle, timestamptz) IS
|
||||
'Like sgp4_propagate but returns NULL on error instead of raising an exception. For batch queries with potentially invalid TLEs.';
|
||||
|
||||
CREATE FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval)
|
||||
RETURNS TABLE(t timestamptz, x float8, y float8, z float8, vx float8, vy float8, vz float8)
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
|
||||
ROWS 100;
|
||||
COMMENT ON FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval) IS
|
||||
'Propagate TLE over a time range at regular intervals. Returns time series of TEME positions.';
|
||||
|
||||
CREATE FUNCTION tle_distance(tle, tle, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_distance(tle, tle, timestamptz) IS
|
||||
'Euclidean distance in km between two TLEs at a reference time';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Coordinate transform functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION eci_to_geodetic(eci_position, timestamptz) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_to_geodetic(eci_position, timestamptz) IS
|
||||
'Convert TEME ECI position to WGS-84 geodetic coordinates at given time';
|
||||
|
||||
CREATE FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) IS
|
||||
'Convert TEME ECI position to topocentric (az/el/range) relative to observer';
|
||||
|
||||
CREATE FUNCTION subsatellite_point(tle, timestamptz) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION subsatellite_point(tle, timestamptz) IS
|
||||
'Subsatellite (nadir) point on WGS-84 ellipsoid at given time';
|
||||
|
||||
CREATE FUNCTION ground_track(tle, timestamptz, timestamptz, interval)
|
||||
RETURNS TABLE(t timestamptz, lat float8, lon float8, alt float8)
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
|
||||
ROWS 100;
|
||||
COMMENT ON FUNCTION ground_track(tle, timestamptz, timestamptz, interval) IS
|
||||
'Ground track as time series of subsatellite points (lat/lon in degrees, alt in km)';
|
||||
|
||||
CREATE FUNCTION observe(tle, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observe(tle, observer, timestamptz) IS
|
||||
'Propagate TLE and compute observer-relative look angles in one call. Returns topocentric (az/el/range/range_rate).';
|
||||
|
||||
CREATE FUNCTION observe_safe(tle, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observe_safe(tle, observer, timestamptz) IS
|
||||
'Like observe() but returns NULL on propagation error. For batch queries with potentially invalid/decayed TLEs.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Pass prediction functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION next_pass(tle, observer, timestamptz) RETURNS pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION next_pass(tle, observer, timestamptz) IS
|
||||
'Find the next satellite pass over observer (searches up to 7 days ahead)';
|
||||
|
||||
CREATE FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8 DEFAULT 0.0)
|
||||
RETURNS SETOF pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE
|
||||
ROWS 10;
|
||||
COMMENT ON FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8) IS
|
||||
'Predict all satellite passes over observer in time window. Optional min_elevation in degrees.';
|
||||
|
||||
CREATE FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) RETURNS boolean
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) IS
|
||||
'True if any pass occurs over observer in the time window';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- GiST operator support functions
|
||||
-- ============================================================
|
||||
|
||||
-- Overlap operator: do orbital keys overlap in altitude AND inclination?
|
||||
CREATE FUNCTION tle_overlap(tle, tle) RETURNS boolean
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
-- Altitude distance operator (altitude-only, for KNN ordering)
|
||||
CREATE FUNCTION tle_alt_distance(tle, tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE OPERATOR && (
|
||||
LEFTARG = tle,
|
||||
RIGHTARG = tle,
|
||||
FUNCTION = tle_overlap,
|
||||
COMMUTATOR = &&,
|
||||
RESTRICT = areasel,
|
||||
JOIN = areajoinsel
|
||||
);
|
||||
|
||||
COMMENT ON OPERATOR && (tle, tle) IS 'Orbital key overlap (altitude band AND inclination range) — necessary condition for conjunction';
|
||||
|
||||
CREATE OPERATOR <-> (
|
||||
LEFTARG = tle,
|
||||
RIGHTARG = tle,
|
||||
FUNCTION = tle_alt_distance,
|
||||
COMMUTATOR = <->
|
||||
);
|
||||
|
||||
COMMENT ON OPERATOR <-> (tle, tle) IS 'Minimum altitude-band separation in km (0 if overlapping). Altitude-only — does not account for inclination. Use && for 2-D filtering.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- GiST operator class for 2-D orbital indexing (altitude + inclination)
|
||||
-- ============================================================
|
||||
|
||||
-- GiST internal support functions
|
||||
CREATE FUNCTION gist_tle_compress(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_decompress(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_consistent(internal, tle, smallint, oid, internal) RETURNS boolean
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_union(internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_penalty(internal, internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_picksplit(internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_same(internal, internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_distance(internal, tle, smallint, oid, internal) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE OPERATOR CLASS tle_ops
|
||||
DEFAULT FOR TYPE tle USING gist AS
|
||||
OPERATOR 3 && ,
|
||||
OPERATOR 15 <-> (tle, tle) FOR ORDER BY float_ops,
|
||||
FUNCTION 1 gist_tle_consistent(internal, tle, smallint, oid, internal),
|
||||
FUNCTION 2 gist_tle_union(internal, internal),
|
||||
FUNCTION 3 gist_tle_compress(internal),
|
||||
FUNCTION 4 gist_tle_decompress(internal),
|
||||
FUNCTION 5 gist_tle_penalty(internal, internal, internal),
|
||||
FUNCTION 6 gist_tle_picksplit(internal, internal),
|
||||
FUNCTION 7 gist_tle_same(internal, internal, internal),
|
||||
FUNCTION 8 gist_tle_distance(internal, tle, smallint, oid, internal);
|
||||
-- pg_orrery 0.1.0 -> 0.2.0 migration
|
||||
--
|
||||
-- Phase 1: Stars, comets, and Keplerian propagation.
|
||||
-- Adds heliocentric type, star observation, and two-body propagation.
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Heliocentric type: ecliptic J2000 position in AU
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE heliocentric;
|
||||
|
||||
CREATE FUNCTION heliocentric_in(cstring) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_out(heliocentric) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_recv(internal) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_send(heliocentric) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE heliocentric (
|
||||
INPUT = heliocentric_in,
|
||||
OUTPUT = heliocentric_out,
|
||||
RECEIVE = heliocentric_recv,
|
||||
SEND = heliocentric_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE heliocentric IS 'Heliocentric position in ecliptic J2000 frame (x, y, z in AU)';
|
||||
|
||||
CREATE FUNCTION helio_x(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_x(heliocentric) IS 'X component in AU (ecliptic J2000, vernal equinox direction)';
|
||||
|
||||
CREATE FUNCTION helio_y(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_y(heliocentric) IS 'Y component in AU (ecliptic J2000)';
|
||||
|
||||
CREATE FUNCTION helio_z(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_z(heliocentric) IS 'Z component in AU (ecliptic J2000, north ecliptic pole)';
|
||||
|
||||
CREATE FUNCTION helio_distance(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_distance(heliocentric) IS 'Heliocentric distance in AU';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Star observation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION star_observe(float8, float8, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION star_observe(float8, float8, observer, timestamptz) IS
|
||||
'Observe a star from (ra_hours J2000, dec_degrees J2000, observer, time). Returns topocentric az/el. Range is 0 (infinite distance).';
|
||||
|
||||
CREATE FUNCTION star_observe_safe(float8, float8, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION star_observe_safe(float8, float8, observer, timestamptz) IS
|
||||
'Like star_observe but returns NULL on invalid inputs. For batch queries over star catalogs.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Keplerian propagation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION kepler_propagate(
|
||||
q_au float8, eccentricity float8,
|
||||
inclination_deg float8, arg_perihelion_deg float8,
|
||||
long_asc_node_deg float8, perihelion_jd float8,
|
||||
t timestamptz
|
||||
) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION kepler_propagate(float8, float8, float8, float8, float8, float8, timestamptz) IS
|
||||
'Two-body Keplerian propagation from classical orbital elements. Returns heliocentric ecliptic J2000 position in AU. Handles elliptic, parabolic, and hyperbolic orbits.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Comet observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION comet_observe(
|
||||
q_au float8, eccentricity float8,
|
||||
inclination_deg float8, arg_perihelion_deg float8,
|
||||
long_asc_node_deg float8, perihelion_jd float8,
|
||||
earth_x_au float8, earth_y_au float8, earth_z_au float8,
|
||||
obs observer, t timestamptz
|
||||
) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION comet_observe(float8, float8, float8, float8, float8, float8, float8, float8, float8, observer, timestamptz) IS
|
||||
'Observe a comet/asteroid from orbital elements. Requires Earth heliocentric ecliptic J2000 position (AU). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 2: VSOP87 planets, ELP82B Moon, Sun observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION planet_heliocentric(int4, timestamptz) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_heliocentric(int4, timestamptz) IS
|
||||
'VSOP87 heliocentric ecliptic J2000 position (AU). Body IDs: 0=Sun, 1=Mercury, ..., 8=Neptune.';
|
||||
|
||||
CREATE FUNCTION planet_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_observe(int4, observer, timestamptz) IS
|
||||
'Observe a planet from (body_id 1-8, observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
CREATE FUNCTION sun_observe(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_observe(observer, timestamptz) IS
|
||||
'Observe the Sun from (observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
CREATE FUNCTION moon_observe(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_observe(observer, timestamptz) IS
|
||||
'Observe the Moon via ELP2000-82B from (observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 3: Planetary moon observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION galilean_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION galilean_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Galilean moon of Jupiter via L1.2 theory. Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.';
|
||||
|
||||
CREATE FUNCTION saturn_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION saturn_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Saturn moon via TASS 1.7. Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion.';
|
||||
|
||||
CREATE FUNCTION uranus_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION uranus_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Uranus moon via GUST86. Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon.';
|
||||
|
||||
CREATE FUNCTION mars_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION mars_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Mars moon via MarsSat. Body IDs: 0=Phobos, 1=Deimos.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 3: Jupiter decametric radio burst prediction
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION io_phase_angle(timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION io_phase_angle(timestamptz) IS
|
||||
'Io orbital phase angle in degrees [0,360). 0=superior conjunction (behind Jupiter). Standard Radio JOVE convention.';
|
||||
|
||||
CREATE FUNCTION jupiter_cml(observer, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION jupiter_cml(observer, timestamptz) IS
|
||||
'Jupiter Central Meridian Longitude, System III (1965.0), in degrees [0,360). Light-time corrected.';
|
||||
|
||||
CREATE FUNCTION jupiter_burst_probability(float8, float8) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION jupiter_burst_probability(float8, float8) IS
|
||||
'Estimated Jupiter decametric burst probability (0-1) from (io_phase_deg, cml_deg). Based on Carr et al. (1983) source regions A, B, C, D.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 4: Interplanetary transfer orbits (Lambert solver)
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION lambert_transfer(
|
||||
dep_body_id int4, arr_body_id int4,
|
||||
dep_time timestamptz, arr_time timestamptz,
|
||||
OUT c3_departure float8, OUT c3_arrival float8,
|
||||
OUT v_inf_departure float8, OUT v_inf_arrival float8,
|
||||
OUT tof_days float8, OUT transfer_sma float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_transfer(int4, int4, timestamptz, timestamptz) IS
|
||||
'Solve Lambert transfer between two planets. Returns C3 (km^2/s^2), v_infinity (km/s), TOF (days), SMA (AU). Body IDs 1-8.';
|
||||
|
||||
CREATE FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) IS
|
||||
'Departure C3 (km^2/s^2) for a Lambert transfer. Returns NULL if solver fails. For pork chop plots.';
|
||||
-- pg_orrery 0.2.0 -> 0.3.0 migration
|
||||
--
|
||||
-- Adds optional JPL DE440/441 ephemeris functions.
|
||||
-- Existing VSOP87/ELP2000-82B functions are unchanged (still IMMUTABLE).
|
||||
-- New _de() functions are STABLE (depend on external DE binary file).
|
||||
-- When DE is unavailable, _de() functions fall back to VSOP87 silently.
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 5: DE ephemeris functions (optional high-precision)
|
||||
-- ============================================================
|
||||
|
||||
-- Planet observation with DE ephemeris
|
||||
|
||||
CREATE FUNCTION planet_heliocentric_de(int4, timestamptz) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_heliocentric_de(int4, timestamptz) IS
|
||||
'Heliocentric ecliptic J2000 position via JPL DE (sub-arcsecond). Falls back to VSOP87 if DE unavailable. Body IDs: 0=Sun, 1-8=Mercury-Neptune.';
|
||||
|
||||
CREATE FUNCTION planet_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe planet via JPL DE. Falls back to VSOP87. Body IDs: 1-8 (Mercury-Neptune).';
|
||||
|
||||
CREATE FUNCTION sun_observe_de(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_observe_de(observer, timestamptz) IS
|
||||
'Observe Sun via JPL DE. Falls back to VSOP87.';
|
||||
|
||||
CREATE FUNCTION moon_observe_de(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_observe_de(observer, timestamptz) IS
|
||||
'Observe Moon via JPL DE. Falls back to ELP2000-82B.';
|
||||
|
||||
|
||||
-- Lambert transfer with DE positions
|
||||
|
||||
CREATE FUNCTION lambert_transfer_de(
|
||||
dep_body_id int4, arr_body_id int4,
|
||||
dep_time timestamptz, arr_time timestamptz,
|
||||
OUT c3_departure float8, OUT c3_arrival float8,
|
||||
OUT v_inf_departure float8, OUT v_inf_arrival float8,
|
||||
OUT tof_days float8, OUT transfer_sma float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_transfer_de(int4, int4, timestamptz, timestamptz) IS
|
||||
'Lambert transfer via JPL DE positions. Falls back to VSOP87. Body IDs 1-8.';
|
||||
|
||||
CREATE FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) IS
|
||||
'Departure C3 via JPL DE. Falls back to VSOP87. For pork chop plots.';
|
||||
|
||||
|
||||
-- Planetary moon observation with DE parent positions
|
||||
|
||||
CREATE FUNCTION galilean_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION galilean_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Galilean moon with JPL DE parent position. L1.2 moon offsets. Body IDs: 0-3 (Io-Callisto).';
|
||||
|
||||
CREATE FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Saturn moon with JPL DE parent position. TASS 1.7 moon offsets. Body IDs: 0-7 (Mimas-Hyperion).';
|
||||
|
||||
CREATE FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Uranus moon with JPL DE parent position. GUST86 moon offsets. Body IDs: 0-4 (Miranda-Oberon).';
|
||||
|
||||
CREATE FUNCTION mars_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION mars_moon_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Mars moon with JPL DE parent position. MarsSat moon offsets. Body IDs: 0-1 (Phobos-Deimos).';
|
||||
|
||||
|
||||
-- Diagnostic function
|
||||
|
||||
CREATE FUNCTION pg_orrery_ephemeris_info(
|
||||
OUT provider text, OUT file_path text,
|
||||
OUT start_jd float8, OUT end_jd float8,
|
||||
OUT version int4, OUT au_km float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pg_orrery_ephemeris_info() IS
|
||||
'Returns current ephemeris provider status: VSOP87 or JPL_DE with file path, JD range, version, and AU value.';
|
||||
-- pg_orrery 0.3.0 -> 0.4.0 migration
|
||||
--
|
||||
-- Adds observation-to-TLE fitting via batch weighted least-squares
|
||||
-- differential correction (Vallado & Crawford 2008, AIAA 2008-6770).
|
||||
-- Uses equinoctial elements internally for singularity-free optimization.
|
||||
-- LAPACK dgelss_() for SVD solve.
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 6: Orbit determination (TLE fitting from observations)
|
||||
-- ============================================================
|
||||
|
||||
-- Fit TLE from ECI position/velocity ephemeris
|
||||
|
||||
CREATE FUNCTION tle_from_eci(
|
||||
positions eci_position[],
|
||||
times timestamptz[],
|
||||
seed tle DEFAULT NULL,
|
||||
fit_bstar boolean DEFAULT false,
|
||||
max_iter int4 DEFAULT 15,
|
||||
OUT fitted_tle tle,
|
||||
OUT iterations int4,
|
||||
OUT rms_final float8,
|
||||
OUT rms_initial float8,
|
||||
OUT status text
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4) IS
|
||||
'Fit a TLE from ECI position/velocity observations via differential correction. Returns fitted TLE, iteration count, RMS residuals, and convergence status. Requires >= 6 observations.';
|
||||
|
||||
-- Fit TLE from topocentric observations (az/el/range)
|
||||
|
||||
CREATE FUNCTION tle_from_topocentric(
|
||||
observations topocentric[],
|
||||
times timestamptz[],
|
||||
obs observer,
|
||||
seed tle DEFAULT NULL,
|
||||
fit_bstar boolean DEFAULT false,
|
||||
max_iter int4 DEFAULT 15,
|
||||
OUT fitted_tle tle,
|
||||
OUT iterations int4,
|
||||
OUT rms_final float8,
|
||||
OUT rms_initial float8,
|
||||
OUT status text
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4) IS
|
||||
'Fit a TLE from topocentric (az/el/range) observations via differential correction. Requires seed TLE and >= 6 observations.';
|
||||
|
||||
-- Per-observation residuals diagnostic
|
||||
|
||||
CREATE FUNCTION tle_fit_residuals(
|
||||
fitted tle,
|
||||
positions eci_position[],
|
||||
times timestamptz[]
|
||||
) RETURNS TABLE (
|
||||
t timestamptz,
|
||||
dx_km float8,
|
||||
dy_km float8,
|
||||
dz_km float8,
|
||||
pos_err_km float8
|
||||
)
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_fit_residuals(tle, eci_position[], timestamptz[]) IS
|
||||
'Compute per-observation position residuals (km) between a TLE and ECI observations. Useful for fit quality assessment.';
|
||||
862
sql/pg_orrery--0.5.0.sql
Normal file
862
sql/pg_orrery--0.5.0.sql
Normal file
@ -0,0 +1,862 @@
|
||||
-- pg_orrery -- Orbital mechanics types and functions for PostgreSQL
|
||||
--
|
||||
-- Types: tle, eci_position, geodetic, topocentric, observer, pass_event
|
||||
-- Provides SGP4/SDP4 propagation, coordinate transforms, pass prediction,
|
||||
-- and GiST indexing on altitude bands for conjunction screening.
|
||||
--
|
||||
-- All propagation uses WGS-72 constants (matching TLE mean element fitting).
|
||||
-- Coordinate output uses WGS-84 (matching modern geodetic standards).
|
||||
|
||||
-- ============================================================
|
||||
-- Shell types (forward declarations)
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE tle;
|
||||
CREATE TYPE eci_position;
|
||||
CREATE TYPE geodetic;
|
||||
CREATE TYPE topocentric;
|
||||
CREATE TYPE observer;
|
||||
CREATE TYPE pass_event;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- TLE type: Two-Line Element set
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION tle_in(cstring) RETURNS tle
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION tle_out(tle) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION tle_recv(internal) RETURNS tle
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION tle_send(tle) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE tle (
|
||||
INPUT = tle_in,
|
||||
OUTPUT = tle_out,
|
||||
RECEIVE = tle_recv,
|
||||
SEND = tle_send,
|
||||
INTERNALLENGTH = 112,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE tle IS 'Two-Line Element set — parsed mean orbital elements for SGP4/SDP4 propagation';
|
||||
|
||||
-- TLE accessor functions
|
||||
|
||||
CREATE FUNCTION tle_epoch(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_epoch(tle) IS 'TLE epoch as Julian date (UTC)';
|
||||
|
||||
CREATE FUNCTION tle_norad_id(tle) RETURNS int4
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_norad_id(tle) IS 'NORAD catalog number';
|
||||
|
||||
CREATE FUNCTION tle_inclination(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_inclination(tle) IS 'Orbital inclination in degrees';
|
||||
|
||||
CREATE FUNCTION tle_eccentricity(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_eccentricity(tle) IS 'Orbital eccentricity (dimensionless)';
|
||||
|
||||
CREATE FUNCTION tle_raan(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_raan(tle) IS 'Right ascension of ascending node in degrees';
|
||||
|
||||
CREATE FUNCTION tle_arg_perigee(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_arg_perigee(tle) IS 'Argument of perigee in degrees';
|
||||
|
||||
CREATE FUNCTION tle_mean_anomaly(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_mean_anomaly(tle) IS 'Mean anomaly in degrees';
|
||||
|
||||
CREATE FUNCTION tle_mean_motion(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_mean_motion(tle) IS 'Mean motion in revolutions per day';
|
||||
|
||||
CREATE FUNCTION tle_bstar(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_bstar(tle) IS 'B* drag coefficient (1/earth-radii)';
|
||||
|
||||
CREATE FUNCTION tle_period(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_period(tle) IS 'Orbital period in minutes';
|
||||
|
||||
CREATE FUNCTION tle_age(tle, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_age(tle, timestamptz) IS 'TLE age in days (positive = past epoch, negative = before epoch)';
|
||||
|
||||
CREATE FUNCTION tle_perigee(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_perigee(tle) IS 'Perigee altitude in km above WGS-72 ellipsoid';
|
||||
|
||||
CREATE FUNCTION tle_apogee(tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_apogee(tle) IS 'Apogee altitude in km above WGS-72 ellipsoid';
|
||||
|
||||
CREATE FUNCTION tle_intl_desig(tle) RETURNS text
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_intl_desig(tle) IS 'International designator (COSPAR ID)';
|
||||
|
||||
CREATE FUNCTION tle_from_lines(text, text) RETURNS tle
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_lines(text, text) IS
|
||||
'Construct TLE from separate line1/line2 text columns';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- ECI position type: True Equator Mean Equinox (TEME) frame
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION eci_in(cstring) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_out(eci_position) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_recv(internal) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_send(eci_position) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE eci_position (
|
||||
INPUT = eci_in,
|
||||
OUTPUT = eci_out,
|
||||
RECEIVE = eci_recv,
|
||||
SEND = eci_send,
|
||||
INTERNALLENGTH = 48,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE eci_position IS 'Earth-Centered Inertial position and velocity in TEME frame (km, km/s)';
|
||||
|
||||
-- ECI accessor functions
|
||||
|
||||
CREATE FUNCTION eci_x(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_y(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_z(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_vx(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_vy(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_vz(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION eci_speed(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_speed(eci_position) IS 'Velocity magnitude in km/s';
|
||||
|
||||
CREATE FUNCTION eci_altitude(eci_position) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_altitude(eci_position) IS 'Approximate geocentric altitude in km (radius - WGS72_AE)';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Geodetic type: WGS-84 latitude/longitude/altitude
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION geodetic_in(cstring) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_out(geodetic) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_recv(internal) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_send(geodetic) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE geodetic (
|
||||
INPUT = geodetic_in,
|
||||
OUTPUT = geodetic_out,
|
||||
RECEIVE = geodetic_recv,
|
||||
SEND = geodetic_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE geodetic IS 'Geodetic coordinates on WGS-84 ellipsoid (lat/lon in degrees, altitude in km)';
|
||||
|
||||
CREATE FUNCTION geodetic_lat(geodetic) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_lon(geodetic) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION geodetic_alt(geodetic) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Topocentric type: observer-relative az/el/range
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION topocentric_in(cstring) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION topocentric_out(topocentric) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION topocentric_recv(internal) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION topocentric_send(topocentric) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE topocentric (
|
||||
INPUT = topocentric_in,
|
||||
OUTPUT = topocentric_out,
|
||||
RECEIVE = topocentric_recv,
|
||||
SEND = topocentric_send,
|
||||
INTERNALLENGTH = 32,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE topocentric IS 'Topocentric coordinates relative to observer (azimuth, elevation, range, range rate)';
|
||||
|
||||
CREATE FUNCTION topo_azimuth(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_azimuth(topocentric) IS 'Azimuth in degrees (0=N, 90=E, 180=S, 270=W)';
|
||||
|
||||
CREATE FUNCTION topo_elevation(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_elevation(topocentric) IS 'Elevation in degrees (0=horizon, 90=zenith)';
|
||||
|
||||
CREATE FUNCTION topo_range(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_range(topocentric) IS 'Slant range in km';
|
||||
|
||||
CREATE FUNCTION topo_range_rate(topocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION topo_range_rate(topocentric) IS 'Range rate in km/s (positive = receding)';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Observer type: ground station location
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION observer_in(cstring) RETURNS observer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION observer_out(observer) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION observer_recv(internal) RETURNS observer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION observer_send(observer) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE observer (
|
||||
INPUT = observer_in,
|
||||
OUTPUT = observer_out,
|
||||
RECEIVE = observer_recv,
|
||||
SEND = observer_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE observer IS 'Ground observer location (accepts: 40.0N 105.3W 1655m or decimal degrees)';
|
||||
|
||||
CREATE FUNCTION observer_lat(observer) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_lat(observer) IS 'Latitude in degrees (positive = North)';
|
||||
|
||||
CREATE FUNCTION observer_lon(observer) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_lon(observer) IS 'Longitude in degrees (positive = East)';
|
||||
|
||||
CREATE FUNCTION observer_alt(observer) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_alt(observer) IS 'Altitude in meters above WGS-84 ellipsoid';
|
||||
|
||||
CREATE FUNCTION observer_from_geodetic(float8, float8, float8 DEFAULT 0.0) RETURNS observer
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observer_from_geodetic(float8, float8, float8) IS
|
||||
'Construct observer from lat (deg), lon (deg), altitude (meters). Avoids text formatting round-trips.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Pass event type: satellite visibility window
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION pass_event_in(cstring) RETURNS pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION pass_event_out(pass_event) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION pass_event_recv(internal) RETURNS pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION pass_event_send(pass_event) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE pass_event (
|
||||
INPUT = pass_event_in,
|
||||
OUTPUT = pass_event_out,
|
||||
RECEIVE = pass_event_recv,
|
||||
SEND = pass_event_send,
|
||||
INTERNALLENGTH = 48,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE pass_event IS 'Satellite pass event (AOS/MAX/LOS times, max elevation, AOS/LOS azimuths)';
|
||||
|
||||
CREATE FUNCTION pass_aos_time(pass_event) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_aos_time(pass_event) IS 'Acquisition of signal time';
|
||||
|
||||
CREATE FUNCTION pass_max_el_time(pass_event) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_max_el_time(pass_event) IS 'Maximum elevation time';
|
||||
|
||||
CREATE FUNCTION pass_los_time(pass_event) RETURNS timestamptz
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_los_time(pass_event) IS 'Loss of signal time';
|
||||
|
||||
CREATE FUNCTION pass_max_elevation(pass_event) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_max_elevation(pass_event) IS 'Maximum elevation in degrees';
|
||||
|
||||
CREATE FUNCTION pass_aos_azimuth(pass_event) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_aos_azimuth(pass_event) IS 'AOS azimuth in degrees (0=N)';
|
||||
|
||||
CREATE FUNCTION pass_los_azimuth(pass_event) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_los_azimuth(pass_event) IS 'LOS azimuth in degrees (0=N)';
|
||||
|
||||
CREATE FUNCTION pass_duration(pass_event) RETURNS interval
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_duration(pass_event) IS 'Pass duration (LOS - AOS)';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- SGP4/SDP4 propagation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION sgp4_propagate(tle, timestamptz) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sgp4_propagate(tle, timestamptz) IS
|
||||
'Propagate TLE to a point in time using SGP4 (near-earth) or SDP4 (deep-space). Returns TEME ECI position/velocity.';
|
||||
|
||||
CREATE FUNCTION sgp4_propagate_safe(tle, timestamptz) RETURNS eci_position
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sgp4_propagate_safe(tle, timestamptz) IS
|
||||
'Like sgp4_propagate but returns NULL on error instead of raising an exception. For batch queries with potentially invalid TLEs.';
|
||||
|
||||
CREATE FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval)
|
||||
RETURNS TABLE(t timestamptz, x float8, y float8, z float8, vx float8, vy float8, vz float8)
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
|
||||
ROWS 100;
|
||||
COMMENT ON FUNCTION sgp4_propagate_series(tle, timestamptz, timestamptz, interval) IS
|
||||
'Propagate TLE over a time range at regular intervals. Returns time series of TEME positions.';
|
||||
|
||||
CREATE FUNCTION tle_distance(tle, tle, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_distance(tle, tle, timestamptz) IS
|
||||
'Euclidean distance in km between two TLEs at a reference time';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Coordinate transform functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION eci_to_geodetic(eci_position, timestamptz) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_to_geodetic(eci_position, timestamptz) IS
|
||||
'Convert TEME ECI position to WGS-84 geodetic coordinates at given time';
|
||||
|
||||
CREATE FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION eci_to_topocentric(eci_position, observer, timestamptz) IS
|
||||
'Convert TEME ECI position to topocentric (az/el/range) relative to observer';
|
||||
|
||||
CREATE FUNCTION subsatellite_point(tle, timestamptz) RETURNS geodetic
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION subsatellite_point(tle, timestamptz) IS
|
||||
'Subsatellite (nadir) point on WGS-84 ellipsoid at given time';
|
||||
|
||||
CREATE FUNCTION ground_track(tle, timestamptz, timestamptz, interval)
|
||||
RETURNS TABLE(t timestamptz, lat float8, lon float8, alt float8)
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE
|
||||
ROWS 100;
|
||||
COMMENT ON FUNCTION ground_track(tle, timestamptz, timestamptz, interval) IS
|
||||
'Ground track as time series of subsatellite points (lat/lon in degrees, alt in km)';
|
||||
|
||||
CREATE FUNCTION observe(tle, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observe(tle, observer, timestamptz) IS
|
||||
'Propagate TLE and compute observer-relative look angles in one call. Returns topocentric (az/el/range/range_rate).';
|
||||
|
||||
CREATE FUNCTION observe_safe(tle, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION observe_safe(tle, observer, timestamptz) IS
|
||||
'Like observe() but returns NULL on propagation error. For batch queries with potentially invalid/decayed TLEs.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Pass prediction functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION next_pass(tle, observer, timestamptz) RETURNS pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION next_pass(tle, observer, timestamptz) IS
|
||||
'Find the next satellite pass over observer (searches up to 7 days ahead)';
|
||||
|
||||
CREATE FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8 DEFAULT 0.0)
|
||||
RETURNS SETOF pass_event
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE
|
||||
ROWS 10;
|
||||
COMMENT ON FUNCTION predict_passes(tle, observer, timestamptz, timestamptz, float8) IS
|
||||
'Predict all satellite passes over observer in time window. Optional min_elevation in degrees.';
|
||||
|
||||
CREATE FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) RETURNS boolean
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pass_visible(tle, observer, timestamptz, timestamptz) IS
|
||||
'True if any pass occurs over observer in the time window';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- GiST operator support functions
|
||||
-- ============================================================
|
||||
|
||||
-- Overlap operator: do orbital keys overlap in altitude AND inclination?
|
||||
CREATE FUNCTION tle_overlap(tle, tle) RETURNS boolean
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
-- Altitude distance operator (altitude-only, for KNN ordering)
|
||||
CREATE FUNCTION tle_alt_distance(tle, tle) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE OPERATOR && (
|
||||
LEFTARG = tle,
|
||||
RIGHTARG = tle,
|
||||
FUNCTION = tle_overlap,
|
||||
COMMUTATOR = &&,
|
||||
RESTRICT = areasel,
|
||||
JOIN = areajoinsel
|
||||
);
|
||||
|
||||
COMMENT ON OPERATOR && (tle, tle) IS 'Orbital key overlap (altitude band AND inclination range) — necessary condition for conjunction';
|
||||
|
||||
CREATE OPERATOR <-> (
|
||||
LEFTARG = tle,
|
||||
RIGHTARG = tle,
|
||||
FUNCTION = tle_alt_distance,
|
||||
COMMUTATOR = <->
|
||||
);
|
||||
|
||||
COMMENT ON OPERATOR <-> (tle, tle) IS 'Minimum altitude-band separation in km (0 if overlapping). Altitude-only — does not account for inclination. Use && for 2-D filtering.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- GiST operator class for 2-D orbital indexing (altitude + inclination)
|
||||
-- ============================================================
|
||||
|
||||
-- GiST internal support functions
|
||||
CREATE FUNCTION gist_tle_compress(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_decompress(internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_consistent(internal, tle, smallint, oid, internal) RETURNS boolean
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_union(internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_penalty(internal, internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_picksplit(internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_same(internal, internal, internal) RETURNS internal
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION gist_tle_distance(internal, tle, smallint, oid, internal) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE OPERATOR CLASS tle_ops
|
||||
DEFAULT FOR TYPE tle USING gist AS
|
||||
OPERATOR 3 && ,
|
||||
OPERATOR 15 <-> (tle, tle) FOR ORDER BY float_ops,
|
||||
FUNCTION 1 gist_tle_consistent(internal, tle, smallint, oid, internal),
|
||||
FUNCTION 2 gist_tle_union(internal, internal),
|
||||
FUNCTION 3 gist_tle_compress(internal),
|
||||
FUNCTION 4 gist_tle_decompress(internal),
|
||||
FUNCTION 5 gist_tle_penalty(internal, internal, internal),
|
||||
FUNCTION 6 gist_tle_picksplit(internal, internal),
|
||||
FUNCTION 7 gist_tle_same(internal, internal, internal),
|
||||
FUNCTION 8 gist_tle_distance(internal, tle, smallint, oid, internal);
|
||||
-- pg_orrery 0.1.0 -> 0.2.0 migration
|
||||
--
|
||||
-- Phase 1: Stars, comets, and Keplerian propagation.
|
||||
-- Adds heliocentric type, star observation, and two-body propagation.
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Heliocentric type: ecliptic J2000 position in AU
|
||||
-- ============================================================
|
||||
|
||||
CREATE TYPE heliocentric;
|
||||
|
||||
CREATE FUNCTION heliocentric_in(cstring) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_out(heliocentric) RETURNS cstring
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_recv(internal) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE FUNCTION heliocentric_send(heliocentric) RETURNS bytea
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
|
||||
CREATE TYPE heliocentric (
|
||||
INPUT = heliocentric_in,
|
||||
OUTPUT = heliocentric_out,
|
||||
RECEIVE = heliocentric_recv,
|
||||
SEND = heliocentric_send,
|
||||
INTERNALLENGTH = 24,
|
||||
ALIGNMENT = double,
|
||||
STORAGE = plain
|
||||
);
|
||||
|
||||
COMMENT ON TYPE heliocentric IS 'Heliocentric position in ecliptic J2000 frame (x, y, z in AU)';
|
||||
|
||||
CREATE FUNCTION helio_x(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_x(heliocentric) IS 'X component in AU (ecliptic J2000, vernal equinox direction)';
|
||||
|
||||
CREATE FUNCTION helio_y(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_y(heliocentric) IS 'Y component in AU (ecliptic J2000)';
|
||||
|
||||
CREATE FUNCTION helio_z(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_z(heliocentric) IS 'Z component in AU (ecliptic J2000, north ecliptic pole)';
|
||||
|
||||
CREATE FUNCTION helio_distance(heliocentric) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION helio_distance(heliocentric) IS 'Heliocentric distance in AU';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Star observation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION star_observe(float8, float8, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION star_observe(float8, float8, observer, timestamptz) IS
|
||||
'Observe a star from (ra_hours J2000, dec_degrees J2000, observer, time). Returns topocentric az/el. Range is 0 (infinite distance).';
|
||||
|
||||
CREATE FUNCTION star_observe_safe(float8, float8, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION star_observe_safe(float8, float8, observer, timestamptz) IS
|
||||
'Like star_observe but returns NULL on invalid inputs. For batch queries over star catalogs.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Keplerian propagation functions
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION kepler_propagate(
|
||||
q_au float8, eccentricity float8,
|
||||
inclination_deg float8, arg_perihelion_deg float8,
|
||||
long_asc_node_deg float8, perihelion_jd float8,
|
||||
t timestamptz
|
||||
) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION kepler_propagate(float8, float8, float8, float8, float8, float8, timestamptz) IS
|
||||
'Two-body Keplerian propagation from classical orbital elements. Returns heliocentric ecliptic J2000 position in AU. Handles elliptic, parabolic, and hyperbolic orbits.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Comet observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION comet_observe(
|
||||
q_au float8, eccentricity float8,
|
||||
inclination_deg float8, arg_perihelion_deg float8,
|
||||
long_asc_node_deg float8, perihelion_jd float8,
|
||||
earth_x_au float8, earth_y_au float8, earth_z_au float8,
|
||||
obs observer, t timestamptz
|
||||
) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION comet_observe(float8, float8, float8, float8, float8, float8, float8, float8, float8, observer, timestamptz) IS
|
||||
'Observe a comet/asteroid from orbital elements. Requires Earth heliocentric ecliptic J2000 position (AU). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 2: VSOP87 planets, ELP82B Moon, Sun observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION planet_heliocentric(int4, timestamptz) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_heliocentric(int4, timestamptz) IS
|
||||
'VSOP87 heliocentric ecliptic J2000 position (AU). Body IDs: 0=Sun, 1=Mercury, ..., 8=Neptune.';
|
||||
|
||||
CREATE FUNCTION planet_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_observe(int4, observer, timestamptz) IS
|
||||
'Observe a planet from (body_id 1-8, observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
CREATE FUNCTION sun_observe(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_observe(observer, timestamptz) IS
|
||||
'Observe the Sun from (observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
CREATE FUNCTION moon_observe(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_observe(observer, timestamptz) IS
|
||||
'Observe the Moon via ELP2000-82B from (observer, time). Returns topocentric az/el with geocentric range in km.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 3: Planetary moon observation
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION galilean_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION galilean_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Galilean moon of Jupiter via L1.2 theory. Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.';
|
||||
|
||||
CREATE FUNCTION saturn_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION saturn_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Saturn moon via TASS 1.7. Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione, 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion.';
|
||||
|
||||
CREATE FUNCTION uranus_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION uranus_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Uranus moon via GUST86. Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon.';
|
||||
|
||||
CREATE FUNCTION mars_moon_observe(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION mars_moon_observe(int4, observer, timestamptz) IS
|
||||
'Observe a Mars moon via MarsSat. Body IDs: 0=Phobos, 1=Deimos.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 3: Jupiter decametric radio burst prediction
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION io_phase_angle(timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION io_phase_angle(timestamptz) IS
|
||||
'Io orbital phase angle in degrees [0,360). 0=superior conjunction (behind Jupiter). Standard Radio JOVE convention.';
|
||||
|
||||
CREATE FUNCTION jupiter_cml(observer, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION jupiter_cml(observer, timestamptz) IS
|
||||
'Jupiter Central Meridian Longitude, System III (1965.0), in degrees [0,360). Light-time corrected.';
|
||||
|
||||
CREATE FUNCTION jupiter_burst_probability(float8, float8) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION jupiter_burst_probability(float8, float8) IS
|
||||
'Estimated Jupiter decametric burst probability (0-1) from (io_phase_deg, cml_deg). Based on Carr et al. (1983) source regions A, B, C, D.';
|
||||
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 4: Interplanetary transfer orbits (Lambert solver)
|
||||
-- ============================================================
|
||||
|
||||
CREATE FUNCTION lambert_transfer(
|
||||
dep_body_id int4, arr_body_id int4,
|
||||
dep_time timestamptz, arr_time timestamptz,
|
||||
OUT c3_departure float8, OUT c3_arrival float8,
|
||||
OUT v_inf_departure float8, OUT v_inf_arrival float8,
|
||||
OUT tof_days float8, OUT transfer_sma float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_transfer(int4, int4, timestamptz, timestamptz) IS
|
||||
'Solve Lambert transfer between two planets. Returns C3 (km^2/s^2), v_infinity (km/s), TOF (days), SMA (AU). Body IDs 1-8.';
|
||||
|
||||
CREATE FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_c3(int4, int4, timestamptz, timestamptz) IS
|
||||
'Departure C3 (km^2/s^2) for a Lambert transfer. Returns NULL if solver fails. For pork chop plots.';
|
||||
-- pg_orrery 0.2.0 -> 0.3.0 migration
|
||||
--
|
||||
-- Adds optional JPL DE440/441 ephemeris functions.
|
||||
-- Existing VSOP87/ELP2000-82B functions are unchanged (still IMMUTABLE).
|
||||
-- New _de() functions are STABLE (depend on external DE binary file).
|
||||
-- When DE is unavailable, _de() functions fall back to VSOP87 silently.
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 5: DE ephemeris functions (optional high-precision)
|
||||
-- ============================================================
|
||||
|
||||
-- Planet observation with DE ephemeris
|
||||
|
||||
CREATE FUNCTION planet_heliocentric_de(int4, timestamptz) RETURNS heliocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_heliocentric_de(int4, timestamptz) IS
|
||||
'Heliocentric ecliptic J2000 position via JPL DE (sub-arcsecond). Falls back to VSOP87 if DE unavailable. Body IDs: 0=Sun, 1-8=Mercury-Neptune.';
|
||||
|
||||
CREATE FUNCTION planet_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION planet_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe planet via JPL DE. Falls back to VSOP87. Body IDs: 1-8 (Mercury-Neptune).';
|
||||
|
||||
CREATE FUNCTION sun_observe_de(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION sun_observe_de(observer, timestamptz) IS
|
||||
'Observe Sun via JPL DE. Falls back to VSOP87.';
|
||||
|
||||
CREATE FUNCTION moon_observe_de(observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION moon_observe_de(observer, timestamptz) IS
|
||||
'Observe Moon via JPL DE. Falls back to ELP2000-82B.';
|
||||
|
||||
|
||||
-- Lambert transfer with DE positions
|
||||
|
||||
CREATE FUNCTION lambert_transfer_de(
|
||||
dep_body_id int4, arr_body_id int4,
|
||||
dep_time timestamptz, arr_time timestamptz,
|
||||
OUT c3_departure float8, OUT c3_arrival float8,
|
||||
OUT v_inf_departure float8, OUT v_inf_arrival float8,
|
||||
OUT tof_days float8, OUT transfer_sma float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_transfer_de(int4, int4, timestamptz, timestamptz) IS
|
||||
'Lambert transfer via JPL DE positions. Falls back to VSOP87. Body IDs 1-8.';
|
||||
|
||||
CREATE FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) RETURNS float8
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION lambert_c3_de(int4, int4, timestamptz, timestamptz) IS
|
||||
'Departure C3 via JPL DE. Falls back to VSOP87. For pork chop plots.';
|
||||
|
||||
|
||||
-- Planetary moon observation with DE parent positions
|
||||
|
||||
CREATE FUNCTION galilean_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION galilean_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Galilean moon with JPL DE parent position. L1.2 moon offsets. Body IDs: 0-3 (Io-Callisto).';
|
||||
|
||||
CREATE FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION saturn_moon_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Saturn moon with JPL DE parent position. TASS 1.7 moon offsets. Body IDs: 0-7 (Mimas-Hyperion).';
|
||||
|
||||
CREATE FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION uranus_moon_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Uranus moon with JPL DE parent position. GUST86 moon offsets. Body IDs: 0-4 (Miranda-Oberon).';
|
||||
|
||||
CREATE FUNCTION mars_moon_observe_de(int4, observer, timestamptz) RETURNS topocentric
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION mars_moon_observe_de(int4, observer, timestamptz) IS
|
||||
'Observe Mars moon with JPL DE parent position. MarsSat moon offsets. Body IDs: 0-1 (Phobos-Deimos).';
|
||||
|
||||
|
||||
-- Diagnostic function
|
||||
|
||||
CREATE FUNCTION pg_orrery_ephemeris_info(
|
||||
OUT provider text, OUT file_path text,
|
||||
OUT start_jd float8, OUT end_jd float8,
|
||||
OUT version int4, OUT au_km float8
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION pg_orrery_ephemeris_info() IS
|
||||
'Returns current ephemeris provider status: VSOP87 or JPL_DE with file path, JD range, version, and AU value.';
|
||||
-- pg_orrery 0.3.0 -> 0.4.0 migration
|
||||
--
|
||||
-- Adds observation-to-TLE fitting via batch weighted least-squares
|
||||
-- differential correction (Vallado & Crawford 2008, AIAA 2008-6770).
|
||||
-- Uses equinoctial elements internally for singularity-free optimization.
|
||||
-- LAPACK dgelss_() for SVD solve.
|
||||
|
||||
-- ============================================================
|
||||
-- Phase 6: Orbit determination (TLE fitting from observations)
|
||||
-- ============================================================
|
||||
|
||||
-- Fit TLE from ECI position/velocity ephemeris
|
||||
|
||||
CREATE FUNCTION tle_from_eci(
|
||||
positions eci_position[], times timestamptz[],
|
||||
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||
max_iter int4 DEFAULT 15,
|
||||
OUT fitted_tle tle, OUT iterations int4,
|
||||
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4) IS
|
||||
'Fit a TLE from ECI position/velocity observations via differential correction. Returns fitted TLE, iteration count, RMS residuals, convergence status, condition number, and formal covariance matrix.';
|
||||
|
||||
-- Fit TLE from topocentric observations (az/el/range) — single observer
|
||||
|
||||
CREATE FUNCTION tle_from_topocentric(
|
||||
observations topocentric[], times timestamptz[],
|
||||
obs observer,
|
||||
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||
max_iter int4 DEFAULT 15,
|
||||
OUT fitted_tle tle, OUT iterations int4,
|
||||
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer, tle, boolean, int4) IS
|
||||
'Fit a TLE from topocentric (az/el/range) observations via differential correction. Returns fitted TLE, RMS residuals, convergence status, condition number, and formal covariance matrix.';
|
||||
|
||||
-- Fit TLE from topocentric observations — multiple observers
|
||||
|
||||
CREATE FUNCTION tle_from_topocentric(
|
||||
observations topocentric[], times timestamptz[],
|
||||
observers observer[], observer_ids int4[],
|
||||
seed tle DEFAULT NULL, fit_bstar boolean DEFAULT false,
|
||||
max_iter int4 DEFAULT 15,
|
||||
OUT fitted_tle tle, OUT iterations int4,
|
||||
OUT rms_final float8, OUT rms_initial float8, OUT status text,
|
||||
OUT condition_number float8, OUT covariance float8[], OUT nstate int4
|
||||
) RETURNS RECORD
|
||||
AS 'MODULE_PATHNAME', 'tle_from_topocentric_multi'
|
||||
LANGUAGE C STABLE PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_from_topocentric(topocentric[], timestamptz[], observer[], int4[], tle, boolean, int4) IS
|
||||
'Fit a TLE from topocentric observations collected by multiple ground stations. observer_ids[i] indexes into observers[]. Returns convergence status, condition number, and formal covariance matrix.';
|
||||
|
||||
-- Per-observation residuals diagnostic
|
||||
|
||||
CREATE FUNCTION tle_fit_residuals(
|
||||
fitted tle,
|
||||
positions eci_position[],
|
||||
times timestamptz[]
|
||||
) RETURNS TABLE (
|
||||
t timestamptz,
|
||||
dx_km float8,
|
||||
dy_km float8,
|
||||
dz_km float8,
|
||||
pos_err_km float8
|
||||
)
|
||||
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
|
||||
COMMENT ON FUNCTION tle_fit_residuals(tle, eci_position[], timestamptz[]) IS
|
||||
'Compute per-observation position residuals (km) between a TLE and ECI observations. Useful for fit quality assessment.';
|
||||
220
src/astro_math.h
Normal file
220
src/astro_math.h
Normal file
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* astro_math.h -- Shared astronomical math for pg_orrery
|
||||
*
|
||||
* Static inline functions used by star_funcs.c, kepler_funcs.c,
|
||||
* and future planet/moon observation code.
|
||||
*
|
||||
* Using static inline preserves the project convention of no
|
||||
* cross-translation-unit symbol coupling.
|
||||
*/
|
||||
|
||||
#ifndef PG_ORRERY_ASTRO_MATH_H
|
||||
#define PG_ORRERY_ASTRO_MATH_H
|
||||
|
||||
#include <math.h>
|
||||
#include "types.h"
|
||||
|
||||
#define DEG_TO_RAD (M_PI / 180.0)
|
||||
#define RAD_TO_DEG (180.0 / M_PI)
|
||||
#define ARCSEC_TO_RAD (M_PI / (180.0 * 3600.0))
|
||||
|
||||
/* Pre-computed obliquity trig (IAU 1976, OBLIQUITY_J2000 = 0.40909280422232897 rad).
|
||||
* Avoids recomputing cos/sin on every frame rotation call. */
|
||||
#define COS_OBLIQUITY_J2000 0.91748206206918181
|
||||
#define SIN_OBLIQUITY_J2000 0.39777715593191371
|
||||
|
||||
/*
|
||||
* Greenwich Mean Sidereal Time from Julian date.
|
||||
* Vallado, "Fundamentals of Astrodynamics", Eq. 3-47.
|
||||
* Returns angle in radians.
|
||||
*/
|
||||
static inline double
|
||||
gmst_from_jd(double jd)
|
||||
{
|
||||
double t_ut1 = (jd - 2451545.0) / 36525.0;
|
||||
double gmst = 67310.54841
|
||||
+ (876600.0 * 3600.0 + 8640184.812866) * t_ut1
|
||||
+ 0.093104 * t_ut1 * t_ut1
|
||||
- 6.2e-6 * t_ut1 * t_ut1 * t_ut1;
|
||||
|
||||
gmst = fmod(gmst * M_PI / 43200.0, 2.0 * M_PI);
|
||||
if (gmst < 0.0)
|
||||
gmst += 2.0 * M_PI;
|
||||
return gmst;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* IAU 1976 precession: rotate J2000 equatorial to mean equatorial of date.
|
||||
*
|
||||
* Source: Lieske (1979), A&A 73, 282.
|
||||
* Three angles zeta_A, z_A, theta_A define the precession rotation.
|
||||
* Valid within ~1 arcsecond for several centuries around J2000.
|
||||
*/
|
||||
static inline void
|
||||
precess_j2000_to_date(double jd, double ra_j2000, double dec_j2000,
|
||||
double *ra_date, double *dec_date)
|
||||
{
|
||||
double T, T2, T3;
|
||||
double zeta_A, z_A, theta_A;
|
||||
double cos_dec, sin_dec, cos_ra_zeta, sin_ra_zeta;
|
||||
double cos_theta, sin_theta;
|
||||
double A, B, C;
|
||||
|
||||
T = (jd - J2000_JD) / 36525.0;
|
||||
T2 = T * T;
|
||||
T3 = T2 * T;
|
||||
|
||||
/* Precession angles in arcseconds (Lieske 1979) */
|
||||
zeta_A = (2306.2181 + 1.39656 * T - 0.000139 * T2) * T
|
||||
+ (0.30188 - 0.000344 * T) * T2 + 0.017998 * T3;
|
||||
z_A = (2306.2181 + 1.39656 * T - 0.000139 * T2) * T
|
||||
+ (1.09468 + 0.000066 * T) * T2 + 0.018203 * T3;
|
||||
theta_A = (2004.3109 - 0.85330 * T - 0.000217 * T2) * T
|
||||
- (0.42665 + 0.000217 * T) * T2 - 0.041833 * T3;
|
||||
|
||||
/* Arcseconds to radians */
|
||||
zeta_A *= ARCSEC_TO_RAD;
|
||||
z_A *= ARCSEC_TO_RAD;
|
||||
theta_A *= ARCSEC_TO_RAD;
|
||||
|
||||
/* Direct formula: R3(-z_A) R2(theta_A) R3(-zeta_A) applied to (ra, dec) */
|
||||
cos_dec = cos(dec_j2000);
|
||||
sin_dec = sin(dec_j2000);
|
||||
cos_ra_zeta = cos(ra_j2000 + zeta_A);
|
||||
sin_ra_zeta = sin(ra_j2000 + zeta_A);
|
||||
cos_theta = cos(theta_A);
|
||||
sin_theta = sin(theta_A);
|
||||
|
||||
A = cos_dec * sin_ra_zeta;
|
||||
B = cos_theta * cos_dec * cos_ra_zeta - sin_theta * sin_dec;
|
||||
C = sin_theta * cos_dec * cos_ra_zeta + cos_theta * sin_dec;
|
||||
|
||||
*dec_date = asin(C);
|
||||
*ra_date = atan2(A, B) + z_A;
|
||||
|
||||
if (*ra_date < 0.0)
|
||||
*ra_date += 2.0 * M_PI;
|
||||
if (*ra_date >= 2.0 * M_PI)
|
||||
*ra_date -= 2.0 * M_PI;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Equatorial (hour angle, declination) to horizontal (azimuth, elevation).
|
||||
* All angles in radians.
|
||||
* Azimuth convention: 0=N, pi/2=E, pi=S, 3*pi/2=W.
|
||||
*/
|
||||
static inline void
|
||||
equatorial_to_horizontal(double ha, double dec, double lat,
|
||||
double *az, double *el)
|
||||
{
|
||||
double sin_lat = sin(lat);
|
||||
double cos_lat = cos(lat);
|
||||
double sin_dec = sin(dec);
|
||||
double cos_dec = cos(dec);
|
||||
double cos_ha = cos(ha);
|
||||
double y, x;
|
||||
|
||||
*el = asin(sin_lat * sin_dec + cos_lat * cos_dec * cos_ha);
|
||||
|
||||
y = -cos_dec * sin(ha);
|
||||
x = cos_lat * sin_dec - sin_lat * cos_dec * cos_ha;
|
||||
*az = atan2(y, x);
|
||||
if (*az < 0.0)
|
||||
*az += 2.0 * M_PI;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Ecliptic J2000 to equatorial J2000.
|
||||
* Simple rotation around X-axis by -obliquity.
|
||||
*
|
||||
* NOT safe for aliased (in-place) calls: ecl and equ must not overlap.
|
||||
* Writing equ[1] before reading ecl[1] for equ[2] produces wrong results
|
||||
* when ecl == equ. The vendored sgp4/sdp4.c has separate in-place versions
|
||||
* that use a temp variable; do not confuse the two.
|
||||
*/
|
||||
static inline void
|
||||
ecliptic_to_equatorial(const double *ecl, double *equ)
|
||||
{
|
||||
equ[0] = ecl[0];
|
||||
equ[1] = ecl[1] * COS_OBLIQUITY_J2000 - ecl[2] * SIN_OBLIQUITY_J2000;
|
||||
equ[2] = ecl[1] * SIN_OBLIQUITY_J2000 + ecl[2] * COS_OBLIQUITY_J2000;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Equatorial J2000 to ecliptic J2000.
|
||||
* Rotation around X-axis by +obliquity.
|
||||
*
|
||||
* NOT safe for aliased (in-place) calls: equ and ecl must not overlap.
|
||||
* Same ordering hazard as ecliptic_to_equatorial() above.
|
||||
*/
|
||||
static inline void
|
||||
equatorial_to_ecliptic(const double *equ, double *ecl)
|
||||
{
|
||||
ecl[0] = equ[0];
|
||||
ecl[1] = equ[1] * COS_OBLIQUITY_J2000 + equ[2] * SIN_OBLIQUITY_J2000;
|
||||
ecl[2] = -equ[1] * SIN_OBLIQUITY_J2000 + equ[2] * COS_OBLIQUITY_J2000;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Cartesian to spherical: (x, y, z) -> (ra, dec, dist).
|
||||
* ra in [0, 2*pi), dec in [-pi/2, pi/2], dist in same units as input.
|
||||
*/
|
||||
static inline void
|
||||
cartesian_to_spherical(const double *xyz, double *ra, double *dec, double *dist)
|
||||
{
|
||||
*dist = sqrt(xyz[0] * xyz[0] + xyz[1] * xyz[1] + xyz[2] * xyz[2]);
|
||||
*dec = asin(xyz[2] / *dist);
|
||||
*ra = atan2(xyz[1], xyz[0]);
|
||||
if (*ra < 0.0)
|
||||
*ra += 2.0 * M_PI;
|
||||
}
|
||||
|
||||
/*
|
||||
* Geocentric observation pipeline (shared by all observation functions).
|
||||
*
|
||||
* Takes geocentric ecliptic J2000 position in AU, observer location,
|
||||
* and Julian date. Converts through equatorial, precesses to date,
|
||||
* and computes topocentric az/el.
|
||||
*
|
||||
* This is the canonical path:
|
||||
* ecliptic J2000 -> equatorial J2000 -> precess to date ->
|
||||
* sidereal time -> hour angle -> az/el
|
||||
*/
|
||||
static inline void
|
||||
observe_from_geocentric(const double geo_ecl_au[3], double jd,
|
||||
const pg_observer *obs, pg_topocentric *result)
|
||||
{
|
||||
double geo_equ[3];
|
||||
double ra_j2000, dec_j2000, geo_dist;
|
||||
double ra_date, dec_date;
|
||||
double gmst_val, lst, ha;
|
||||
double az, el;
|
||||
|
||||
/* Ecliptic J2000 -> equatorial J2000 */
|
||||
ecliptic_to_equatorial(geo_ecl_au, geo_equ);
|
||||
|
||||
/* Cartesian -> spherical */
|
||||
cartesian_to_spherical(geo_equ, &ra_j2000, &dec_j2000, &geo_dist);
|
||||
|
||||
/* Precess J2000 -> date */
|
||||
precess_j2000_to_date(jd, ra_j2000, dec_j2000, &ra_date, &dec_date);
|
||||
|
||||
/* Hour angle and az/el */
|
||||
gmst_val = gmst_from_jd(jd);
|
||||
lst = gmst_val + obs->lon;
|
||||
ha = lst - ra_date;
|
||||
|
||||
equatorial_to_horizontal(ha, dec_date, obs->lat, &az, &el);
|
||||
|
||||
result->azimuth = az;
|
||||
result->elevation = el;
|
||||
result->range_km = geo_dist * AU_KM;
|
||||
result->range_rate = 0.0; /* no velocity computation yet */
|
||||
}
|
||||
|
||||
#endif /* PG_ORRERY_ASTRO_MATH_H */
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* coord_funcs.c -- Coordinate transform functions for pg_orbit
|
||||
* coord_funcs.c -- Coordinate transform functions for pg_orrery
|
||||
*
|
||||
* TEME -> WGS-84 geodetic and TEME -> topocentric transforms.
|
||||
*
|
||||
|
||||
666
src/de_funcs.c
Normal file
666
src/de_funcs.c
Normal file
@ -0,0 +1,666 @@
|
||||
/*
|
||||
* de_funcs.c -- SQL-facing DE ephemeris function variants
|
||||
*
|
||||
* Each _de() function is a STABLE STRICT PARALLEL SAFE variant of an
|
||||
* existing IMMUTABLE function. On any DE failure, falls back to the
|
||||
* compiled-in VSOP87/ELP2000-82B equivalent with a NOTICE.
|
||||
*
|
||||
* The observation pipeline is identical:
|
||||
* 1. Heliocentric ecliptic J2000 position (DE or fallback)
|
||||
* 2. Geocentric ecliptic (subtract Earth's heliocentric)
|
||||
* 3. observe_from_geocentric() -> topocentric az/el
|
||||
*
|
||||
* Constant chain of custody rule 7:
|
||||
* Both target and Earth ALWAYS come from the same provider.
|
||||
* If DE fails for the target, we don't use DE for Earth either.
|
||||
*/
|
||||
|
||||
#include "postgres.h"
|
||||
#include "fmgr.h"
|
||||
#include "funcapi.h"
|
||||
#include "catalog/pg_type.h"
|
||||
#include "utils/builtins.h"
|
||||
#include "utils/timestamp.h"
|
||||
|
||||
#include "types.h"
|
||||
#include "astro_math.h"
|
||||
#include "eph_provider.h"
|
||||
#include "vsop87.h"
|
||||
#include "elp82b.h"
|
||||
#include "lambert.h"
|
||||
#include "l12.h"
|
||||
#include "tass17.h"
|
||||
#include "gust86.h"
|
||||
#include "marssat.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Forward declarations */
|
||||
PG_FUNCTION_INFO_V1(planet_heliocentric_de);
|
||||
PG_FUNCTION_INFO_V1(planet_observe_de);
|
||||
PG_FUNCTION_INFO_V1(sun_observe_de);
|
||||
PG_FUNCTION_INFO_V1(moon_observe_de);
|
||||
PG_FUNCTION_INFO_V1(lambert_transfer_de);
|
||||
PG_FUNCTION_INFO_V1(lambert_c3_de);
|
||||
PG_FUNCTION_INFO_V1(galilean_observe_de);
|
||||
PG_FUNCTION_INFO_V1(saturn_moon_observe_de);
|
||||
PG_FUNCTION_INFO_V1(uranus_moon_observe_de);
|
||||
PG_FUNCTION_INFO_V1(mars_moon_observe_de);
|
||||
PG_FUNCTION_INFO_V1(pg_orrery_ephemeris_info);
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* planet_heliocentric_de(body_id int, timestamptz) -> heliocentric
|
||||
*
|
||||
* DE variant of planet_heliocentric(). STABLE.
|
||||
* Falls back to VSOP87 if DE is unavailable.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
planet_heliocentric_de(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 body_id = PG_GETARG_INT32(0);
|
||||
int64 ts = PG_GETARG_INT64(1);
|
||||
double jd;
|
||||
double xyz[6];
|
||||
pg_heliocentric *result;
|
||||
|
||||
if (body_id == BODY_SUN)
|
||||
{
|
||||
result = (pg_heliocentric *) palloc(sizeof(pg_heliocentric));
|
||||
result->x = 0.0;
|
||||
result->y = 0.0;
|
||||
result->z = 0.0;
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
if (body_id < BODY_MERCURY || body_id > BODY_NEPTUNE)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("invalid body_id %d: must be 0 (Sun) or 1-8 (Mercury-Neptune)",
|
||||
body_id)));
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
|
||||
/* Try DE first */
|
||||
if (!eph_de_planet(body_id, jd, xyz))
|
||||
{
|
||||
int vsop_body = body_id - 1;
|
||||
|
||||
if (eph_de_available())
|
||||
ereport(NOTICE,
|
||||
(errmsg("DE ephemeris unavailable for this query, falling back to VSOP87")));
|
||||
|
||||
GetVsop87Coor(jd, vsop_body, xyz);
|
||||
}
|
||||
|
||||
result = (pg_heliocentric *) palloc(sizeof(pg_heliocentric));
|
||||
result->x = xyz[0];
|
||||
result->y = xyz[1];
|
||||
result->z = xyz[2];
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* planet_observe_de(body_id int, observer, timestamptz) -> topocentric
|
||||
*
|
||||
* DE variant of planet_observe(). STABLE.
|
||||
* Rule 7: both planet and Earth from the same provider.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
planet_observe_de(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 body_id = PG_GETARG_INT32(0);
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
||||
int64 ts = PG_GETARG_INT64(2);
|
||||
double jd;
|
||||
double earth_xyz[6];
|
||||
double planet_xyz[6];
|
||||
double geo_ecl[3];
|
||||
pg_topocentric *result;
|
||||
|
||||
if (body_id < BODY_MERCURY || body_id > BODY_NEPTUNE)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("planet_observe_de: body_id %d must be 1-8 (Mercury-Neptune)",
|
||||
body_id)));
|
||||
|
||||
if (body_id == BODY_EARTH)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("cannot observe Earth from Earth")));
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
|
||||
/* Try DE for both planet and Earth (rule 7: same provider) */
|
||||
if (eph_de_planet(body_id, jd, planet_xyz) &&
|
||||
eph_de_earth(jd, earth_xyz))
|
||||
{
|
||||
/* DE succeeded */
|
||||
}
|
||||
else
|
||||
{
|
||||
int vsop_body = body_id - 1;
|
||||
|
||||
if (eph_de_available())
|
||||
ereport(NOTICE,
|
||||
(errmsg("DE ephemeris unavailable for this query, falling back to VSOP87")));
|
||||
|
||||
GetVsop87Coor(jd, 2, earth_xyz);
|
||||
GetVsop87Coor(jd, vsop_body, planet_xyz);
|
||||
}
|
||||
|
||||
/* Geocentric ecliptic = planet - Earth */
|
||||
geo_ecl[0] = planet_xyz[0] - earth_xyz[0];
|
||||
geo_ecl[1] = planet_xyz[1] - earth_xyz[1];
|
||||
geo_ecl[2] = planet_xyz[2] - earth_xyz[2];
|
||||
|
||||
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
|
||||
observe_from_geocentric(geo_ecl, jd, obs, result);
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* sun_observe_de(observer, timestamptz) -> topocentric
|
||||
*
|
||||
* DE variant of sun_observe(). STABLE.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
sun_observe_de(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
||||
int64 ts = PG_GETARG_INT64(1);
|
||||
double jd;
|
||||
double earth_xyz[6];
|
||||
double geo_ecl[3];
|
||||
pg_topocentric *result;
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
|
||||
/* Sun geocentric = -Earth_heliocentric */
|
||||
if (eph_de_earth(jd, earth_xyz))
|
||||
{
|
||||
geo_ecl[0] = -earth_xyz[0];
|
||||
geo_ecl[1] = -earth_xyz[1];
|
||||
geo_ecl[2] = -earth_xyz[2];
|
||||
}
|
||||
else
|
||||
{
|
||||
if (eph_de_available())
|
||||
ereport(NOTICE,
|
||||
(errmsg("DE ephemeris unavailable, falling back to VSOP87")));
|
||||
|
||||
GetVsop87Coor(jd, 2, earth_xyz);
|
||||
geo_ecl[0] = -earth_xyz[0];
|
||||
geo_ecl[1] = -earth_xyz[1];
|
||||
geo_ecl[2] = -earth_xyz[2];
|
||||
}
|
||||
|
||||
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
|
||||
observe_from_geocentric(geo_ecl, jd, obs, result);
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* moon_observe_de(observer, timestamptz) -> topocentric
|
||||
*
|
||||
* DE variant of moon_observe(). STABLE.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
moon_observe_de(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(0);
|
||||
int64 ts = PG_GETARG_INT64(1);
|
||||
double jd;
|
||||
double moon_ecl[3];
|
||||
pg_topocentric *result;
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
|
||||
/* Moon geocentric ecliptic J2000 */
|
||||
if (!eph_de_moon(jd, moon_ecl))
|
||||
{
|
||||
if (eph_de_available())
|
||||
ereport(NOTICE,
|
||||
(errmsg("DE ephemeris unavailable, falling back to ELP2000-82B")));
|
||||
|
||||
GetElp82bCoor(jd, moon_ecl);
|
||||
}
|
||||
|
||||
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
|
||||
observe_from_geocentric(moon_ecl, jd, obs, result);
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* Lambert transfer functions with DE positions
|
||||
* ================================================================
|
||||
*/
|
||||
|
||||
/*
|
||||
* Compute planet heliocentric velocity via numerical differentiation.
|
||||
*
|
||||
* use_de: if true, use DE positions; if false, use VSOP87.
|
||||
* Must match the provider used for the corresponding position query
|
||||
* (rule 7: same provider for position and velocity).
|
||||
*/
|
||||
static void
|
||||
planet_velocity_de(int body_id, double jd, bool use_de, double vel[3])
|
||||
{
|
||||
double pos_fwd[6], pos_bwd[6];
|
||||
double dt = 0.01; /* days */
|
||||
|
||||
if (use_de)
|
||||
{
|
||||
bool got_fwd = eph_de_planet(body_id, jd + dt, pos_fwd);
|
||||
bool got_bwd = eph_de_planet(body_id, jd - dt, pos_bwd);
|
||||
|
||||
if (!got_fwd || !got_bwd)
|
||||
{
|
||||
/* DE boundary straddled — use VSOP87 for both to stay consistent */
|
||||
int vsop_body = body_id - 1;
|
||||
GetVsop87Coor(jd + dt, vsop_body, pos_fwd);
|
||||
GetVsop87Coor(jd - dt, vsop_body, pos_bwd);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int vsop_body = body_id - 1;
|
||||
GetVsop87Coor(jd + dt, vsop_body, pos_fwd);
|
||||
GetVsop87Coor(jd - dt, vsop_body, pos_bwd);
|
||||
}
|
||||
|
||||
vel[0] = (pos_fwd[0] - pos_bwd[0]) / (2.0 * dt);
|
||||
vel[1] = (pos_fwd[1] - pos_bwd[1]) / (2.0 * dt);
|
||||
vel[2] = (pos_fwd[2] - pos_bwd[2]) / (2.0 * dt);
|
||||
}
|
||||
|
||||
|
||||
Datum
|
||||
lambert_transfer_de(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 dep_body = PG_GETARG_INT32(0);
|
||||
int32 arr_body = PG_GETARG_INT32(1);
|
||||
int64 dep_ts = PG_GETARG_INT64(2);
|
||||
int64 arr_ts = PG_GETARG_INT64(3);
|
||||
double dep_jd, arr_jd, tof_days;
|
||||
double r1[6], r2[6];
|
||||
double v_planet_dep[3], v_planet_arr[3];
|
||||
double v_inf_dep[3], v_inf_arr[3];
|
||||
double v_inf_dep_mag, v_inf_arr_mag;
|
||||
double c3_dep, c3_arr;
|
||||
lambert_result lr;
|
||||
TupleDesc tupdesc;
|
||||
Datum values[6];
|
||||
bool nulls[6];
|
||||
HeapTuple tuple;
|
||||
double au_per_day_to_km_per_s;
|
||||
int k;
|
||||
bool have_de;
|
||||
|
||||
if (dep_body < 1 || dep_body > 8)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("lambert_transfer_de: dep_body_id %d must be 1-8", dep_body)));
|
||||
if (arr_body < 1 || arr_body > 8)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("lambert_transfer_de: arr_body_id %d must be 1-8", arr_body)));
|
||||
if (dep_body == arr_body)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("lambert_transfer_de: departure and arrival bodies must differ")));
|
||||
|
||||
dep_jd = timestamptz_to_jd(dep_ts);
|
||||
arr_jd = timestamptz_to_jd(arr_ts);
|
||||
tof_days = arr_jd - dep_jd;
|
||||
|
||||
if (tof_days <= 0.0)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("lambert_transfer_de: arrival must be after departure")));
|
||||
|
||||
/* Try DE for both positions (rule 7: same provider) */
|
||||
have_de = eph_de_planet(dep_body, dep_jd, r1) &&
|
||||
eph_de_planet(arr_body, arr_jd, r2);
|
||||
|
||||
if (!have_de)
|
||||
{
|
||||
int dep_vsop = dep_body - 1;
|
||||
int arr_vsop = arr_body - 1;
|
||||
|
||||
if (eph_de_available())
|
||||
ereport(NOTICE,
|
||||
(errmsg("DE ephemeris unavailable, falling back to VSOP87")));
|
||||
|
||||
GetVsop87Coor(dep_jd, dep_vsop, r1);
|
||||
GetVsop87Coor(arr_jd, arr_vsop, r2);
|
||||
}
|
||||
|
||||
if (!lambert_solve_uv(r1, r2, tof_days, GAUSS_K2, 1, &lr))
|
||||
PG_RETURN_NULL();
|
||||
|
||||
/* Planet velocities (same provider as positions — rule 7) */
|
||||
planet_velocity_de(dep_body, dep_jd, have_de, v_planet_dep);
|
||||
planet_velocity_de(arr_body, arr_jd, have_de, v_planet_arr);
|
||||
|
||||
au_per_day_to_km_per_s = AU_KM / 86400.0;
|
||||
|
||||
for (k = 0; k < 3; k++) {
|
||||
v_inf_dep[k] = (lr.v1[k] - v_planet_dep[k]) * au_per_day_to_km_per_s;
|
||||
v_inf_arr[k] = (lr.v2[k] - v_planet_arr[k]) * au_per_day_to_km_per_s;
|
||||
}
|
||||
|
||||
v_inf_dep_mag = sqrt(v_inf_dep[0]*v_inf_dep[0] +
|
||||
v_inf_dep[1]*v_inf_dep[1] +
|
||||
v_inf_dep[2]*v_inf_dep[2]);
|
||||
v_inf_arr_mag = sqrt(v_inf_arr[0]*v_inf_arr[0] +
|
||||
v_inf_arr[1]*v_inf_arr[1] +
|
||||
v_inf_arr[2]*v_inf_arr[2]);
|
||||
|
||||
c3_dep = v_inf_dep_mag * v_inf_dep_mag;
|
||||
c3_arr = v_inf_arr_mag * v_inf_arr_mag;
|
||||
|
||||
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("function returning record called in context "
|
||||
"that cannot accept type record")));
|
||||
|
||||
tupdesc = BlessTupleDesc(tupdesc);
|
||||
|
||||
memset(nulls, 0, sizeof(nulls));
|
||||
values[0] = Float8GetDatum(c3_dep);
|
||||
values[1] = Float8GetDatum(c3_arr);
|
||||
values[2] = Float8GetDatum(v_inf_dep_mag);
|
||||
values[3] = Float8GetDatum(v_inf_arr_mag);
|
||||
values[4] = Float8GetDatum(tof_days);
|
||||
values[5] = Float8GetDatum(lr.sma);
|
||||
|
||||
tuple = heap_form_tuple(tupdesc, values, nulls);
|
||||
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
|
||||
}
|
||||
|
||||
|
||||
Datum
|
||||
lambert_c3_de(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 dep_body = PG_GETARG_INT32(0);
|
||||
int32 arr_body = PG_GETARG_INT32(1);
|
||||
int64 dep_ts = PG_GETARG_INT64(2);
|
||||
int64 arr_ts = PG_GETARG_INT64(3);
|
||||
double dep_jd, arr_jd, tof_days;
|
||||
double r1[6], r2[6];
|
||||
double v_planet_dep[3];
|
||||
double v_inf_dep[3];
|
||||
double c3_dep;
|
||||
lambert_result lr;
|
||||
double au_per_day_to_km_per_s;
|
||||
int k;
|
||||
bool have_de;
|
||||
|
||||
if (dep_body < 1 || dep_body > 8 || arr_body < 1 || arr_body > 8)
|
||||
PG_RETURN_NULL();
|
||||
if (dep_body == arr_body)
|
||||
PG_RETURN_NULL();
|
||||
|
||||
dep_jd = timestamptz_to_jd(dep_ts);
|
||||
arr_jd = timestamptz_to_jd(arr_ts);
|
||||
tof_days = arr_jd - dep_jd;
|
||||
|
||||
if (tof_days <= 0.0)
|
||||
PG_RETURN_NULL();
|
||||
|
||||
have_de = eph_de_planet(dep_body, dep_jd, r1) &&
|
||||
eph_de_planet(arr_body, arr_jd, r2);
|
||||
|
||||
if (!have_de)
|
||||
{
|
||||
int dep_vsop = dep_body - 1;
|
||||
int arr_vsop = arr_body - 1;
|
||||
|
||||
GetVsop87Coor(dep_jd, dep_vsop, r1);
|
||||
GetVsop87Coor(arr_jd, arr_vsop, r2);
|
||||
}
|
||||
|
||||
if (!lambert_solve_uv(r1, r2, tof_days, GAUSS_K2, 1, &lr))
|
||||
PG_RETURN_NULL();
|
||||
|
||||
planet_velocity_de(dep_body, dep_jd, have_de, v_planet_dep);
|
||||
|
||||
au_per_day_to_km_per_s = AU_KM / 86400.0;
|
||||
|
||||
for (k = 0; k < 3; k++)
|
||||
v_inf_dep[k] = (lr.v1[k] - v_planet_dep[k]) * au_per_day_to_km_per_s;
|
||||
|
||||
c3_dep = v_inf_dep[0]*v_inf_dep[0] +
|
||||
v_inf_dep[1]*v_inf_dep[1] +
|
||||
v_inf_dep[2]*v_inf_dep[2];
|
||||
|
||||
PG_RETURN_FLOAT8(c3_dep);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* Planetary moon observation with DE parent positions
|
||||
*
|
||||
* For each planetary moon, the moon-theory offset (L1.2, TASS17,
|
||||
* GUST86, MarsSat) is computed relative to its parent planet.
|
||||
* The parent's position comes from DE instead of VSOP87, giving
|
||||
* sub-arcsecond accuracy for the parent while keeping the
|
||||
* moon-theory accuracy for the relative offset.
|
||||
* ================================================================
|
||||
*/
|
||||
|
||||
/*
|
||||
* Internal: observe a planetary moon using DE for the parent planet
|
||||
* and Earth positions. Falls back to VSOP87 if DE is unavailable.
|
||||
*
|
||||
* moon_rel[3]: moon position relative to parent (ecliptic J2000, AU)
|
||||
* parent_body_id: pg_orrery body ID of parent (5=Jupiter, 6=Saturn, etc.)
|
||||
*/
|
||||
static void
|
||||
observe_moon_de(const double moon_rel[3], int parent_body_id,
|
||||
double jd, const pg_observer *obs,
|
||||
pg_topocentric *result)
|
||||
{
|
||||
double parent_xyz[6];
|
||||
double earth_xyz[6];
|
||||
double geo_ecl[3];
|
||||
bool have_de;
|
||||
|
||||
/* Rule 7: both parent and Earth from same provider */
|
||||
have_de = eph_de_planet(parent_body_id, jd, parent_xyz) &&
|
||||
eph_de_earth(jd, earth_xyz);
|
||||
|
||||
if (!have_de)
|
||||
{
|
||||
int vsop_parent = parent_body_id - 1;
|
||||
|
||||
if (eph_de_available())
|
||||
ereport(NOTICE,
|
||||
(errmsg("DE ephemeris unavailable, falling back to VSOP87")));
|
||||
|
||||
GetVsop87Coor(jd, vsop_parent, parent_xyz);
|
||||
GetVsop87Coor(jd, 2, earth_xyz); /* VSOP87 body 2 = Earth */
|
||||
}
|
||||
|
||||
/* Moon geocentric = (parent + moon_relative) - Earth */
|
||||
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];
|
||||
|
||||
observe_from_geocentric(geo_ecl, jd, obs, result);
|
||||
}
|
||||
|
||||
|
||||
Datum
|
||||
galilean_observe_de(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 body_id = PG_GETARG_INT32(0);
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
||||
int64 ts = PG_GETARG_INT64(2);
|
||||
double jd;
|
||||
double moon_xyz[3];
|
||||
pg_topocentric *result;
|
||||
|
||||
if (body_id < L12_IO || body_id > L12_CALLISTO)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("galilean_observe_de: body_id %d must be 0-3 (Io-Callisto)",
|
||||
body_id)));
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
GetL12Coor(jd, body_id, moon_xyz, NULL);
|
||||
|
||||
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
|
||||
observe_moon_de(moon_xyz, BODY_JUPITER, jd, obs, result);
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
|
||||
Datum
|
||||
saturn_moon_observe_de(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 body_id = PG_GETARG_INT32(0);
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
||||
int64 ts = PG_GETARG_INT64(2);
|
||||
double jd;
|
||||
double moon_xyz[3];
|
||||
pg_topocentric *result;
|
||||
|
||||
if (body_id < TASS17_MIMAS || body_id > TASS17_HYPERION)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("saturn_moon_observe_de: 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_topocentric *) palloc(sizeof(pg_topocentric));
|
||||
observe_moon_de(moon_xyz, BODY_SATURN, jd, obs, result);
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
|
||||
Datum
|
||||
uranus_moon_observe_de(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 body_id = PG_GETARG_INT32(0);
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
||||
int64 ts = PG_GETARG_INT64(2);
|
||||
double jd;
|
||||
double moon_xyz[3];
|
||||
pg_topocentric *result;
|
||||
|
||||
if (body_id < GUST86_MIRANDA || body_id > GUST86_OBERON)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("uranus_moon_observe_de: 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_topocentric *) palloc(sizeof(pg_topocentric));
|
||||
observe_moon_de(moon_xyz, BODY_URANUS, jd, obs, result);
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
|
||||
Datum
|
||||
mars_moon_observe_de(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 body_id = PG_GETARG_INT32(0);
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
||||
int64 ts = PG_GETARG_INT64(2);
|
||||
double jd;
|
||||
double moon_xyz[3];
|
||||
pg_topocentric *result;
|
||||
|
||||
if (body_id < MARS_SAT_PHOBOS || body_id > MARS_SAT_DEIMOS)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("mars_moon_observe_de: 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_topocentric *) palloc(sizeof(pg_topocentric));
|
||||
observe_moon_de(moon_xyz, BODY_MARS, jd, obs, result);
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* pg_orrery_ephemeris_info() -> RECORD
|
||||
*
|
||||
* Diagnostic function: returns current ephemeris provider status.
|
||||
* STABLE (not STRICT — no args), PARALLEL SAFE.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
pg_orrery_ephemeris_info(PG_FUNCTION_ARGS)
|
||||
{
|
||||
TupleDesc tupdesc;
|
||||
Datum values[6];
|
||||
bool nulls[6];
|
||||
HeapTuple tuple;
|
||||
|
||||
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("function returning record called in context "
|
||||
"that cannot accept type record")));
|
||||
|
||||
tupdesc = BlessTupleDesc(tupdesc);
|
||||
memset(nulls, 0, sizeof(nulls));
|
||||
|
||||
if (eph_de_available())
|
||||
{
|
||||
const char *path = eph_get_path();
|
||||
|
||||
values[0] = CStringGetTextDatum("JPL_DE");
|
||||
values[1] = path ? CStringGetTextDatum(path) : CStringGetTextDatum("");
|
||||
values[2] = Float8GetDatum(eph_de_start_jd());
|
||||
values[3] = Float8GetDatum(eph_de_end_jd());
|
||||
values[4] = Int32GetDatum(eph_de_version());
|
||||
values[5] = Float8GetDatum(eph_de_au_km());
|
||||
}
|
||||
else
|
||||
{
|
||||
values[0] = CStringGetTextDatum("VSOP87");
|
||||
|
||||
values[1] = (Datum) 0;
|
||||
values[2] = (Datum) 0;
|
||||
values[3] = (Datum) 0;
|
||||
values[4] = (Datum) 0;
|
||||
nulls[1] = true; /* no file path */
|
||||
nulls[2] = true; /* no start_jd */
|
||||
nulls[3] = true; /* no end_jd */
|
||||
nulls[4] = true; /* no version */
|
||||
|
||||
values[5] = Float8GetDatum((double)AU_KM);
|
||||
}
|
||||
|
||||
tuple = heap_form_tuple(tupdesc, values, nulls);
|
||||
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
|
||||
}
|
||||
694
src/de_reader.c
Normal file
694
src/de_reader.c
Normal file
@ -0,0 +1,694 @@
|
||||
/*
|
||||
* de_reader.c -- Clean-room JPL Development Ephemeris reader
|
||||
*
|
||||
* Reads JPL DE binary format (DE430/DE440/DE441).
|
||||
* No GPL dependency: derived from the public format specification.
|
||||
*
|
||||
* The JPL binary ephemeris consists of fixed-size records:
|
||||
* Record 1: Header (titles, constant names, JD range, layout table)
|
||||
* Record 2: Constant values
|
||||
* Records 3+: Chebyshev coefficients for all bodies
|
||||
*
|
||||
* Each data record covers a time interval (32 days for DE441).
|
||||
* Within a record, each body has Chebyshev coefficients for x,y,z
|
||||
* (or longitude/latitude/distance), possibly split into sub-intervals.
|
||||
*
|
||||
* Evaluation uses the Clenshaw recurrence for Chebyshev polynomials.
|
||||
*
|
||||
* Reference:
|
||||
* JPL IOM 312.N-03-009 (Standish 1998)
|
||||
* "Explanatory Supplement to the Astronomical Almanac" Ch. 8
|
||||
*/
|
||||
|
||||
#include "de_reader.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <math.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
/* swap_double() assumes 8-byte doubles (IEEE 754 on all platforms PG supports) */
|
||||
_Static_assert(sizeof(double) == 8, "DE reader requires 8-byte doubles");
|
||||
|
||||
/* Known AU value for byte-order detection */
|
||||
#define DE_AU_KNOWN 149597870.700
|
||||
|
||||
|
||||
/*
|
||||
* Byte-swap a double (for big-endian DE files on little-endian hosts).
|
||||
*/
|
||||
static void
|
||||
swap_double(double *val)
|
||||
{
|
||||
unsigned char *p = (unsigned char *)val;
|
||||
unsigned char tmp;
|
||||
|
||||
tmp = p[0]; p[0] = p[7]; p[7] = tmp;
|
||||
tmp = p[1]; p[1] = p[6]; p[6] = tmp;
|
||||
tmp = p[2]; p[2] = p[5]; p[5] = tmp;
|
||||
tmp = p[3]; p[3] = p[4]; p[4] = tmp;
|
||||
}
|
||||
|
||||
/*
|
||||
* Swap an array of doubles in-place.
|
||||
*/
|
||||
static void
|
||||
swap_doubles(double *arr, int count)
|
||||
{
|
||||
int i;
|
||||
for (i = 0; i < count; i++)
|
||||
swap_double(&arr[i]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Read exactly n bytes from fd at the given offset.
|
||||
* Returns 0 on success, -1 on failure.
|
||||
*/
|
||||
static int
|
||||
read_at(int fd, void *buf, size_t n, off_t offset)
|
||||
{
|
||||
ssize_t total = 0;
|
||||
ssize_t got;
|
||||
|
||||
if (lseek(fd, offset, SEEK_SET) == (off_t)-1)
|
||||
return -1;
|
||||
|
||||
while ((size_t)total < n)
|
||||
{
|
||||
got = read(fd, (char *)buf + total, n - total);
|
||||
if (got <= 0)
|
||||
return -1;
|
||||
total += got;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Evaluate a Chebyshev polynomial using the Clenshaw recurrence.
|
||||
*
|
||||
* coeffs: array of Chebyshev coefficients (T0, T1, ..., T_{n-1})
|
||||
* n: number of coefficients
|
||||
* x: normalized argument in [-1, +1]
|
||||
*
|
||||
* Returns: sum_{i=0}^{n-1} coeffs[i] * T_i(x)
|
||||
*/
|
||||
static double
|
||||
chebyshev_eval(const double *coeffs, int n, double x)
|
||||
{
|
||||
double bk1 = 0.0, bk2 = 0.0, bk;
|
||||
int i;
|
||||
|
||||
for (i = n - 1; i >= 1; i--)
|
||||
{
|
||||
bk = 2.0 * x * bk1 - bk2 + coeffs[i];
|
||||
bk2 = bk1;
|
||||
bk1 = bk;
|
||||
}
|
||||
return x * bk1 - bk2 + coeffs[0];
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Interpolate a single component (x, y, or z) for a body at a JD.
|
||||
*
|
||||
* h: reader handle (with record already loaded in record_buf)
|
||||
* body: body group index (0-12)
|
||||
* comp: component (0=x, 1=y, 2=z)
|
||||
* jd: Julian date
|
||||
*
|
||||
* Returns the interpolated value.
|
||||
*/
|
||||
static double
|
||||
interp_component(de_handle *h, int body, int comp, double jd)
|
||||
{
|
||||
de_body_layout *lay = &h->layout[body];
|
||||
double rec_start, sub_length, t_sub, x;
|
||||
int sub_idx, coeff_offset;
|
||||
|
||||
/* Record start JD */
|
||||
rec_start = h->record_buf[0];
|
||||
|
||||
/* Sub-interval length in days */
|
||||
sub_length = h->interval_days / lay->nsub;
|
||||
|
||||
/* Which sub-interval? */
|
||||
t_sub = jd - rec_start;
|
||||
sub_idx = (int)(t_sub / sub_length);
|
||||
if (sub_idx >= lay->nsub)
|
||||
sub_idx = lay->nsub - 1;
|
||||
if (sub_idx < 0)
|
||||
sub_idx = 0;
|
||||
|
||||
/* Normalize to [-1, +1] within the sub-interval */
|
||||
x = 2.0 * (t_sub - sub_idx * sub_length) / sub_length - 1.0;
|
||||
|
||||
/* Clamp to [-1, +1]: floating-point arithmetic at interval
|
||||
* boundaries can produce |x| slightly > 1.0, which causes
|
||||
* Chebyshev polynomials to diverge. Legitimate rounding should
|
||||
* never exceed ~1e-14; anything larger indicates a structural
|
||||
* arithmetic error in the normalization above. */
|
||||
if (x > 1.0)
|
||||
{
|
||||
assert(x < 1.0 + 1e-10);
|
||||
x = 1.0;
|
||||
}
|
||||
if (x < -1.0)
|
||||
{
|
||||
assert(x > -1.0 - 1e-10);
|
||||
x = -1.0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Coefficient offset in the record buffer.
|
||||
* Layout offset is 1-based (Fortran convention), convert to 0-based.
|
||||
* Each sub-interval has ncoeff coefficients for each of 3 components.
|
||||
*/
|
||||
coeff_offset = (lay->offset - 1)
|
||||
+ sub_idx * lay->ncoeff * 3
|
||||
+ comp * lay->ncoeff;
|
||||
|
||||
return chebyshev_eval(&h->record_buf[coeff_offset], lay->ncoeff, x);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Load the record containing the given JD into the handle's buffer.
|
||||
* Returns DE_OK or DE_ERR_*.
|
||||
*/
|
||||
static int
|
||||
load_record(de_handle *h, double jd)
|
||||
{
|
||||
int recno;
|
||||
off_t offset;
|
||||
|
||||
if (jd < h->start_jd || jd > h->end_jd)
|
||||
return DE_ERR_RANGE;
|
||||
|
||||
recno = (int)((jd - h->start_jd) / h->interval_days);
|
||||
|
||||
/* Clamp to last record if jd == end_jd */
|
||||
{
|
||||
int max_rec = (int)((h->end_jd - h->start_jd) / h->interval_days) - 1;
|
||||
if (recno > max_rec)
|
||||
recno = max_rec;
|
||||
}
|
||||
|
||||
/* Already cached? */
|
||||
if (recno == h->cached_recno)
|
||||
return DE_OK;
|
||||
|
||||
/* Seek and read the record (skip 2 header records) */
|
||||
offset = h->data_offset + (off_t)recno * h->record_bytes;
|
||||
|
||||
if (read_at(h->fd, h->record_buf, h->record_bytes, offset) != 0)
|
||||
return DE_ERR_READ;
|
||||
|
||||
if (h->swap_bytes)
|
||||
swap_doubles(h->record_buf, h->ncoeff);
|
||||
|
||||
h->cached_recno = recno;
|
||||
return DE_OK;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Parse the header and validate the ephemeris file.
|
||||
* Supports DE405 and later (DE430, DE440, DE441).
|
||||
* Earlier formats have different header sizes and are not supported.
|
||||
*/
|
||||
static int
|
||||
parse_header(de_handle *h)
|
||||
{
|
||||
/*
|
||||
* The header spans 2 records of ncoeff doubles each.
|
||||
* But we don't know ncoeff yet — it's at a fixed position
|
||||
* in the first record.
|
||||
*
|
||||
* Header layout (byte offsets for first record):
|
||||
* 0..251: 3 title lines (84 chars each)
|
||||
* 252..2651: 400 constant names (6 chars each)
|
||||
* 2652..2675: SS[3] = {start_jd, end_jd, interval_days}
|
||||
* 2676..2679: NCON (int32)
|
||||
* 2680..2687: AU (double)
|
||||
* 2688..2695: EMRAT (double)
|
||||
* 2696..2839: IPT[12][3] = 12 body groups x 3 ints (offset, ncoeff, nsub)
|
||||
* 2840..2843: DE version number (int32)
|
||||
* 2844..2855: IPT[12][3] for 13th body (librations)
|
||||
*
|
||||
* After IPT parsing, we know ncoeff and can size the record buffer.
|
||||
*
|
||||
* All byte offsets assume the first record starts at byte 0.
|
||||
* The record size in bytes = ncoeff * 8.
|
||||
*/
|
||||
|
||||
unsigned char buf[4096]; /* DE405+ header fits in 4096 bytes */
|
||||
double ss[3];
|
||||
double au_val;
|
||||
int32_t ncon;
|
||||
int ipt[13][3];
|
||||
int32_t de_ver;
|
||||
int i, j;
|
||||
int max_offset_needed;
|
||||
|
||||
/* Read the first 4096 bytes of the file */
|
||||
if (read_at(h->fd, buf, 4096, 0) != 0)
|
||||
return DE_ERR_READ;
|
||||
|
||||
/* SS: start_jd, end_jd, interval (at byte 2652) */
|
||||
memcpy(ss, buf + 2652, 3 * sizeof(double));
|
||||
|
||||
/* AU (at byte 2680) */
|
||||
memcpy(&au_val, buf + 2680, sizeof(double));
|
||||
|
||||
/*
|
||||
* Byte-order detection: compare AU against known value.
|
||||
* If it doesn't match, try byte-swapping.
|
||||
*/
|
||||
h->swap_bytes = 0;
|
||||
if (fabs(au_val - DE_AU_KNOWN) > 1.0)
|
||||
{
|
||||
swap_double(&au_val);
|
||||
if (fabs(au_val - DE_AU_KNOWN) > 1.0)
|
||||
return DE_ERR_ENDIAN;
|
||||
h->swap_bytes = 1;
|
||||
swap_doubles(ss, 3);
|
||||
}
|
||||
|
||||
h->start_jd = ss[0];
|
||||
h->end_jd = ss[1];
|
||||
h->interval_days = ss[2];
|
||||
h->au_km = au_val;
|
||||
|
||||
/* NCON at byte 2676 */
|
||||
memcpy(&ncon, buf + 2676, sizeof(int32_t));
|
||||
if (h->swap_bytes)
|
||||
{
|
||||
unsigned char *p = (unsigned char *)&ncon;
|
||||
unsigned char tmp;
|
||||
tmp = p[0]; p[0] = p[3]; p[3] = tmp;
|
||||
tmp = p[1]; p[1] = p[2]; p[2] = tmp;
|
||||
}
|
||||
|
||||
/* EMRAT at byte 2688 */
|
||||
memcpy(&h->emrat, buf + 2688, sizeof(double));
|
||||
if (h->swap_bytes)
|
||||
swap_double(&h->emrat);
|
||||
|
||||
/* IPT: 12 body groups at byte 2696, each 3 x int32 */
|
||||
for (i = 0; i < 12; i++)
|
||||
{
|
||||
int32_t vals[3];
|
||||
memcpy(vals, buf + 2696 + i * 12, 12);
|
||||
if (h->swap_bytes)
|
||||
{
|
||||
for (j = 0; j < 3; j++)
|
||||
{
|
||||
unsigned char *p = (unsigned char *)&vals[j];
|
||||
unsigned char tmp;
|
||||
tmp = p[0]; p[0] = p[3]; p[3] = tmp;
|
||||
tmp = p[1]; p[1] = p[2]; p[2] = tmp;
|
||||
}
|
||||
}
|
||||
ipt[i][0] = vals[0];
|
||||
ipt[i][1] = vals[1];
|
||||
ipt[i][2] = vals[2];
|
||||
}
|
||||
|
||||
/* DE version at byte 2840 */
|
||||
memcpy(&de_ver, buf + 2840, sizeof(int32_t));
|
||||
if (h->swap_bytes)
|
||||
{
|
||||
unsigned char *p = (unsigned char *)&de_ver;
|
||||
unsigned char tmp;
|
||||
tmp = p[0]; p[0] = p[3]; p[3] = tmp;
|
||||
tmp = p[1]; p[1] = p[2]; p[2] = tmp;
|
||||
}
|
||||
h->de_version = de_ver;
|
||||
|
||||
/* IPT[12] (librations) at byte 2844 */
|
||||
{
|
||||
int32_t vals[3];
|
||||
memcpy(vals, buf + 2844, 12);
|
||||
if (h->swap_bytes)
|
||||
{
|
||||
for (j = 0; j < 3; j++)
|
||||
{
|
||||
unsigned char *p = (unsigned char *)&vals[j];
|
||||
unsigned char tmp;
|
||||
tmp = p[0]; p[0] = p[3]; p[3] = tmp;
|
||||
tmp = p[1]; p[1] = p[2]; p[2] = tmp;
|
||||
}
|
||||
}
|
||||
ipt[12][0] = vals[0];
|
||||
ipt[12][1] = vals[1];
|
||||
ipt[12][2] = vals[2];
|
||||
}
|
||||
|
||||
/* Store layout and compute ncoeff */
|
||||
max_offset_needed = 0;
|
||||
for (i = 0; i < DE_NUM_BODIES; i++)
|
||||
{
|
||||
h->layout[i].offset = ipt[i][0];
|
||||
h->layout[i].ncoeff = ipt[i][1];
|
||||
h->layout[i].nsub = ipt[i][2];
|
||||
|
||||
if (ipt[i][0] > 0 && ipt[i][1] > 0 && ipt[i][2] > 0)
|
||||
{
|
||||
int end;
|
||||
int ncomp = (i == DE_NUTATION) ? 2 : 3;
|
||||
end = (ipt[i][0] - 1) + ipt[i][1] * ncomp * ipt[i][2];
|
||||
if (end > max_offset_needed)
|
||||
max_offset_needed = end;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* ncoeff: the header record size in doubles.
|
||||
* The record must be large enough to hold all coefficient data
|
||||
* plus the 2-double JD range at the start of each data record.
|
||||
*/
|
||||
h->ncoeff = max_offset_needed;
|
||||
if (h->ncoeff < 2)
|
||||
return DE_ERR_HEADER;
|
||||
|
||||
h->record_bytes = h->ncoeff * sizeof(double);
|
||||
|
||||
/* Data starts after 2 header records */
|
||||
h->data_offset = 2L * h->record_bytes;
|
||||
|
||||
/* Validate: start_jd < end_jd, interval > 0 */
|
||||
if (h->start_jd >= h->end_jd || h->interval_days <= 0.0)
|
||||
return DE_ERR_HEADER;
|
||||
|
||||
return DE_OK;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Canary validation: evaluate Earth at J2000.0 and check against
|
||||
* known DE441 reference position.
|
||||
*
|
||||
* Expected Earth ICRS position at J2000.0 (JD 2451545.0):
|
||||
* x ~ -0.1771 AU, y ~ 0.8873 AU, z ~ 0.3848 AU
|
||||
*
|
||||
* Tolerance: 0.01 AU (very loose — just catches gross file corruption).
|
||||
*/
|
||||
static int
|
||||
canary_check(de_handle *h)
|
||||
{
|
||||
double pos[3];
|
||||
int err;
|
||||
|
||||
err = de_reader_get_pos(h, 2451545.0, DE_EMB, DE_SUN, pos);
|
||||
if (err != DE_OK)
|
||||
return DE_ERR_CANARY;
|
||||
|
||||
/* Earth-Sun distance varies 0.983-1.017 AU over the year.
|
||||
* Tight tolerance catches garbled files that 0.9-1.1 would miss. */
|
||||
{
|
||||
double dist = sqrt(pos[0]*pos[0] + pos[1]*pos[1] + pos[2]*pos[2]);
|
||||
if (dist < 0.97 || dist > 1.04)
|
||||
return DE_ERR_CANARY;
|
||||
}
|
||||
|
||||
return DE_OK;
|
||||
}
|
||||
|
||||
|
||||
de_handle *
|
||||
de_reader_open(const char *path, int *errcode)
|
||||
{
|
||||
de_handle *h;
|
||||
int err;
|
||||
|
||||
h = (de_handle *)calloc(1, sizeof(de_handle));
|
||||
if (!h)
|
||||
{
|
||||
*errcode = DE_ERR_OPEN;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
h->fd = -1;
|
||||
h->cached_recno = -1;
|
||||
h->record_buf = NULL;
|
||||
|
||||
/* Open the ephemeris file (read-only, close-on-exec to prevent FD
|
||||
* leaks to child processes such as COPY ... PROGRAM or archiving) */
|
||||
h->fd = open(path, O_RDONLY | O_CLOEXEC);
|
||||
if (h->fd < 0)
|
||||
{
|
||||
*errcode = DE_ERR_OPEN;
|
||||
free(h);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Parse header */
|
||||
err = parse_header(h);
|
||||
if (err != DE_OK)
|
||||
{
|
||||
*errcode = err;
|
||||
close(h->fd);
|
||||
free(h);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Allocate record buffer */
|
||||
h->record_buf = (double *)calloc(h->ncoeff, sizeof(double));
|
||||
if (!h->record_buf)
|
||||
{
|
||||
*errcode = DE_ERR_OPEN;
|
||||
close(h->fd);
|
||||
free(h);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Canary check */
|
||||
err = canary_check(h);
|
||||
if (err != DE_OK)
|
||||
{
|
||||
*errcode = err;
|
||||
free(h->record_buf);
|
||||
close(h->fd);
|
||||
free(h);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*errcode = DE_OK;
|
||||
return h;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Get raw body position (3 components) from the ephemeris.
|
||||
* No center subtraction. Returns position in AU (ICRS equatorial).
|
||||
*/
|
||||
static int
|
||||
get_raw_pos(de_handle *h, double jd, int body, double pos[3])
|
||||
{
|
||||
int err;
|
||||
|
||||
if (body < 0 || body > DE_SUN)
|
||||
return DE_ERR_BODY;
|
||||
|
||||
/* Nutation and libration are not position queries */
|
||||
if (body == DE_NUTATION || body == DE_LIBRATION)
|
||||
return DE_ERR_BODY;
|
||||
|
||||
/* Body exists in the DE body table but may have no coefficients
|
||||
* in this particular DE edition (e.g., Pluto omitted from some files).
|
||||
* Returns DE_ERR_BODY since callers handle both "invalid index" and
|
||||
* "not present" by falling back to VSOP87. */
|
||||
{
|
||||
de_body_layout *lay = &h->layout[body];
|
||||
if (lay->offset <= 0 || lay->ncoeff <= 0 || lay->nsub <= 0)
|
||||
return DE_ERR_BODY;
|
||||
}
|
||||
|
||||
err = load_record(h, jd);
|
||||
if (err != DE_OK)
|
||||
return err;
|
||||
|
||||
pos[0] = interp_component(h, body, 0, jd);
|
||||
pos[1] = interp_component(h, body, 1, jd);
|
||||
pos[2] = interp_component(h, body, 2, jd);
|
||||
|
||||
/* Convert from km to AU */
|
||||
pos[0] /= h->au_km;
|
||||
pos[1] /= h->au_km;
|
||||
pos[2] /= h->au_km;
|
||||
|
||||
return DE_OK;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Derive Earth position from Earth-Moon Barycenter and Moon.
|
||||
*
|
||||
* Earth = EMB - Moon * (1 / (1 + EMRAT))
|
||||
*
|
||||
* where EMRAT = M_earth / M_moon ~ 81.3.
|
||||
* The Moon position in the DE file is geocentric, so:
|
||||
* Earth = EMB - Moon / (1 + EMRAT)
|
||||
*/
|
||||
static int
|
||||
get_earth_pos(de_handle *h, double jd, double pos[3])
|
||||
{
|
||||
double emb[3], moon[3];
|
||||
double factor;
|
||||
int err;
|
||||
|
||||
err = get_raw_pos(h, jd, DE_EMB, emb);
|
||||
if (err != DE_OK)
|
||||
return err;
|
||||
|
||||
err = get_raw_pos(h, jd, DE_MOON, moon);
|
||||
if (err != DE_OK)
|
||||
return err;
|
||||
|
||||
factor = 1.0 / (1.0 + h->emrat);
|
||||
|
||||
pos[0] = emb[0] - moon[0] * factor;
|
||||
pos[1] = emb[1] - moon[1] * factor;
|
||||
pos[2] = emb[2] - moon[2] * factor;
|
||||
|
||||
return DE_OK;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
de_reader_get_pos(de_handle *h, double jd, int target, int center,
|
||||
double pos[3])
|
||||
{
|
||||
double tpos[3], cpos[3];
|
||||
int err;
|
||||
|
||||
if (!h)
|
||||
return DE_ERR_OPEN;
|
||||
|
||||
/* Get target position */
|
||||
if (target == DE_EMB)
|
||||
{
|
||||
/* EMB is stored directly */
|
||||
err = get_raw_pos(h, jd, DE_EMB, tpos);
|
||||
}
|
||||
else if (target == DE_MOON && center != -1)
|
||||
{
|
||||
/*
|
||||
* Moon in the file is geocentric. For Moon relative to
|
||||
* something other than Earth, we need Earth + geocentric Moon.
|
||||
*/
|
||||
double earth[3], moon_geo[3];
|
||||
|
||||
err = get_earth_pos(h, jd, earth);
|
||||
if (err != DE_OK)
|
||||
return err;
|
||||
|
||||
err = get_raw_pos(h, jd, DE_MOON, moon_geo);
|
||||
if (err != DE_OK)
|
||||
return err;
|
||||
|
||||
/* Moon barycentric = Earth + geocentric_moon */
|
||||
tpos[0] = earth[0] + moon_geo[0];
|
||||
tpos[1] = earth[1] + moon_geo[1];
|
||||
tpos[2] = earth[2] + moon_geo[2];
|
||||
}
|
||||
else if (target >= DE_MERCURY && target <= DE_PLUTO && target != DE_EMB)
|
||||
{
|
||||
/* Planets are stored directly as SSB-relative */
|
||||
err = get_raw_pos(h, jd, target, tpos);
|
||||
}
|
||||
else if (target == DE_SUN)
|
||||
{
|
||||
err = get_raw_pos(h, jd, DE_SUN, tpos);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Raw mode for other body types */
|
||||
err = get_raw_pos(h, jd, target, tpos);
|
||||
}
|
||||
|
||||
if (err != DE_OK)
|
||||
return err;
|
||||
|
||||
/* No center subtraction requested */
|
||||
if (center < 0)
|
||||
{
|
||||
pos[0] = tpos[0];
|
||||
pos[1] = tpos[1];
|
||||
pos[2] = tpos[2];
|
||||
return DE_OK;
|
||||
}
|
||||
|
||||
/* Get center position */
|
||||
if (center == DE_SUN)
|
||||
{
|
||||
err = get_raw_pos(h, jd, DE_SUN, cpos);
|
||||
}
|
||||
else if (center == DE_EMB)
|
||||
{
|
||||
err = get_raw_pos(h, jd, DE_EMB, cpos);
|
||||
}
|
||||
else if (center == 99)
|
||||
{
|
||||
/* Special code for "Earth" as center */
|
||||
err = get_earth_pos(h, jd, cpos);
|
||||
}
|
||||
else if (center >= DE_MERCURY && center <= DE_PLUTO)
|
||||
{
|
||||
err = get_raw_pos(h, jd, center, cpos);
|
||||
}
|
||||
else
|
||||
{
|
||||
return DE_ERR_BODY;
|
||||
}
|
||||
|
||||
if (err != DE_OK)
|
||||
return err;
|
||||
|
||||
pos[0] = tpos[0] - cpos[0];
|
||||
pos[1] = tpos[1] - cpos[1];
|
||||
pos[2] = tpos[2] - cpos[2];
|
||||
|
||||
return DE_OK;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
de_reader_close(de_handle *h)
|
||||
{
|
||||
if (!h)
|
||||
return;
|
||||
|
||||
if (h->fd >= 0)
|
||||
close(h->fd);
|
||||
|
||||
if (h->record_buf)
|
||||
free(h->record_buf);
|
||||
|
||||
free(h);
|
||||
}
|
||||
|
||||
|
||||
double
|
||||
de_reader_get_const(de_handle *h, const char *name)
|
||||
{
|
||||
/*
|
||||
* Constant lookup requires reading constant names from record 1
|
||||
* and values from record 2. For now, we expose the key values
|
||||
* that are already parsed from the header.
|
||||
*/
|
||||
if (!h || !name)
|
||||
return NAN;
|
||||
|
||||
if (strcmp(name, "AU") == 0)
|
||||
return h->au_km;
|
||||
if (strcmp(name, "EMRAT") == 0)
|
||||
return h->emrat;
|
||||
|
||||
/* NAN sentinel: callers use isnan() to distinguish "not found"
|
||||
* from a legitimate zero-valued constant. */
|
||||
return NAN;
|
||||
}
|
||||
140
src/de_reader.h
Normal file
140
src/de_reader.h
Normal file
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* de_reader.h -- Clean-room JPL Development Ephemeris reader
|
||||
*
|
||||
* Reads JPL DE430/DE440/DE441 binary ephemeris files.
|
||||
* Implements Chebyshev polynomial evaluation via Clenshaw recurrence.
|
||||
*
|
||||
* No GPL dependency: written from the public JPL binary format spec.
|
||||
* No global state: each handle is independently opened/managed.
|
||||
*
|
||||
* Reference:
|
||||
* JPL IOM 312.N-03-009 "The JPL Planetary and Lunar Ephemerides,
|
||||
* DE405/LE405" (Standish 1998) — format description.
|
||||
*/
|
||||
|
||||
#ifndef PG_ORRERY_DE_READER_H
|
||||
#define PG_ORRERY_DE_READER_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* JPL DE body targets.
|
||||
*
|
||||
* The ephemeris stores 13 "body groups" in a specific order.
|
||||
* Each group has its own coefficient layout (offset, ncoeff, nsub).
|
||||
*/
|
||||
#define DE_MERCURY 0
|
||||
#define DE_VENUS 1
|
||||
#define DE_EMB 2 /* Earth-Moon Barycenter */
|
||||
#define DE_MARS 3
|
||||
#define DE_JUPITER 4
|
||||
#define DE_SATURN 5
|
||||
#define DE_URANUS 6
|
||||
#define DE_NEPTUNE 7
|
||||
#define DE_PLUTO 8
|
||||
#define DE_MOON 9 /* Moon (geocentric) */
|
||||
#define DE_SUN 10
|
||||
#define DE_NUTATION 11 /* nutations (dpsi, deps) */
|
||||
#define DE_LIBRATION 12 /* lunar librations */
|
||||
#define DE_NUM_BODIES 13
|
||||
|
||||
/*
|
||||
* DE reader error codes.
|
||||
*/
|
||||
#define DE_OK 0
|
||||
#define DE_ERR_OPEN -1 /* cannot open file */
|
||||
#define DE_ERR_READ -2 /* read() failed or short read */
|
||||
#define DE_ERR_HEADER -3 /* header validation failed */
|
||||
#define DE_ERR_RANGE -4 /* JD out of ephemeris range */
|
||||
#define DE_ERR_BODY -5 /* invalid body target */
|
||||
#define DE_ERR_CANARY -6 /* canary validation failed */
|
||||
#define DE_ERR_ENDIAN -7 /* byte order detection failed */
|
||||
|
||||
/*
|
||||
* Coefficient layout for one body group.
|
||||
* Parsed from the header's IPT array.
|
||||
*/
|
||||
typedef struct de_body_layout
|
||||
{
|
||||
int offset; /* word offset within record (1-based) */
|
||||
int ncoeff; /* number of Chebyshev coefficients per component */
|
||||
int nsub; /* number of sub-intervals per record interval */
|
||||
} de_body_layout;
|
||||
|
||||
/*
|
||||
* DE reader handle.
|
||||
*
|
||||
* One per PostgreSQL backend. Owns its file descriptor and
|
||||
* a pre-allocated coefficient buffer.
|
||||
*/
|
||||
typedef struct de_handle
|
||||
{
|
||||
int fd; /* file descriptor */
|
||||
int swap_bytes; /* 1 if byte-swapping needed */
|
||||
|
||||
/* Header metadata */
|
||||
double start_jd; /* first valid JD */
|
||||
double end_jd; /* last valid JD */
|
||||
double interval_days; /* days per record */
|
||||
double au_km; /* AU in km (from header) */
|
||||
double emrat; /* Earth-Moon mass ratio */
|
||||
int ncoeff; /* number of doubles per record */
|
||||
int de_version; /* e.g. 441 */
|
||||
|
||||
/* Coefficient layout per body group */
|
||||
de_body_layout layout[DE_NUM_BODIES];
|
||||
|
||||
/* Record buffer: re-used across queries */
|
||||
double *record_buf; /* ncoeff doubles */
|
||||
int cached_recno; /* which record is loaded, -1 = none */
|
||||
|
||||
/* Record geometry */
|
||||
int record_bytes; /* ncoeff * sizeof(double) */
|
||||
long data_offset; /* byte offset to first data record */
|
||||
} de_handle;
|
||||
|
||||
|
||||
/*
|
||||
* Open and validate a JPL DE binary file.
|
||||
*
|
||||
* Returns a heap-allocated handle on success, NULL on failure.
|
||||
* Sets *errcode to one of DE_OK / DE_ERR_*.
|
||||
* Caller must eventually call de_reader_close().
|
||||
*
|
||||
* Does NOT use ereport() — caller translates error codes.
|
||||
*/
|
||||
de_handle *de_reader_open(const char *path, int *errcode);
|
||||
|
||||
|
||||
/*
|
||||
* Get the position of a body relative to a center.
|
||||
*
|
||||
* target: DE_MERCURY..DE_SUN (0-10)
|
||||
* center: DE_SUN (10) for heliocentric, DE_EMB (2) for geocentric, etc.
|
||||
* Use -1 for "raw" (no center subtraction)
|
||||
* jd: Julian date (TDB)
|
||||
* pos[3]: output position in AU (ICRS equatorial frame)
|
||||
*
|
||||
* Returns DE_OK on success, DE_ERR_* on failure.
|
||||
*
|
||||
* For Earth position: returns Earth (derived from EMB and Moon)
|
||||
* For Moon position with center=Earth: returns geocentric Moon
|
||||
*/
|
||||
int de_reader_get_pos(de_handle *h, double jd, int target, int center,
|
||||
double pos[3]);
|
||||
|
||||
|
||||
/*
|
||||
* Close the DE reader handle and free all resources.
|
||||
* Safe to call with NULL handle.
|
||||
*/
|
||||
void de_reader_close(de_handle *h);
|
||||
|
||||
|
||||
/*
|
||||
* Look up a named constant from the DE header.
|
||||
* Returns the value, or NAN if not found (use isnan() to check).
|
||||
*/
|
||||
double de_reader_get_const(de_handle *h, const char *name);
|
||||
|
||||
#endif /* PG_ORRERY_DE_READER_H */
|
||||
149
src/elliptic_to_rectangular.c
Normal file
149
src/elliptic_to_rectangular.c
Normal file
@ -0,0 +1,149 @@
|
||||
/************************************************************************
|
||||
|
||||
The code in this file is heavily inspired by the TASS17 and GUST86 theories
|
||||
found on
|
||||
ftp://ftp.imcce.fr/pub/ephem/satel
|
||||
|
||||
I (Johannes Gajdosik) have just taken the Fortran code and data
|
||||
obtained from above and rearranged it into this piece of software.
|
||||
|
||||
I can neither allow nor forbid the above theories.
|
||||
The copyright notice below covers just my work,
|
||||
that is the compilation of the data obtained from above
|
||||
into the software supplied in this file.
|
||||
|
||||
|
||||
Copyright (c) 2005 Johannes Gajdosik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
****************************************************************/
|
||||
|
||||
#include "elliptic_to_rectangular.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
|
||||
/*
|
||||
Given the orbital elements at some time t0 calculate the
|
||||
rectangular coordinates at time (t0+dt).
|
||||
|
||||
mu = G*(m1+m2) .. gravitational constant of the two body problem
|
||||
a .. semi major axis
|
||||
n = mean motion = 2*M_PI/(orbit period)
|
||||
|
||||
elem[0] .. irrelevant (either a (called by EllipticToRectangularA()) or n (called by EllipticToRectangularN())
|
||||
elem[1] .. L
|
||||
elem[2] .. K=e*cos(Omega+omega)
|
||||
elem[3] .. H=e*sin(Omega+omega)
|
||||
elem[4] .. Q=sin(i/2)*cos(Omega)
|
||||
elem[5] .. P=sin(i/2)*sin(Omega)
|
||||
|
||||
Omega = longitude of ascending node
|
||||
omega = argument of pericenter
|
||||
L = mean longitude = Omega + omega + M
|
||||
M = mean anomaly
|
||||
i = inclination
|
||||
e = eccentricity
|
||||
|
||||
Units are suspected to be: Julian days, AU, rad
|
||||
*/
|
||||
static void
|
||||
EllipticToRectangular(const double a,const double n,
|
||||
const double elem[6],const double dt,double xyz[]) {
|
||||
const double L = fmod(elem[1]+n*dt,2.0*M_PI);
|
||||
/* solve Keplers equation
|
||||
x = L - elem[2]*sin(x) + elem[3]*cos(x)
|
||||
not by trivially iterating
|
||||
x_0 = L
|
||||
x_{j+1} = L - elem[2]*sin(x_j) + elem[3]*cos(x_j)
|
||||
but instead by Newton approximation:
|
||||
0 = f(x) = x - L - elem[2]*sin(x) + elem[3]*cos(x)
|
||||
f'(x) = 1 - elem[2]*cos(x) - elem[3]*sin(x)
|
||||
x_0 = L or whatever, perhaps first step of trivial iteration
|
||||
x_{j+1} = x_j - f(x_j)/f'(x_j)
|
||||
*/
|
||||
double Le = L - elem[2]*sin(L) + elem[3]*cos(L);
|
||||
for (;;) {
|
||||
const double cLe = cos(Le);
|
||||
const double sLe = sin(Le);
|
||||
/* for eccentricity < 1 we have denominator > 0 */
|
||||
const double dLe = (L - Le + elem[2]*sLe - elem[3]*cLe)
|
||||
/ (1.0 - elem[2]*cLe - elem[3]*sLe);
|
||||
Le += dLe;
|
||||
if (fabs(dLe) <= 1e-14) break; /* L1: <1e-12 */
|
||||
}
|
||||
|
||||
{
|
||||
const double cLe = cos(Le);
|
||||
const double sLe = sin(Le);
|
||||
|
||||
const double dlf = -elem[2]*sLe + elem[3]*cLe;
|
||||
const double phi = sqrt(1.0 - elem[2]*elem[2] - elem[3]*elem[3]);
|
||||
const double psi = 1.0 / (1.0 + phi);
|
||||
|
||||
const double x1 = a * (cLe - elem[2] - psi*dlf*elem[3]);
|
||||
const double y1 = a * (sLe - elem[3] + psi*dlf*elem[2]);
|
||||
|
||||
const double elem_4q = elem[4] * elem[4]; // Q²
|
||||
const double elem_5q = elem[5] * elem[5]; // P²
|
||||
const double dwho = 2.0 * sqrt(1.0 - elem_4q - elem_5q);
|
||||
const double rtp = 1.0 - elem_5q - elem_5q;
|
||||
const double rtq = 1.0 - elem_4q - elem_4q;
|
||||
const double rdg = 2.0 * elem[5] * elem[4];
|
||||
|
||||
xyz[0] = x1 * rtp + y1 * rdg;
|
||||
xyz[1] = x1 * rdg + y1 * rtq;
|
||||
xyz[2] = (-x1 * elem[5] + y1 * elem[4]) * dwho;
|
||||
|
||||
// GZ 2017-11: Re-enable these lines, they provide velocity!
|
||||
const double rsam1 = -elem[2]*cLe - elem[3]*sLe;
|
||||
const double h = a*n / (1.0 + rsam1);
|
||||
const double vx1 = h * (-sLe - psi*rsam1*elem[3]);
|
||||
const double vy1 = h * ( cLe + psi*rsam1*elem[2]);
|
||||
|
||||
xyz[3] = vx1 * rtp + vy1 * rdg;
|
||||
xyz[4] = vx1 * rdg + vy1 * rtq;
|
||||
xyz[5] = (-vx1 * elem[5] + vy1 * elem[4]) * dwho;
|
||||
}
|
||||
}
|
||||
|
||||
void EllipticToRectangularN(double mu,const double elem[6],double dt,
|
||||
double xyz[]) {
|
||||
const double n = elem[0];
|
||||
#if defined __USE_MISC || defined __USE_XOPEN_EXTENDED || defined __USE_ISOC99
|
||||
/* linux math.h declares cbrt: */
|
||||
const double a = cbrt(mu/(n*n));
|
||||
#else
|
||||
const double a = exp(log(mu/(n*n))/3.0);
|
||||
#endif
|
||||
EllipticToRectangular(a,n,elem,dt,xyz);
|
||||
}
|
||||
|
||||
void EllipticToRectangularA(double mu,const double elem[6],double dt,
|
||||
double xyz[]) {
|
||||
const double a = elem[0];
|
||||
const double n = sqrt(mu/(a*a*a)); // mean motion
|
||||
EllipticToRectangular(a,n,elem,dt,xyz);
|
||||
}
|
||||
|
||||
84
src/elliptic_to_rectangular.h
Normal file
84
src/elliptic_to_rectangular.h
Normal file
@ -0,0 +1,84 @@
|
||||
/************************************************************************
|
||||
|
||||
The code in this file is heavily inspired by the TASS17 and GUST86 theories
|
||||
found on
|
||||
ftp://ftp.imcce.fr/pub/ephem/satel
|
||||
|
||||
I (Johannes Gajdosik) have just taken the Fortran code and data
|
||||
obtained from above and rearranged it into this piece of software.
|
||||
|
||||
I can neither allow nor forbid the above theories.
|
||||
The copyright notice below covers just my work,
|
||||
that is the compilation of the data obtained from above
|
||||
into the software supplied in this file.
|
||||
|
||||
|
||||
Copyright (c) 2005 Johannes Gajdosik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
****************************************************************/
|
||||
|
||||
#ifndef ELLIPTIC_TO_RECTANGULAR_H
|
||||
#define ELLIPTIC_TO_RECTANGULAR_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
Given the orbital elements at some time t0 calculate the
|
||||
rectangular coordinates at time (t0+dt).
|
||||
|
||||
mu = G*(m1+m2) .. gravitational constant of the two body problem
|
||||
a .. semi major axis
|
||||
n = mean motion = 2*M_PI/(orbit period)
|
||||
|
||||
elem[0] .. either a (EllipticToRectangularA()) or n (EllipticToRectangularN())
|
||||
elem[1] .. L
|
||||
elem[2] .. K=e*cos(Omega+omega)
|
||||
elem[3] .. H=e*sin(Omega+omega)
|
||||
elem[4] .. Q=sin(i/2)*cos(Omega)
|
||||
elem[5] .. P=sin(i/2)*sin(Omega)
|
||||
|
||||
Omega = longitude of ascending node
|
||||
omega = argument of pericenter
|
||||
L = mean longitude = Omega + omega + M
|
||||
M = mean anomaly
|
||||
i = inclination
|
||||
e = eccentricity
|
||||
|
||||
Units are suspected to be: Julian days, AU, rad
|
||||
|
||||
Results:
|
||||
xyz[0,1,2]=Position [AU]
|
||||
xyz[3,4,5]=Velocity [AU/d]
|
||||
|
||||
*/
|
||||
|
||||
void EllipticToRectangularN(double mu,const double elem[6],double dt,
|
||||
double xyz[]);
|
||||
void EllipticToRectangularA(double mu,const double elem[6],double dt,
|
||||
double xyz[]);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
162761
src/elp82b.c
Normal file
162761
src/elp82b.c
Normal file
File diff suppressed because it is too large
Load Diff
92
src/elp82b.h
Normal file
92
src/elp82b.h
Normal file
@ -0,0 +1,92 @@
|
||||
/************************************************************************
|
||||
|
||||
LUNAR SOLUTION ELP2000-82B
|
||||
by Chapront-Touze M., Chapront J.
|
||||
ftp://ftp.imcce.fr/pub/ephem/moon/elp82b
|
||||
|
||||
I (Johannes Gajdosik) have just taken the Fortran code and data
|
||||
obtained from above and used it to create this piece of software.
|
||||
|
||||
I can neither allow nor forbid the usage of ELP2000-82B.
|
||||
The copyright notice below covers not the works of
|
||||
Chapront-Touze M. and Chapront J., but just my work,
|
||||
that is the compilation and rearrangement of
|
||||
the Fortran code and data obtained from above
|
||||
into the software supplied in this file.
|
||||
|
||||
|
||||
Copyright (c) 2005 Johannes Gajdosik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
My implementation of ELP2000-82B has the following modifications compared to
|
||||
the original Fortran code:
|
||||
1) fundamentally rearrange the series into optimized instructions
|
||||
for fast calculation of the results
|
||||
2) units are: julian day, AU
|
||||
|
||||
This file is derived from the Stellarium project
|
||||
(https://github.com/Stellarium/stellarium). Modified for pg_orrery:
|
||||
removed static mutable state for thread safety (PostgreSQL PARALLEL SAFE).
|
||||
|
||||
****************************************************************/
|
||||
|
||||
|
||||
#ifndef ELP82B_H
|
||||
#define ELP82B_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
void GetElp82bCoor(double jd,double xyz[3]);
|
||||
|
||||
/* Return the rectangular coordinates of the earths moon
|
||||
on the given julian date jd expressed in dynamical time (TAI+32.184s).
|
||||
The origin of the xyz-coordinates is the center of the earth.
|
||||
The reference frame is "dynamical equinox and ecliptic J2000",
|
||||
which is the reference frame in VSOP87 and VSOP87A.
|
||||
|
||||
According to vsop87.doc VSOP87 coordinates can be transformed to FK5 by
|
||||
X cos(psi) -sin(psi) 0 1 0 0 X
|
||||
Y = sin(psi) cos(psi) 0 * 0 cos(eps) -sin(eps) * Y
|
||||
Z FK5 0 0 1 0 sin(eps) cos(eps) Z VSOP87
|
||||
with psi = -0.0000275 degrees = -0.099 arcsec and
|
||||
eps = 23.4392803055556 degrees = 23d26m21.4091sec.
|
||||
|
||||
http://ssd.jpl.nasa.gov/horizons_doc.html#frames says:
|
||||
"J2000" selects an Earth Mean-Equator and dynamical Equinox of
|
||||
Epoch J2000.0 inertial reference system, where the Epoch of J2000.0
|
||||
is the Julian date 2451545.0. "Mean" indicates nutation effects are
|
||||
ignored in the frame definition. The system is aligned with the
|
||||
IAU-sponsored J2000 frame of the Radio Source Catalog of the
|
||||
International Earth Rotational Service (ICRF).
|
||||
The ICRF is thought to differ from FK5 by at most 0.01 arcsec.
|
||||
|
||||
From this I conclude that in the context of stellarium
|
||||
ICRF, J2000 and FK5 are the same, while the transformation
|
||||
ICRF <-> VSOP87 must be done with the matrix given above.
|
||||
*/
|
||||
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
349
src/eph_provider.c
Normal file
349
src/eph_provider.c
Normal file
@ -0,0 +1,349 @@
|
||||
/*
|
||||
* eph_provider.c -- Ephemeris provider management for pg_orrery
|
||||
*
|
||||
* Manages the lifecycle of the optional JPL DE ephemeris reader
|
||||
* within PostgreSQL's multi-process architecture.
|
||||
*
|
||||
* Critical design:
|
||||
* - Each backend (including parallel workers) opens its own
|
||||
* file descriptor via lazy initialization on first DE call.
|
||||
* - The postmaster never opens the file (_PG_init only registers GUCs).
|
||||
* - on_proc_exit callback ensures cleanup.
|
||||
* - ICRS equatorial -> ecliptic J2000 rotation happens here,
|
||||
* at the provider boundary, before data enters the observation
|
||||
* pipeline.
|
||||
*
|
||||
* Constant chain of custody:
|
||||
* - DE returns positions in ICRS equatorial (AU, km internally)
|
||||
* - We rotate to ecliptic J2000 via equatorial_to_ecliptic()
|
||||
* - Both target and Earth always come from the same provider
|
||||
*/
|
||||
|
||||
#include "postgres.h"
|
||||
#include "fmgr.h"
|
||||
#include "utils/guc.h"
|
||||
#include "storage/ipc.h"
|
||||
|
||||
#include "eph_provider.h"
|
||||
#include "de_reader.h"
|
||||
#include "astro_math.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
/* GUC variable: path to DE ephemeris file */
|
||||
static char *eph_path_guc = NULL;
|
||||
|
||||
/*
|
||||
* Per-backend state (safe: each process gets its own copy after fork).
|
||||
* These are NOT shared memory — they live in each backend's address space.
|
||||
*/
|
||||
static de_handle *de_handle_ptr = NULL;
|
||||
static bool de_init_attempted = false;
|
||||
static bool de_init_success = false;
|
||||
|
||||
|
||||
void
|
||||
eph_register_gucs(void)
|
||||
{
|
||||
DefineCustomStringVariable(
|
||||
"pg_orrery.ephemeris_path",
|
||||
"Path to JPL DE binary ephemeris file (DE430/DE440/DE441).",
|
||||
"When set, enables high-precision _de() function variants. "
|
||||
"Default empty = no DE support (VSOP87 only). "
|
||||
"Relative paths are relative to $PGDATA.",
|
||||
&eph_path_guc,
|
||||
"", /* default: empty = disabled */
|
||||
PGC_BACKEND, /* Set at backend startup only.
|
||||
* Live reload (PGC_SIGHUP) would require an
|
||||
* assign_hook that resets de_init_attempted,
|
||||
* closes any open handle, and re-initializes. */
|
||||
GUC_SUPERUSER_ONLY, /* only superuser can set */
|
||||
NULL, /* check_hook */
|
||||
NULL, /* assign_hook */
|
||||
NULL /* show_hook */
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Lazy initialization: open the DE file on first call.
|
||||
* Never called from _PG_init() (postmaster context).
|
||||
*/
|
||||
static void
|
||||
ensure_de_init(void)
|
||||
{
|
||||
int errcode;
|
||||
|
||||
if (de_init_attempted)
|
||||
return;
|
||||
|
||||
de_init_attempted = true;
|
||||
de_init_success = false;
|
||||
|
||||
/* No path configured? DE is simply unavailable. */
|
||||
if (eph_path_guc == NULL || eph_path_guc[0] == '\0')
|
||||
return;
|
||||
|
||||
de_handle_ptr = de_reader_open(eph_path_guc, &errcode);
|
||||
if (de_handle_ptr == NULL)
|
||||
{
|
||||
ereport(NOTICE,
|
||||
(errmsg("pg_orrery: cannot open DE ephemeris at \"%s\" (error %d), "
|
||||
"DE functions will fall back to VSOP87",
|
||||
eph_path_guc, errcode)));
|
||||
return;
|
||||
}
|
||||
|
||||
/* Verify AU consistency with pg_orrery's compiled-in value */
|
||||
if (fabs(de_handle_ptr->au_km - AU_KM) > 0.01)
|
||||
{
|
||||
ereport(NOTICE,
|
||||
(errmsg("pg_orrery: DE ephemeris AU = %.3f km, expected %.3f km; "
|
||||
"DE functions will fall back to VSOP87",
|
||||
de_handle_ptr->au_km, (double)AU_KM)));
|
||||
de_reader_close(de_handle_ptr);
|
||||
de_handle_ptr = NULL;
|
||||
return;
|
||||
}
|
||||
|
||||
ereport(DEBUG1,
|
||||
(errmsg("pg_orrery: DE%d ephemeris loaded, JD %.1f to %.1f",
|
||||
de_handle_ptr->de_version,
|
||||
de_handle_ptr->start_jd,
|
||||
de_handle_ptr->end_jd)));
|
||||
|
||||
de_init_success = true;
|
||||
}
|
||||
|
||||
|
||||
void
|
||||
eph_cleanup(int code, Datum arg)
|
||||
{
|
||||
if (de_handle_ptr != NULL)
|
||||
{
|
||||
de_reader_close(de_handle_ptr);
|
||||
de_handle_ptr = NULL;
|
||||
}
|
||||
de_init_attempted = false;
|
||||
de_init_success = false;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
eph_de_available(void)
|
||||
{
|
||||
ensure_de_init();
|
||||
return de_init_success;
|
||||
}
|
||||
|
||||
|
||||
EphProvider
|
||||
eph_current_provider(void)
|
||||
{
|
||||
if (eph_de_available())
|
||||
return EPH_JPL_DE;
|
||||
return EPH_VSOP87;
|
||||
}
|
||||
|
||||
|
||||
const char *
|
||||
eph_get_path(void)
|
||||
{
|
||||
return eph_path_guc;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Internal: get heliocentric position of a DE body and convert
|
||||
* from ICRS equatorial to ecliptic J2000.
|
||||
*
|
||||
* Returns true on success.
|
||||
*/
|
||||
static bool
|
||||
de_get_helio_ecliptic(int de_target, double jd, double ecl[3])
|
||||
{
|
||||
double icrs[3];
|
||||
int err;
|
||||
|
||||
if (!de_handle_ptr)
|
||||
return false;
|
||||
|
||||
/* Get position relative to Sun (DE_SUN = 10) */
|
||||
err = de_reader_get_pos(de_handle_ptr, jd, de_target, 10, icrs);
|
||||
if (err != DE_OK)
|
||||
return false;
|
||||
|
||||
/* ICRS equatorial -> ecliptic J2000 */
|
||||
equatorial_to_ecliptic(icrs, ecl);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Internal: get Earth's heliocentric position from DE.
|
||||
*
|
||||
* Earth = EMB(helio) - Moon(geocentric) / (1 + EMRAT).
|
||||
* The EMB-to-Earth correction is also in de_reader.c:get_earth_pos();
|
||||
* both must use the same formula.
|
||||
*/
|
||||
static bool
|
||||
de_get_earth_helio_ecliptic(double jd, double ecl[3])
|
||||
{
|
||||
double icrs[3];
|
||||
int err;
|
||||
|
||||
if (!de_handle_ptr)
|
||||
return false;
|
||||
|
||||
/* EMB heliocentric, then correct to Earth below */
|
||||
err = de_reader_get_pos(de_handle_ptr, jd, DE_EMB, DE_SUN, icrs);
|
||||
if (err != DE_OK)
|
||||
return false;
|
||||
|
||||
/* Correct EMB-helio to Earth-helio: subtract Moon/(1+EMRAT) */
|
||||
{
|
||||
double moon_icrs[3];
|
||||
double factor;
|
||||
|
||||
err = de_reader_get_pos(de_handle_ptr, jd, DE_MOON, -1, moon_icrs);
|
||||
if (err != DE_OK)
|
||||
return false;
|
||||
|
||||
factor = 1.0 / (1.0 + de_handle_ptr->emrat);
|
||||
icrs[0] -= moon_icrs[0] * factor;
|
||||
icrs[1] -= moon_icrs[1] * factor;
|
||||
icrs[2] -= moon_icrs[2] * factor;
|
||||
}
|
||||
|
||||
equatorial_to_ecliptic(icrs, ecl);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
eph_de_planet(int body_id, double jd, double xyz[6])
|
||||
{
|
||||
int de_target;
|
||||
double ecl[3];
|
||||
|
||||
if (!eph_de_available())
|
||||
return false;
|
||||
|
||||
de_target = pgbody_to_de_target(body_id);
|
||||
if (de_target < 0)
|
||||
return false;
|
||||
|
||||
/* Earth is special: derived from EMB and Moon */
|
||||
if (body_id == BODY_EARTH)
|
||||
{
|
||||
if (!de_get_earth_helio_ecliptic(jd, ecl))
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!de_get_helio_ecliptic(de_target, jd, ecl))
|
||||
return false;
|
||||
}
|
||||
|
||||
xyz[0] = ecl[0];
|
||||
xyz[1] = ecl[1];
|
||||
xyz[2] = ecl[2];
|
||||
xyz[3] = 0.0; /* velocity not yet implemented */
|
||||
xyz[4] = 0.0;
|
||||
xyz[5] = 0.0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
eph_de_earth(double jd, double xyz[6])
|
||||
{
|
||||
double ecl[3];
|
||||
|
||||
if (!eph_de_available())
|
||||
return false;
|
||||
|
||||
if (!de_get_earth_helio_ecliptic(jd, ecl))
|
||||
return false;
|
||||
|
||||
xyz[0] = ecl[0];
|
||||
xyz[1] = ecl[1];
|
||||
xyz[2] = ecl[2];
|
||||
xyz[3] = 0.0;
|
||||
xyz[4] = 0.0;
|
||||
xyz[5] = 0.0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
eph_de_moon(double jd, double xyz[3])
|
||||
{
|
||||
double icrs[3];
|
||||
int err;
|
||||
|
||||
if (!eph_de_available())
|
||||
return false;
|
||||
|
||||
/* Moon geocentric: use center=-1 for raw geocentric Moon directly.
|
||||
* The Moon is stored geocentric in the DE file, so center=-1 avoids
|
||||
* the roundabout of computing Earth (EMB - Moon/(1+EMRAT)) just to
|
||||
* subtract it back out (~4x fewer interpolations). */
|
||||
err = de_reader_get_pos(de_handle_ptr, jd, DE_MOON, -1, icrs);
|
||||
if (err != DE_OK)
|
||||
return false;
|
||||
|
||||
/* ICRS -> ecliptic J2000 */
|
||||
equatorial_to_ecliptic(icrs, xyz);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool
|
||||
eph_de_sun(double jd, double xyz[6])
|
||||
{
|
||||
/* Sun is at the origin of heliocentric coordinates */
|
||||
(void)jd;
|
||||
if (!eph_de_available())
|
||||
return false;
|
||||
|
||||
xyz[0] = xyz[1] = xyz[2] = 0.0;
|
||||
xyz[3] = xyz[4] = xyz[5] = 0.0;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
double
|
||||
eph_de_start_jd(void)
|
||||
{
|
||||
if (de_handle_ptr)
|
||||
return de_handle_ptr->start_jd;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double
|
||||
eph_de_end_jd(void)
|
||||
{
|
||||
if (de_handle_ptr)
|
||||
return de_handle_ptr->end_jd;
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
int
|
||||
eph_de_version(void)
|
||||
{
|
||||
if (de_handle_ptr)
|
||||
return de_handle_ptr->de_version;
|
||||
return 0;
|
||||
}
|
||||
|
||||
double
|
||||
eph_de_au_km(void)
|
||||
{
|
||||
if (de_handle_ptr)
|
||||
return de_handle_ptr->au_km;
|
||||
return 0.0;
|
||||
}
|
||||
110
src/eph_provider.h
Normal file
110
src/eph_provider.h
Normal file
@ -0,0 +1,110 @@
|
||||
/*
|
||||
* eph_provider.h -- Ephemeris provider dispatch for pg_orrery
|
||||
*
|
||||
* Manages the optional JPL DE ephemeris alongside the compiled-in
|
||||
* VSOP87 and ELP2000-82B theories.
|
||||
*
|
||||
* Key design constraints:
|
||||
* - Per-backend lazy initialization (never in _PG_init/postmaster)
|
||||
* - ICRS equatorial -> ecliptic J2000 frame rotation at the boundary
|
||||
* - Automatic VSOP87 fallback on any DE error
|
||||
* - No shared state between PostgreSQL backends
|
||||
*/
|
||||
|
||||
#ifndef PG_ORRERY_EPH_PROVIDER_H
|
||||
#define PG_ORRERY_EPH_PROVIDER_H
|
||||
|
||||
#include "postgres.h"
|
||||
#include "types.h"
|
||||
|
||||
/*
|
||||
* Ephemeris provider identifiers.
|
||||
*/
|
||||
typedef enum
|
||||
{
|
||||
EPH_VSOP87, /* compiled-in VSOP87 / ELP2000-82B (always available) */
|
||||
EPH_JPL_DE /* JPL DE binary file (optional, STABLE) */
|
||||
} EphProvider;
|
||||
|
||||
|
||||
/*
|
||||
* Initialize the GUC variable (pg_orrery.ephemeris_path).
|
||||
* Called from _PG_init(). Does NOT open the DE file.
|
||||
*/
|
||||
void eph_register_gucs(void);
|
||||
|
||||
/*
|
||||
* Cleanup callback for on_proc_exit.
|
||||
* Closes the DE file descriptor if open.
|
||||
*/
|
||||
void eph_cleanup(int code, Datum arg);
|
||||
|
||||
/*
|
||||
* Attempt to initialize the DE reader for this backend.
|
||||
* Returns true if DE is available and ready, false otherwise.
|
||||
* On first call, opens the file and validates. On subsequent calls,
|
||||
* returns cached status.
|
||||
*
|
||||
* Thread-safe per backend (each backend has its own static state).
|
||||
*/
|
||||
bool eph_de_available(void);
|
||||
|
||||
/*
|
||||
* Which provider is currently active?
|
||||
*/
|
||||
EphProvider eph_current_provider(void);
|
||||
|
||||
/*
|
||||
* Get the configured ephemeris file path (or NULL if not set).
|
||||
*/
|
||||
const char *eph_get_path(void);
|
||||
|
||||
|
||||
/*
|
||||
* Get planet heliocentric ecliptic J2000 position via DE.
|
||||
*
|
||||
* body_id: pg_orrery body ID (1=Mercury..8=Neptune)
|
||||
* jd: Julian date (TDB)
|
||||
* xyz[6]: output position (AU) in ecliptic J2000 frame
|
||||
* (xyz[3..5] are zero — velocity not yet implemented)
|
||||
*
|
||||
* Returns true on success. On failure, xyz is unchanged and
|
||||
* caller should fall back to VSOP87.
|
||||
*/
|
||||
bool eph_de_planet(int body_id, double jd, double xyz[6]);
|
||||
|
||||
/*
|
||||
* Get Earth heliocentric ecliptic J2000 position via DE.
|
||||
*
|
||||
* jd: Julian date (TDB)
|
||||
* xyz[6]: output position (AU) in ecliptic J2000 frame
|
||||
*
|
||||
* Returns true on success.
|
||||
*/
|
||||
bool eph_de_earth(double jd, double xyz[6]);
|
||||
|
||||
/*
|
||||
* Get Moon geocentric ecliptic J2000 position via DE.
|
||||
*
|
||||
* jd: Julian date (TDB)
|
||||
* xyz[3]: output position (AU) in ecliptic J2000 frame
|
||||
*
|
||||
* Returns true on success. On failure, caller falls back to ELP2000-82B.
|
||||
*/
|
||||
bool eph_de_moon(double jd, double xyz[3]);
|
||||
|
||||
/*
|
||||
* Get Sun "heliocentric" position (always 0,0,0 but consistent API).
|
||||
*/
|
||||
bool eph_de_sun(double jd, double xyz[6]);
|
||||
|
||||
/*
|
||||
* DE metadata accessors (for pg_orrery_ephemeris_info).
|
||||
* Return 0/0.0 if DE is not loaded.
|
||||
*/
|
||||
double eph_de_start_jd(void);
|
||||
double eph_de_end_jd(void);
|
||||
int eph_de_version(void);
|
||||
double eph_de_au_km(void);
|
||||
|
||||
#endif /* PG_ORRERY_EPH_PROVIDER_H */
|
||||
440
src/gust86.c
Normal file
440
src/gust86.c
Normal file
@ -0,0 +1,440 @@
|
||||
/************************************************************************
|
||||
|
||||
COMPUTATION OF THE COORDINATES OF THE URANIAN SATELLITES (GUST86),
|
||||
version 0.1 (1988,1995) by LASKAR J. and JACOBSON, R. can be found at
|
||||
ftp://ftp.imcce.fr/pub/ephem/satel/gust86
|
||||
|
||||
I (Johannes Gajdosik) have just taken the Fortran code and data
|
||||
obtained from above and rearranged it into this piece of software.
|
||||
|
||||
I can neither allow nor forbid the usage of the GUST86 theory.
|
||||
The copyright notice below covers not the works of LASKAR J. and JACOBSON, R.,
|
||||
but just my work, that is the compilation of the GUST86 theory
|
||||
into the software supplied in this file.
|
||||
|
||||
|
||||
Copyright (c) 2005 Johannes Gajdosik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Derived from Stellarium's GUST86 implementation.
|
||||
Modified for pg_orrery: removed all static mutable state for thread safety
|
||||
(PostgreSQL PARALLEL SAFE). The original used static caching arrays and
|
||||
CalcInterpolatedElements for performance; this version computes fresh on
|
||||
each call which is acceptable for SQL query workloads.
|
||||
|
||||
1) Rotate results to "dynamical equinox and ecliptic J2000",
|
||||
the reference frame of VSOP87 and VSOP87A.
|
||||
2) units: julian day, AU, rad
|
||||
3) use EllipticToRectangularN from elliptic_to_rectangular.h
|
||||
|
||||
****************************************************************/
|
||||
|
||||
#include "gust86.h"
|
||||
#include "elliptic_to_rectangular.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
static
|
||||
const double fqn[5] = {4.44519055,
|
||||
2.492952519,
|
||||
1.516148111,
|
||||
0.721718509,
|
||||
0.46669212};
|
||||
static
|
||||
const double fqe[5] = {20.082*M_PI/(180*365.25),
|
||||
6.217*M_PI/(180*365.25),
|
||||
2.865*M_PI/(180*365.25),
|
||||
2.078*M_PI/(180*365.25),
|
||||
0.386*M_PI/(180*365.25)};
|
||||
static
|
||||
const double fqi[5] = {-20.309*M_PI/(180*365.25),
|
||||
-6.288*M_PI/(180*365.25),
|
||||
-2.836*M_PI/(180*365.25),
|
||||
-1.843*M_PI/(180*365.25),
|
||||
-0.259*M_PI/(180*365.25)};
|
||||
static
|
||||
const double phn[5] = {-0.238051,
|
||||
3.098046,
|
||||
2.285402,
|
||||
0.856359,
|
||||
-0.915592};
|
||||
static
|
||||
const double phe[5] = {0.611392,
|
||||
2.408974,
|
||||
2.067774,
|
||||
0.735131,
|
||||
0.426767};
|
||||
static
|
||||
const double phi[5] = {5.702313,
|
||||
0.395757,
|
||||
0.589326,
|
||||
1.746237,
|
||||
4.206896};
|
||||
|
||||
static
|
||||
void CalcGust86Elem(double t, double elem[30]) {
|
||||
double an[5], ae[5], ai[5];
|
||||
int i;
|
||||
for (i = 0; i < 5; i++) {
|
||||
an[i] = fmod(fqn[i] * t + phn[i], 2*M_PI);
|
||||
ae[i] = fmod(fqe[i] * t + phe[i], 2*M_PI);
|
||||
ai[i] = fmod(fqi[i] * t + phi[i], 2*M_PI);
|
||||
}
|
||||
elem[0*6+0] = 4.44352267
|
||||
- cos(an[0] - an[1] * 3. + an[2] * 2.) * 3.492e-5
|
||||
+ cos(an[0] * 2. - an[1] * 6. + an[2] * 4.) * 8.47e-6
|
||||
+ cos(an[0] * 3. - an[1] * 9. + an[2] * 6.) * 1.31e-6
|
||||
- cos(an[0] - an[1] ) * 5.228e-5
|
||||
- cos(an[0] * 2. - an[1] * 2. ) * 1.3665e-4;
|
||||
elem[0*6+1] =
|
||||
sin(an[0] - an[1] * 3. + an[2] * 2.) * .02547217
|
||||
- sin(an[0] * 2. - an[1] * 6. + an[2] * 4.) * .00308831
|
||||
- sin(an[0] * 3. - an[1] * 9. + an[2] * 6.) * 3.181e-4
|
||||
- sin(an[0] * 4. - an[1] * 12 + an[2] * 8.) * 3.749e-5
|
||||
- sin(an[0] - an[1] ) * 5.785e-5
|
||||
- sin(an[0] * 2. - an[1] * 2. ) * 6.232e-5
|
||||
- sin(an[0] * 3. - an[1] * 3. ) * 2.795e-5
|
||||
+ t * 4.44519055 - .23805158;
|
||||
elem[0*6+2] = cos(ae[0]) * .00131238
|
||||
+ cos(ae[1]) * 7.181e-5
|
||||
+ cos(ae[2]) * 6.977e-5
|
||||
+ cos(ae[3]) * 6.75e-6
|
||||
+ cos(ae[4]) * 6.27e-6
|
||||
+ cos(an[0]) * 1.941e-4
|
||||
- cos(-an[0] + an[1] * 2.) * 1.2331e-4
|
||||
+ cos(an[0] * -2. + an[1] * 3.) * 3.952e-5;
|
||||
elem[0*6+3] = sin(ae[0]) * .00131238
|
||||
+ sin(ae[1]) * 7.181e-5
|
||||
+ sin(ae[2]) * 6.977e-5
|
||||
+ sin(ae[3]) * 6.75e-6
|
||||
+ sin(ae[4]) * 6.27e-6
|
||||
+ sin(an[0]) * 1.941e-4
|
||||
- sin(-an[0] + an[1] * 2.) * 1.2331e-4
|
||||
+ sin(an[0] * -2. + an[1] * 3.) * 3.952e-5;
|
||||
elem[0*6+4] = cos(ai[0]) * .03787171
|
||||
+ cos(ai[1]) * 2.701e-5
|
||||
+ cos(ai[2]) * 3.076e-5
|
||||
+ cos(ai[3]) * 1.218e-5
|
||||
+ cos(ai[4]) * 5.37e-6;
|
||||
elem[0*6+5] = sin(ai[0]) * .03787171
|
||||
+ sin(ai[1]) * 2.701e-5
|
||||
+ sin(ai[2]) * 3.076e-5
|
||||
+ sin(ai[3]) * 1.218e-5
|
||||
+ sin(ai[4]) * 5.37e-6;
|
||||
elem[1*6+0] = 2.49254257
|
||||
+ cos(an[0] - an[1] * 3. + an[2] * 2.) * 2.55e-6
|
||||
- cos( an[1] - an[2] ) * 4.216e-5
|
||||
- cos( an[1] * 2. - an[2] * 2.) * 1.0256e-4;
|
||||
elem[1*6+1] =
|
||||
- sin(an[0] - an[1] * 3. + an[2] * 2.) * .0018605
|
||||
+ sin(an[0] * 2. - an[1] * 6. + an[2] * 4.) * 2.1999e-4
|
||||
+ sin(an[0] * 3. - an[1] * 9. + an[2] * 6.) * 2.31e-5
|
||||
+ sin(an[0] * 4. - an[1] * 12 + an[2] * 8.) * 4.3e-6
|
||||
- sin( an[1] - an[2] ) * 9.011e-5
|
||||
- sin( an[1] * 2. - an[2] * 2.) * 9.107e-5
|
||||
- sin( an[1] * 3. - an[2] * 3.) * 4.275e-5
|
||||
- sin( an[1] * 2. - an[3] * 2.) * 1.649e-5
|
||||
+ t * 2.49295252 + 3.09804641;
|
||||
elem[1*6+2] = cos(ae[0]) * -3.35e-6
|
||||
+ cos(ae[1]) * .00118763
|
||||
+ cos(ae[2]) * 8.6159e-4
|
||||
+ cos(ae[3]) * 7.15e-5
|
||||
+ cos(ae[4]) * 5.559e-5
|
||||
- cos(-an[1] + an[2] * 2.) * 8.46e-5
|
||||
+ cos(an[1] * -2. + an[2] * 3.) * 9.181e-5
|
||||
+ cos(-an[1] + an[3] * 2.) * 2.003e-5
|
||||
+ cos(an[1]) * 8.977e-5;
|
||||
elem[1*6+3] = sin(ae[0]) * -3.35e-6
|
||||
+ sin(ae[1]) * .00118763
|
||||
+ sin(ae[2]) * 8.6159e-4
|
||||
+ sin(ae[3]) * 7.15e-5
|
||||
+ sin(ae[4]) * 5.559e-5
|
||||
- sin(-an[1] + an[2] * 2.) * 8.46e-5
|
||||
+ sin(an[1] * -2. + an[2] * 3.) * 9.181e-5
|
||||
+ sin(-an[1] + an[3] * 2.) * 2.003e-5
|
||||
+ sin(an[1]) * 8.977e-5;
|
||||
elem[1*6+4] = cos(ai[0]) * -1.2175e-4
|
||||
+ cos(ai[1]) * 3.5825e-4
|
||||
+ cos(ai[2]) * 2.9008e-4
|
||||
+ cos(ai[3]) * 9.778e-5
|
||||
+ cos(ai[4]) * 3.397e-5;
|
||||
elem[1*6+5] = sin(ai[0]) * -1.2175e-4
|
||||
+ sin(ai[1]) * 3.5825e-4
|
||||
+ sin(ai[2]) * 2.9008e-4
|
||||
+ sin(ai[3]) * 9.778e-5
|
||||
+ sin(ai[4]) * 3.397e-5;
|
||||
elem[2*6+0] = 1.5159549
|
||||
+ cos(an[2] - an[3] * 2. + ae[2]) * 9.74e-6
|
||||
- cos(an[1] - an[2]) * 1.06e-4
|
||||
+ cos(an[1] * 2. - an[2] * 2.) * 5.416e-5
|
||||
- cos(an[2] - an[3]) * 2.359e-5
|
||||
- cos(an[2] * 2. - an[3] * 2.) * 7.07e-5
|
||||
- cos(an[2] * 3. - an[3] * 3.) * 3.628e-5;
|
||||
elem[2*6+1] =
|
||||
sin(an[0] - an[1] * 3. + an[2] * 2.) * 6.6057e-4
|
||||
- sin(an[0] * 2. - an[1] * 6. + an[2] * 4.) * 7.651e-5
|
||||
- sin(an[0] * 3. - an[1] * 9. + an[2] * 6.) * 8.96e-6
|
||||
- sin(an[0] * 4. - an[1] * 12. + an[2] * 8.) * 2.53e-6
|
||||
- sin(an[2] - an[3] * 4. + an[4] * 3.) * 5.291e-5
|
||||
- sin(an[2] - an[3] * 2. + ae[4]) * 7.34e-6
|
||||
- sin(an[2] - an[3] * 2. + ae[3]) * 1.83e-6
|
||||
+ sin(an[2] - an[3] * 2. + ae[2]) * 1.4791e-4
|
||||
+ sin(an[2] - an[3] * 2. + ae[1]) * -7.77e-6
|
||||
+ sin(an[1] - an[2]) * 9.776e-5
|
||||
+ sin(an[1] * 2. - an[2] * 2.) * 7.313e-5
|
||||
+ sin(an[1] * 3. - an[2] * 3.) * 3.471e-5
|
||||
+ sin(an[1] * 4. - an[2] * 4.) * 1.889e-5
|
||||
- sin(an[2] - an[3]) * 6.789e-5
|
||||
- sin(an[2] * 2. - an[3] * 2.) * 8.286e-5
|
||||
+ sin(an[2] * 3. - an[3] * 3.) * -3.381e-5
|
||||
- sin(an[2] * 4. - an[3] * 4.) * 1.579e-5
|
||||
- sin(an[2] - an[4]) * 1.021e-5
|
||||
- sin(an[2] * 2. - an[4] * 2.) * 1.708e-5
|
||||
+ t * 1.51614811 + 2.28540169;
|
||||
elem[2*6+2] = cos(ae[0]) * -2.1e-7
|
||||
- cos(ae[1]) * 2.2795e-4
|
||||
+ cos(ae[2]) * .00390469
|
||||
+ cos(ae[3]) * 3.0917e-4
|
||||
+ cos(ae[4]) * 2.2192e-4
|
||||
+ cos(an[1]) * 2.934e-5
|
||||
+ cos(an[2]) * 2.62e-5
|
||||
+ cos(-an[1] + an[2] * 2.) * 5.119e-5
|
||||
- cos(an[1] * -2. + an[2] * 3.) * 1.0386e-4
|
||||
- cos(an[1] * -3. + an[2] * 4.) * 2.716e-5
|
||||
+ cos(an[3]) * -1.622e-5
|
||||
+ cos(-an[2] + an[3] * 2.) * 5.4923e-4
|
||||
+ cos(an[2] * -2. + an[3] * 3.) * 3.47e-5
|
||||
+ cos(an[2] * -3. + an[3] * 4.) * 1.281e-5
|
||||
+ cos(-an[2] + an[4] * 2.) * 2.181e-5
|
||||
+ cos(an[2]) * 4.625e-5;
|
||||
elem[2*6+3] = sin(ae[0]) * -2.1e-7
|
||||
- sin(ae[1]) * 2.2795e-4
|
||||
+ sin(ae[2]) * .00390469
|
||||
+ sin(ae[3]) * 3.0917e-4
|
||||
+ sin(ae[4]) * 2.2192e-4
|
||||
+ sin(an[1]) * 2.934e-5
|
||||
+ sin(an[2]) * 2.62e-5
|
||||
+ sin(-an[1] + an[2] * 2.) * 5.119e-5
|
||||
- sin(an[1] * -2. + an[2] * 3.) * 1.0386e-4
|
||||
- sin(an[1] * -3. + an[2] * 4.) * 2.716e-5
|
||||
+ sin(an[3]) * -1.622e-5
|
||||
+ sin(-an[2] + an[3] * 2.) * 5.4923e-4
|
||||
+ sin(an[2] * -2. + an[3] * 3.) * 3.47e-5
|
||||
+ sin(an[2] * -3. + an[3] * 4.) * 1.281e-5
|
||||
+ sin(-an[2] + an[4] * 2.) * 2.181e-5
|
||||
+ sin(an[2]) * 4.625e-5;
|
||||
elem[2*6+4] = cos(ai[0]) * -1.086e-5
|
||||
- cos(ai[1]) * 8.151e-5
|
||||
+ cos(ai[2]) * .00111336
|
||||
+ cos(ai[3]) * 3.5014e-4
|
||||
+ cos(ai[4]) * 1.065e-4;
|
||||
elem[2*6+5] = sin(ai[0]) * -1.086e-5
|
||||
- sin(ai[1]) * 8.151e-5
|
||||
+ sin(ai[2]) * .00111336
|
||||
+ sin(ai[3]) * 3.5014e-4
|
||||
+ sin(ai[4]) * 1.065e-4;
|
||||
elem[3*6+0] = .72166316
|
||||
- cos(an[2] - an[3] * 2. + ae[2]) * 2.64e-6
|
||||
- cos(an[3] * 2. - an[4] * 3. + ae[4]) * 2.16e-6
|
||||
+ cos(an[3] * 2. - an[4] * 3. + ae[3]) * 6.45e-6
|
||||
- cos(an[3] * 2. - an[4] * 3. + ae[2]) * 1.11e-6
|
||||
+ cos(an[1] - an[3]) * -6.223e-5
|
||||
- cos(an[2] - an[3]) * 5.613e-5
|
||||
- cos(an[3] - an[4]) * 3.994e-5
|
||||
- cos(an[3] * 2. - an[4] * 2.) * 9.185e-5
|
||||
- cos(an[3] * 3. - an[4] * 3.) * 5.831e-5
|
||||
- cos(an[3] * 4. - an[4] * 4.) * 3.86e-5
|
||||
- cos(an[3] * 5. - an[4] * 5.) * 2.618e-5
|
||||
- cos(an[3] * 6. - an[4] * 6.) * 1.806e-5;
|
||||
elem[3*6+1] =
|
||||
sin(an[2] - an[3] * 4. + an[4] * 3.) * 2.061e-5
|
||||
- sin(an[2] - an[3] * 2. + ae[4]) * 2.07e-6
|
||||
- sin(an[2] - an[3] * 2. + ae[3]) * 2.88e-6
|
||||
- sin(an[2] - an[3] * 2. + ae[2]) * 4.079e-5
|
||||
+ sin(an[2] - an[3] * 2. + ae[1]) * 2.11e-6
|
||||
- sin(an[3] * 2. - an[4] * 3. + ae[4]) * 5.183e-5
|
||||
+ sin(an[3] * 2. - an[4] * 3. + ae[3]) * 1.5987e-4
|
||||
+ sin(an[3] * 2. - an[4] * 3. + ae[2]) * -3.505e-5
|
||||
- sin(an[3] * 3. - an[4] * 4. + ae[4]) * 1.56e-6
|
||||
+ sin(an[1] - an[3]) * 4.054e-5
|
||||
+ sin(an[2] - an[3]) * 4.617e-5
|
||||
- sin(an[3] - an[4]) * 3.1776e-4
|
||||
- sin(an[3] * 2. - an[4] * 2.) * 3.0559e-4
|
||||
- sin(an[3] * 3. - an[4] * 3.) * 1.4836e-4
|
||||
- sin(an[3] * 4. - an[4] * 4.) * 8.292e-5
|
||||
+ sin(an[3] * 5. - an[4] * 5.) * -4.998e-5
|
||||
- sin(an[3] * 6. - an[4] * 6.) * 3.156e-5
|
||||
- sin(an[3] * 7. - an[4] * 7.) * 2.056e-5
|
||||
- sin(an[3] * 8. - an[4] * 8.) * 1.369e-5
|
||||
+ t * .72171851 + .85635879;
|
||||
elem[3*6+2] = cos(ae[0]) * -2e-8
|
||||
- cos(ae[1]) * 1.29e-6
|
||||
- cos(ae[2]) * 3.2451e-4
|
||||
+ cos(ae[3]) * 9.3281e-4
|
||||
+ cos(ae[4]) * .00112089
|
||||
+ cos(an[1]) * 3.386e-5
|
||||
+ cos(an[3]) * 1.746e-5
|
||||
+ cos(-an[1] + an[3] * 2.) * 1.658e-5
|
||||
+ cos(an[2]) * 2.889e-5
|
||||
- cos(-an[2] + an[3] * 2.) * 3.586e-5
|
||||
+ cos(an[3]) * -1.786e-5
|
||||
- cos(an[4]) * 3.21e-5
|
||||
- cos(-an[3] + an[4] * 2.) * 1.7783e-4
|
||||
+ cos(an[3] * -2. + an[4] * 3.) * 7.9343e-4
|
||||
+ cos(an[3] * -3. + an[4] * 4.) * 9.948e-5
|
||||
+ cos(an[3] * -4. + an[4] * 5.) * 4.483e-5
|
||||
+ cos(an[3] * -5. + an[4] * 6.) * 2.513e-5
|
||||
+ cos(an[3] * -6. + an[4] * 7.) * 1.543e-5;
|
||||
elem[3*6+3] = sin(ae[0]) * -2e-8
|
||||
- sin(ae[1]) * 1.29e-6
|
||||
- sin(ae[2]) * 3.2451e-4
|
||||
+ sin(ae[3]) * 9.3281e-4
|
||||
+ sin(ae[4]) * .00112089
|
||||
+ sin(an[1]) * 3.386e-5
|
||||
+ sin(an[3]) * 1.746e-5
|
||||
+ sin(-an[1] + an[3] * 2.) * 1.658e-5
|
||||
+ sin(an[2]) * 2.889e-5
|
||||
- sin(-an[2] + an[3] * 2.) * 3.586e-5
|
||||
+ sin(an[3]) * -1.786e-5
|
||||
- sin(an[4]) * 3.21e-5
|
||||
- sin(-an[3] + an[4] * 2.) * 1.7783e-4
|
||||
+ sin(an[3] * -2. + an[4] * 3.) * 7.9343e-4
|
||||
+ sin(an[3] * -3. + an[4] * 4.) * 9.948e-5
|
||||
+ sin(an[3] * -4. + an[4] * 5.) * 4.483e-5
|
||||
+ sin(an[3] * -5. + an[4] * 6.) * 2.513e-5
|
||||
+ sin(an[3] * -6. + an[4] * 7.) * 1.543e-5;
|
||||
elem[3*6+4] = cos(ai[0]) * -1.43e-6
|
||||
- cos(ai[1]) * 1.06e-6
|
||||
- cos(ai[2]) * 1.4013e-4
|
||||
+ cos(ai[3]) * 6.8572e-4
|
||||
+ cos(ai[4]) * 3.7832e-4;
|
||||
elem[3*6+5] = sin(ai[0]) * -1.43e-6
|
||||
- sin(ai[1]) * 1.06e-6
|
||||
- sin(ai[2]) * 1.4013e-4
|
||||
+ sin(ai[3]) * 6.8572e-4
|
||||
+ sin(ai[4]) * 3.7832e-4;
|
||||
elem[4*6+0] = .46658054
|
||||
+ cos(an[3] * 2. - an[4] * 3. + ae[4]) * 2.08e-6
|
||||
- cos(an[3] * 2. - an[4] * 3. + ae[3]) * 6.22e-6
|
||||
+ cos(an[3] * 2. - an[4] * 3. + ae[2]) * 1.07e-6
|
||||
- cos(an[1] - an[4]) * 4.31e-5
|
||||
+ cos(an[2] - an[4]) * -3.894e-5
|
||||
- cos(an[3] - an[4]) * 8.011e-5
|
||||
+ cos(an[3] * 2. - an[4] * 2.) * 5.906e-5
|
||||
+ cos(an[3] * 3. - an[4] * 3.) * 3.749e-5
|
||||
+ cos(an[3] * 4. - an[4] * 4.) * 2.482e-5
|
||||
+ cos(an[3] * 5. - an[4] * 5.) * 1.684e-5;
|
||||
elem[4*6+1] =
|
||||
- sin(an[2] - an[3] * 4. + an[4] * 3.) * 7.82e-6
|
||||
+ sin(an[3] * 2. - an[4] * 3. + ae[4]) * 5.129e-5
|
||||
- sin(an[3] * 2. - an[4] * 3. + ae[3]) * 1.5824e-4
|
||||
+ sin(an[3] * 2. - an[4] * 3. + ae[2]) * 3.451e-5
|
||||
+ sin(an[1] - an[4]) * 4.751e-5
|
||||
+ sin(an[2] - an[4]) * 3.896e-5
|
||||
+ sin(an[3] - an[4]) * 3.5973e-4
|
||||
+ sin(an[3] * 2. - an[4] * 2.) * 2.8278e-4
|
||||
+ sin(an[3] * 3. - an[4] * 3.) * 1.386e-4
|
||||
+ sin(an[3] * 4. - an[4] * 4.) * 7.803e-5
|
||||
+ sin(an[3] * 5. - an[4] * 5.) * 4.729e-5
|
||||
+ sin(an[3] * 6. - an[4] * 6.) * 3e-5
|
||||
+ sin(an[3] * 7. - an[4] * 7.) * 1.962e-5
|
||||
+ sin(an[3] * 8. - an[4] * 8.) * 1.311e-5
|
||||
+ t * .46669212 - .9155918;
|
||||
elem[4*6+2] = cos(ae[1]) * -3.5e-7
|
||||
+ cos(ae[2]) * 7.453e-5
|
||||
- cos(ae[3]) * 7.5868e-4
|
||||
+ cos(ae[4]) * .00139734
|
||||
+ cos(an[1]) * 3.9e-5
|
||||
+ cos(-an[1] + an[4] * 2.) * 1.766e-5
|
||||
+ cos(an[2]) * 3.242e-5
|
||||
+ cos(an[3]) * 7.975e-5
|
||||
+ cos(an[4]) * 7.566e-5
|
||||
+ cos(-an[3] + an[4] * 2.) * 1.3404e-4
|
||||
- cos(an[3] * -2. + an[4] * 3.) * 9.8726e-4
|
||||
- cos(an[3] * -3. + an[4] * 4.) * 1.2609e-4
|
||||
- cos(an[3] * -4. + an[4] * 5.) * 5.742e-5
|
||||
- cos(an[3] * -5. + an[4] * 6.) * 3.241e-5
|
||||
- cos(an[3] * -6. + an[4] * 7.) * 1.999e-5
|
||||
- cos(an[3] * -7. + an[4] * 8.) * 1.294e-5;
|
||||
elem[4*6+3] = sin(ae[1]) * -3.5e-7
|
||||
+ sin(ae[2]) * 7.453e-5
|
||||
- sin(ae[3]) * 7.5868e-4
|
||||
+ sin(ae[4]) * .00139734
|
||||
+ sin(an[1]) * 3.9e-5
|
||||
+ sin(-an[1] + an[4] * 2.) * 1.766e-5
|
||||
+ sin(an[2]) * 3.242e-5
|
||||
+ sin(an[3]) * 7.975e-5
|
||||
+ sin(an[4]) * 7.566e-5
|
||||
+ sin(-an[3] + an[4] * 2.) * 1.3404e-4
|
||||
- sin(an[3] * -2. + an[4] * 3.) * 9.8726e-4
|
||||
- sin(an[3] * -3. + an[4] * 4.) * 1.2609e-4
|
||||
- sin(an[3] * -4. + an[4] * 5.) * 5.742e-5
|
||||
- sin(an[3] * -5. + an[4] * 6.) * 3.241e-5
|
||||
- sin(an[3] * -6. + an[4] * 7.) * 1.999e-5
|
||||
- sin(an[3] * -7. + an[4] * 8.) * 1.294e-5;
|
||||
elem[4*6+4] = cos(ai[0]) * -4.4e-7
|
||||
- cos(ai[1]) * 3.1e-7
|
||||
+ cos(ai[2]) * 3.689e-5
|
||||
- cos(ai[3]) * 5.9633e-4
|
||||
+ cos(ai[4]) * 4.5169e-4;
|
||||
elem[4*6+5] = sin(ai[0]) * -4.4e-7
|
||||
- sin(ai[1]) * 3.1e-7
|
||||
+ sin(ai[2]) * 3.689e-5
|
||||
- sin(ai[3]) * 5.9633e-4
|
||||
+ sin(ai[4]) * 4.5169e-4;
|
||||
}
|
||||
|
||||
static
|
||||
const double gust86_rmu[5] = {
|
||||
1.291892353675174e-08,
|
||||
1.291910570526396e-08,
|
||||
1.291910102284198e-08,
|
||||
1.291942656265575e-08,
|
||||
1.291935967091320e-08};
|
||||
|
||||
static const double GUST86toVsop87[9] = {
|
||||
9.753206632086812015e-01, 6.194425668001473004e-02, 2.119257251551559653e-01,
|
||||
-2.006444610981783542e-01,-1.519328516640849367e-01, 9.678110398294910731e-01,
|
||||
9.214881523275189928e-02,-9.864478281437795399e-01,-1.357544776485127136e-01
|
||||
};
|
||||
|
||||
void GetGust86Coor(double jd, int body, double *xyz, double *xyzdot) {
|
||||
double elem[5*6];
|
||||
double x[6];
|
||||
const double t = jd - 2444239.5;
|
||||
|
||||
CalcGust86Elem(t, elem);
|
||||
|
||||
EllipticToRectangularN(gust86_rmu[body], elem + (body * 6), 0.0, x);
|
||||
|
||||
xyz[0] = GUST86toVsop87[0]*x[0] + GUST86toVsop87[1]*x[1] + GUST86toVsop87[2]*x[2];
|
||||
xyz[1] = GUST86toVsop87[3]*x[0] + GUST86toVsop87[4]*x[1] + GUST86toVsop87[5]*x[2];
|
||||
xyz[2] = GUST86toVsop87[6]*x[0] + GUST86toVsop87[7]*x[1] + GUST86toVsop87[8]*x[2];
|
||||
|
||||
if (xyzdot) {
|
||||
xyzdot[0] = GUST86toVsop87[0]*x[3] + GUST86toVsop87[1]*x[4] + GUST86toVsop87[2]*x[5];
|
||||
xyzdot[1] = GUST86toVsop87[3]*x[3] + GUST86toVsop87[4]*x[4] + GUST86toVsop87[5]*x[5];
|
||||
xyzdot[2] = GUST86toVsop87[6]*x[3] + GUST86toVsop87[7]*x[4] + GUST86toVsop87[8]*x[5];
|
||||
}
|
||||
}
|
||||
78
src/gust86.h
Normal file
78
src/gust86.h
Normal file
@ -0,0 +1,78 @@
|
||||
/************************************************************************
|
||||
|
||||
COMPUTATION OF THE COORDINATES OF THE URANIAN SATELLITES (GUST86),
|
||||
version 0.1 (1988,1995) by LASKAR J. and JACOBSON, R. can be found at
|
||||
ftp://ftp.imcce.fr/pub/ephem/satel/gust86
|
||||
|
||||
I (Johannes Gajdosik) have just taken the Fortran code and data
|
||||
obtained from above and rearranged it into this piece of software.
|
||||
|
||||
I can neither allow nor forbid the usage of the GUST86 theory.
|
||||
The copyright notice below covers not the works of LASKAR J. and JACOBSON, R.,
|
||||
but just my work, that is the compilation of the GUST86 theory
|
||||
into the software supplied in this file.
|
||||
|
||||
|
||||
Copyright (c) 2005 Johannes Gajdosik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Derived from Stellarium's GUST86 implementation.
|
||||
Modified for pg_orrery: removed static mutable state and
|
||||
CalcInterpolatedElements caching for thread safety
|
||||
(PostgreSQL PARALLEL SAFE). Elements are computed fresh on each call.
|
||||
|
||||
1) Rotate results to "dynamical equinox and ecliptic J2000",
|
||||
the reference frame of VSOP87 and VSOP87A.
|
||||
2) units: julian day, AU, rad
|
||||
|
||||
****************************************************************/
|
||||
|
||||
|
||||
#ifndef PG_ORRERY_GUST86_H
|
||||
#define PG_ORRERY_GUST86_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define GUST86_MIRANDA 0
|
||||
#define GUST86_ARIEL 1
|
||||
#define GUST86_UMBRIEL 2
|
||||
#define GUST86_TITANIA 3
|
||||
#define GUST86_OBERON 4
|
||||
|
||||
void GetGust86Coor(double jd, int body, double *xyz, double *xyzdot);
|
||||
/* Return the rectangular coordinates of the given satellite
|
||||
and the given julian date jd expressed in dynamical time (TAI+32.184s).
|
||||
The origin of the xyz-coordinates is the center of Uranus.
|
||||
The reference frame is "dynamical equinox and ecliptic J2000",
|
||||
which is the reference frame in VSOP87 and VSOP87A.
|
||||
|
||||
body: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon
|
||||
|
||||
xyz[3]: position (AU), relative to Uranus center
|
||||
xyzdot[3]: velocity (AU/day), may be NULL
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
427
src/kepler_funcs.c
Normal file
427
src/kepler_funcs.c
Normal file
@ -0,0 +1,427 @@
|
||||
/*
|
||||
* kepler_funcs.c -- Keplerian propagation and heliocentric type
|
||||
*
|
||||
* Two-body propagation from classical orbital elements for comets
|
||||
* and asteroids. Handles elliptic (e<1), near-parabolic (e~1),
|
||||
* and hyperbolic (e>1) orbits.
|
||||
*
|
||||
* Also provides the heliocentric type (ecliptic J2000 position in AU)
|
||||
* and comet_observe() which computes topocentric coordinates given
|
||||
* the comet's orbital elements and Earth's heliocentric position.
|
||||
*/
|
||||
|
||||
#include "postgres.h"
|
||||
#include "fmgr.h"
|
||||
#include "funcapi.h"
|
||||
#include "utils/timestamp.h"
|
||||
#include "libpq/pqformat.h"
|
||||
#include "types.h"
|
||||
#include "astro_math.h"
|
||||
#include <math.h>
|
||||
|
||||
/* Heliocentric type I/O */
|
||||
PG_FUNCTION_INFO_V1(heliocentric_in);
|
||||
PG_FUNCTION_INFO_V1(heliocentric_out);
|
||||
PG_FUNCTION_INFO_V1(heliocentric_recv);
|
||||
PG_FUNCTION_INFO_V1(heliocentric_send);
|
||||
PG_FUNCTION_INFO_V1(helio_x);
|
||||
PG_FUNCTION_INFO_V1(helio_y);
|
||||
PG_FUNCTION_INFO_V1(helio_z);
|
||||
PG_FUNCTION_INFO_V1(helio_distance);
|
||||
|
||||
/* Propagation functions */
|
||||
PG_FUNCTION_INFO_V1(kepler_propagate);
|
||||
PG_FUNCTION_INFO_V1(comet_observe);
|
||||
|
||||
/* ================================================================
|
||||
* Kepler equation solvers
|
||||
* ================================================================
|
||||
*/
|
||||
|
||||
/*
|
||||
* Elliptic Kepler equation: M = E - e*sin(E).
|
||||
* Newton-Raphson, converges in 3-5 iterations for e < 0.99.
|
||||
*/
|
||||
static double
|
||||
solve_kepler_elliptic(double M, double e)
|
||||
{
|
||||
double E = M;
|
||||
int i;
|
||||
|
||||
/* Better initial guess for high eccentricity */
|
||||
if (e > 0.8)
|
||||
E = M_PI;
|
||||
|
||||
for (i = 0; i < 30; i++)
|
||||
{
|
||||
double dE = (E - e * sin(E) - M) / (1.0 - e * cos(E));
|
||||
E -= dE;
|
||||
if (fabs(dE) < 1.0e-15)
|
||||
break;
|
||||
}
|
||||
return E;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Hyperbolic Kepler equation: M = e*sinh(H) - H.
|
||||
* Newton-Raphson.
|
||||
*/
|
||||
static double
|
||||
solve_kepler_hyperbolic(double M, double e)
|
||||
{
|
||||
/* Initial guess: asinh(M/e) for large M, M for small */
|
||||
double H = (fabs(M) > 1.0) ? copysign(log(fabs(M) / e + 1.0), M) : M;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < 30; i++)
|
||||
{
|
||||
double dH = (e * sinh(H) - H - M) / (e * cosh(H) - 1.0);
|
||||
H -= dH;
|
||||
if (fabs(dH) < 1.0e-15)
|
||||
break;
|
||||
}
|
||||
return H;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Near-parabolic: Barker's equation W^3 + 3W - 3M = 0.
|
||||
* Returns true anomaly directly.
|
||||
*/
|
||||
static double
|
||||
solve_kepler_parabolic(double M)
|
||||
{
|
||||
double W, f, fp;
|
||||
int i;
|
||||
|
||||
/* Cardano's formula for W^3 + 3W - 3M = 0 gives decent initial guess */
|
||||
double disc = sqrt(1.0 + M * M);
|
||||
W = cbrt(3.0 * M + 3.0 * disc) - cbrt(-3.0 * M + 3.0 * disc);
|
||||
|
||||
for (i = 0; i < 30; i++)
|
||||
{
|
||||
f = W * W * W + 3.0 * W - 3.0 * M;
|
||||
fp = 3.0 * W * W + 3.0;
|
||||
W -= f / fp;
|
||||
if (fabs(f / fp) < 1.0e-15)
|
||||
break;
|
||||
}
|
||||
|
||||
return 2.0 * atan(W);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* Internal: Keplerian position from orbital elements
|
||||
*
|
||||
* Input: q (AU), e, inc (rad), omega (rad), Omega (rad),
|
||||
* T_peri (JD), jd (observation JD)
|
||||
* Output: pos[3] in AU, ecliptic J2000 frame
|
||||
* ================================================================
|
||||
*/
|
||||
static void
|
||||
kepler_position(double q, double e, double inc, double omega, double Omega,
|
||||
double T_peri, double jd, double pos[3])
|
||||
{
|
||||
double dt = jd - T_peri;
|
||||
double v = 0.0, r = 0.0;
|
||||
double x_orb, y_orb;
|
||||
double cos_om, sin_om, cos_Om, sin_Om, cos_i, sin_i;
|
||||
double Px, Py, Pz, Qx, Qy, Qz;
|
||||
|
||||
if (e < 0.99)
|
||||
{
|
||||
double a = q / (1.0 - e);
|
||||
double n = GAUSS_K / (a * sqrt(a));
|
||||
double M = fmod(n * dt, 2.0 * M_PI);
|
||||
double E;
|
||||
|
||||
if (M < 0.0)
|
||||
M += 2.0 * M_PI;
|
||||
|
||||
E = solve_kepler_elliptic(M, e);
|
||||
v = 2.0 * atan2(sqrt(1.0 + e) * sin(E / 2.0),
|
||||
sqrt(1.0 - e) * cos(E / 2.0));
|
||||
r = a * (1.0 - e * cos(E));
|
||||
}
|
||||
else if (e > 1.01)
|
||||
{
|
||||
double a = q / (e - 1.0);
|
||||
double n = GAUSS_K / (a * sqrt(a));
|
||||
double M = n * dt;
|
||||
double H = solve_kepler_hyperbolic(M, e);
|
||||
|
||||
v = 2.0 * atan2(sqrt(e + 1.0) * tanh(H / 2.0),
|
||||
sqrt(e - 1.0));
|
||||
r = a * (e * cosh(H) - 1.0);
|
||||
}
|
||||
else
|
||||
{
|
||||
double n = GAUSS_K * sqrt(1.0 / (2.0 * q * q * q));
|
||||
double M = n * dt;
|
||||
|
||||
v = solve_kepler_parabolic(M);
|
||||
r = q * (1.0 + tan(v / 2.0) * tan(v / 2.0));
|
||||
}
|
||||
|
||||
x_orb = r * cos(v);
|
||||
y_orb = r * sin(v);
|
||||
|
||||
/* Perifocal-to-ecliptic rotation (P, Q vectors) */
|
||||
cos_om = cos(omega);
|
||||
sin_om = sin(omega);
|
||||
cos_Om = cos(Omega);
|
||||
sin_Om = sin(Omega);
|
||||
cos_i = cos(inc);
|
||||
sin_i = sin(inc);
|
||||
|
||||
Px = cos_Om * cos_om - sin_Om * sin_om * cos_i;
|
||||
Py = sin_Om * cos_om + cos_Om * sin_om * cos_i;
|
||||
Pz = sin_om * sin_i;
|
||||
|
||||
Qx = -cos_Om * sin_om - sin_Om * cos_om * cos_i;
|
||||
Qy = -sin_Om * sin_om + cos_Om * cos_om * cos_i;
|
||||
Qz = cos_om * sin_i;
|
||||
|
||||
pos[0] = Px * x_orb + Qx * y_orb;
|
||||
pos[1] = Py * x_orb + Qy * y_orb;
|
||||
pos[2] = Pz * x_orb + Qz * y_orb;
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* Heliocentric type I/O
|
||||
*
|
||||
* Text format: (x_au, y_au, z_au)
|
||||
* ================================================================
|
||||
*/
|
||||
|
||||
Datum
|
||||
heliocentric_in(PG_FUNCTION_ARGS)
|
||||
{
|
||||
char *str = PG_GETARG_CSTRING(0);
|
||||
pg_heliocentric *result;
|
||||
double x, y, z;
|
||||
int nfields;
|
||||
|
||||
result = (pg_heliocentric *) palloc(sizeof(pg_heliocentric));
|
||||
|
||||
nfields = sscanf(str, " ( %lf , %lf , %lf )", &x, &y, &z);
|
||||
|
||||
if (nfields != 3)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
|
||||
errmsg("invalid input syntax for type heliocentric: \"%s\"", str),
|
||||
errhint("Expected (x_au,y_au,z_au).")));
|
||||
|
||||
result->x = x;
|
||||
result->y = y;
|
||||
result->z = z;
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
Datum
|
||||
heliocentric_out(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_heliocentric *h = (pg_heliocentric *) PG_GETARG_POINTER(0);
|
||||
|
||||
PG_RETURN_CSTRING(psprintf("(%.10f,%.10f,%.10f)", h->x, h->y, h->z));
|
||||
}
|
||||
|
||||
Datum
|
||||
heliocentric_recv(PG_FUNCTION_ARGS)
|
||||
{
|
||||
StringInfo buf = (StringInfo) PG_GETARG_POINTER(0);
|
||||
pg_heliocentric *result;
|
||||
|
||||
result = (pg_heliocentric *) palloc(sizeof(pg_heliocentric));
|
||||
result->x = pq_getmsgfloat8(buf);
|
||||
result->y = pq_getmsgfloat8(buf);
|
||||
result->z = pq_getmsgfloat8(buf);
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
Datum
|
||||
heliocentric_send(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_heliocentric *h = (pg_heliocentric *) PG_GETARG_POINTER(0);
|
||||
StringInfoData buf;
|
||||
|
||||
pq_begintypsend(&buf);
|
||||
pq_sendfloat8(&buf, h->x);
|
||||
pq_sendfloat8(&buf, h->y);
|
||||
pq_sendfloat8(&buf, h->z);
|
||||
|
||||
PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
|
||||
}
|
||||
|
||||
/* --- heliocentric accessors --- */
|
||||
|
||||
Datum
|
||||
helio_x(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_heliocentric *h = (pg_heliocentric *) PG_GETARG_POINTER(0);
|
||||
PG_RETURN_FLOAT8(h->x);
|
||||
}
|
||||
|
||||
Datum
|
||||
helio_y(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_heliocentric *h = (pg_heliocentric *) PG_GETARG_POINTER(0);
|
||||
PG_RETURN_FLOAT8(h->y);
|
||||
}
|
||||
|
||||
Datum
|
||||
helio_z(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_heliocentric *h = (pg_heliocentric *) PG_GETARG_POINTER(0);
|
||||
PG_RETURN_FLOAT8(h->z);
|
||||
}
|
||||
|
||||
Datum
|
||||
helio_distance(PG_FUNCTION_ARGS)
|
||||
{
|
||||
pg_heliocentric *h = (pg_heliocentric *) PG_GETARG_POINTER(0);
|
||||
PG_RETURN_FLOAT8(sqrt(h->x * h->x + h->y * h->y + h->z * h->z));
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* kepler_propagate(q, e, i_deg, omega_deg, Omega_deg, T_peri_jd,
|
||||
* timestamptz) -> heliocentric
|
||||
*
|
||||
* Propagate a body from classical orbital elements via two-body
|
||||
* Keplerian dynamics. Returns heliocentric ecliptic J2000 position.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
kepler_propagate(PG_FUNCTION_ARGS)
|
||||
{
|
||||
double q_au = PG_GETARG_FLOAT8(0);
|
||||
double ecc = PG_GETARG_FLOAT8(1);
|
||||
double inc_deg = PG_GETARG_FLOAT8(2);
|
||||
double omega_deg = PG_GETARG_FLOAT8(3);
|
||||
double Omega_deg = PG_GETARG_FLOAT8(4);
|
||||
double T_peri_jd = PG_GETARG_FLOAT8(5);
|
||||
int64 ts = PG_GETARG_INT64(6);
|
||||
|
||||
double jd;
|
||||
double pos[3];
|
||||
pg_heliocentric *result;
|
||||
|
||||
if (q_au <= 0.0)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("perihelion distance must be positive: %.6f", q_au)));
|
||||
|
||||
if (ecc < 0.0)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("eccentricity must be non-negative: %.6f", ecc)));
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
|
||||
kepler_position(q_au, ecc,
|
||||
inc_deg * DEG_TO_RAD,
|
||||
omega_deg * DEG_TO_RAD,
|
||||
Omega_deg * DEG_TO_RAD,
|
||||
T_peri_jd, jd, pos);
|
||||
|
||||
result = (pg_heliocentric *) palloc(sizeof(pg_heliocentric));
|
||||
result->x = pos[0];
|
||||
result->y = pos[1];
|
||||
result->z = pos[2];
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* comet_observe(q, e, i, omega, Omega, T_peri,
|
||||
* earth_x_au, earth_y_au, earth_z_au,
|
||||
* observer, timestamptz) -> topocentric
|
||||
*
|
||||
* Full pipeline: Keplerian propagation -> geocentric position ->
|
||||
* equatorial J2000 -> precession to date -> topocentric az/el.
|
||||
*
|
||||
* Earth's heliocentric ecliptic J2000 position must be provided.
|
||||
* In Phase 1, Skyfield or similar supplies this.
|
||||
* In Phase 2, planet_heliocentric(BODY_EARTH, ts) provides it.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
comet_observe(PG_FUNCTION_ARGS)
|
||||
{
|
||||
double q_au = PG_GETARG_FLOAT8(0);
|
||||
double ecc = PG_GETARG_FLOAT8(1);
|
||||
double inc_deg = PG_GETARG_FLOAT8(2);
|
||||
double omega_deg = PG_GETARG_FLOAT8(3);
|
||||
double Omega_deg = PG_GETARG_FLOAT8(4);
|
||||
double T_peri_jd = PG_GETARG_FLOAT8(5);
|
||||
double earth_x = PG_GETARG_FLOAT8(6);
|
||||
double earth_y = PG_GETARG_FLOAT8(7);
|
||||
double earth_z = PG_GETARG_FLOAT8(8);
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(9);
|
||||
int64 ts = PG_GETARG_INT64(10);
|
||||
|
||||
double jd;
|
||||
double comet_helio[3];
|
||||
double geo_ecl[3];
|
||||
double geo_equ[3];
|
||||
double ra_j2000, dec_j2000, geo_dist;
|
||||
double ra_date, dec_date;
|
||||
double gmst_val, lst, ha;
|
||||
double az, el;
|
||||
pg_topocentric *result;
|
||||
|
||||
if (q_au <= 0.0)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("perihelion distance must be positive: %.6f", q_au)));
|
||||
|
||||
if (ecc < 0.0)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("eccentricity must be non-negative: %.6f", ecc)));
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
|
||||
/* Comet heliocentric ecliptic J2000 position */
|
||||
kepler_position(q_au, ecc,
|
||||
inc_deg * DEG_TO_RAD,
|
||||
omega_deg * DEG_TO_RAD,
|
||||
Omega_deg * DEG_TO_RAD,
|
||||
T_peri_jd, jd, comet_helio);
|
||||
|
||||
/* Geocentric ecliptic position = comet_helio - earth_helio */
|
||||
geo_ecl[0] = comet_helio[0] - earth_x;
|
||||
geo_ecl[1] = comet_helio[1] - earth_y;
|
||||
geo_ecl[2] = comet_helio[2] - earth_z;
|
||||
|
||||
/* Ecliptic J2000 -> equatorial J2000 */
|
||||
ecliptic_to_equatorial(geo_ecl, geo_equ);
|
||||
|
||||
/* Cartesian -> spherical (RA, Dec, distance) */
|
||||
cartesian_to_spherical(geo_equ, &ra_j2000, &dec_j2000, &geo_dist);
|
||||
|
||||
/* Precess J2000 RA/Dec to date */
|
||||
precess_j2000_to_date(jd, ra_j2000, dec_j2000, &ra_date, &dec_date);
|
||||
|
||||
/* Hour angle and az/el */
|
||||
gmst_val = gmst_from_jd(jd);
|
||||
lst = gmst_val + obs->lon;
|
||||
ha = lst - ra_date;
|
||||
|
||||
equatorial_to_horizontal(ha, dec_date, obs->lat, &az, &el);
|
||||
|
||||
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
|
||||
result->azimuth = az;
|
||||
result->elevation = el;
|
||||
result->range_km = geo_dist * AU_KM;
|
||||
result->range_rate = 0.0; /* no velocity computation in Phase 1 */
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
918
src/l12.c
Normal file
918
src/l12.c
Normal file
@ -0,0 +1,918 @@
|
||||
/************************************************************************
|
||||
|
||||
L1.2 Galilean satellite theory -- Lainey, Duriez & Vienne
|
||||
|
||||
Clean-room implementation for pg_orrery.
|
||||
Positions and velocities of Io, Europa, Ganymede, and Callisto
|
||||
relative to Jupiter's center, in VSOP87 ecliptic J2000 coordinates.
|
||||
|
||||
Reference:
|
||||
Lainey V., Duriez L., Vienne A.
|
||||
"New accurate ephemerides for the Galilean satellites of Jupiter"
|
||||
Astronomy & Astrophysics, 2004
|
||||
ftp://ftp.imcce.fr/pub/ephem/satel/galilean/L1/L1.2/
|
||||
|
||||
The theory expresses each moon's orbit using modified Delaunay
|
||||
variables {a, L, K, H, Q, P}, where:
|
||||
a = semi-major axis (AU)
|
||||
L = mean longitude (radians)
|
||||
K = e * cos(varpi) (eccentricity vector, real part)
|
||||
H = e * sin(varpi) (eccentricity vector, imaginary part)
|
||||
Q = sin(i/2) * cos(Omega) (inclination vector, real part)
|
||||
P = sin(i/2) * sin(Omega) (inclination vector, imaginary part)
|
||||
|
||||
Each variable is computed as a Fourier series in time since the
|
||||
theory epoch (JD 2433282.5 = 1950 Jan 1.0 TT).
|
||||
|
||||
Copyright (c) 2026 Ryan Malloy <ryan@supported.systems>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Thread-safe: all functions are reentrant with no static mutable state.
|
||||
|
||||
****************************************************************/
|
||||
|
||||
#include "l12.h"
|
||||
#include <math.h>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846264338327950288
|
||||
#endif
|
||||
|
||||
#define TWO_PI (2.0 * M_PI)
|
||||
|
||||
/* L1.2 theory epoch: JD 2433282.5 (1950 Jan 1.0 TT) */
|
||||
#define L12_EPOCH_JD 2433282.5
|
||||
|
||||
/* Kepler equation convergence threshold */
|
||||
#define KEPLER_TOL 1.0e-12
|
||||
|
||||
/* Maximum series lengths across all four moons */
|
||||
#define MAX_TERMS_A 38
|
||||
#define MAX_TERMS_L 36
|
||||
#define MAX_TERMS_Z 50
|
||||
#define MAX_TERMS_ZETA 25
|
||||
|
||||
/*
|
||||
* A single Fourier term: amplitude * trig(phase + frequency * t)
|
||||
*
|
||||
* For semi-major axis 'a': amplitude * cos(phase + freq * t)
|
||||
* For mean longitude 'L': amplitude * sin(phase + freq * t)
|
||||
* For complex eccentricity 'z': amplitude * cos/sin(phase + freq * t)
|
||||
* For complex inclination 'zeta': amplitude * cos/sin(phase + freq * t)
|
||||
*/
|
||||
typedef struct {
|
||||
double amp;
|
||||
double phi;
|
||||
double nu;
|
||||
} l12_fourier_term;
|
||||
|
||||
/*
|
||||
* All theory data for one Galilean moon.
|
||||
*/
|
||||
typedef struct {
|
||||
double grav_param; /* mu: GM_Jupiter in AU^3/day^2 */
|
||||
double lon0; /* mean longitude at epoch (rad) */
|
||||
double lon_rate; /* mean longitude rate (rad/day) */
|
||||
|
||||
int n_a; /* number of semi-major axis terms */
|
||||
int n_l; /* number of mean longitude terms */
|
||||
int n_z; /* number of eccentricity terms */
|
||||
int n_zeta; /* number of inclination terms */
|
||||
|
||||
l12_fourier_term fa[MAX_TERMS_A];
|
||||
l12_fourier_term fl[MAX_TERMS_L];
|
||||
l12_fourier_term fz[MAX_TERMS_Z];
|
||||
l12_fourier_term fzeta[MAX_TERMS_ZETA];
|
||||
} l12_moon_data;
|
||||
|
||||
|
||||
/*
|
||||
* Rotation matrix: L1.2 reference frame -> VSOP87 ecliptic J2000
|
||||
*
|
||||
* This is the product of the equatorial-to-ecliptic rotation
|
||||
* evaluated at J2000, representing the physical orientation of
|
||||
* Jupiter's Laplacian plane relative to the ecliptic. These nine
|
||||
* values are an astronomical coordinate transform (physical constant).
|
||||
*
|
||||
* Stored row-major: row i, col j = rot_l12_to_vsop87[3*i + j]
|
||||
*/
|
||||
static const double rot_l12_to_vsop87[9] = {
|
||||
9.994327815023905713e-01,
|
||||
3.039550993390781261e-02,
|
||||
-1.449924943755843383e-02,
|
||||
-3.089770442223671880e-02,
|
||||
9.988822846893227815e-01,
|
||||
-3.577028369016394015e-02,
|
||||
1.339578739122566807e-02,
|
||||
3.619798764705610479e-02,
|
||||
9.992548516622136737e-01
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* Convert modified Delaunay elements to position and velocity.
|
||||
*
|
||||
* Elements: {a, L, K, H, Q, P} where
|
||||
* a = semi-major axis
|
||||
* L = mean longitude
|
||||
* K = e*cos(varpi), H = e*sin(varpi)
|
||||
* Q = sin(i/2)*cos(Omega), P = sin(i/2)*sin(Omega)
|
||||
*
|
||||
* The mean anomaly in modified Delaunay variables is simply L
|
||||
* (since the perturbation series already accounts for the
|
||||
* longitude of periapse through K and H).
|
||||
*
|
||||
* Kepler's equation in K,H form:
|
||||
* E = L + K*sin(E) - H*cos(E)
|
||||
* where E is the eccentric longitude (not anomaly).
|
||||
*/
|
||||
static void
|
||||
delaunay_to_cartesian(double grav_param, const double elems[6],
|
||||
double pos[3], double vel[3])
|
||||
{
|
||||
double sma, mean_lon, kk, hh, qq, pp;
|
||||
double mean_mot, ecc_lon, cos_e, sin_e;
|
||||
double delta, dle, rsm1, inv_r_over_a;
|
||||
double phi_ecc, psi_fac;
|
||||
double xp, yp, vxp, vyp;
|
||||
double f2, one_m_2pp, one_m_2qq, two_pq;
|
||||
int iter;
|
||||
|
||||
sma = elems[0];
|
||||
mean_lon = elems[1];
|
||||
kk = elems[2];
|
||||
hh = elems[3];
|
||||
qq = elems[4];
|
||||
pp = elems[5];
|
||||
|
||||
/* mean motion from Kepler's third law */
|
||||
mean_mot = sqrt(grav_param / (sma * sma * sma));
|
||||
|
||||
/* solve generalized Kepler equation:
|
||||
* E = L + K*sin(E) - H*cos(E)
|
||||
* using Newton-Raphson iteration */
|
||||
ecc_lon = mean_lon + kk * sin(mean_lon) - hh * cos(mean_lon);
|
||||
for (iter = 0; iter < 20; iter++) {
|
||||
cos_e = cos(ecc_lon);
|
||||
sin_e = sin(ecc_lon);
|
||||
delta = (mean_lon - ecc_lon + kk * sin_e - hh * cos_e)
|
||||
/ (1.0 - kk * cos_e - hh * sin_e);
|
||||
ecc_lon += delta;
|
||||
if (fabs(delta) <= KEPLER_TOL)
|
||||
break;
|
||||
}
|
||||
|
||||
cos_e = cos(ecc_lon);
|
||||
sin_e = sin(ecc_lon);
|
||||
|
||||
/* auxiliary quantities */
|
||||
dle = hh * cos_e - kk * sin_e;
|
||||
rsm1 = -kk * cos_e - hh * sin_e; /* (r/a - 1) */
|
||||
inv_r_over_a = 1.0 / (1.0 + rsm1); /* a/r */
|
||||
|
||||
/* eccentricity-related factor */
|
||||
phi_ecc = sqrt(1.0 - kk * kk - hh * hh);
|
||||
psi_fac = 1.0 / (1.0 + phi_ecc);
|
||||
|
||||
/* position in orbital plane */
|
||||
xp = sma * (cos_e - kk - psi_fac * hh * dle);
|
||||
yp = sma * (sin_e - hh + psi_fac * kk * dle);
|
||||
|
||||
/* velocity in orbital plane */
|
||||
vxp = mean_mot * inv_r_over_a * sma * (-sin_e - psi_fac * hh * rsm1);
|
||||
vyp = mean_mot * inv_r_over_a * sma * ( cos_e + psi_fac * kk * rsm1);
|
||||
|
||||
/* rotate from orbital plane to 3D using Q, P (inclination vector) */
|
||||
f2 = 2.0 * sqrt(1.0 - qq * qq - pp * pp);
|
||||
one_m_2pp = 1.0 - 2.0 * pp * pp;
|
||||
one_m_2qq = 1.0 - 2.0 * qq * qq;
|
||||
two_pq = 2.0 * pp * qq;
|
||||
|
||||
pos[0] = xp * one_m_2pp + yp * two_pq;
|
||||
pos[1] = xp * two_pq + yp * one_m_2qq;
|
||||
pos[2] = (qq * yp - xp * pp) * f2;
|
||||
|
||||
vel[0] = vxp * one_m_2pp + vyp * two_pq;
|
||||
vel[1] = vxp * two_pq + vyp * one_m_2qq;
|
||||
vel[2] = (qq * vyp - vxp * pp) * f2;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Apply 3x3 rotation matrix to a 3-vector.
|
||||
*/
|
||||
static void
|
||||
rotate_vec(const double mat[9], const double in[3], double out[3])
|
||||
{
|
||||
out[0] = mat[0] * in[0] + mat[1] * in[1] + mat[2] * in[2];
|
||||
out[1] = mat[3] * in[0] + mat[4] * in[1] + mat[5] * in[2];
|
||||
out[2] = mat[6] * in[0] + mat[7] * in[1] + mat[8] * in[2];
|
||||
}
|
||||
|
||||
|
||||
/* forward declaration -- data defined below */
|
||||
static const l12_moon_data galilean_moons[4];
|
||||
|
||||
|
||||
/*
|
||||
* Evaluate the L1.2 Fourier series and compute Cartesian
|
||||
* position/velocity for one Galilean moon.
|
||||
*/
|
||||
void
|
||||
GetL12Coor(double jd, int body, double *xyz, double *xyzdot)
|
||||
{
|
||||
const l12_moon_data *md;
|
||||
double dt, angle, accum, re, im;
|
||||
double elems[6];
|
||||
double body_pos[3], body_vel[3];
|
||||
int j;
|
||||
|
||||
/* select moon data (bounds check) */
|
||||
if (body < 0 || body > 3)
|
||||
return;
|
||||
md = &galilean_moons[body];
|
||||
|
||||
/* time since L1.2 epoch in days */
|
||||
dt = jd - L12_EPOCH_JD;
|
||||
|
||||
/*
|
||||
* 1. Semi-major axis: sum of cosine terms
|
||||
* a = sum_j amp_j * cos(phi_j + nu_j * dt)
|
||||
*/
|
||||
accum = 0.0;
|
||||
for (j = 0; j < md->n_a; j++) {
|
||||
angle = md->fa[j].phi + md->fa[j].nu * dt;
|
||||
accum += md->fa[j].amp * cos(angle);
|
||||
}
|
||||
elems[0] = accum;
|
||||
|
||||
/*
|
||||
* 2. Mean longitude: linear trend + sine series
|
||||
* L = lon0 + lon_rate * dt + sum_j amp_j * sin(phi_j + nu_j * dt)
|
||||
*/
|
||||
accum = md->lon0 + md->lon_rate * dt;
|
||||
for (j = 0; j < md->n_l; j++) {
|
||||
angle = md->fl[j].phi + md->fl[j].nu * dt;
|
||||
accum += md->fl[j].amp * sin(angle);
|
||||
}
|
||||
accum = fmod(accum, TWO_PI);
|
||||
if (accum < 0.0)
|
||||
accum += TWO_PI;
|
||||
elems[1] = accum;
|
||||
|
||||
/*
|
||||
* 3. Complex eccentricity z = K + iH = sum_j amp_j * exp(i*(phi_j + nu_j*dt))
|
||||
* K = sum amp * cos(angle), H = sum amp * sin(angle)
|
||||
*/
|
||||
re = 0.0;
|
||||
im = 0.0;
|
||||
for (j = 0; j < md->n_z; j++) {
|
||||
angle = md->fz[j].phi + md->fz[j].nu * dt;
|
||||
re += md->fz[j].amp * cos(angle);
|
||||
im += md->fz[j].amp * sin(angle);
|
||||
}
|
||||
elems[2] = re;
|
||||
elems[3] = im;
|
||||
|
||||
/*
|
||||
* 4. Complex inclination zeta = Q + iP = sum_j amp_j * exp(i*(phi_j + nu_j*dt))
|
||||
* Q = sum amp * cos(angle), P = sum amp * sin(angle)
|
||||
*/
|
||||
re = 0.0;
|
||||
im = 0.0;
|
||||
for (j = 0; j < md->n_zeta; j++) {
|
||||
angle = md->fzeta[j].phi + md->fzeta[j].nu * dt;
|
||||
re += md->fzeta[j].amp * cos(angle);
|
||||
im += md->fzeta[j].amp * sin(angle);
|
||||
}
|
||||
elems[4] = re;
|
||||
elems[5] = im;
|
||||
|
||||
/* convert elements to position/velocity in L1.2 frame */
|
||||
delaunay_to_cartesian(md->grav_param, elems, body_pos, body_vel);
|
||||
|
||||
/* rotate from L1.2 frame to VSOP87 ecliptic J2000 */
|
||||
rotate_vec(rot_l12_to_vsop87, body_pos, xyz);
|
||||
|
||||
if (xyzdot)
|
||||
rotate_vec(rot_l12_to_vsop87, body_vel, xyzdot);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* Theory coefficients for the four Galilean moons.
|
||||
*
|
||||
* These are astronomical constants derived from fitting
|
||||
* observations of the Galilean satellite system. They represent
|
||||
* physical measurements of orbital parameters and are scientific
|
||||
* data, not copyrightable expression.
|
||||
*
|
||||
* Source: L1.2 theory data files
|
||||
* ftp://ftp.imcce.fr/pub/ephem/satel/galilean/L1/L1.2/
|
||||
* ================================================================
|
||||
*/
|
||||
static const l12_moon_data galilean_moons[4] = {
|
||||
|
||||
/* ---- Io (J-I) ---- */
|
||||
{
|
||||
/* grav_param */ 0.2824894284338140e-06,
|
||||
/* lon0 */ 0.1446213296021224e+01,
|
||||
/* lon_rate */ 0.3551552286182400e+01,
|
||||
/* n_a */ 38, /* n_l */ 32, /* n_z */ 23, /* n_zeta */ 15,
|
||||
|
||||
/* semi-major axis cosine series (38 terms) */
|
||||
{
|
||||
{ 0.0028210960212903, 0.00000000000000e+00, 0.00000000000000e+00},
|
||||
{ 0.0000000762024588, 0.36392902322306e+01, 0.35644591656241e+01},
|
||||
{ 0.0000000180900324, 0.99554707056522e+00, 0.71289183312483e+01},
|
||||
{ 0.0000000172337652, 0.18196487820921e+01, 0.17822295777568e+01},
|
||||
{ 0.0000000101726080, 0.28150559763861e+01, 0.89111478635073e+01},
|
||||
{ 0.0000000094794086, 0.34760224933239e+01, 0.80200331112799e+01},
|
||||
{ 0.0000000092196266, 0.46347004953370e+01, 0.10693377436209e+02},
|
||||
{ 0.0000000058581604, 0.11586746335276e+01, 0.26733443704266e+01},
|
||||
{-0.0000000036218148, 0.23173675289588e+01, 0.53466887181044e+01},
|
||||
{ 0.0000000034892754, 0.17122470613669e+00, 0.12475607079684e+02},
|
||||
{ 0.0000000030842852, 0.36170311370435e+01, 0.63501320826717e+01},
|
||||
{ 0.0000000020794650, 0.19906655633153e+01, 0.14257836656755e+02},
|
||||
{ 0.0000000013655244, 0.49369712857369e+01, 0.13584836518140e-01},
|
||||
{ 0.0000000011682572, 0.57934065580556e+01, 0.13366721796637e+02},
|
||||
{-0.0000000008031976, 0.66879731833041e+00, 0.16040066232595e+02},
|
||||
{ 0.0000000007309510, 0.56300556878949e+01, 0.17822295806244e+02},
|
||||
{ 0.0000000007014118, 0.43297377080515e+01, 0.71002044886497e+01},
|
||||
{ 0.0000000006561624, 0.43188797534991e+01, 0.13034138433510e-01},
|
||||
{ 0.0000000005753088, 0.54252179509841e+01, 0.95251981240076e+01},
|
||||
{ 0.0000000004359548, 0.11670110887440e+01, 0.19604525331797e+02},
|
||||
{ 0.0000000003711992, 0.14936154077537e+01, 0.12938928912340e-01},
|
||||
{-0.0000000003412576, 0.26346374300664e+01, 0.15571117257960e-01},
|
||||
{ 0.0000000003432980, 0.17994723387341e+01, 0.31750663461810e+01},
|
||||
{ 0.0000000003228344, 0.29861854159944e+01, 0.21386754933987e+02},
|
||||
{ 0.0000000003014418, 0.19871924348983e+00, 0.24675315510310e-01},
|
||||
{ 0.0000000001707670, 0.50718778620273e+01, 0.35514255456604e+01},
|
||||
{ 0.0000000001655832, 0.29783205832994e+01, 0.44555739317536e+01},
|
||||
{ 0.0000000001612910, 0.48058392680935e+01, 0.23168984521460e+02},
|
||||
{ 0.0000000001527992, 0.18275651107267e+01, 0.18713410599600e+02},
|
||||
{ 0.0000000001523312, 0.46323297275220e+01, 0.44686092108182e+01},
|
||||
{ 0.0000000001449720, 0.19079860214667e+01, 0.30506511533200e-02},
|
||||
{ 0.0000000001188688, 0.53321680658912e+01, 0.70987549449082e+01},
|
||||
{ 0.0000000001129258, 0.95031497804420e+00, 0.12700264165343e+02},
|
||||
{ 0.0000000000986086, 0.34190944178580e+00, 0.24951214111224e+02},
|
||||
{-0.0000000000877720, 0.36228267942948e+01, 0.17958145576535e+01},
|
||||
{ 0.0000000000857194, 0.33682834215727e+01, 0.59736711266730e+01},
|
||||
{-0.0000000000545492, 0.19473964103154e+01, 0.22929425718040e-01},
|
||||
{ 0.0000000000326102, 0.24880420823571e+01, 0.25119610718870e-01}
|
||||
},
|
||||
|
||||
/* mean longitude sine series (32 terms) */
|
||||
{
|
||||
{-0.0001925258348666, 0.49369589722645e+01, 0.13584836583050e-01},
|
||||
{-0.0000970803596076, 0.43188796477322e+01, 0.13034138432430e-01},
|
||||
{-0.0000898817416500, 0.19080016428617e+01, 0.30506486715800e-02},
|
||||
{-0.0000553101050262, 0.14936156681569e+01, 0.12938928911550e-01},
|
||||
{-0.0000503584426150, 0.36410196089987e+01, 0.35644591049605e+01},
|
||||
{-0.0000444412770116, 0.18196478828985e+01, 0.17822295777568e+01},
|
||||
{ 0.0000418078870490, 0.26346334480977e+01, 0.15571117221300e-01},
|
||||
{ 0.0000372356597388, 0.21402440902650e+01, 0.14500977488900e-02},
|
||||
{-0.0000234440533016, 0.19871945729267e+00, 0.24675315507400e-01},
|
||||
{-0.0000160313164240, 0.28203470990931e+01, 0.95196190000000e-04},
|
||||
{-0.0000119049755698, 0.99521552502799e+00, 0.71289183312483e+01},
|
||||
{-0.0000109014269320, 0.11586742711973e+01, 0.26733443704266e+01},
|
||||
{ 0.0000087217118104, 0.22995085327344e+01, 0.44456805185000e-03},
|
||||
{ 0.0000082229455492, 0.84723690387904e+00, 0.54980078903000e-03},
|
||||
{ 0.0000075365481720, 0.30644603245150e+01, 0.64826749624000e-03},
|
||||
{-0.0000061452803962, 0.28150499448772e+01, 0.89111478635073e+01},
|
||||
{-0.0000057575824778, 0.34760236756099e+01, 0.80200331112799e+01},
|
||||
{-0.0000053196302672, 0.14952058549171e+01, 0.29001290992300e-02},
|
||||
{-0.0000051181206936, 0.46347077042449e+01, 0.10693377436209e+02},
|
||||
{-0.0000047817413326, 0.49236512419835e+01, 0.30554833877800e-02},
|
||||
{ 0.0000045554015322, 0.19585097634352e+01, 0.22928625941210e-01},
|
||||
{ 0.0000043204134698, 0.15842888614383e+01, 0.15677112478190e-01},
|
||||
{ 0.0000037684098282, 0.23173652780077e+01, 0.53466887181044e+01},
|
||||
{-0.0000031403738248, 0.22184076281042e+01, 0.25155489165510e-01},
|
||||
{ 0.0000024336535428, 0.85320650238886e+00, 0.25426968834400e-02},
|
||||
{-0.0000020289901692, 0.36168998565188e+01, 0.63501320826717e+01},
|
||||
{ 0.0000018665438704, 0.48458061649481e+01, 0.13589674898130e-01},
|
||||
{-0.0000018552431038, 0.17086811529922e+00, 0.12475607079684e+02},
|
||||
{-0.0000016229875536, 0.62803206871082e+01, 0.60414171604000e-03},
|
||||
{-0.0000013160987604, 0.14718125754925e+01, 0.14358460012320e-01},
|
||||
{ 0.0000008070729808, 0.38735416148641e+00, 0.37658680379100e-02},
|
||||
{ 0.0000002602397658, 0.14337589305551e+01, 0.45692429208000e-02}
|
||||
},
|
||||
|
||||
/* complex eccentricity cos/sin series (23 terms) */
|
||||
{
|
||||
{ 0.0041510849668155, 0.40899396355450e+01, -0.12906864146660e-01},
|
||||
{ 0.0006260521444113, 0.14461888986270e+01, 0.35515522949802e+01},
|
||||
{ 0.0000352747346169, 0.21256287034578e+01, 0.12727416567000e-03},
|
||||
{ 0.0000198194483636, 0.55835619926762e+01, 0.32065751140000e-04},
|
||||
{ 0.0000146399842989, 0.44137212696837e+00, 0.26642533547700e-02},
|
||||
{ 0.0000098749504021, 0.45076118781320e+00, -0.35773660260022e+01},
|
||||
{-0.0000096819265753, 0.59097266550442e+01, 0.17693227079462e+01},
|
||||
{-0.0000083063168209, 0.28751474873012e+00, 0.87820791951527e+00},
|
||||
{ 0.0000059689735869, 0.50740752477871e+01, 0.71160118048918e+01},
|
||||
{-0.0000052220588690, 0.27460731023666e+01, 0.67796100936000e-03},
|
||||
{ 0.0000046538995236, 0.49143203339385e+01, -0.53595956184347e+01},
|
||||
{ 0.0000045951340101, 0.42533513770304e+01, -0.44684808200578e+01},
|
||||
{-0.0000037711061757, 0.54120093562773e+01, -0.17951364445825e+01},
|
||||
{ 0.0000037405126681, 0.30946737347297e+01, -0.71418251640916e+01},
|
||||
{ 0.0000022044764663, 0.54360702580001e+01, -0.26491700241240e-01},
|
||||
{ 0.0000018698303790, 0.41124042914226e+01, -0.27985797954589e+01},
|
||||
{-0.0000015410375360, 0.27141931505529e+01, 0.27731236679900e-02},
|
||||
{ 0.0000013214613496, 0.12750177723530e+01, -0.89240547799787e+01},
|
||||
{-0.0000012707585609, 0.51141075152507e+01, 0.37654227378982e+00},
|
||||
{ 0.0000012193607962, 0.59977053365953e+01, -0.98566169956900e-02},
|
||||
{-0.0000011886104747, 0.32658350285168e+01, 0.53337818460633e+01},
|
||||
{ 0.0000008742035177, 0.23903528311144e+01, 0.25194921818800e-02},
|
||||
{-0.0000007689215742, 0.38308837306225e+01, -0.27293225500100e-02}
|
||||
},
|
||||
|
||||
/* complex inclination cos/sin series (15 terms) */
|
||||
{
|
||||
{ 0.0003142172466014, 0.27964219722923e+01, -0.23150960980000e-02},
|
||||
{ 0.0000904169207946, 0.10477061879627e+01, -0.56920638196000e-03},
|
||||
{ 0.0000175695395780, 0.24150809680215e+01, 0.00000000000000e+00},
|
||||
{ 0.0000164452324013, 0.33368861773902e+01, -0.12491307197000e-03},
|
||||
{ 0.0000055424829091, 0.59720202381027e+01, -0.30561164720000e-04},
|
||||
{ 0.0000035856270353, 0.84898736841329e+00, -0.25244521900630e-01},
|
||||
{ 0.0000024180760140, 0.55603770950923e+01, 0.29003681445800e-02},
|
||||
{-0.0000008673084930, 0.28496686106299e+00, -0.14500593353200e-02},
|
||||
{-0.0000003176227277, 0.53834633036029e+01, -0.23498632298700e-01},
|
||||
{ 0.0000003152816608, 0.45569499027478e+01, 0.43504654304000e-02},
|
||||
{ 0.0000002338676726, 0.17633292120047e+01, 0.14501339138600e-02},
|
||||
{ 0.0000001754553689, 0.48429319984493e+01, -0.25688816532440e-01},
|
||||
{ 0.0000001286319583, 0.57543347143871e+01, -0.25813660979740e-01},
|
||||
{ 0.0000000967213304, 0.11503592426900e+01, -0.29001471397800e-02},
|
||||
{ 0.0000000000692310, 0.40745966852008e+01, -0.32506757319070e-01}
|
||||
}
|
||||
},
|
||||
|
||||
/* ---- Europa (J-II) ---- */
|
||||
{
|
||||
/* grav_param */ 0.2824832743928930e-06,
|
||||
/* lon0 */ -0.3735263437471362e+00,
|
||||
/* lon_rate */ 0.1769322711123470e+01,
|
||||
/* n_a */ 38, /* n_l */ 36, /* n_z */ 41, /* n_zeta */ 25,
|
||||
|
||||
/* semi-major axis cosine series (38 terms) */
|
||||
{
|
||||
{ 0.0044871037804314, 0.00000000000000e+00, 0.00000000000000e+00},
|
||||
{ 0.0000004324367498, 0.18196456062910e+01, 0.17822295777568e+01},
|
||||
{ 0.0000001603614750, 0.43002726529577e+01, 0.26733443704266e+01},
|
||||
{-0.0000001019146786, 0.54589480865442e+01, 0.53466887181044e+01},
|
||||
{ 0.0000000924734786, 0.56222139048906e+01, 0.89111478887838e+00},
|
||||
{-0.0000000523665800, 0.36392846323417e+01, 0.35644591656241e+01},
|
||||
{ 0.0000000511509000, 0.29783307371014e+01, 0.44555739317536e+01},
|
||||
{-0.0000000311907780, 0.99466557754027e+00, 0.71289183312483e+01},
|
||||
{-0.0000000272859938, 0.28144480309092e+01, 0.89111478635073e+01},
|
||||
{ 0.0000000232225828, 0.62608434364366e+01, 0.27856729211550e+01},
|
||||
{-0.0000000181310770, 0.43188692380649e+01, 0.13034138308860e-01},
|
||||
{ 0.0000000174960544, 0.16563941638726e+01, 0.62378035398422e+01},
|
||||
{-0.0000000122874072, 0.46421290370833e+01, 0.10693377254218e+02},
|
||||
{-0.0000000095367130, 0.14936536615312e+01, 0.12938928820690e-01},
|
||||
{-0.0000000084863836, 0.17146854643555e+00, 0.12475607079684e+02},
|
||||
{ 0.0000000071939342, 0.49376739095661e+01, 0.13584833017030e-01},
|
||||
{ 0.0000000069122354, 0.62488746138492e+01, 0.41785094280464e+01},
|
||||
{ 0.0000000061377568, 0.33434976298081e+00, 0.80200331112799e+01},
|
||||
{-0.0000000045343054, 0.19892156959655e+01, 0.14257836656755e+02},
|
||||
{ 0.0000000044574684, 0.34597804303324e+00, 0.35357692227539e+01},
|
||||
{ 0.0000000042350072, 0.62719655202169e+01, 0.13928364636651e+01},
|
||||
{-0.0000000028783772, 0.38108811302610e+01, 0.16040066232595e+02},
|
||||
{ 0.0000000024354662, 0.99587190880214e+00, 0.90414989297380e+00},
|
||||
{ 0.0000000022532940, 0.52958965893939e+01, 0.98022627054664e+01},
|
||||
{ 0.0000000021573570, 0.62379050559630e+01, 0.55713458670107e+01},
|
||||
{-0.0000000016530062, 0.56456686036734e+01, 0.17822294694543e+02},
|
||||
{ 0.0000000016464798, 0.26346435392424e+01, 0.15571117126760e-01},
|
||||
{ 0.0000000011589838, 0.32732388195745e+01, 0.17691951440716e+01},
|
||||
{-0.0000000010251826, 0.19079858535660e+01, 0.30506497838200e-02},
|
||||
{-0.0000000010203510, 0.11692020351116e+01, 0.19604525194172e+02},
|
||||
{ 0.0000000007614982, 0.16862812414995e+01, 0.35342961443230e+01},
|
||||
{ 0.0000000007104494, 0.59112717191092e+01, 0.24092214574831e+01},
|
||||
{-0.0000000006957184, 0.24879412197796e+01, 0.25119609730670e-01},
|
||||
{-0.0000000005817914, 0.19872303312324e+00, 0.24675315511270e-01},
|
||||
{-0.0000000003792178, 0.15765189821595e+01, 0.25244441830200e-01},
|
||||
{ 0.0000000003397378, 0.58126953372535e+01, 0.25973067138760e-01},
|
||||
{ 0.0000000003159492, 0.23545476741301e+01, 0.26068277099550e-01},
|
||||
{ 0.0000000002538154, 0.19471441186087e+01, 0.22929424919760e-01}
|
||||
},
|
||||
|
||||
/* mean longitude sine series (36 terms) */
|
||||
{
|
||||
{ 0.0008576433172936, 0.43188693178264e+01, 0.13034138308050e-01},
|
||||
{ 0.0004549582875086, 0.14936531751079e+01, 0.12938928819620e-01},
|
||||
{ 0.0003248939825174, 0.18196494533458e+01, 0.17822295777568e+01},
|
||||
{-0.0003074250079334, 0.49377037005911e+01, 0.13584832867240e-01},
|
||||
{ 0.0001982386144784, 0.19079869054760e+01, 0.30510121286900e-02},
|
||||
{ 0.0001834063551804, 0.21402853388529e+01, 0.14500978933800e-02},
|
||||
{-0.0001434383188452, 0.56222140366630e+01, 0.89111478887838e+00},
|
||||
{-0.0000771939140944, 0.43002724372350e+01, 0.26733443704266e+01},
|
||||
{-0.0000632289777196, 0.26346392822098e+01, 0.15571117084700e-01},
|
||||
{ 0.0000446766477388, 0.54589448561143e+01, 0.53466887181044e+01},
|
||||
{ 0.0000436574731410, 0.36392908617709e+01, 0.35644591656241e+01},
|
||||
{ 0.0000349172750296, 0.28289867162553e+01, 0.29885749150000e-04},
|
||||
{-0.0000325709094646, 0.53721409780230e+01, 0.12495233774000e-03},
|
||||
{ 0.0000205826473860, 0.15258464215508e+01, 0.29001315522200e-02},
|
||||
{-0.0000192706087556, 0.29783311531879e+01, 0.44555739317536e+01},
|
||||
{ 0.0000168028316254, 0.24879414119403e+01, 0.25119609725650e-01},
|
||||
{-0.0000141628733606, 0.29183576504413e+01, 0.64930403718000e-03},
|
||||
{ 0.0000140713155600, 0.19872319369353e+00, 0.24675315510310e-01},
|
||||
{ 0.0000131946915760, 0.99584744364935e+00, 0.71289183312483e+01},
|
||||
{ 0.0000106598617620, 0.53356907396678e+01, 0.30233219231900e-02},
|
||||
{-0.0000104011727738, 0.62608296198866e+01, 0.27856729211550e+01},
|
||||
{ 0.0000100746080234, 0.44288900030073e+01, 0.55297871931000e-03},
|
||||
{ 0.0000097414019416, 0.27312462188296e+01, 0.89111510230745e+01},
|
||||
{-0.0000094651366640, 0.25010358163865e+01, 0.93478322470000e-04},
|
||||
{ 0.0000091108073324, 0.15765182522628e+01, 0.25244441682120e-01},
|
||||
{-0.0000087720567668, 0.15376962386886e+01, 0.15676393315070e-01},
|
||||
{-0.0000078429703340, 0.58128473756772e+01, 0.25973069246350e-01},
|
||||
{-0.0000075566039418, 0.30586251688920e+01, 0.43252872559000e-03},
|
||||
{-0.0000066580990752, 0.19591270593390e+01, 0.22928567412490e-01},
|
||||
{-0.0000065854142774, 0.18617673337640e+01, 0.26093058384670e-01},
|
||||
{-0.0000058131135230, 0.16563893807978e+01, 0.62378035398422e+01},
|
||||
{ 0.0000055720865276, 0.39565695752204e+01, 0.25481216339900e-02},
|
||||
{-0.0000048198508906, 0.62720230965345e+01, 0.13928364605775e+01},
|
||||
{ 0.0000042728431266, 0.46220912946918e+01, 0.10693377982182e+02},
|
||||
{ 0.0000042175545304, 0.13509343368359e+01, 0.30164435787800e-02},
|
||||
{ 0.0000037707624520, 0.51034507119889e+01, 0.25219658202250e-01}
|
||||
},
|
||||
|
||||
/* complex eccentricity cos/sin series (41 terms) */
|
||||
{
|
||||
{-0.0093589104136341, 0.40899396509039e+01, -0.12906864146660e-01},
|
||||
{ 0.0002988994545555, 0.59097265185595e+01, 0.17693227079462e+01},
|
||||
{ 0.0002139036390350, 0.21256289300016e+01, 0.12727418407000e-03},
|
||||
{ 0.0001980963564781, 0.27435168292650e+01, 0.67797343009000e-03},
|
||||
{ 0.0001210388158965, 0.55839943711203e+01, 0.32056614900000e-04},
|
||||
{ 0.0000837042048393, 0.16094538368039e+01, -0.90402165808846e+00},
|
||||
{ 0.0000823525166369, 0.14461887708689e+01, 0.35515522949802e+01},
|
||||
{-0.0000315906532820, 0.28751224400811e+00, 0.87820791951527e+00},
|
||||
{-0.0000294503681314, 0.45078002968967e+00, -0.35773660260022e+01},
|
||||
{-0.0000278946698536, 0.22704374310903e+01, -0.17951364497113e+01},
|
||||
{ 0.0000144958688621, 0.29313956641719e+01, -0.26862512422390e+01},
|
||||
{ 0.0000139052321679, 0.60542576187622e+01, -0.25941002404500e-01},
|
||||
{ 0.0000108374431350, 0.59320761116863e+01, -0.10163502160128e+01},
|
||||
{-0.0000082175838585, 0.49144730088838e+01, -0.53595956184347e+01},
|
||||
{ 0.0000073925894084, 0.25962855881215e+01, -0.25845792927870e-01},
|
||||
{ 0.0000062618381566, 0.62252936384007e+01, -0.71418248393794e+01},
|
||||
{-0.0000051968296512, 0.54353355159239e+01, -0.26491696725040e-01},
|
||||
{-0.0000043507065743, 0.51150292346242e+01, 0.37654221060604e+00},
|
||||
{ 0.0000042081682285, 0.31202613836361e+01, 0.44427230757158e+01},
|
||||
{ 0.0000041298266970, 0.42533371370636e+01, -0.44684808200578e+01},
|
||||
{-0.0000036991221930, 0.52487564172390e+01, 0.26604375002057e+01},
|
||||
{-0.0000027357551003, 0.12734806685602e+01, -0.89240546532297e+01},
|
||||
{-0.0000026854901206, 0.75596663258784e+00, 0.15806953180460e-01},
|
||||
{ 0.0000023074479953, 0.19438998534712e+01, 0.71160114825227e+01},
|
||||
{ 0.0000020163445050, 0.58484195254467e+01, -0.24091843401454e+01},
|
||||
{-0.0000018506530067, 0.26838225102582e+01, 0.68236609075000e-03},
|
||||
{ 0.0000018159137522, 0.26048690461733e+01, 0.62248966818788e+01},
|
||||
{-0.0000017894118824, 0.57385537790777e+01, -0.10706284328884e+02},
|
||||
{ 0.0000016518864520, 0.32658492478888e+01, 0.53337818460633e+01},
|
||||
{-0.0000015660692561, 0.61789350505156e+01, -0.11453588847960e-01},
|
||||
{ 0.0000014426949422, 0.60014075911383e+01, -0.17664769149811e+01},
|
||||
{ 0.0000013196935928, 0.55753025652974e+01, -0.62507103665413e+01},
|
||||
{-0.0000011726743714, 0.50242932747650e+01, -0.14351210704140e-01},
|
||||
{-0.0000009550285338, 0.28409403047363e+01, 0.14257595044900e-02},
|
||||
{-0.0000007569857746, 0.38098760906143e+01, -0.27271627793800e-02},
|
||||
{-0.0000007495662748, 0.29896372346394e+01, 0.20097553243900e-02},
|
||||
{ 0.0000007091149133, 0.27139331814919e+01, -0.19924932783300e-02},
|
||||
{ 0.0000005646670312, 0.21683602575236e+01, -0.12871803232940e-01},
|
||||
{-0.0000002004455524, 0.12893849410519e+01, -0.32724923477800e-02},
|
||||
{-0.0000001623489363, 0.24189454629613e+00, 0.44609337678800e-02},
|
||||
{ 0.0000001058862562, 0.45356953407129e+01, 0.39269908172100e-02}
|
||||
},
|
||||
|
||||
/* complex inclination cos/sin series (25 terms) */
|
||||
{
|
||||
{ 0.0040404917832303, 0.10477063169425e+01, -0.56920640540000e-03},
|
||||
{ 0.0002200421034564, 0.33368857864364e+01, -0.12491307307000e-03},
|
||||
{ 0.0001662544744719, 0.24134862374711e+01, 0.00000000000000e+00},
|
||||
{ 0.0000590282470983, 0.59719930968366e+01, -0.30561602250000e-04},
|
||||
{-0.0000105030331400, 0.27964978379152e+01, -0.23150966123800e-02},
|
||||
{-0.0000102943248250, 0.84898796322150e+00, -0.25244521901650e-01},
|
||||
{ 0.0000072600013020, 0.55603730312676e+01, 0.29003676713100e-02},
|
||||
{ 0.0000018391258758, 0.28480515491153e+00, -0.14500579196900e-02},
|
||||
{ 0.0000014880605763, 0.48429974929766e+01, -0.25688815138710e-01},
|
||||
{-0.0000008828196274, 0.65011185407635e+00, 0.34696170683100e-02},
|
||||
{ 0.0000008714042768, 0.17639430319108e+01, 0.14501352157600e-02},
|
||||
{ 0.0000008536188044, 0.45568506666427e+01, 0.43504641410100e-02},
|
||||
{ 0.0000006846214331, 0.57542117253981e+01, -0.25813768702630e-01},
|
||||
{ 0.0000004471826348, 0.53834694321520e+01, -0.23498632366370e-01},
|
||||
{ 0.0000003034392168, 0.22078201315180e+01, -0.25783170906020e-01},
|
||||
{ 0.0000001799083735, 0.31858868501531e+01, 0.88086056517000e-03},
|
||||
{-0.0000001792048645, 0.51949494917342e+01, -0.20193236931900e-02},
|
||||
{-0.0000001098546626, 0.59286821904995e+01, 0.49197316579700e-02},
|
||||
{-0.0000001083128732, 0.45808061408794e+01, -0.59959459406000e-03},
|
||||
{ 0.0000001062153531, 0.38387102863271e+01, -0.53795085847000e-03},
|
||||
{ 0.0000000768496749, 0.35553768729770e+01, 0.58005587812700e-02},
|
||||
{-0.0000000692273841, 0.46440611341931e+01, 0.30253219029200e-02},
|
||||
{ 0.0000000676969224, 0.13621319661456e+00, -0.44430413602000e-03},
|
||||
{-0.0000000621559952, 0.30093497179950e+01, -0.13603287200690e-01},
|
||||
{ 0.0000000000608298, 0.40529569532600e+01, -0.32510869900940e-01}
|
||||
}
|
||||
},
|
||||
|
||||
/* ---- Ganymede (J-III) ---- */
|
||||
{
|
||||
/* grav_param */ 0.2824981841847230e-06,
|
||||
/* lon0 */ 0.2874089391143348e+00,
|
||||
/* lon_rate */ 0.8782079235893280e+00,
|
||||
/* n_a */ 38, /* n_l */ 31, /* n_z */ 50, /* n_zeta */ 18,
|
||||
|
||||
/* semi-major axis cosine series (38 terms) */
|
||||
{
|
||||
{ 0.0071566594572575, 0.00000000000000e+00, 0.00000000000000e+00},
|
||||
{ 0.0000013930299110, 0.11586745884981e+01, 0.26733443704266e+01},
|
||||
{ 0.0000006449829346, 0.56222145702102e+01, 0.89111478887838e+00},
|
||||
{ 0.0000002298059520, 0.12995924044108e+01, 0.10034433456729e+01},
|
||||
{-0.0000001221434370, 0.49612436330515e+01, 0.17822295777568e+01},
|
||||
{ 0.0000001095798176, 0.19486708778350e+01, 0.15051650461529e+01},
|
||||
{ 0.0000000701435616, 0.64978508114196e+00, 0.50172166963138e+00},
|
||||
{ 0.0000000547868566, 0.25992050672074e+01, 0.20068866945508e+01},
|
||||
{-0.0000000394635858, 0.23173535605652e+01, 0.53466887181044e+01},
|
||||
{-0.0000000363221428, 0.36393008632056e+01, 0.35644591656241e+01},
|
||||
{ 0.0000000290949364, 0.20123392230605e+01, 0.17535157350384e+01},
|
||||
{ 0.0000000281244968, 0.32490010762048e+01, 0.25086083721948e+01},
|
||||
{-0.0000000207924698, 0.29783308899923e+01, 0.44555739317536e+01},
|
||||
{ 0.0000000146896774, 0.38988244013504e+01, 0.30103300418262e+01},
|
||||
{-0.0000000119930042, 0.16563968316083e+01, 0.62378035398422e+01},
|
||||
{ 0.0000000112067460, 0.43188665692819e+01, 0.13034138285340e-01},
|
||||
{-0.0000000109535132, 0.49372826282154e+01, 0.13584834937940e-01},
|
||||
{ 0.0000000099867772, 0.96700720263958e+00, 0.62699633776977e+00},
|
||||
{ 0.0000000077668260, 0.45486373016444e+01, 0.35120517182683e+01},
|
||||
{ 0.0000000074143972, 0.16140449852661e+00, 0.12531361661566e+00},
|
||||
{ 0.0000000066346638, 0.33441073536010e+00, 0.80200331112799e+01},
|
||||
{ 0.0000000057842684, 0.14936630646671e+01, 0.12938928799370e-01},
|
||||
{-0.0000000055768352, 0.44651777597613e+01, 0.11287386144710e+01},
|
||||
{-0.0000000049395106, 0.61563894598809e+01, 0.17520662093793e+01},
|
||||
{ 0.0000000041439704, 0.51984558307998e+01, 0.40137734147421e+01},
|
||||
{-0.0000000040765630, 0.99543742426922e+00, 0.71289183312483e+01},
|
||||
{-0.0000000036862062, 0.46386836178626e+01, 0.10693377254218e+02},
|
||||
{ 0.0000000033617538, 0.37493658441448e+01, 0.87808669180168e+00},
|
||||
{ 0.0000000033348284, 0.22668196818990e+01, 0.16304394485673e+01},
|
||||
{-0.0000000025754698, 0.33293196902303e+00, 0.17952648120307e+01},
|
||||
{ 0.0000000024363084, 0.19604838407749e+01, 0.11232854513197e+00},
|
||||
{ 0.0000000022265432, 0.58482745704418e+01, 0.45154950951905e+01},
|
||||
{ 0.0000000020032676, 0.29166648062069e+01, 0.21321610765333e+01},
|
||||
{-0.0000000018115706, 0.99782757414001e+00, 0.90414978368384e+00},
|
||||
{ 0.0000000014535006, 0.18748212041600e+01, 0.89112137093506e+01},
|
||||
{-0.0000000006819260, 0.19871670124324e+00, 0.24675315493830e-01},
|
||||
{ 0.0000000004433776, 0.24880003003965e+01, 0.25119610196650e-01},
|
||||
{-0.0000000002836658, 0.58126277034761e+01, 0.25973068607520e-01}
|
||||
},
|
||||
|
||||
/* mean longitude sine series (31 terms) */
|
||||
{
|
||||
{ 0.0002310797886226, 0.21402987195942e+01, 0.14500978438400e-02},
|
||||
{-0.0001828635964118, 0.43188672736968e+01, 0.13034138282630e-01},
|
||||
{ 0.0001512378778204, 0.49373102372298e+01, 0.13584834812520e-01},
|
||||
{-0.0001163720969778, 0.43002659861490e+01, 0.26733443704266e+01},
|
||||
{-0.0000955478069846, 0.14936612842567e+01, 0.12938928798570e-01},
|
||||
{ 0.0000815246854464, 0.56222137132535e+01, 0.89111478887838e+00},
|
||||
{-0.0000801219679602, 0.12995922951532e+01, 0.10034433456729e+01},
|
||||
{-0.0000607017260182, 0.64978769669238e+00, 0.50172167043264e+00},
|
||||
{ 0.0000543922473002, 0.27927547440639e+01, 0.29880873700000e-04},
|
||||
{-0.0000489253646474, 0.53711728089803e+01, 0.12495278292000e-03},
|
||||
{-0.0000427574981536, 0.18196513407448e+01, 0.17822295777568e+01},
|
||||
{-0.0000307360417826, 0.19498372703786e+01, 0.15051650064903e+01},
|
||||
{-0.0000169767346458, 0.19078637281659e+01, 0.30507678226700e-02},
|
||||
{ 0.0000154725890508, 0.56912713028984e+01, 0.65164073556000e-03},
|
||||
{-0.0000145268863648, 0.18863875475387e+00, 0.12530827181195e+00},
|
||||
{-0.0000135654458738, 0.27930238268852e+01, 0.55663681407000e-03},
|
||||
{-0.0000134648621904, 0.25991972928128e+01, 0.20068866945508e+01},
|
||||
{ 0.0000095524017320, 0.23173520454449e+01, 0.53466887181044e+01},
|
||||
{ 0.0000087955125170, 0.36393024031096e+01, 0.35644591656241e+01},
|
||||
{ 0.0000075462003630, 0.53560617584395e+01, 0.92426977490000e-04},
|
||||
{-0.0000071146195958, 0.20120561622463e+01, 0.17535157644008e+01},
|
||||
{ 0.0000064153141218, 0.15526366820734e+01, 0.29001309732400e-02},
|
||||
{-0.0000063221625942, 0.32490122452649e+01, 0.25086083721948e+01},
|
||||
{-0.0000056564973024, 0.24862139082596e+01, 0.44834622386000e-03},
|
||||
{ 0.0000052570245720, 0.19871532348033e+00, 0.24675315501580e-01},
|
||||
{ 0.0000047020767994, 0.29783317790630e+01, 0.44555739317536e+01},
|
||||
{-0.0000047004229470, 0.96617050453708e+00, 0.62699712737505e+00},
|
||||
{-0.0000046565198820, 0.36125113449716e+01, 0.43633231340000e-03},
|
||||
{-0.0000042349322008, 0.19604744669606e+01, 0.11232854282257e+00},
|
||||
{-0.0000038755741918, 0.22619624763183e+01, 0.25146663939730e-01},
|
||||
{-0.0000032577733688, 0.56861827246039e+01, 0.17074576501600e-02}
|
||||
},
|
||||
|
||||
/* complex eccentricity cos/sin series (50 terms) */
|
||||
{
|
||||
{ 0.0014289811307319, 0.21256295942739e+01, 0.12727413029000e-03},
|
||||
{ 0.0007710931226760, 0.55836330003496e+01, 0.32064341100000e-04},
|
||||
{ 0.0005925911780766, 0.40899396636448e+01, -0.12906864146660e-01},
|
||||
{ 0.0002045597496146, 0.52713683670372e+01, -0.12523544076106e+00},
|
||||
{ 0.0001785118648258, 0.28743156721063e+00, 0.87820792442520e+00},
|
||||
{ 0.0001131999784893, 0.14462127277818e+01, 0.35515522949802e+01},
|
||||
{-0.0000658778169210, 0.22702423990985e+01, -0.17951364394537e+01},
|
||||
{ 0.0000497058888328, 0.59096792204858e+01, 0.17693227129285e+01},
|
||||
{-0.0000316384926978, 0.16093054939404e+01, -0.90402165028424e+00},
|
||||
{ 0.0000287801237327, 0.46217321268757e+01, -0.62695712341840e+00},
|
||||
{-0.0000181744317896, 0.59210641379360e+01, 0.37648623991673e+00},
|
||||
{ 0.0000105558175161, 0.39720191398746e+01, -0.11286788041058e+01},
|
||||
{-0.0000070808673396, 0.60542548894164e+01, -0.25941002415210e-01},
|
||||
{-0.0000070804404020, 0.27978433776854e+01, 0.67774258703000e-03},
|
||||
{-0.0000061046181888, 0.14151685760988e+01, -0.87530769416913e+00},
|
||||
{-0.0000057610853129, 0.42530537622646e+01, -0.44684807882788e+01},
|
||||
{-0.0000057310334964, 0.29311803223072e+01, -0.26862512192699e+01},
|
||||
{ 0.0000048299146941, 0.27138294508149e+01, 0.27731329671900e-02},
|
||||
{ 0.0000046610005483, 0.33222980229554e+01, -0.16304004832039e+01},
|
||||
{-0.0000038142769361, 0.25962943627643e+01, -0.25845792955510e-01},
|
||||
{ 0.0000034982417330, 0.15866568011217e+01, 0.18816512920593e+01},
|
||||
{-0.0000030091617315, 0.35921173988567e+01, -0.35773660056343e+01},
|
||||
{-0.0000024732926446, 0.53461730094807e+01, 0.25122576835111e+00},
|
||||
{ 0.0000024416432533, 0.47049477027963e+01, -0.25049613834712e+00},
|
||||
{ 0.0000024171568015, 0.34508032389167e+01, 0.00000000000000e+00},
|
||||
{ 0.0000023143850535, 0.55385759257808e+01, 0.28683339028800e-02},
|
||||
{ 0.0000022651772374, 0.55608006706192e+01, 0.14501892967800e-02},
|
||||
{ 0.0000022247695560, 0.26725424635341e+01, -0.21321221654766e+01},
|
||||
{ 0.0000020947921969, 0.22350374116258e+01, 0.23833730192673e+01},
|
||||
{-0.0000014042712722, 0.93718044411525e+00, 0.13799296041822e+01},
|
||||
{ 0.0000011932531874, 0.28861941414418e+01, 0.28850946416201e+01},
|
||||
{-0.0000011180389240, 0.49139919849718e+01, -0.53595955727170e+01},
|
||||
{ 0.0000011076384510, 0.20227538540345e+01, -0.26338438454332e+01},
|
||||
{-0.0000010371714944, 0.40722739402948e+00, -0.87385759089274e+00},
|
||||
{-0.0000008993128501, 0.30942691883530e+01, -0.71418251640916e+01},
|
||||
{ 0.0000007268381420, 0.54334774230433e+01, -0.26491687896550e-01},
|
||||
{-0.0000007178049665, 0.52487423493616e+01, 0.26604375002057e+01},
|
||||
{ 0.0000006908412319, 0.40596134184175e+01, -0.75221793556997e+00},
|
||||
{-0.0000006784151570, 0.38846818226669e+01, 0.42496535534400e-02},
|
||||
{ 0.0000006772314920, 0.23013479896873e+01, 0.26317235358158e+01},
|
||||
{ 0.0000006659820028, 0.35359530295550e+01, 0.33868163258510e+01},
|
||||
{-0.0000006339665249, 0.39268665697903e+01, 0.44426670658559e+01},
|
||||
{-0.0000006286307601, 0.19440608894162e+01, 0.71160114019304e+01},
|
||||
{-0.0000006128705113, 0.25027415074658e+01, 0.62249001971556e+01},
|
||||
{ 0.0000005660807396, 0.13729316457251e+01, -0.31355655165873e+01},
|
||||
{-0.0000005206551413, 0.55749300982469e+01, -0.62507103665413e+01},
|
||||
{-0.0000004718481418, 0.45366605084874e+01, 0.16786677353000e-03},
|
||||
{-0.0000004583970422, 0.19351070248496e+01, -0.98151695574500e+01},
|
||||
{-0.0000004577854173, 0.62350780976534e+01, 0.17563373058434e+01},
|
||||
{ 0.0000003466029660, 0.75412427489767e+00, 0.15807097495700e-01}
|
||||
},
|
||||
|
||||
/* complex inclination cos/sin series (18 terms) */
|
||||
{
|
||||
{ 0.0015932721570848, 0.33368862796665e+01, -0.12491307058000e-03},
|
||||
{ 0.0008533093128905, 0.24133881688166e+01, 0.00000000000000e+00},
|
||||
{ 0.0003513347911037, 0.59720789850127e+01, -0.30561017710000e-04},
|
||||
{-0.0001441929255483, 0.10477061764435e+01, -0.56920632124000e-03},
|
||||
{ 0.0000157303527750, 0.55604041197704e+01, 0.29003665011200e-02},
|
||||
{ 0.0000025161319881, 0.28477653709685e+00, -0.14500554486800e-02},
|
||||
{ 0.0000020438305183, 0.17652628559998e+01, 0.14501383926500e-02},
|
||||
{ 0.0000017939612784, 0.45568977341583e+01, 0.43504621590400e-02},
|
||||
{ 0.0000013614276895, 0.84898872627945e+00, -0.25244521900630e-01},
|
||||
{-0.0000008996109017, 0.46441156003340e+01, 0.30253214588300e-02},
|
||||
{-0.0000008702078430, 0.27972000093551e+01, -0.23150965645100e-02},
|
||||
{-0.0000004371144064, 0.48429530385679e+01, -0.25688816011500e-01},
|
||||
{-0.0000002174259374, 0.57543785603741e+01, -0.25813642993310e-01},
|
||||
{-0.0000001926397869, 0.20118539705648e+01, 0.29330596864500e-02},
|
||||
{ 0.0000001589279656, 0.35554727018503e+01, 0.58005577768400e-02},
|
||||
{-0.0000001432228753, 0.11966574544002e+01, -0.15750124983800e-02},
|
||||
{-0.0000000926213408, 0.22052538606469e+01, -0.25782797426020e-01},
|
||||
{ 0.0000000000106902, 0.45764213311755e+01, -0.32611614716800e-01}
|
||||
}
|
||||
},
|
||||
|
||||
/* ---- Callisto (J-IV) ---- */
|
||||
{
|
||||
/* grav_param */ 0.2824921448899090e-06,
|
||||
/* lon0 */ -0.3620341291375704e+00,
|
||||
/* lon_rate */ 0.3764862334338280e+00,
|
||||
/* n_a */ 22, /* n_l */ 19, /* n_z */ 46, /* n_zeta */ 18,
|
||||
|
||||
/* semi-major axis cosine series (22 terms) */
|
||||
{
|
||||
{ 0.0125879701715314, 0.00000000000000e+00, 0.00000000000000e+00},
|
||||
{ 0.0000035952049470, 0.64965776007116e+00, 0.50172168165034e+00},
|
||||
{ 0.0000027580210652, 0.18084235781510e+01, 0.31750660413359e+01},
|
||||
{ 0.0000012874896172, 0.62718908285025e+01, 0.13928364698403e+01},
|
||||
{-0.0000004173729106, 0.12990650292663e+01, 0.10034433697108e+01},
|
||||
{ 0.0000002790757718, 0.71428870045577e+00, 0.75007225869130e+00},
|
||||
{-0.0000001998252258, 0.19489881012004e+01, 0.15051650461529e+01},
|
||||
{-0.0000001001149838, 0.25987168731338e+01, 0.20068867266014e+01},
|
||||
{-0.0000000513967092, 0.32484798706247e+01, 0.25086084022422e+01},
|
||||
{-0.0000000475687992, 0.48635521917696e+01, 0.74862216593606e+00},
|
||||
{ 0.0000000348242240, 0.15082713497295e+00, 0.37645917070525e+00},
|
||||
{ 0.0000000283840630, 0.51672973364888e+01, 0.12530678073049e+00},
|
||||
{-0.0000000263234638, 0.33499822210495e+01, 0.30103491232578e+01},
|
||||
{ 0.0000000239106346, 0.43573519442736e+01, 0.62698238798737e+00},
|
||||
{ 0.0000000219977422, 0.15075404808879e+01, 0.27986109086768e+01},
|
||||
{-0.0000000171144478, 0.62607361864777e+01, 0.27856729335053e+01},
|
||||
{-0.0000000141956834, 0.45481077718910e+01, 0.35120517575303e+01},
|
||||
{-0.0000000120003630, 0.18583887479127e+01, 0.11287042579152e+01},
|
||||
{ 0.0000000108418904, 0.54873138800427e+01, 0.67395238593127e+01},
|
||||
{ 0.0000000108218254, 0.59772630516669e+01, 0.10163811590412e+01},
|
||||
{ 0.0000000002477642, 0.56894071957878e+01, 0.65165021654000e-03},
|
||||
{-0.0000000001874576, 0.28598333265121e+01, 0.55639542661000e-03}
|
||||
},
|
||||
|
||||
/* mean longitude sine series (19 terms) */
|
||||
{
|
||||
{ 0.0005586040123824, 0.21404207189815e+01, 0.14500979323100e-02},
|
||||
{-0.0003805813868176, 0.27358844897853e+01, 0.29729650620000e-04},
|
||||
{ 0.0002205152863262, 0.64979652596400e+00, 0.50172167243580e+00},
|
||||
{ 0.0001877895151158, 0.18084787604005e+01, 0.31750660413359e+01},
|
||||
{ 0.0000766916975242, 0.62720114319755e+01, 0.13928364636651e+01},
|
||||
{ 0.0000747056855106, 0.12995916202344e+01, 0.10034433456729e+01},
|
||||
{-0.0000388323297366, 0.71289234751879e+00, 0.75007236972328e+00},
|
||||
{ 0.0000335036484314, 0.53712641184981e+01, 0.12494011725000e-03},
|
||||
{ 0.0000293032677938, 0.19493939340593e+01, 0.15051650209131e+01},
|
||||
{ 0.0000185940935472, 0.14630998372377e+01, 0.29001339405200e-02},
|
||||
{-0.0000170438022886, 0.56893382353856e+01, 0.65165044781000e-03},
|
||||
{ 0.0000151393833114, 0.28749516044614e+01, 0.55646069067000e-03},
|
||||
{-0.0000148825637256, 0.33321074618840e+01, 0.12530790075011e+00},
|
||||
{ 0.0000129927896682, 0.25991973549465e+01, 0.20068866945508e+01},
|
||||
{-0.0000116117398772, 0.56192268627131e+01, 0.93166256720000e-04},
|
||||
{ 0.0000066211702894, 0.48564958193206e+01, 0.74862286166569e+00},
|
||||
{ 0.0000065387442486, 0.35580120361824e+01, 0.16550513741900e-02},
|
||||
{ 0.0000061580798140, 0.32490037889701e+01, 0.25086083721948e+01},
|
||||
{ 0.0000046797140778, 0.96612169919707e+00, 0.62699716616712e+00}
|
||||
},
|
||||
|
||||
/* complex eccentricity cos/sin series (46 terms) */
|
||||
{
|
||||
{ 0.0073755808467977, 0.55836071576084e+01, 0.32065099140000e-04},
|
||||
{ 0.0002065924169942, 0.59209831565786e+01, 0.37648624194703e+00},
|
||||
{ 0.0001589869764021, 0.28744006242623e+00, 0.87820792442520e+00},
|
||||
{-0.0001561131605348, 0.21257397865089e+01, 0.12727441285000e-03},
|
||||
{ 0.0001486043380971, 0.14462134301023e+01, 0.35515522949802e+01},
|
||||
{ 0.0000635073108731, 0.59096803285954e+01, 0.17693227129285e+01},
|
||||
{ 0.0000599351698525, 0.41125517584798e+01, -0.27985797954589e+01},
|
||||
{ 0.0000540660842731, 0.55390350845569e+01, 0.28683408228300e-02},
|
||||
{-0.0000489596900866, 0.46218149483338e+01, -0.62695712529519e+00},
|
||||
{ 0.0000333682283528, 0.52066975238880e+01, -0.37358601734497e+00},
|
||||
{ 0.0000295832427279, 0.59322697896516e+01, -0.10163502275209e+01},
|
||||
{ 0.0000292325461337, 0.52707623402008e+01, -0.12523542448602e+00},
|
||||
{ 0.0000197588369441, 0.33317768022759e+01, 0.00000000000000e+00},
|
||||
{-0.0000183551029746, 0.39720443249757e+01, -0.11286788041058e+01},
|
||||
{ 0.0000090411191759, 0.55606719963947e+01, 0.14501837490800e-02},
|
||||
{-0.0000081987970452, 0.33223313720086e+01, -0.16304004832039e+01},
|
||||
{-0.0000060406575087, 0.13970265485562e+01, 0.43191832032300e-02},
|
||||
{ 0.0000056895636122, 0.41990956668120e+01, -0.37213592656720e+00},
|
||||
{-0.0000040434854859, 0.47008406172134e+01, -0.25049602889288e+00},
|
||||
{-0.0000039403527376, 0.26725832255243e+01, -0.21321221654766e+01},
|
||||
{ 0.0000036901291978, 0.35207772267753e+00, 0.11265585018525e+01},
|
||||
{-0.0000028551622596, 0.55601265129356e+01, -0.31584886140000e-04},
|
||||
{-0.0000026588026505, 0.25969882784477e+00, 0.14182025553300e-02},
|
||||
{-0.0000019711212463, 0.20228019680496e+01, -0.26338438454332e+01},
|
||||
{ 0.0000019322089806, 0.51418595457408e+01, 0.25123117908847e+00},
|
||||
{-0.0000018673159813, 0.93674892088247e+00, 0.13799296163047e+01},
|
||||
{ 0.0000016838424078, 0.60796033426941e+01, 0.75294520843775e+00},
|
||||
{-0.0000016695689644, 0.15867810488422e+01, 0.18816512864243e+01},
|
||||
{ 0.0000016317841395, 0.45789534393209e+01, 0.14822429153000e-02},
|
||||
{-0.0000016159095087, 0.30157253757329e+00, -0.14180284447900e-02},
|
||||
{-0.0000014034621874, 0.59433512039442e+01, -0.24091866865037e+01},
|
||||
{-0.0000012029942283, 0.27137754880270e+01, 0.27731373092900e-02},
|
||||
{-0.0000011758260607, 0.40581098970285e+01, -0.75221789504525e+00},
|
||||
{-0.0000010798624964, 0.22364861319452e+01, 0.23833729650229e+01},
|
||||
{-0.0000010108880552, 0.13729872033949e+01, -0.31355655165873e+01},
|
||||
{-0.0000008876681807, 0.50534107615010e+01, -0.50169281575682e+00},
|
||||
{ 0.0000008869382117, 0.50147420853991e+01, 0.68353864231000e-03},
|
||||
{-0.0000008194699011, 0.62190878357566e+01, -0.37503615934219e+00},
|
||||
{ 0.0000007093782158, 0.44118312641559e+01, -0.24221246131672e+01},
|
||||
{ 0.0000006728737059, 0.31910016062920e+01, -0.37068584881726e+00},
|
||||
{ 0.0000006297345982, 0.13595719733984e+01, 0.11251084135564e+01},
|
||||
{ 0.0000006128899757, 0.51402161299290e+01, 0.71160095483087e+01},
|
||||
{-0.0000005580987049, 0.34117733109010e+01, -0.12539396666771e+01},
|
||||
{ 0.0000005321318002, 0.35377046967957e+01, 0.57685340271300e-02},
|
||||
{-0.0000004739086661, 0.21645217929478e+01, 0.58845474482000e-03},
|
||||
{ 0.0000004518928658, 0.44963664372727e+01, 0.29023325111200e-02}
|
||||
},
|
||||
|
||||
/* complex inclination cos/sin series (18 terms) */
|
||||
{
|
||||
{ 0.0038422977898495, 0.24133922085557e+01, 0.00000000000000e+00},
|
||||
{ 0.0022453891791894, 0.59721736773277e+01, -0.30561255250000e-04},
|
||||
{-0.0002604479450559, 0.33368746306409e+01, -0.12491309972000e-03},
|
||||
{ 0.0000332112143230, 0.55604137742337e+01, 0.29003768850700e-02},
|
||||
{ 0.0000049727136261, 0.28488229706820e+00, -0.14500571761900e-02},
|
||||
{-0.0000049416729114, 0.10476908456459e+01, -0.56920298857000e-03},
|
||||
{ 0.0000043945193428, 0.17684273746003e+01, 0.14501344524700e-02},
|
||||
{ 0.0000037630501589, 0.45567680530533e+01, 0.43504645407000e-02},
|
||||
{-0.0000030823418750, 0.20094360655956e+01, 0.29313051376700e-02},
|
||||
{ 0.0000004719790711, 0.18055417618741e+01, 0.14195445432000e-02},
|
||||
{-0.0000004637177865, 0.38277528822158e+01, -0.14808731001600e-02},
|
||||
{ 0.0000003497224175, 0.46444360330108e+01, 0.30253130162300e-02},
|
||||
{-0.0000003467132626, 0.10120757927163e+01, 0.43816126822900e-02},
|
||||
{ 0.0000003324412570, 0.35549391686606e+01, 0.58005379032100e-02},
|
||||
{ 0.0000001945374351, 0.61251687150860e+01, 0.28808264872800e-02},
|
||||
{ 0.0000001727743329, 0.11900773236610e+01, -0.29001068524700e-02},
|
||||
{-0.0000001485176585, 0.62335834706368e+01, 0.14807679092700e-02},
|
||||
{ 0.0000000000666922, 0.40616225761771e+01, -0.32724923474890e-01}
|
||||
}
|
||||
}
|
||||
};
|
||||
73
src/l12.h
Normal file
73
src/l12.h
Normal file
@ -0,0 +1,73 @@
|
||||
/************************************************************************
|
||||
|
||||
L1.2 Galilean satellite theory -- Lainey, Duriez & Vienne
|
||||
|
||||
Clean-room implementation for pg_orrery.
|
||||
The L1.2 theory provides positions and velocities of Jupiter's four
|
||||
Galilean moons (Io, Europa, Ganymede, Callisto) relative to Jupiter's
|
||||
center, in the VSOP87 ecliptic J2000 reference frame.
|
||||
|
||||
Reference:
|
||||
Lainey V., Duriez L., Vienne A.
|
||||
"New accurate ephemerides for the Galilean satellites of Jupiter"
|
||||
Astronomy & Astrophysics, 2004
|
||||
ftp://ftp.imcce.fr/pub/ephem/satel/galilean/L1/L1.2/
|
||||
|
||||
The theory coefficients are astronomical constants derived from
|
||||
fitting observations of the Galilean system. They represent
|
||||
physical measurements and are not copyrightable.
|
||||
|
||||
Copyright (c) 2026 Ryan Malloy <ryan@supported.systems>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Thread-safe: all functions are reentrant with no static mutable state.
|
||||
|
||||
****************************************************************/
|
||||
|
||||
#ifndef L12_H
|
||||
#define L12_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define L12_IO 0
|
||||
#define L12_EUROPA 1
|
||||
#define L12_GANYMEDE 2
|
||||
#define L12_CALLISTO 3
|
||||
|
||||
/*
|
||||
* Compute the position (and optionally velocity) of a Galilean moon
|
||||
* using the L1.2 semi-analytic theory.
|
||||
*
|
||||
* jd: Julian date in Terrestrial Time (TT).
|
||||
* body: Moon index: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto.
|
||||
* xyz: Output position in AU, VSOP87 ecliptic J2000 frame,
|
||||
* relative to Jupiter's center. Must point to double[3].
|
||||
* xyzdot: Output velocity in AU/day, same frame. May be NULL
|
||||
* to skip velocity computation.
|
||||
*/
|
||||
void GetL12Coor(double jd, int body, double *xyz, double *xyzdot);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* L12_H */
|
||||
249
src/lambert.c
Normal file
249
src/lambert.c
Normal file
@ -0,0 +1,249 @@
|
||||
/*
|
||||
* lambert.c -- Lambert transfer orbit solver (Universal Variable)
|
||||
*
|
||||
* Solves Lambert's problem using the Universal Variable formulation
|
||||
* with Stumpff functions c2(psi) and c3(psi). This handles elliptic,
|
||||
* parabolic, and hyperbolic transfers with one unified algorithm.
|
||||
*
|
||||
* The approach:
|
||||
* 1. Compute geometry: r1_mag, r2_mag, delta_theta
|
||||
* 2. Compute A from geometry (determines short/long way)
|
||||
* 3. Iterate on universal variable z to match time of flight
|
||||
* 4. Extract departure/arrival velocities from Lagrange coefficients
|
||||
*
|
||||
* References:
|
||||
* Curtis, "Orbital Mechanics for Engineering Students" (2014), Ch. 5
|
||||
* Bate, Mueller & White, "Fundamentals of Astrodynamics" (1971), Ch. 5
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include "lambert.h"
|
||||
|
||||
/* Stumpff function c2(psi) = (1 - cos(sqrt(psi))) / psi */
|
||||
static double
|
||||
stumpff_c2(double psi)
|
||||
{
|
||||
double sq;
|
||||
|
||||
if (psi > 1e-6) {
|
||||
sq = sqrt(psi);
|
||||
return (1.0 - cos(sq)) / psi;
|
||||
}
|
||||
if (psi < -1e-6) {
|
||||
sq = sqrt(-psi);
|
||||
return (1.0 - cosh(sq)) / psi;
|
||||
}
|
||||
/* Taylor series near zero: 1/2 - psi/24 + psi^2/720 */
|
||||
return 1.0/2.0 - psi/24.0 + psi*psi/720.0;
|
||||
}
|
||||
|
||||
/* Stumpff function c3(psi) = (sqrt(psi) - sin(sqrt(psi))) / (psi * sqrt(psi)) */
|
||||
static double
|
||||
stumpff_c3(double psi)
|
||||
{
|
||||
double sq;
|
||||
|
||||
if (psi > 1e-6) {
|
||||
sq = sqrt(psi);
|
||||
return (sq - sin(sq)) / (psi * sq);
|
||||
}
|
||||
if (psi < -1e-6) {
|
||||
sq = sqrt(-psi);
|
||||
return (sinh(sq) - sq) / (-psi * sq);
|
||||
}
|
||||
/* Taylor series near zero: 1/6 - psi/120 + psi^2/5040 */
|
||||
return 1.0/6.0 - psi/120.0 + psi*psi/5040.0;
|
||||
}
|
||||
|
||||
int
|
||||
lambert_solve_uv(const double r1[3], const double r2[3],
|
||||
double tof_days, double mu, int prograde,
|
||||
lambert_result *result)
|
||||
{
|
||||
double r1_mag, r2_mag;
|
||||
double cos_dtheta, sin_dtheta, dtheta;
|
||||
double A, z, z_lo, z_hi;
|
||||
double c2, c3, y, x, t, dt_dz;
|
||||
double f, g, gdot;
|
||||
int i;
|
||||
int max_iter = 50;
|
||||
double tol = 1e-10;
|
||||
double cross_z;
|
||||
|
||||
result->converged = 0;
|
||||
|
||||
if (tof_days <= 0.0)
|
||||
return 0;
|
||||
|
||||
/* Magnitudes */
|
||||
r1_mag = sqrt(r1[0]*r1[0] + r1[1]*r1[1] + r1[2]*r1[2]);
|
||||
r2_mag = sqrt(r2[0]*r2[0] + r2[1]*r2[1] + r2[2]*r2[2]);
|
||||
|
||||
if (r1_mag < 1e-12 || r2_mag < 1e-12)
|
||||
return 0;
|
||||
|
||||
/* Transfer angle from cross product */
|
||||
cos_dtheta = (r1[0]*r2[0] + r1[1]*r2[1] + r1[2]*r2[2]) / (r1_mag * r2_mag);
|
||||
|
||||
/* Clamp for numerical safety */
|
||||
if (cos_dtheta > 1.0) cos_dtheta = 1.0;
|
||||
if (cos_dtheta < -1.0) cos_dtheta = -1.0;
|
||||
|
||||
/* Cross product z-component determines orbit direction */
|
||||
cross_z = r1[0]*r2[1] - r1[1]*r2[0];
|
||||
|
||||
if (prograde) {
|
||||
if (cross_z < 0.0)
|
||||
sin_dtheta = -sqrt(1.0 - cos_dtheta*cos_dtheta);
|
||||
else
|
||||
sin_dtheta = sqrt(1.0 - cos_dtheta*cos_dtheta);
|
||||
} else {
|
||||
if (cross_z >= 0.0)
|
||||
sin_dtheta = -sqrt(1.0 - cos_dtheta*cos_dtheta);
|
||||
else
|
||||
sin_dtheta = sqrt(1.0 - cos_dtheta*cos_dtheta);
|
||||
}
|
||||
|
||||
dtheta = atan2(sin_dtheta, cos_dtheta);
|
||||
if (dtheta < 0.0)
|
||||
dtheta += 2.0 * M_PI;
|
||||
|
||||
/* A parameter (Curtis eq. 5.35) */
|
||||
A = sin_dtheta * sqrt(r1_mag * r2_mag / (1.0 - cos_dtheta));
|
||||
|
||||
if (fabs(A) < 1e-14)
|
||||
return 0; /* Degenerate: 0 or 180 deg transfer */
|
||||
|
||||
/*
|
||||
* Newton-Raphson iteration on z (universal variable).
|
||||
* z > 0: elliptic, z = 0: parabolic, z < 0: hyperbolic.
|
||||
* Initial bracket: z_lo = -4*pi^2 (max hyperbolic), z_hi from geometry.
|
||||
*/
|
||||
z_lo = -4.0 * M_PI * M_PI;
|
||||
z_hi = 4.0 * M_PI * M_PI;
|
||||
z = 0.0; /* Start at parabolic */
|
||||
|
||||
for (i = 0; i < max_iter; i++) {
|
||||
c2 = stumpff_c2(z);
|
||||
c3 = stumpff_c3(z);
|
||||
|
||||
y = r1_mag + r2_mag + A * (z * c3 - 1.0) / sqrt(c2);
|
||||
|
||||
if (y < 0.0) {
|
||||
/* y must be positive; adjust z upward */
|
||||
z_lo = z;
|
||||
z = (z + z_hi) * 0.5;
|
||||
continue;
|
||||
}
|
||||
|
||||
x = sqrt(y / c2);
|
||||
|
||||
/* Time of flight for this z */
|
||||
t = (x*x*x * c3 + A * sqrt(y)) / sqrt(mu);
|
||||
|
||||
/* Check convergence */
|
||||
if (fabs(t - tof_days) < tol * fabs(tof_days) + 1e-12) {
|
||||
result->converged = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
/* Derivative dt/dz for Newton step */
|
||||
if (fabs(z) > 1e-6) {
|
||||
dt_dz = (x*x*x * (stumpff_c2(z) - 3.0*c3/(2.0*c2)) / (2.0*c2)
|
||||
+ (3.0*c3*sqrt(y)/c2 + A/sqrt(y)*(1.0 - z*c3/c2)) * A / (2.0*c2*sqrt(c2)))
|
||||
/ sqrt(mu);
|
||||
/* Simplified: use bisection if Newton overshoots */
|
||||
if (fabs(dt_dz) < 1e-20) {
|
||||
/* Fall back to bisection */
|
||||
if (t < tof_days)
|
||||
z_lo = z;
|
||||
else
|
||||
z_hi = z;
|
||||
z = (z_lo + z_hi) * 0.5;
|
||||
continue;
|
||||
}
|
||||
z = z - (t - tof_days) / dt_dz;
|
||||
} else {
|
||||
/* Near parabolic: bisection */
|
||||
if (t < tof_days)
|
||||
z_lo = z;
|
||||
else
|
||||
z_hi = z;
|
||||
z = (z_lo + z_hi) * 0.5;
|
||||
}
|
||||
|
||||
/* Keep z in bounds */
|
||||
if (z < z_lo) z = z_lo + 0.1 * (z_hi - z_lo);
|
||||
if (z > z_hi) z = z_hi - 0.1 * (z_hi - z_lo);
|
||||
}
|
||||
|
||||
if (!result->converged) {
|
||||
/*
|
||||
* Newton didn't converge; try pure bisection as fallback.
|
||||
* Reset bounds and iterate.
|
||||
*/
|
||||
z_lo = -4.0 * M_PI * M_PI;
|
||||
z_hi = 4.0 * M_PI * M_PI;
|
||||
|
||||
for (i = 0; i < 100; i++) {
|
||||
z = (z_lo + z_hi) * 0.5;
|
||||
c2 = stumpff_c2(z);
|
||||
c3 = stumpff_c3(z);
|
||||
|
||||
y = r1_mag + r2_mag + A * (z * c3 - 1.0) / sqrt(c2);
|
||||
if (y < 0.0) {
|
||||
z_lo = z;
|
||||
continue;
|
||||
}
|
||||
|
||||
x = sqrt(y / c2);
|
||||
t = (x*x*x * c3 + A * sqrt(y)) / sqrt(mu);
|
||||
|
||||
if (fabs(t - tof_days) < tol * fabs(tof_days) + 1e-12) {
|
||||
result->converged = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (t < tof_days)
|
||||
z_lo = z;
|
||||
else
|
||||
z_hi = z;
|
||||
|
||||
if (fabs(z_hi - z_lo) < 1e-14)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!result->converged)
|
||||
return 0;
|
||||
|
||||
/* Lagrange coefficients (Curtis eqs. 5.46) */
|
||||
c2 = stumpff_c2(z);
|
||||
c3 = stumpff_c3(z);
|
||||
y = r1_mag + r2_mag + A * (z * c3 - 1.0) / sqrt(c2);
|
||||
|
||||
f = 1.0 - y / r1_mag;
|
||||
g = A * sqrt(y / mu);
|
||||
gdot = 1.0 - y / r2_mag;
|
||||
|
||||
/* Departure and arrival velocity vectors */
|
||||
{
|
||||
int k;
|
||||
for (k = 0; k < 3; k++) {
|
||||
result->v1[k] = (r2[k] - f * r1[k]) / g;
|
||||
result->v2[k] = (gdot * r2[k] - r1[k]) / g;
|
||||
}
|
||||
}
|
||||
|
||||
/* Semi-major axis */
|
||||
if (fabs(c2) > 1e-14)
|
||||
result->sma = y / c2 / (2.0); /* a = y / (2 * c2(z)) ... but simpler: */
|
||||
else
|
||||
result->sma = 1e30; /* Near-parabolic */
|
||||
|
||||
/* Correct SMA from z */
|
||||
if (fabs(z) > 1e-10)
|
||||
result->sma = y / (2.0 * c2);
|
||||
|
||||
return 1;
|
||||
}
|
||||
60
src/lambert.h
Normal file
60
src/lambert.h
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* lambert.h -- Lambert transfer orbit solver
|
||||
*
|
||||
* Solves Lambert's problem: given two position vectors and a
|
||||
* time of flight, find the orbit connecting them.
|
||||
*
|
||||
* Uses the Universal Variable formulation with Stumpff functions,
|
||||
* handling elliptic, parabolic, and hyperbolic transfers uniformly.
|
||||
*
|
||||
* Reference:
|
||||
* Bate, Mueller & White, "Fundamentals of Astrodynamics" (1971)
|
||||
* Battin, "An Introduction to the Methods of Astrodynamics" (1999)
|
||||
* Curtis, "Orbital Mechanics for Engineering Students" (2014)
|
||||
*
|
||||
* All units: AU, days, AU/day. Gravitational parameter is Gauss's
|
||||
* constant squared (k^2 = GM_sun in AU^3/day^2).
|
||||
*
|
||||
* Thread-safe: no static mutable state.
|
||||
*/
|
||||
|
||||
#ifndef PG_ORRERY_LAMBERT_H
|
||||
#define PG_ORRERY_LAMBERT_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Result of a Lambert transfer orbit solution.
|
||||
* All velocities in AU/day.
|
||||
*/
|
||||
typedef struct lambert_result
|
||||
{
|
||||
double v1[3]; /* departure velocity vector (AU/day) */
|
||||
double v2[3]; /* arrival velocity vector (AU/day) */
|
||||
double sma; /* semi-major axis (AU), negative for hyperbolic */
|
||||
int converged; /* 1 if solver converged, 0 otherwise */
|
||||
} lambert_result;
|
||||
|
||||
/*
|
||||
* Solve Lambert's problem.
|
||||
*
|
||||
* r1[3]: departure position (AU, heliocentric ecliptic J2000)
|
||||
* r2[3]: arrival position (AU, heliocentric ecliptic J2000)
|
||||
* tof_days: time of flight in days (must be > 0)
|
||||
* mu: gravitational parameter (AU^3/day^2), use GAUSS_K2 for Sun
|
||||
* prograde: 1 for prograde (short way), 0 for retrograde (long way)
|
||||
* result: output lambert_result
|
||||
*
|
||||
* Returns 1 on success, 0 on failure.
|
||||
*/
|
||||
int lambert_solve_uv(const double r1[3], const double r2[3],
|
||||
double tof_days, double mu, int prograde,
|
||||
lambert_result *result);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PG_ORRERY_LAMBERT_H */
|
||||
425
src/marssat.c
Normal file
425
src/marssat.c
Normal file
@ -0,0 +1,425 @@
|
||||
/************************************************************************
|
||||
|
||||
The Ephemerides of the Martian satellites
|
||||
(adjustement from 1877 to 2005, Version 1.0)
|
||||
by Valery Lainey can be obtained from Valery Lainey:
|
||||
|
||||
V.Lainey (Lainey@oma.be)
|
||||
ROB- 3, Avenue Circulaire, B-1180 Bruxelles (Belgium)
|
||||
IMCCE - 77, Avenue Denfert-Rochereau 75014 Paris (France)
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
I (Johannes Gajdosik) have just taken Valery Laineys Fortran code,
|
||||
MarsSatV1-0.f, which he kindly supplied, and rearranged it into
|
||||
this piece of software.
|
||||
|
||||
I can neither allow nor forbid the usage of Valery Laineys
|
||||
Ephemerides of the Martian satellites.
|
||||
The copyright notice below covers not the work of Valery Lainey
|
||||
but just my work, that is the compilation of Valery Laineys
|
||||
Ephemerides of the Martian satellites into the software supplied in this file.
|
||||
|
||||
|
||||
Copyright (c) 2006 Johannes Gajdosik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Derived from Stellarium's MARSSAT implementation.
|
||||
Modified for pg_orrery: removed all static mutable state for thread safety
|
||||
(PostgreSQL PARALLEL SAFE). The original used static caching arrays and
|
||||
CalcInterpolatedElements for performance; this version computes fresh on
|
||||
each call which is acceptable for SQL query workloads.
|
||||
|
||||
1) do not calculate constant terms at runtime but beforehand
|
||||
2) unite terms with the same frequencies
|
||||
|
||||
****************************************************************/
|
||||
|
||||
#include "marssat.h"
|
||||
#include "elliptic_to_rectangular.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifndef M_PI
|
||||
#define M_PI 3.14159265358979323846
|
||||
#endif
|
||||
|
||||
|
||||
struct MarsSatTerm {
|
||||
double phase, frequency, amplitude;
|
||||
};
|
||||
|
||||
struct MarsSatTermList {
|
||||
const struct MarsSatTerm *terms;
|
||||
int size;
|
||||
};
|
||||
|
||||
struct MarsSatBody {
|
||||
double mu, l, acc;
|
||||
double constants[6];
|
||||
const struct MarsSatTermList lists[4];
|
||||
};
|
||||
|
||||
static const struct MarsSatTerm mars_sat_phobos_0[16] = {
|
||||
{ 5.013490350126586e+00, 2.715567382195733e+01, 4.537539999999999e-09},
|
||||
{ 6.839910590780000e-01, 1.969446151585829e+01, 7.312000000000000e-10},
|
||||
{ 4.514031245592586e+00, 4.073350897245015e+01, 7.785599999999993e-10},
|
||||
{ 5.531351678889586e+00, 1.357783661756435e+01, 3.529399999999985e-10},
|
||||
{ 5.700307292822586e+00, 4.685013393001369e+01, 2.094599999999994e-10},
|
||||
{ 4.228062220664587e+00, 5.431134764391466e+01, 9.139999999999999e-11},
|
||||
{ 1.167907583017500e+00, 7.461211875946264e+00, 6.204000000000000e-11},
|
||||
{ 5.197274703601086e+00, 6.042797388545593e+01, 5.295999999999951e-11},
|
||||
{ 5.160045430868086e+00, 3.938582310889583e+01, 1.811999999999999e-11},
|
||||
{ 2.223277229900500e+00, 5.777452001970681e+01, 1.563999999999897e-11},
|
||||
{ 6.921616240614999e-01, 2.103904892768837e+01, 1.535999999999999e-11},
|
||||
{ 6.215802752555586e+00, 3.327229783044108e+01, 1.304000000000000e-11},
|
||||
{ 1.368231494011000e+00, 3.938892179708230e+01, 1.661999999999999e-11},
|
||||
{ 2.601539199675000e-01, 7.446011234647927e+00, 1.328000000000000e-11},
|
||||
{ 5.903845687100000e-02, 3.941932465627423e+01, 8.559999999999978e-12},
|
||||
{ 6.179310844209586e+00, 5.911910805492755e+01, 7.240000000000000e-12},
|
||||
};
|
||||
|
||||
static const struct MarsSatTerm mars_sat_phobos_1[42] = {
|
||||
{ 5.334169568271586e+00, 9.145914066599032e-03, 5.016848130000000e-05},
|
||||
{ 5.334169148295587e+00, 9.145914113327495e-03, 5.016848130000000e-05},
|
||||
{ 3.805345265960000e-01, 1.540952541904328e-03, 3.350264970000000e-05},
|
||||
{ 3.805339484960000e-01, 1.540952613104260e-03, 3.350264903000000e-05},
|
||||
{ 5.288363718535586e+00, 1.829243888046791e-02, 2.827839093000000e-05},
|
||||
{ 5.288363809359586e+00, 1.829243887034939e-02, 2.827839093000000e-05},
|
||||
{ 3.412685718051586e+00, 7.604696023359408e-03, 1.578862428000000e-05},
|
||||
{ 3.412685998796586e+00, 7.604695992157328e-03, 1.578862427000000e-05},
|
||||
{ 3.448999855869586e+00, 2.715567616927210e+01, 3.008844320000000e-05},
|
||||
{ 9.194445444030001e-01, 3.101144183892481e-03, 8.178563170000001e-06},
|
||||
{ 9.194443192780000e-01, 3.101144208596499e-03, 8.178563340000000e-06},
|
||||
{ 5.387625479307586e+00, 1.969446275049270e+01, 2.044327843999999e-05},
|
||||
{ 2.848996739890000e+00, 2.743829098982358e-02, 6.516951720000000e-06},
|
||||
{ 2.848996748767000e+00, 2.743829098862537e-02, 6.516951720000000e-06},
|
||||
{ 3.954411167839586e+00, 1.357783808463605e+01, 1.013316988000000e-05},
|
||||
{ 2.912659283736000e+00, 4.073351425390815e+01, 7.483468759999740e-06},
|
||||
{ 4.067858164824586e+00, 2.589727369338346e-02, 2.589359390000000e-06},
|
||||
{ 4.067857973471586e+00, 2.589727371473152e-02, 2.589359390000000e-06},
|
||||
{ 3.776271613850000e-01, 3.658417828853650e-02, 1.125147420000000e-06},
|
||||
{ 3.776267985560000e-01, 3.658417832900902e-02, 1.125147420000000e-06},
|
||||
{ 4.121936540919586e+00, 4.685014091671000e+01, 1.523532599999976e-06},
|
||||
{ 1.231836210236000e+00, 1.674957754399965e-02, 4.976512300000000e-07},
|
||||
{ 1.231836585135000e+00, 1.674957750247454e-02, 4.976512300000000e-07},
|
||||
{ 2.488083095032000e+00, 1.520386207875350e-02, 6.841966800000000e-07},
|
||||
{ 2.488083105051000e+00, 1.520386207728190e-02, 6.841966800000000e-07},
|
||||
{ 1.576141880659500e+00, 3.504321962396548e-02, 1.124868580000000e-06},
|
||||
{ 6.145328698142587e+00, 6.116624841770791e+00, 7.873411399999999e-07},
|
||||
{ 2.626756105114500e+00, 5.431135233854420e+01, 1.040645720000000e-06},
|
||||
{ 2.759579551372000e+00, 7.461213382164448e+00, 7.166789399999994e-07},
|
||||
{ 6.005673273380586e+00, 1.314187651672942e+00, 6.022022799999936e-07},
|
||||
{ 9.353090056600000e-02, 7.532115638546220e-04, 1.964976900000000e-07},
|
||||
{ 9.343265452500001e-02, 7.532237916966188e-04, 1.964964500000000e-07},
|
||||
{ 4.302928383505000e-01, 3.938582557777600e+01, 4.987307800000000e-07},
|
||||
{ 3.587439959397586e+00, 6.042797969706294e+01, 4.804827999999994e-07},
|
||||
{ 4.183547650369587e+00, 4.572998195120822e-02, 1.747133100000000e-07},
|
||||
{ 4.183547815926586e+00, 4.572998193123842e-02, 1.747133100000000e-07},
|
||||
{ 5.589444380180586e+00, 6.068411018120026e-03, 1.793076700000000e-07},
|
||||
{ 5.589445119118587e+00, 6.068410936124942e-03, 1.793076800000000e-07},
|
||||
{ 6.062696336024086e+00, 3.938892673561993e+01, 2.653755199999908e-07},
|
||||
{ 4.628882088425586e+00, 3.327230135427769e+01, 2.539272800000000e-07},
|
||||
{ 4.736462571780586e+00, 1.224558395734566e-02, 1.254715400000000e-07},
|
||||
{ 4.736462588777586e+00, 1.224558396211886e-02, 1.254715500000000e-07},
|
||||
};
|
||||
|
||||
static const struct MarsSatTerm mars_sat_phobos_2[27] = {
|
||||
{ 1.404382124885000e+00, 7.595588511174286e-03, 1.514110912521000e-02},
|
||||
{ 2.088385523034000e+00, 1.970205682773437e+01, 3.849620867400000e-04},
|
||||
{ 3.895377361148586e+00, 1.069670280268190e-02, 6.903413242000000e-05},
|
||||
{ 4.989572094750000e-01,-7.605272825959576e-03, 4.946101994000000e-05},
|
||||
{ 8.215210221720000e-01, 4.685772969905357e+01, 3.671320788000000e-05},
|
||||
{ 3.378076387479586e+00,-7.453616316894914e+00, 3.267983782000000e-05},
|
||||
{ 2.772375719698000e+00, 3.939651873194720e+01, 8.750483050000000e-06},
|
||||
{ 2.839303414769000e+00, 6.124220388371520e+00, 6.170938810000000e-06},
|
||||
{ 3.184503170430000e-01, 6.043556479692822e+01, 6.957005940000000e-06},
|
||||
{ 1.410329121503000e+00, 1.984322681200531e-02, 5.734088960000000e-06},
|
||||
{ 1.336796551701000e+00, 3.327989343110183e+01, 3.431577860000000e-06},
|
||||
{ 3.853646745987586e+00,-2.103145308554279e+01, 4.037409510000000e-06},
|
||||
{ 2.547067366210000e-01, 1.549665594221090e-03, 2.115296220000000e-06},
|
||||
{ 1.508224230072000e+00,-5.911151611514718e+01, 1.904827620000000e-06},
|
||||
{ 3.462607551400000e-02,-5.165030141480156e+01, 8.696094700000001e-07},
|
||||
{ 3.198655314207586e+00, 9.152774292423818e-03, 6.329868200000000e-07},
|
||||
{ 5.163954670916587e+00, 1.674080796065678e-02, 6.130317800000000e-07},
|
||||
{ 7.803937436990001e-01,-1.550610622055567e-03, 5.737217100000000e-07},
|
||||
{ 3.264588234538586e+00, 2.716326950857758e+01, 8.181088300000000e-07},
|
||||
{ 5.211329429707586e+00, 2.898903518276566e-02, 7.434145300000000e-07},
|
||||
{ 2.691716763490000e+00,-2.714807769012140e+01, 5.441647800000001e-07},
|
||||
{ 1.001733960626000e+00,-4.553367734785991e+01, 5.044515000000000e-07},
|
||||
{ 3.212149602842586e+00,-1.968376490094933e+01, 4.284599100000000e-07},
|
||||
{ 4.145658249560586e+00,-3.460928932187340e+01, 4.224937300000000e-07},
|
||||
{ 5.063879137454586e+00, 2.589097180762378e-02, 3.581949800000000e-07},
|
||||
{ 2.558639707373000e+00, 6.064928432880783e-03, 3.808881200000000e-07},
|
||||
{ 5.292499025555586e+00, 3.075065445597794e-03, 4.055739200000000e-07},
|
||||
};
|
||||
|
||||
static const struct MarsSatTerm mars_sat_phobos_3[28] = {
|
||||
{ 2.058107128488000e+00,-7.604861328578004e-03, 9.408605183120001e-03},
|
||||
{ 3.248856489376586e+00,-9.145863467943084e-03, 5.699538102000000e-05},
|
||||
{ 4.557280987272586e+00, 1.829238959116374e-02, 2.474369483000000e-05},
|
||||
{ 6.113423353213586e+00, 7.595702278066188e-03, 2.062715397000000e-05},
|
||||
{ 2.067112903892000e+00, 2.743825478464904e-02, 5.945574160000000e-06},
|
||||
{ 1.700689207004000e+00, 9.145729527513899e-03, 3.920190200000000e-06},
|
||||
{ 3.577059353926586e+00,-1.829259493439447e-02, 2.167791970000000e-06},
|
||||
{ 4.940284891770586e+00,-7.453616316894914e+00, 2.769763750000000e-06},
|
||||
{ 2.117885747855000e+00, 3.941171892425920e+01, 1.813539170000000e-06},
|
||||
{ 5.176055365230000e-01, 1.970205682773437e+01, 1.083429180000000e-06},
|
||||
{ 5.864248782211586e+00, 3.658425999636452e-02, 1.066657010000000e-06},
|
||||
{ 3.240692168340586e+00, 2.589639535977297e-02, 7.909853300000000e-07},
|
||||
{ 7.570751994340000e-01,-1.321790869500707e+00, 8.880885099999999e-07},
|
||||
{ 6.608910796060000e-01, 6.124220388371520e+00, 5.520758900000000e-07},
|
||||
{ 5.519669134088586e+00, 4.685772969905357e+01, 4.397886000000000e-07},
|
||||
{ 5.844000094873587e+00,-2.743825733444110e-02, 4.583609100000000e-07},
|
||||
{ 4.602755395228586e+00,-1.675516082048598e-02, 3.533131100000000e-07},
|
||||
{ 2.685649467156000e+00, 1.542518910643103e-03, 3.502988500000000e-07},
|
||||
{ 3.353176877809586e+00, 1.225604581839110e+01, 3.828851700000000e-07},
|
||||
{ 2.556761413073000e+00, 1.068971473943851e-02, 2.711247300000000e-07},
|
||||
{ 4.622369718415587e+00,-2.589682634097252e-02, 1.991745400000000e-07},
|
||||
{ 7.497690894330000e-01, 3.504362063026418e-02, 2.036897700000000e-07},
|
||||
{ 3.965205602751586e+00, 2.714806830611846e+01, 1.726779600000000e-07},
|
||||
{ 4.743858488241586e+00,-2.103145308554279e+01, 1.574472800000000e-07},
|
||||
{ 3.405870070134586e+00, 4.572380100924578e-02, 1.698567700000000e-07},
|
||||
{ 3.742380849210000e-01, 3.327989343110183e+01, 1.015004500000000e-07},
|
||||
{ 1.374678754422000e+00,-1.970206671243377e+01, 8.259149000000001e-08},
|
||||
{ 1.140160118874000e+00,-6.108652383295211e-03, 8.504923000000000e-08},
|
||||
};
|
||||
|
||||
static const struct MarsSatTerm mars_sat_deimos_0[25] = {
|
||||
{ 6.146803530569087e+00, 2.294413036846356e+00, 5.398360000000000e-09},
|
||||
{ 3.432805186019586e+00, 9.935735425667586e+00, 7.095199999999997e-10},
|
||||
{ 4.356581106602587e+00, 3.441619555269535e+00, 3.737800000000000e-10},
|
||||
{ 5.915601483397086e+00, 9.926589650598594e+00, 2.337400000000000e-10},
|
||||
{ 1.615243161446500e+00, 1.147206518423178e+00, 1.697000000000000e-10},
|
||||
{ 2.114704906116000e+00, 9.917443791162608e+00, 5.314000000000000e-11},
|
||||
{ 9.393388277920001e-01, 9.954027953247621e+00, 7.046000000000000e-11},
|
||||
{ 3.949494792732586e+00, 9.944881541450725e+00, 4.233999999999998e-11},
|
||||
{ 2.089322391969500e+00, 2.294097606691310e+00, 2.813999999999983e-11},
|
||||
{ 7.795571756865000e-01, 2.294728469946876e+00, 2.549999999999971e-11},
|
||||
{ 2.361851669664500e+00, 4.588826073692712e+00, 1.676000000000000e-11},
|
||||
{ 2.770026883578500e+00, 9.954658773106329e+00, 9.719999999999976e-12},
|
||||
{ 4.733522308629587e+00, 9.963173826265681e+00, 9.519999999999998e-12},
|
||||
{ 3.258781379519086e+00, 1.472506551307767e+01, 9.739999999999990e-12},
|
||||
{ 4.596299836113586e+00, 9.908297894321850e+00, 1.027999999999982e-11},
|
||||
{ 3.057688443346500e+00, 2.682916339604879e+00, 6.679999999999999e-12},
|
||||
{ 5.453932177963086e+00, 2.682285947505377e+00, 6.879999999999995e-12},
|
||||
{ 4.328499239462586e+00, 9.936050924435195e+00, 6.199999999999996e-12},
|
||||
{ 2.168321380562000e+00, 4.976699000854042e+00, 5.359999999999916e-12},
|
||||
{ 2.516818588675000e+00, 9.935420104041299e+00, 3.359999999999995e-12},
|
||||
{ 2.997827070725000e-01, 3.441304123224338e+00, 2.919999999999998e-12},
|
||||
{ 2.143143420462500e+00, 2.682600911813873e+00, 2.979999999999999e-12},
|
||||
{ 5.271781540988586e+00, 3.441934988580561e+00, 2.640000000000000e-12},
|
||||
{ 5.382287205930000e-01, 9.926905039305762e+00, 2.039999999999966e-12},
|
||||
{ 3.629187963751586e+00, 2.303558945733479e+00, 1.090000000000000e-12},
|
||||
};
|
||||
|
||||
static const struct MarsSatTerm mars_sat_deimos_1[21] = {
|
||||
{ 2.488175621789000e+00, 3.153377646687549e-04, 2.483031660190000e-03},
|
||||
{ 2.488175634463000e+00, 3.153377641537758e-04, 2.483031659490000e-03},
|
||||
{ 5.336889264964586e+00, 9.145915158660612e-03, 2.023426978200000e-04},
|
||||
{ 5.336889346905586e+00, 9.145915155199243e-03, 2.023426978200000e-04},
|
||||
{ 5.281538321363586e+00, 1.829244561727392e-02, 1.043167564200000e-04},
|
||||
{ 5.281537958522586e+00, 1.829244563271799e-02, 1.043167564300000e-04},
|
||||
{ 1.433490018835500e+00, 2.294413045224799e+00, 1.637133774599998e-04},
|
||||
{ 2.852007788337500e+00, 2.743818628432718e-02, 4.820671420000000e-05},
|
||||
{ 3.147176604035586e+00, 1.860783561096842e-02, 1.497066023000000e-05},
|
||||
{ 3.147176307034586e+00, 1.860783562364318e-02, 1.497066034000000e-05},
|
||||
{ 3.182516662993086e+00, 1.147206709032795e+00, 1.137028858000000e-05},
|
||||
{ 5.917181875408586e+00, 3.441620120814550e+00, 8.350609860000001e-06},
|
||||
{ 3.544871659744586e+00, 6.190897763118658e-04, 4.383254500000000e-06},
|
||||
{ 3.544871649035586e+00, 6.190897769462623e-04, 4.383254500000000e-06},
|
||||
{ 4.614193383590000e-01, 3.657933964533806e-02, 8.316176420000001e-06},
|
||||
{ 6.754222685930000e-01, 2.775277029221846e-02, 6.447587580000001e-06},
|
||||
{ 5.001640850015586e+00, 9.935735582783501e+00, 7.926687400000001e-06},
|
||||
{ 4.485218623803586e+00, 8.828439970895009e-03, 3.905064210000000e-06},
|
||||
{ 2.221013917980000e-01, 9.464721723028089e-03, 3.117801830000000e-06},
|
||||
{ 4.485218622895586e+00, 8.828439971019056e-03, 3.905064210000000e-06},
|
||||
{ 2.221013944660000e-01, 9.464721722885516e-03, 3.117801830000000e-06},
|
||||
};
|
||||
|
||||
static const struct MarsSatTerm mars_sat_deimos_2[15] = {
|
||||
{ 2.198649419514000e+00, 3.148401892560942e-04, 2.744131534600000e-04},
|
||||
{ 4.366636518977586e+00, 4.977013897776327e+00, 6.015912711000000e-05},
|
||||
{ 1.463816210370000e+00,-3.153164045300449e-04, 3.614585349000000e-05},
|
||||
{ 1.362467588064000e+00, 2.682600831640474e+00, 2.577157903000000e-05},
|
||||
{ 9.336792893320000e-01,-4.958721582505435e+00, 6.801997830000000e-06},
|
||||
{ 3.152432010349586e+00, 1.535394310272536e+00, 4.452979140000000e-06},
|
||||
{ 4.734051493918586e+00,-4.949575691510945e+00, 2.238482920000000e-06},
|
||||
{ 5.736642853342587e+00, 2.327524649334390e-04, 1.478747570000000e-06},
|
||||
{ 5.084945958505586e+00, 3.912335403634271e-04, 1.457720040000000e-06},
|
||||
{ 4.232770650771586e+00, 7.271426904652134e+00, 1.347030310000000e-06},
|
||||
{ 1.516439412109000e+00, 1.491274947692300e+01, 7.538179100000001e-07},
|
||||
{ 3.100618466711000e+00, 1.797748798526167e-02, 8.716229700000000e-07},
|
||||
{ 5.155432690964586e+00, 3.881877876517008e-01, 1.015648880000000e-06},
|
||||
{ 2.250768124831000e+00,-4.940429834739583e+00, 5.090989300000000e-07},
|
||||
{ 3.422251338868586e+00,-4.977013897776327e+00, 6.537077500000000e-07},
|
||||
};
|
||||
|
||||
static const struct MarsSatTerm mars_sat_deimos_3[27] = {
|
||||
{ 2.981506933511000e+00,-3.154811355556041e-04, 1.562693319959000e-02},
|
||||
{ 4.557500894366586e+00, 1.829233626168517e-02, 1.321818631100000e-04},
|
||||
{ 3.248124065112586e+00,-9.145934570587952e-03, 3.833652719000000e-05},
|
||||
{ 1.701551338710000e+00, 9.145665986601770e-03, 2.660211842000000e-05},
|
||||
{ 2.067662003165000e+00, 2.743821959085702e-02, 2.882438535000000e-05},
|
||||
{ 4.805327222478586e+00, 3.158564952832469e-04, 2.713213911000000e-05},
|
||||
{ 2.318788380092000e+00, 1.860776219134252e-02, 9.296324950000000e-06},
|
||||
{ 3.613580709014586e+00,-1.829261290520875e-02, 4.460960290000000e-06},
|
||||
{ 5.867810550672586e+00, 3.658405181871115e-02, 4.904156030000000e-06},
|
||||
{ 5.496515547817586e+00,-9.461294099629817e-03, 2.366881150000000e-06},
|
||||
{ 6.120362957845586e+00, 2.775361064625920e-02, 2.057552850000000e-06},
|
||||
{ 3.612389208841586e+00, 8.830178627407989e-03, 2.365750140000000e-06},
|
||||
{ 5.554163001934587e+00,-1.860800990265229e-02, 1.228814440000000e-06},
|
||||
{ 3.561485983136586e+00, 1.797648948023003e-02, 1.215189460000000e-06},
|
||||
{ 1.941975361869000e+00,-6.259843043363501e-04, 9.602856999999999e-07},
|
||||
{ 5.845244776759587e+00,-2.743819348074506e-02, 1.174471770000000e-06},
|
||||
{ 1.821048365911000e+00, 9.460829229838182e-03, 1.058441050000000e-06},
|
||||
{ 7.587135454610000e-01, 1.682177864988152e-04, 2.383147490000000e-06},
|
||||
{ 3.292891638604586e+00, 4.573365667404765e-02, 7.656303800000000e-07},
|
||||
{ 3.766016108380586e+00, 3.683140215171970e-02, 4.075824300000000e-07},
|
||||
{ 5.747244956219586e+00, 9.954342722363187e+00, 4.962340900000000e-07},
|
||||
{ 1.724062868741000e+00,-2.774017744689255e-02, 3.003077900000000e-07},
|
||||
{ 4.429747729642586e+00,-8.904754331211824e-03, 2.613416400000000e-07},
|
||||
{ 8.659161909010000e-01, 2.718340176462802e-02, 2.891945200000000e-07},
|
||||
{ 2.259603352363000e+00,-3.660152378331885e-02, 2.213788200000000e-07},
|
||||
{ 1.108725025984000e+00, 9.935732440466172e+00, 2.449950300000000e-07},
|
||||
{ 1.680391963791000e+00, 9.076040299352415e-03, 1.652416600000000e-07},
|
||||
};
|
||||
|
||||
static const struct MarsSatBody mars_sat_bodies[2] = {
|
||||
{
|
||||
9.549547741038312e-11, 1.970205562831390e+01, 1.657852042683113e-10,
|
||||
{
|
||||
0.0000626916188000,
|
||||
2.0912973926417302,
|
||||
-0.0000005774003141,
|
||||
0.0000006185052790,
|
||||
-0.0000575018919826,
|
||||
-0.0000540676493351,
|
||||
},
|
||||
{
|
||||
{mars_sat_phobos_0, 16},
|
||||
{mars_sat_phobos_1, 42},
|
||||
{mars_sat_phobos_2, 27},
|
||||
{mars_sat_phobos_3, 28}
|
||||
}
|
||||
},
|
||||
{
|
||||
9.549547622768120e-11, 4.977013889652300e+00, -2.331793571572226e-14,
|
||||
{
|
||||
0.0001568134086700,
|
||||
-1.9167537810740201,
|
||||
-0.0000239579419518,
|
||||
0.0000257893300229,
|
||||
-0.0056443721379635,
|
||||
-0.0053121806978560,
|
||||
},
|
||||
{
|
||||
{mars_sat_deimos_0, 25},
|
||||
{mars_sat_deimos_1, 21},
|
||||
{mars_sat_deimos_2, 15},
|
||||
{mars_sat_deimos_3, 27}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
static
|
||||
void CalcMarsSatElem(double t, int body, double elem[6]) {
|
||||
int j;
|
||||
const struct MarsSatBody *bp = mars_sat_bodies + body;
|
||||
memcpy(elem, bp->constants, 6 * sizeof(double));
|
||||
for (j = 0; j < 2; j++) {
|
||||
const struct MarsSatTerm *const begin = bp->lists[j].terms;
|
||||
const struct MarsSatTerm *p = begin + bp->lists[j].size;
|
||||
while (--p >= begin) {
|
||||
const double d = p->phase + t * p->frequency;
|
||||
elem[j] += p->amplitude * cos(d);
|
||||
}
|
||||
}
|
||||
for (j = 2; j < 4; j++) {
|
||||
const struct MarsSatTerm *const begin = bp->lists[j].terms;
|
||||
const struct MarsSatTerm *p = begin + bp->lists[j].size;
|
||||
while (--p >= begin) {
|
||||
const double d = p->phase + t * p->frequency;
|
||||
elem[2*j-2] += p->amplitude * cos(d);
|
||||
elem[2*j-1] += p->amplitude * sin(d);
|
||||
}
|
||||
}
|
||||
elem[1] += (bp->l + bp->acc * t) * t;
|
||||
}
|
||||
|
||||
|
||||
static
|
||||
void MultMat(const double a[9], const double b[9], double c[9]) {
|
||||
int i, j;
|
||||
for (i = 0; i < 3; i++) {
|
||||
for (j = 0; j < 3; j++) {
|
||||
c[i*3+j] = a[i*3]*b[j] + a[i*3+1]*b[3+j] + a[i*3+2]*b[6+j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const double J2000_to_VSOP87[9] = {
|
||||
9.999999999998848e-01,-4.799655442984222e-07, 0.000000000000000e+00,
|
||||
4.403598133110236e-07, 9.174821370868568e-01, 3.977769829016507e-01,
|
||||
-1.909192461077750e-07,-3.977769829016049e-01, 9.174821370869625e-01};
|
||||
|
||||
static const double ome0 = 47.68143;
|
||||
static const double inc0 = 37.1135;
|
||||
static const double dome = -0.1061;
|
||||
static const double dinc = 0.0609;
|
||||
|
||||
static
|
||||
void GenerateMarsSatToVSOP87(double t, double mat[9]) {
|
||||
double ome, inc, co, so, ci, si;
|
||||
double m[9];
|
||||
t -= 6491.5;
|
||||
ome = (ome0 + dome * t / 36525.) * (M_PI / 180.0);
|
||||
inc = (inc0 + dinc * t / 36525.) * (M_PI / 180.0);
|
||||
co = cos(ome);
|
||||
so = sin(ome);
|
||||
ci = cos(inc);
|
||||
si = sin(inc);
|
||||
m[0] = co; m[1] = -ci*so; m[2] = si*so;
|
||||
m[3] = so; m[4] = ci*co; m[5] = -si*co;
|
||||
m[6] = 0.0; m[7] = si; m[8] = ci;
|
||||
MultMat(J2000_to_VSOP87, m, mat);
|
||||
}
|
||||
|
||||
void GetMarsSatCoor(double jd, int body, double *xyz, double *xyzdot) {
|
||||
double elem[6];
|
||||
double x[6];
|
||||
double mat[9];
|
||||
const double t = jd - 2451545.0 + 6491.5;
|
||||
|
||||
CalcMarsSatElem(t, body, elem);
|
||||
GenerateMarsSatToVSOP87(t, mat);
|
||||
EllipticToRectangularA(mars_sat_bodies[body].mu, elem, 0.0, x);
|
||||
|
||||
xyz[0] = mat[0]*x[0] + mat[1]*x[1] + mat[2]*x[2];
|
||||
xyz[1] = mat[3]*x[0] + mat[4]*x[1] + mat[5]*x[2];
|
||||
xyz[2] = mat[6]*x[0] + mat[7]*x[1] + mat[8]*x[2];
|
||||
|
||||
if (xyzdot) {
|
||||
xyzdot[0] = mat[0]*x[3] + mat[1]*x[4] + mat[2]*x[5];
|
||||
xyzdot[1] = mat[3]*x[3] + mat[4]*x[4] + mat[5]*x[5];
|
||||
xyzdot[2] = mat[6]*x[3] + mat[7]*x[4] + mat[8]*x[5];
|
||||
}
|
||||
}
|
||||
81
src/marssat.h
Normal file
81
src/marssat.h
Normal file
@ -0,0 +1,81 @@
|
||||
/************************************************************************
|
||||
|
||||
The Ephemerides of the Martian satellites
|
||||
(adjustement from 1877 to 2005, Version 1.0)
|
||||
by Valery Lainey can be obtained from Valery Lainey:
|
||||
|
||||
V.Lainey (Lainey@oma.be)
|
||||
ROB- 3, Avenue Circulaire, B-1180 Bruxelles (Belgium)
|
||||
IMCCE - 77, Avenue Denfert-Rochereau 75014 Paris (France)
|
||||
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
I (Johannes Gajdosik) have just taken Valery Laineys Fortran code,
|
||||
MarsSatV1-0.f, which he kindly supplied, and rearranged it into
|
||||
this piece of software.
|
||||
|
||||
I can neither allow nor forbid the usage of Valery Laineys
|
||||
Ephemerides of the Martian satellites.
|
||||
The copyright notice below covers not the work of Valery Lainey
|
||||
but just my work, that is the compilation of Valery Laineys
|
||||
Ephemerides of the Martian satellites into the software supplied in this file.
|
||||
|
||||
|
||||
Copyright (c) 2006 Johannes Gajdosik
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
Derived from Stellarium's MARSSAT implementation.
|
||||
Modified for pg_orrery: removed static mutable state and
|
||||
CalcInterpolatedElements caching for thread safety
|
||||
(PostgreSQL PARALLEL SAFE). Elements are computed fresh on each call.
|
||||
|
||||
1) do not calculate constant terms at runtime but beforehand
|
||||
2) unite terms with the same frequencies
|
||||
|
||||
****************************************************************/
|
||||
|
||||
#ifndef PG_ORRERY_MARSSAT_H
|
||||
#define PG_ORRERY_MARSSAT_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define MARS_SAT_PHOBOS 0
|
||||
#define MARS_SAT_DEIMOS 1
|
||||
|
||||
void GetMarsSatCoor(double jd, int body, double *xyz, double *xyzdot);
|
||||
/* Return the rectangular coordinates and velocity of the given satellite
|
||||
and the given julian date jd expressed in dynamical time (TAI+32.184s).
|
||||
The origin of the xyz-coordinates is the center of Mars.
|
||||
The reference frame is "dynamical equinox and ecliptic J2000",
|
||||
which is the reference frame in VSOP87 and VSOP87A.
|
||||
|
||||
body: 0=Phobos, 1=Deimos
|
||||
|
||||
xyz[3]: position (AU), relative to Mars center
|
||||
xyzdot[3]: velocity (AU/day), may be NULL
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
220
src/moon_funcs.c
Normal file
220
src/moon_funcs.c
Normal file
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* moon_funcs.c -- Planetary moon observation
|
||||
*
|
||||
* SQL functions for observing moons of Jupiter, Saturn, Uranus, and Mars.
|
||||
* Each moon's position is computed in the VSOP87 ecliptic J2000 frame
|
||||
* relative to its parent planet, then combined with the parent's
|
||||
* heliocentric position to get geocentric az/el.
|
||||
*
|
||||
* Pipeline for each moon:
|
||||
* 1. Parent planet heliocentric position (VSOP87)
|
||||
* 2. Moon position relative to parent (L12/TASS17/GUST86/MARSSAT)
|
||||
* 3. Moon heliocentric = parent + moon_relative
|
||||
* 4. Moon geocentric = moon_heliocentric - Earth_heliocentric
|
||||
* 5. Ecliptic -> equatorial -> precess -> az/el
|
||||
*/
|
||||
|
||||
#include "postgres.h"
|
||||
#include "fmgr.h"
|
||||
#include "funcapi.h"
|
||||
#include "utils/timestamp.h"
|
||||
#include "types.h"
|
||||
#include "astro_math.h"
|
||||
#include "vsop87.h"
|
||||
#include "l12.h"
|
||||
#include "tass17.h"
|
||||
#include "gust86.h"
|
||||
#include "marssat.h"
|
||||
#include <math.h>
|
||||
|
||||
PG_FUNCTION_INFO_V1(galilean_observe);
|
||||
PG_FUNCTION_INFO_V1(saturn_moon_observe);
|
||||
PG_FUNCTION_INFO_V1(uranus_moon_observe);
|
||||
PG_FUNCTION_INFO_V1(mars_moon_observe);
|
||||
|
||||
|
||||
/*
|
||||
* observe_from_geocentric() is now in astro_math.h as a static inline,
|
||||
* shared by planet_funcs.c, moon_funcs.c, and de_funcs.c.
|
||||
*/
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* Internal: common pattern for all planetary moons
|
||||
*
|
||||
* Given: moon position relative to parent (VSOP87 ecliptic J2000, AU)
|
||||
* parent's VSOP87 body index (0-based)
|
||||
* observer, JD
|
||||
*
|
||||
* Computes geocentric position and returns topocentric az/el.
|
||||
* ================================================================
|
||||
*/
|
||||
static void
|
||||
observe_planetary_moon(const double moon_rel[3], int vsop_parent,
|
||||
double jd, const pg_observer *obs,
|
||||
pg_topocentric *result)
|
||||
{
|
||||
double parent_xyz[6];
|
||||
double earth_xyz[6];
|
||||
double geo_ecl[3];
|
||||
|
||||
/* Parent planet heliocentric */
|
||||
GetVsop87Coor(jd, vsop_parent, parent_xyz);
|
||||
|
||||
/* Earth heliocentric */
|
||||
GetVsop87Coor(jd, 2, earth_xyz); /* VSOP87 body 2 = Earth */
|
||||
|
||||
/* Moon geocentric = (parent + moon_relative) - Earth */
|
||||
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];
|
||||
|
||||
observe_from_geocentric(geo_ecl, jd, obs, result);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* galilean_observe(body_id int, observer, timestamptz) -> topocentric
|
||||
*
|
||||
* Observe a Galilean moon of Jupiter.
|
||||
* Body IDs: 0=Io, 1=Europa, 2=Ganymede, 3=Callisto
|
||||
*
|
||||
* Uses L1.2 theory (Lainey, Duriez & Vienne) for moon positions
|
||||
* and VSOP87 for Jupiter and Earth.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
galilean_observe(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 body_id = PG_GETARG_INT32(0);
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
||||
int64 ts = PG_GETARG_INT64(2);
|
||||
double jd;
|
||||
double moon_xyz[3];
|
||||
pg_topocentric *result;
|
||||
|
||||
if (body_id < L12_IO || body_id > L12_CALLISTO)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("galilean_observe: body_id %d must be 0-3 (Io, Europa, Ganymede, Callisto)",
|
||||
body_id)));
|
||||
|
||||
jd = timestamptz_to_jd(ts);
|
||||
|
||||
/* Moon position relative to Jupiter, VSOP87 ecliptic J2000, AU */
|
||||
GetL12Coor(jd, body_id, moon_xyz, NULL);
|
||||
|
||||
result = (pg_topocentric *) palloc(sizeof(pg_topocentric));
|
||||
observe_planetary_moon(moon_xyz, 4, jd, obs, result); /* VSOP87 body 4 = Jupiter */
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* saturn_moon_observe(body_id int, observer, timestamptz) -> topocentric
|
||||
*
|
||||
* Observe a moon of Saturn.
|
||||
* Body IDs: 0=Mimas, 1=Enceladus, 2=Tethys, 3=Dione,
|
||||
* 4=Rhea, 5=Titan, 6=Iapetus, 7=Hyperion
|
||||
*
|
||||
* Uses TASS 1.7 theory (Vienne & Duriez).
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
saturn_moon_observe(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 body_id = PG_GETARG_INT32(0);
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
||||
int64 ts = PG_GETARG_INT64(2);
|
||||
double jd;
|
||||
double moon_xyz[3];
|
||||
pg_topocentric *result;
|
||||
|
||||
if (body_id < TASS17_MIMAS || body_id > TASS17_HYPERION)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("saturn_moon_observe: 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_topocentric *) palloc(sizeof(pg_topocentric));
|
||||
observe_planetary_moon(moon_xyz, 5, jd, obs, result); /* VSOP87 body 5 = Saturn */
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* uranus_moon_observe(body_id int, observer, timestamptz) -> topocentric
|
||||
*
|
||||
* Observe a moon of Uranus.
|
||||
* Body IDs: 0=Miranda, 1=Ariel, 2=Umbriel, 3=Titania, 4=Oberon
|
||||
*
|
||||
* Uses GUST86 theory (Laskar & Jacobson).
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
uranus_moon_observe(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 body_id = PG_GETARG_INT32(0);
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
||||
int64 ts = PG_GETARG_INT64(2);
|
||||
double jd;
|
||||
double moon_xyz[3];
|
||||
pg_topocentric *result;
|
||||
|
||||
if (body_id < GUST86_MIRANDA || body_id > GUST86_OBERON)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("uranus_moon_observe: 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_topocentric *) palloc(sizeof(pg_topocentric));
|
||||
observe_planetary_moon(moon_xyz, 6, jd, obs, result); /* VSOP87 body 6 = Uranus */
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* mars_moon_observe(body_id int, observer, timestamptz) -> topocentric
|
||||
*
|
||||
* Observe a moon of Mars.
|
||||
* Body IDs: 0=Phobos, 1=Deimos
|
||||
*
|
||||
* Uses MarsSat theory (Lainey, 2007).
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
mars_moon_observe(PG_FUNCTION_ARGS)
|
||||
{
|
||||
int32 body_id = PG_GETARG_INT32(0);
|
||||
pg_observer *obs = (pg_observer *) PG_GETARG_POINTER(1);
|
||||
int64 ts = PG_GETARG_INT64(2);
|
||||
double jd;
|
||||
double moon_xyz[3];
|
||||
pg_topocentric *result;
|
||||
|
||||
if (body_id < MARS_SAT_PHOBOS || body_id > MARS_SAT_DEIMOS)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
|
||||
errmsg("mars_moon_observe: 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_topocentric *) palloc(sizeof(pg_topocentric));
|
||||
observe_planetary_moon(moon_xyz, 3, jd, obs, result); /* VSOP87 body 3 = Mars */
|
||||
|
||||
PG_RETURN_POINTER(result);
|
||||
}
|
||||
701
src/od_funcs.c
Normal file
701
src/od_funcs.c
Normal file
@ -0,0 +1,701 @@
|
||||
/*
|
||||
* od_funcs.c -- SQL bindings for TLE fitting functions
|
||||
*
|
||||
* Exposes od_solver.c to PostgreSQL as SQL-callable functions:
|
||||
* tle_from_eci() -- fit TLE from ECI ephemeris
|
||||
* tle_from_topocentric() -- fit TLE from topocentric observations
|
||||
* tle_fit_residuals() -- per-observation residuals diagnostic
|
||||
*
|
||||
* Placeholder: full implementation in Commit 3.
|
||||
*/
|
||||
|
||||
#include "postgres.h"
|
||||
#include "fmgr.h"
|
||||
#include "funcapi.h"
|
||||
#include "utils/timestamp.h"
|
||||
#include "utils/array.h"
|
||||
#include "utils/builtins.h"
|
||||
#include "catalog/pg_type.h"
|
||||
#include "norad.h"
|
||||
#include "types.h"
|
||||
#include "od_solver.h"
|
||||
#include "od_math.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <string.h>
|
||||
|
||||
/* For construct_array() — covariance output */
|
||||
#include "utils/lsyscache.h"
|
||||
|
||||
PG_FUNCTION_INFO_V1(tle_from_eci);
|
||||
PG_FUNCTION_INFO_V1(tle_from_topocentric);
|
||||
PG_FUNCTION_INFO_V1(tle_from_topocentric_multi);
|
||||
PG_FUNCTION_INFO_V1(tle_fit_residuals);
|
||||
|
||||
/* ================================================================
|
||||
* Helper: pg_tle ↔ tle_t conversion (local copy, avoids symbol coupling)
|
||||
* ================================================================
|
||||
*/
|
||||
|
||||
static void
|
||||
pg_tle_to_sat_code_od(const pg_tle *src, tle_t *dst)
|
||||
{
|
||||
memset(dst, 0, sizeof(tle_t));
|
||||
|
||||
dst->epoch = src->epoch;
|
||||
dst->xincl = src->inclination;
|
||||
dst->xnodeo = src->raan;
|
||||
dst->eo = src->eccentricity;
|
||||
dst->omegao = src->arg_perigee;
|
||||
dst->xmo = src->mean_anomaly;
|
||||
dst->xno = src->mean_motion;
|
||||
dst->xndt2o = src->mean_motion_dot;
|
||||
dst->xndd6o = src->mean_motion_ddot;
|
||||
dst->bstar = src->bstar;
|
||||
|
||||
dst->norad_number = src->norad_id;
|
||||
dst->bulletin_number = src->elset_num;
|
||||
dst->revolution_number = src->rev_num;
|
||||
dst->classification = src->classification;
|
||||
dst->ephemeris_type = src->ephemeris_type;
|
||||
|
||||
memcpy(dst->intl_desig, src->intl_desig, 9);
|
||||
}
|
||||
|
||||
static void
|
||||
sat_code_to_pg_tle(const tle_t *src, pg_tle *dst)
|
||||
{
|
||||
memset(dst, 0, sizeof(pg_tle));
|
||||
|
||||
dst->epoch = src->epoch;
|
||||
dst->inclination = src->xincl;
|
||||
dst->raan = src->xnodeo;
|
||||
dst->eccentricity = src->eo;
|
||||
dst->arg_perigee = src->omegao;
|
||||
dst->mean_anomaly = src->xmo;
|
||||
dst->mean_motion = src->xno;
|
||||
dst->mean_motion_dot = src->xndt2o;
|
||||
dst->mean_motion_ddot = src->xndd6o;
|
||||
dst->bstar = src->bstar;
|
||||
|
||||
dst->norad_id = src->norad_number;
|
||||
dst->elset_num = src->bulletin_number;
|
||||
dst->rev_num = src->revolution_number;
|
||||
dst->classification = src->classification;
|
||||
dst->ephemeris_type = src->ephemeris_type;
|
||||
|
||||
memcpy(dst->intl_desig, src->intl_desig, 9);
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* tle_from_eci(eci_position[], timestamptz[], tle, boolean, int4)
|
||||
* -> RECORD (fitted_tle tle, iterations int4, rms_final float8,
|
||||
* rms_initial float8, status text)
|
||||
*
|
||||
* Fit a TLE from an array of ECI position/velocity observations.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
tle_from_eci(PG_FUNCTION_ARGS)
|
||||
{
|
||||
ArrayType *pos_arr = PG_GETARG_ARRAYTYPE_P(0);
|
||||
ArrayType *time_arr = PG_GETARG_ARRAYTYPE_P(1);
|
||||
bool has_seed = !PG_ARGISNULL(2);
|
||||
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(2) : NULL;
|
||||
bool fit_bstar = PG_ARGISNULL(3) ? false : PG_GETARG_BOOL(3);
|
||||
int32 max_iter = PG_ARGISNULL(4) ? 15 : PG_GETARG_INT32(4);
|
||||
|
||||
int n_pos, n_times;
|
||||
Datum *pos_datums, *time_datums;
|
||||
bool *pos_nulls, *time_nulls;
|
||||
od_observation_t *obs;
|
||||
od_config_t config;
|
||||
od_result_t result;
|
||||
tle_t seed_sat;
|
||||
int i, rc;
|
||||
|
||||
TupleDesc tupdesc;
|
||||
Datum values[8];
|
||||
bool nulls[8];
|
||||
HeapTuple tuple;
|
||||
|
||||
/* Deconstruct arrays.
|
||||
* pg_eci is 48 bytes = pass-by-reference (byval=false).
|
||||
* timestamptz is int64 = pass-by-value on 64-bit. */
|
||||
deconstruct_array(pos_arr, pos_arr->elemtype, sizeof(pg_eci), false,
|
||||
TYPALIGN_DOUBLE, &pos_datums, &pos_nulls, &n_pos);
|
||||
deconstruct_array(time_arr, TIMESTAMPTZOID, sizeof(int64), FLOAT8PASSBYVAL,
|
||||
TYPALIGN_DOUBLE, &time_datums, &time_nulls, &n_times);
|
||||
|
||||
if (n_pos != n_times)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||
errmsg("positions and times arrays must have same length: "
|
||||
"%d vs %d", n_pos, n_times)));
|
||||
|
||||
if (n_pos < 6)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("at least 6 observations required, got %d", n_pos)));
|
||||
|
||||
/* Build observation array */
|
||||
obs = (od_observation_t *) palloc(sizeof(od_observation_t) * n_pos);
|
||||
|
||||
for (i = 0; i < n_pos; i++)
|
||||
{
|
||||
pg_eci *eci = (pg_eci *) DatumGetPointer(pos_datums[i]);
|
||||
int64 ts = DatumGetInt64(time_datums[i]);
|
||||
double jd = PG_EPOCH_JD + ((double)ts / (double)USECS_PER_DAY);
|
||||
|
||||
obs[i].jd = jd;
|
||||
obs[i].data[0] = eci->x;
|
||||
obs[i].data[1] = eci->y;
|
||||
obs[i].data[2] = eci->z;
|
||||
obs[i].data[3] = eci->vx; /* already km/s */
|
||||
obs[i].data[4] = eci->vy;
|
||||
obs[i].data[5] = eci->vz;
|
||||
}
|
||||
|
||||
/* Configure solver */
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.obs_type = OD_OBS_ECI;
|
||||
config.fit_bstar = fit_bstar ? 1 : 0;
|
||||
config.max_iter = max_iter;
|
||||
config.observers = NULL;
|
||||
config.n_observers = 0;
|
||||
|
||||
/* Convert seed TLE if provided */
|
||||
if (has_seed)
|
||||
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
||||
|
||||
memset(&result, 0, sizeof(result));
|
||||
|
||||
/* Run solver */
|
||||
rc = od_fit_tle(obs, n_pos, has_seed ? &seed_sat : NULL, &config, &result);
|
||||
|
||||
pfree(obs);
|
||||
|
||||
if (rc != 0)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_DATA_EXCEPTION),
|
||||
errmsg("TLE fitting failed: %s", result.status)));
|
||||
|
||||
/* Build result tuple */
|
||||
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("function returning record called in context "
|
||||
"that cannot accept type record")));
|
||||
tupdesc = BlessTupleDesc(tupdesc);
|
||||
|
||||
memset(nulls, 0, sizeof(nulls));
|
||||
|
||||
{
|
||||
pg_tle *fitted = (pg_tle *) palloc(sizeof(pg_tle));
|
||||
sat_code_to_pg_tle(&result.fitted_tle, fitted);
|
||||
values[0] = PointerGetDatum(fitted);
|
||||
}
|
||||
values[1] = Int32GetDatum(result.iterations);
|
||||
values[2] = Float8GetDatum(result.rms_final);
|
||||
values[3] = Float8GetDatum(result.rms_initial);
|
||||
values[4] = CStringGetTextDatum(result.status);
|
||||
values[5] = Float8GetDatum(result.condition_number);
|
||||
|
||||
/* Covariance: float8[] or NULL */
|
||||
if (result.cov_size > 0)
|
||||
{
|
||||
Datum *cov_datums = (Datum *) palloc(sizeof(Datum) * result.cov_size);
|
||||
int ci;
|
||||
for (ci = 0; ci < result.cov_size; ci++)
|
||||
cov_datums[ci] = Float8GetDatum(result.covariance[ci]);
|
||||
values[6] = PointerGetDatum(
|
||||
construct_array(cov_datums, result.cov_size,
|
||||
FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||
TYPALIGN_DOUBLE));
|
||||
}
|
||||
else
|
||||
{
|
||||
nulls[6] = true;
|
||||
}
|
||||
|
||||
values[7] = Int32GetDatum(result.cov_size > 0
|
||||
? (result.cov_size == 28 ? 7 : 6)
|
||||
: 0);
|
||||
|
||||
tuple = heap_form_tuple(tupdesc, values, nulls);
|
||||
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* tle_from_topocentric(topocentric[], timestamptz[], observer,
|
||||
* tle, boolean, int4)
|
||||
* -> RECORD (same as tle_from_eci)
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
tle_from_topocentric(PG_FUNCTION_ARGS)
|
||||
{
|
||||
ArrayType *topo_arr = PG_GETARG_ARRAYTYPE_P(0);
|
||||
ArrayType *time_arr = PG_GETARG_ARRAYTYPE_P(1);
|
||||
pg_observer *obs_pg = (pg_observer *) PG_GETARG_POINTER(2);
|
||||
bool has_seed = !PG_ARGISNULL(3);
|
||||
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(3) : NULL;
|
||||
bool fit_bstar = PG_ARGISNULL(4) ? false : PG_GETARG_BOOL(4);
|
||||
int32 max_iter = PG_ARGISNULL(5) ? 15 : PG_GETARG_INT32(5);
|
||||
|
||||
int n_topo, n_times;
|
||||
od_observation_t *obs;
|
||||
od_config_t config;
|
||||
od_observer_t observer;
|
||||
od_result_t result;
|
||||
tle_t seed_sat;
|
||||
int i, rc;
|
||||
|
||||
TupleDesc tupdesc;
|
||||
Datum values[8];
|
||||
bool nulls[8];
|
||||
HeapTuple tuple;
|
||||
|
||||
/* Build observer */
|
||||
observer.lat = obs_pg->lat;
|
||||
observer.lon = obs_pg->lon;
|
||||
observer.alt_m = obs_pg->alt_m;
|
||||
od_observer_to_ecef(observer.lat, observer.lon, observer.alt_m,
|
||||
observer.ecef);
|
||||
|
||||
/* Deconstruct arrays */
|
||||
{
|
||||
Datum *topo_datums;
|
||||
bool *topo_nulls;
|
||||
Datum *time_datums;
|
||||
bool *time_nulls;
|
||||
|
||||
deconstruct_array(topo_arr, topo_arr->elemtype, sizeof(pg_topocentric),
|
||||
false, TYPALIGN_DOUBLE,
|
||||
&topo_datums, &topo_nulls, &n_topo);
|
||||
deconstruct_array(time_arr, TIMESTAMPTZOID, sizeof(int64), FLOAT8PASSBYVAL,
|
||||
TYPALIGN_DOUBLE, &time_datums, &time_nulls, &n_times);
|
||||
|
||||
if (n_topo != n_times)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||
errmsg("observations and times arrays must have same length: "
|
||||
"%d vs %d", n_topo, n_times)));
|
||||
|
||||
if (n_topo < 6)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("at least 6 observations required, got %d", n_topo)));
|
||||
|
||||
obs = (od_observation_t *) palloc(sizeof(od_observation_t) * n_topo);
|
||||
|
||||
for (i = 0; i < n_topo; i++)
|
||||
{
|
||||
pg_topocentric *topo = (pg_topocentric *) DatumGetPointer(topo_datums[i]);
|
||||
int64 ts = DatumGetInt64(time_datums[i]);
|
||||
double jd = PG_EPOCH_JD + ((double)ts / (double)USECS_PER_DAY);
|
||||
|
||||
obs[i].jd = jd;
|
||||
obs[i].data[0] = topo->azimuth;
|
||||
obs[i].data[1] = topo->elevation;
|
||||
obs[i].data[2] = topo->range_km;
|
||||
obs[i].observer_idx = 0; /* single observer */
|
||||
}
|
||||
}
|
||||
|
||||
/* Configure solver */
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.obs_type = OD_OBS_TOPO;
|
||||
config.fit_bstar = fit_bstar ? 1 : 0;
|
||||
config.max_iter = max_iter;
|
||||
config.observers = &observer;
|
||||
config.n_observers = 1;
|
||||
|
||||
if (has_seed)
|
||||
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
||||
|
||||
memset(&result, 0, sizeof(result));
|
||||
|
||||
rc = od_fit_tle(obs, n_topo, has_seed ? &seed_sat : NULL, &config, &result);
|
||||
|
||||
pfree(obs);
|
||||
|
||||
if (rc != 0)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_DATA_EXCEPTION),
|
||||
errmsg("TLE fitting failed: %s", result.status)));
|
||||
|
||||
/* Build result tuple */
|
||||
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("function returning record called in context "
|
||||
"that cannot accept type record")));
|
||||
tupdesc = BlessTupleDesc(tupdesc);
|
||||
|
||||
memset(nulls, 0, sizeof(nulls));
|
||||
|
||||
{
|
||||
pg_tle *fitted = (pg_tle *) palloc(sizeof(pg_tle));
|
||||
sat_code_to_pg_tle(&result.fitted_tle, fitted);
|
||||
values[0] = PointerGetDatum(fitted);
|
||||
}
|
||||
values[1] = Int32GetDatum(result.iterations);
|
||||
values[2] = Float8GetDatum(result.rms_final);
|
||||
values[3] = Float8GetDatum(result.rms_initial);
|
||||
values[4] = CStringGetTextDatum(result.status);
|
||||
values[5] = Float8GetDatum(result.condition_number);
|
||||
|
||||
if (result.cov_size > 0)
|
||||
{
|
||||
Datum *cov_datums = (Datum *) palloc(sizeof(Datum) * result.cov_size);
|
||||
int ci;
|
||||
for (ci = 0; ci < result.cov_size; ci++)
|
||||
cov_datums[ci] = Float8GetDatum(result.covariance[ci]);
|
||||
values[6] = PointerGetDatum(
|
||||
construct_array(cov_datums, result.cov_size,
|
||||
FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||
TYPALIGN_DOUBLE));
|
||||
}
|
||||
else
|
||||
{
|
||||
nulls[6] = true;
|
||||
}
|
||||
|
||||
values[7] = Int32GetDatum(result.cov_size > 0
|
||||
? (result.cov_size == 28 ? 7 : 6)
|
||||
: 0);
|
||||
|
||||
tuple = heap_form_tuple(tupdesc, values, nulls);
|
||||
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* tle_from_topocentric_multi(topocentric[], timestamptz[],
|
||||
* observer[], int4[],
|
||||
* tle, boolean, int4)
|
||||
* -> RECORD (same as tle_from_eci)
|
||||
*
|
||||
* Multi-observer variant: observations from different ground stations.
|
||||
* observer_ids[i] indexes into the observers[] array.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
tle_from_topocentric_multi(PG_FUNCTION_ARGS)
|
||||
{
|
||||
ArrayType *topo_arr = PG_GETARG_ARRAYTYPE_P(0);
|
||||
ArrayType *time_arr = PG_GETARG_ARRAYTYPE_P(1);
|
||||
ArrayType *obs_arr = PG_GETARG_ARRAYTYPE_P(2);
|
||||
ArrayType *id_arr = PG_GETARG_ARRAYTYPE_P(3);
|
||||
bool has_seed = !PG_ARGISNULL(4);
|
||||
pg_tle *seed_pg = has_seed ? (pg_tle *) PG_GETARG_POINTER(4) : NULL;
|
||||
bool fit_bstar = PG_ARGISNULL(5) ? false : PG_GETARG_BOOL(5);
|
||||
int32 max_iter = PG_ARGISNULL(6) ? 15 : PG_GETARG_INT32(6);
|
||||
|
||||
int n_topo, n_times, n_obs_sites, n_ids;
|
||||
od_observation_t *obs;
|
||||
od_config_t config;
|
||||
od_observer_t *observers;
|
||||
od_result_t result;
|
||||
tle_t seed_sat;
|
||||
int i, rc;
|
||||
|
||||
TupleDesc tupdesc;
|
||||
Datum values[8];
|
||||
bool nulls[8];
|
||||
HeapTuple tuple;
|
||||
|
||||
/* Deconstruct all arrays */
|
||||
Datum *topo_datums, *time_datums, *obs_datums, *id_datums;
|
||||
bool *topo_nulls, *time_nulls, *obs_nulls, *id_nulls;
|
||||
|
||||
deconstruct_array(topo_arr, topo_arr->elemtype, sizeof(pg_topocentric),
|
||||
false, TYPALIGN_DOUBLE,
|
||||
&topo_datums, &topo_nulls, &n_topo);
|
||||
deconstruct_array(time_arr, TIMESTAMPTZOID, sizeof(int64), FLOAT8PASSBYVAL,
|
||||
TYPALIGN_DOUBLE, &time_datums, &time_nulls, &n_times);
|
||||
deconstruct_array(obs_arr, obs_arr->elemtype, sizeof(pg_observer),
|
||||
false, TYPALIGN_DOUBLE,
|
||||
&obs_datums, &obs_nulls, &n_obs_sites);
|
||||
deconstruct_array(id_arr, INT4OID, sizeof(int32), true,
|
||||
TYPALIGN_INT, &id_datums, &id_nulls, &n_ids);
|
||||
|
||||
if (n_topo != n_times)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||
errmsg("observations and times arrays must have same length: "
|
||||
"%d vs %d", n_topo, n_times)));
|
||||
|
||||
if (n_topo != n_ids)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||
errmsg("observations and observer_ids arrays must have same length: "
|
||||
"%d vs %d", n_topo, n_ids)));
|
||||
|
||||
if (n_topo < 6)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("at least 6 observations required, got %d", n_topo)));
|
||||
|
||||
if (n_obs_sites < 1)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("at least 1 observer required")));
|
||||
|
||||
/* Build observer array (pre-compute ECEF for each) */
|
||||
observers = (od_observer_t *) palloc(sizeof(od_observer_t) * n_obs_sites);
|
||||
for (i = 0; i < n_obs_sites; i++)
|
||||
{
|
||||
pg_observer *op = (pg_observer *) DatumGetPointer(obs_datums[i]);
|
||||
observers[i].lat = op->lat;
|
||||
observers[i].lon = op->lon;
|
||||
observers[i].alt_m = op->alt_m;
|
||||
od_observer_to_ecef(op->lat, op->lon, op->alt_m, observers[i].ecef);
|
||||
}
|
||||
|
||||
/* Build observation array with per-observation observer index */
|
||||
obs = (od_observation_t *) palloc(sizeof(od_observation_t) * n_topo);
|
||||
for (i = 0; i < n_topo; i++)
|
||||
{
|
||||
pg_topocentric *topo = (pg_topocentric *) DatumGetPointer(topo_datums[i]);
|
||||
int64 ts = DatumGetInt64(time_datums[i]);
|
||||
int32 oid = DatumGetInt32(id_datums[i]);
|
||||
|
||||
if (oid < 0 || oid >= n_obs_sites)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
|
||||
errmsg("observer_id %d out of range [0, %d)",
|
||||
oid, n_obs_sites)));
|
||||
|
||||
obs[i].jd = PG_EPOCH_JD + ((double)ts / (double)USECS_PER_DAY);
|
||||
obs[i].data[0] = topo->azimuth;
|
||||
obs[i].data[1] = topo->elevation;
|
||||
obs[i].data[2] = topo->range_km;
|
||||
obs[i].observer_idx = oid;
|
||||
}
|
||||
|
||||
/* Configure solver */
|
||||
memset(&config, 0, sizeof(config));
|
||||
config.obs_type = OD_OBS_TOPO;
|
||||
config.fit_bstar = fit_bstar ? 1 : 0;
|
||||
config.max_iter = max_iter;
|
||||
config.observers = observers;
|
||||
config.n_observers = n_obs_sites;
|
||||
|
||||
if (has_seed)
|
||||
pg_tle_to_sat_code_od(seed_pg, &seed_sat);
|
||||
|
||||
memset(&result, 0, sizeof(result));
|
||||
|
||||
rc = od_fit_tle(obs, n_topo, has_seed ? &seed_sat : NULL, &config, &result);
|
||||
|
||||
pfree(obs);
|
||||
pfree(observers);
|
||||
|
||||
if (rc != 0)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_DATA_EXCEPTION),
|
||||
errmsg("TLE fitting failed: %s", result.status)));
|
||||
|
||||
/* Build result tuple */
|
||||
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("function returning record called in context "
|
||||
"that cannot accept type record")));
|
||||
tupdesc = BlessTupleDesc(tupdesc);
|
||||
|
||||
memset(nulls, 0, sizeof(nulls));
|
||||
|
||||
{
|
||||
pg_tle *fitted = (pg_tle *) palloc(sizeof(pg_tle));
|
||||
sat_code_to_pg_tle(&result.fitted_tle, fitted);
|
||||
values[0] = PointerGetDatum(fitted);
|
||||
}
|
||||
values[1] = Int32GetDatum(result.iterations);
|
||||
values[2] = Float8GetDatum(result.rms_final);
|
||||
values[3] = Float8GetDatum(result.rms_initial);
|
||||
values[4] = CStringGetTextDatum(result.status);
|
||||
values[5] = Float8GetDatum(result.condition_number);
|
||||
|
||||
if (result.cov_size > 0)
|
||||
{
|
||||
Datum *cov_datums = (Datum *) palloc(sizeof(Datum) * result.cov_size);
|
||||
int ci;
|
||||
for (ci = 0; ci < result.cov_size; ci++)
|
||||
cov_datums[ci] = Float8GetDatum(result.covariance[ci]);
|
||||
values[6] = PointerGetDatum(
|
||||
construct_array(cov_datums, result.cov_size,
|
||||
FLOAT8OID, sizeof(float8), FLOAT8PASSBYVAL,
|
||||
TYPALIGN_DOUBLE));
|
||||
}
|
||||
else
|
||||
{
|
||||
nulls[6] = true;
|
||||
}
|
||||
|
||||
values[7] = Int32GetDatum(result.cov_size > 0
|
||||
? (result.cov_size == 28 ? 7 : 6)
|
||||
: 0);
|
||||
|
||||
tuple = heap_form_tuple(tupdesc, values, nulls);
|
||||
PG_RETURN_DATUM(HeapTupleGetDatum(tuple));
|
||||
}
|
||||
|
||||
|
||||
/* ================================================================
|
||||
* tle_fit_residuals(tle, eci_position[], timestamptz[])
|
||||
* -> TABLE (t timestamptz, dx_km float8, dy_km float8, dz_km float8,
|
||||
* pos_err_km float8)
|
||||
*
|
||||
* Compute per-observation residuals between a TLE and ECI observations.
|
||||
* ================================================================
|
||||
*/
|
||||
Datum
|
||||
tle_fit_residuals(PG_FUNCTION_ARGS)
|
||||
{
|
||||
FuncCallContext *funcctx;
|
||||
|
||||
if (SRF_IS_FIRSTCALL())
|
||||
{
|
||||
MemoryContext oldctx;
|
||||
pg_tle *tle;
|
||||
ArrayType *pos_arr;
|
||||
ArrayType *time_arr;
|
||||
int n_pos, n_times;
|
||||
Datum *pos_datums;
|
||||
bool *pos_nulls;
|
||||
Datum *time_datums;
|
||||
bool *time_nulls;
|
||||
TupleDesc tupdesc;
|
||||
|
||||
/* Context for residual data (persists across calls) */
|
||||
typedef struct {
|
||||
tle_t sat;
|
||||
double *params;
|
||||
int is_deep;
|
||||
int n_obs;
|
||||
double *jds; /* Julian dates */
|
||||
double *obs_pos; /* observed positions (3 * n_obs) */
|
||||
} residual_ctx;
|
||||
|
||||
residual_ctx *ctx;
|
||||
int i;
|
||||
|
||||
funcctx = SRF_FIRSTCALL_INIT();
|
||||
oldctx = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
|
||||
|
||||
tle = (pg_tle *) PG_GETARG_POINTER(0);
|
||||
pos_arr = PG_GETARG_ARRAYTYPE_P(1);
|
||||
time_arr = PG_GETARG_ARRAYTYPE_P(2);
|
||||
|
||||
deconstruct_array(pos_arr, pos_arr->elemtype, sizeof(pg_eci), false,
|
||||
TYPALIGN_DOUBLE, &pos_datums, &pos_nulls, &n_pos);
|
||||
deconstruct_array(time_arr, TIMESTAMPTZOID, sizeof(int64), FLOAT8PASSBYVAL,
|
||||
TYPALIGN_DOUBLE, &time_datums, &time_nulls, &n_times);
|
||||
|
||||
if (n_pos != n_times)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
|
||||
errmsg("positions and times arrays must have same length")));
|
||||
|
||||
ctx = (residual_ctx *) palloc0(sizeof(residual_ctx));
|
||||
pg_tle_to_sat_code_od(tle, &ctx->sat);
|
||||
|
||||
ctx->is_deep = select_ephemeris(&ctx->sat);
|
||||
if (ctx->is_deep < 0)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_DATA_EXCEPTION),
|
||||
errmsg("invalid TLE for residual computation")));
|
||||
|
||||
ctx->params = (double *) palloc(sizeof(double) * N_SAT_PARAMS);
|
||||
if (ctx->is_deep)
|
||||
SDP4_init(ctx->params, &ctx->sat);
|
||||
else
|
||||
SGP4_init(ctx->params, &ctx->sat);
|
||||
|
||||
ctx->n_obs = n_pos;
|
||||
ctx->jds = (double *) palloc(sizeof(double) * n_pos);
|
||||
ctx->obs_pos = (double *) palloc(sizeof(double) * 3 * n_pos);
|
||||
|
||||
for (i = 0; i < n_pos; i++)
|
||||
{
|
||||
pg_eci *eci = (pg_eci *) DatumGetPointer(pos_datums[i]);
|
||||
int64 ts = DatumGetInt64(time_datums[i]);
|
||||
|
||||
ctx->jds[i] = PG_EPOCH_JD + ((double)ts / (double)USECS_PER_DAY);
|
||||
ctx->obs_pos[i * 3 + 0] = eci->x;
|
||||
ctx->obs_pos[i * 3 + 1] = eci->y;
|
||||
ctx->obs_pos[i * 3 + 2] = eci->z;
|
||||
}
|
||||
|
||||
funcctx->max_calls = n_pos;
|
||||
funcctx->user_fctx = ctx;
|
||||
|
||||
tupdesc = CreateTemplateTupleDesc(5);
|
||||
TupleDescInitEntry(tupdesc, 1, "t", TIMESTAMPTZOID, -1, 0);
|
||||
TupleDescInitEntry(tupdesc, 2, "dx_km", FLOAT8OID, -1, 0);
|
||||
TupleDescInitEntry(tupdesc, 3, "dy_km", FLOAT8OID, -1, 0);
|
||||
TupleDescInitEntry(tupdesc, 4, "dz_km", FLOAT8OID, -1, 0);
|
||||
TupleDescInitEntry(tupdesc, 5, "pos_err_km", FLOAT8OID, -1, 0);
|
||||
|
||||
funcctx->tuple_desc = BlessTupleDesc(tupdesc);
|
||||
|
||||
MemoryContextSwitchTo(oldctx);
|
||||
}
|
||||
|
||||
funcctx = SRF_PERCALL_SETUP();
|
||||
|
||||
if (funcctx->call_cntr < funcctx->max_calls)
|
||||
{
|
||||
typedef struct {
|
||||
tle_t sat;
|
||||
double *params;
|
||||
int is_deep;
|
||||
int n_obs;
|
||||
double *jds;
|
||||
double *obs_pos;
|
||||
} residual_ctx;
|
||||
|
||||
residual_ctx *ctx = (residual_ctx *) funcctx->user_fctx;
|
||||
int idx = funcctx->call_cntr;
|
||||
double tsince, pos[3], vel[3];
|
||||
double dx, dy, dz, pos_err;
|
||||
int err;
|
||||
int64 ts;
|
||||
Datum values[5];
|
||||
bool nulls[5];
|
||||
HeapTuple tuple;
|
||||
|
||||
tsince = (ctx->jds[idx] - ctx->sat.epoch) * 1440.0;
|
||||
|
||||
if (ctx->is_deep)
|
||||
err = SDP4(tsince, &ctx->sat, ctx->params, pos, vel);
|
||||
else
|
||||
err = SGP4(tsince, &ctx->sat, ctx->params, pos, vel);
|
||||
|
||||
dx = ctx->obs_pos[idx * 3 + 0] - pos[0];
|
||||
dy = ctx->obs_pos[idx * 3 + 1] - pos[1];
|
||||
dz = ctx->obs_pos[idx * 3 + 2] - pos[2];
|
||||
pos_err = sqrt(dx * dx + dy * dy + dz * dz);
|
||||
|
||||
ts = (int64)((ctx->jds[idx] - PG_EPOCH_JD) * (double)USECS_PER_DAY);
|
||||
|
||||
memset(nulls, 0, sizeof(nulls));
|
||||
values[0] = Int64GetDatum(ts);
|
||||
values[1] = Float8GetDatum(dx);
|
||||
values[2] = Float8GetDatum(dy);
|
||||
values[3] = Float8GetDatum(dz);
|
||||
values[4] = Float8GetDatum(pos_err);
|
||||
|
||||
(void)err;
|
||||
|
||||
tuple = heap_form_tuple(funcctx->tuple_desc, values, nulls);
|
||||
SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuple));
|
||||
}
|
||||
|
||||
SRF_RETURN_DONE(funcctx);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user