Understand the caelundas ephemeris calculation pipeline - NASA JPL API integration, astronomical event detection, and calendar generation. Use this skill when working on caelundas.
This skill covers the caelundas astronomical calendar generation pipeline, including NASA JPL Horizons API integration, ephemeris caching, event detection, and output formatting.
Caelundas generates astronomical calendars by:
For comprehensive architecture details, see applications/caelundas/AGENTS.md.
┌─────────────────┐
│ Configuration │ (dates, location, output format)
└────────┬────────┘
│
v
┌─────────────────┐
│ Fetch Data │ NASA JPL Horizons API
│ ├─ Planets │ (Sun, Moon, Mercury...Pluto)
│ ├─ Lunar Node │
│ └─ Chiron │
└────────┬────────┘
│
v
┌─────────────────┐
│ SQLite Cache │ (ephemeris + active aspects)
│ ├─ Check cache │
│ ├─ Store new │
│ └─ Query │
└────────┬────────┘
│
v
┌─────────────────┐
│ Event Detection │
│ ├─ Aspects │ (conjunctions, oppositions, etc.)
│ ├─ Phases │ (new moon, full moon, quarters)
│ ├─ Stelliums │ (3+ planets close together)
│ ├─ Eclipses │
│ └─ Retrogrades │
└────────┬────────┘
│
v
┌─────────────────┐
│ Output Format │
│ ├─ iCalendar │ (.ics)
│ └─ JSON │
└─────────────────┘
Caelundas uses NASA's JPL Horizons system for high-precision ephemeris data:
// Fetch positions for a planet over date range
const response = await fetch("https://ssd.jpl.nasa.gov/api/horizons.api", {
method: "GET",
params: {
COMMAND: "10", // Body ID (Mercury)
EPHEM_TYPE: "OBSERVER",
CENTER: "coord@399", // Geocentric
START_TIME: "2024-01-01",
STOP_TIME: "2024-12-31",
STEP_SIZE: "1d", // Daily positions
QUANTITIES: "1,3", // RA/Dec and distance
CSV_FORMAT: "YES",
},
});
JPL returns CSV with columns:
Date, Julian Day, RA (deg), Dec (deg), Distance (AU)
2024-01-01, 2460310.5, 280.5, -23.0, 0.983
Caelundas parses and converts to internal format with:
API is rate-limited. Caelundas implements:
ephemerides table:
CREATE TABLE ephemerides (
id INTEGER PRIMARY KEY,
body TEXT NOT NULL, -- 'Sun', 'Moon', 'Mercury', etc.
date TEXT NOT NULL, -- ISO date
longitude REAL NOT NULL, -- Ecliptic longitude (0-360)
latitude REAL, -- Ecliptic latitude
distance REAL, -- Distance from Earth (AU)
speed REAL, -- Daily motion (degrees/day)
UNIQUE(body, date)
);
active_aspects table:
CREATE TABLE active_aspects (
id INTEGER PRIMARY KEY,
body1 TEXT NOT NULL,
body2 TEXT NOT NULL,
aspect_type TEXT NOT NULL, -- 'conjunction', 'opposition', etc.
exact_time TEXT NOT NULL, -- ISO timestamp
orb REAL NOT NULL, -- Degrees from exact
applying BOOLEAN NOT NULL, -- Approaching (true) or separating (false)
start_date TEXT NOT NULL,
end_date TEXT NOT NULL
);
Check cache before API request:
const cached = await db.get(
"SELECT * FROM ephemerides WHERE body = ? AND date = ?",
[body, date],
);
if (cached) return cached;
Fetch from API if not cached:
const data = await fetchFromJPL(body, date)
await db.run(
'INSERT INTO ephemerides (body, date, longitude, ...) VALUES (?, ?, ?, ...)',
[body, date, data.longitude, ...]
)
Query cached data for event detection:
const positions = await db.all(
"SELECT * FROM ephemerides WHERE date BETWEEN ? AND ? ORDER BY date",
[startDate, endDate],
);
Major aspects (exact angles between planets):
Minor aspects:
Specialty aspects:
Orbs define how close to exact an aspect must be:
const MAJOR_ORB = 8; // ±8° for major aspects
const MINOR_ORB = 3; // ±3° for minor aspects
Example: Mars at 45° and Jupiter at 50° form a semi-square (45° aspect) with 5° orb.
For each date in range:
Special case for Moon phases (Sun-Moon angles):
A stellium is 3+ planets within 10° in the same zodiac sign:
// Group planets by sign
const planetsBySign = positions.reduce((acc, planet) => {
const sign = getZodiacSign(planet.longitude);
acc[sign] = acc[sign] || [];
acc[sign].push(planet);
return acc;
}, {});
// Find stelliums
const stelliums = Object.entries(planetsBySign)
.filter(([sign, planets]) => planets.length >= 3)
.map(([sign, planets]) => ({ sign, planets }));
Solar and lunar eclipses occur when:
Solar Eclipse (Sun-Moon conjunction):
Lunar Eclipse (Sun-Moon opposition):
A planet is retrograde when its speed (daily motion) is negative:
const isRetrograde = planet.speed < 0;
Retrograde periods are when a planet appears to move backward in the sky from Earth's perspective.
Standard calendar format compatible with Google Calendar, Apple Calendar, Outlook, etc.
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//caelundas//astronomical-calendar//EN
BEGIN:VEVENT
UID:mars-square-saturn-2024-01-15@caelundas
DTSTART:20240115T143000Z
SUMMARY:Mars square Saturn
DESCRIPTION:Exact aspect at 14:30 UTC
LOCATION:Geocentric
END:VEVENT
END:VCALENDAR
Event properties:
Structured data for programmatic access:
{
"events": [
{
"type": "aspect",
"aspect": "square",
"body1": "Mars",
"body2": "Saturn",
"exactTime": "2024-01-15T14:30:00Z",
"orb": 0.5,
"applying": false,
"longitude1": 45.5,
"longitude2": 135.5
}
]
}
More flexible for custom processing and analysis.
# Date range
START_DATE=2024-01-01
END_DATE=2024-12-31
# Location (for local coordinates)
LATITUDE=40.7128
LONGITUDE=-74.0060
TIMEZONE=America/New_York
# Output format
OUTPUT_FORMAT=ical # or 'json' or 'both'
OUTPUT_PATH=/app/output/calendar.ics
# Event types to include
INCLUDE_MAJOR_ASPECTS=true
INCLUDE_MINOR_ASPECTS=true
INCLUDE_PHASES=true
INCLUDE_ECLIPSES=true
INCLUDE_RETROGRADES=true
INCLUDE_STELLIUMS=true
Override default orbs in configuration:
const customOrbs = {
conjunction: 10,
opposition: 10,
trine: 8,
square: 8,
sextile: 6,
// ...minor aspects
};
For full year calendar (365 days, 11 bodies):
Unit tests (*.unit.test.ts):
Integration tests (*.integration.test.ts):
End-to-end tests (*.end-to-end.test.ts):
nx run caelundas:develop
rm applications/caelundas/ephemeris.db
# Enable debug logging
DEBUG=nasa-api nx run caelundas:develop
# Check iCal syntax
icalendar-validate output/calendar.ics
# Parse JSON
jq . output/calendar.json