Ephemeriden mit Skyfield optimieren und dokumentieren

This commit is contained in:
2026-09-12 19:35:15 +02:00
parent 32d9c22c1b
commit 0357d957c0
3 changed files with 160 additions and 15 deletions
+79
View File
@@ -0,0 +1,79 @@
# Berechnungsgrundlagen
Stand: 12.09.2026
Diese Datei dokumentiert, welche Berechnungsbibliothek beziehungsweise welche
Datenquelle die einzelnen Seiten verwenden.
## Die drei wichtigsten Grundlagen
### Astronomy Engine
Die lokale Python-Version liegt in `public/py/astronomy.py`. Im Browser wird
`public/js/astronomy.browser.min.js` verwendet.
Astronomy Engine liefert auf vielen Seiten die Positionen von Sonne, Mond und
Planeten sowie Ereigniszeiten wie Mondphasen, Finsternisse, Bedeckungen und
Konjunktionen.
### Skyfield mit `de421.bsp`
Skyfield liegt lokal in `public/py/vendor` beziehungsweise
`public/py/vendor_linux`. Die verwendete JPL-Ephemeride ist:
`public/py/vendor_data/de421.bsp`
Die zentrale Einrichtung erfolgt in `public/py/astronomical_conversions.py`.
`de421.bsp` wird derzeit für die Planet-Ephemeriden in `ephemeriden_api.py`
und für die Dämmerungsgrafik verwendet.
Die Datei enthält Jupiter bis Neptun als Systemschwerpunkte. Die lokale
`jup365`-Datei wird von den hier dokumentierten Seiten derzeit nicht verwendet.
### Weitere Berechnungen
Nicht jede Berechnung ist Skyfield oder Astronomy Engine:
- Satellitenbahnen werden mit SGP4 aus TLE-Daten berechnet.
- Kometen und Kleinplaneten werden derzeit aus ihren Bahnelementen mit eigenen
Kepler-Berechnungen bestimmt.
## Seitenübersicht
| Seite | Berechnungsgrundlage |
|---|---|
| `ephemeriden.php` | **Skyfield + `de421.bsp`** für Sonne, Mond und Planeten. Auf- und Untergang sowie Horizontkoordinaten werden ebenfalls mit Skyfield berechnet. Kometen und Kleinplaneten bleiben bei den eigenen Bahnelement-Berechnungen. Die interaktive Karte nutzt im Browser zusätzlich Astronomy Engine für Horizontberechnungen und Sterne. |
| `monatsvorhersage.php` | Überwiegend **Astronomy Engine** über `public/py/api.py`. Satellitenereignisse verwenden SGP4. Die Dämmerungsgrafik verwendet Skyfield + `de421.bsp`. Jupiter- und Jupitermond-Ereignisse verwenden aktuell Astronomy Engine. |
| `wochenvorhersage.php` | **Astronomy Engine** über `public/py/api.py`; je nach Bereich zusätzlich SGP4 für Satelliten. |
| `jupitersystem.php` | **Astronomy Engine**. Die Darstellung im Browser und die Galileischen Monde werden über Astronomy Engine berechnet. `jup365.bsp` wird hier aktuell nicht geladen. |
| `sonnenfinsternis.php` | **Astronomy Engine**, einschließlich lokaler Berechnung für den Standardstandort. |
| `mondfinsternis.php` | **Astronomy Engine**. Der Zeitpunkt der Finsternis ist global; eine lokale Sichtbarkeitsbewertung ist davon getrennt. |
| `sternbedeckungen.php` | **Astronomy Engine** über die lokale Python-API. Topozentrische Koordinaten und der Standardstandort werden berücksichtigt. |
| `moonphase.php` | **Astronomy Engine** im Browser beziehungsweise über die lokale Python-API. |
| `moonyear.php` | **Astronomy Engine** für Mondphasen und Mondpositionen. |
| `satellitenhimmel.php` | **SGP4** für Satellitenbahnen; Astronomy Engine für astronomischen Kontext wie Sonne, Mond und Dämmerung. |
| `drehbare_sternkarte.php` | **Astronomy Engine** im Browser für Horizont- und Koordinatenberechnungen. |
| `sternenhimmel.php` | **Astronomy Engine** im Browser beziehungsweise über die lokale API. |
| `tagbogen.php` | **Astronomy Engine** für Sonnen-, Mond- und Planetenbahnen. |
| `solarsystem.php` | **Astronomy Engine** im Browser für das Sonnensystemmodell. |
| `geocron3d.php` | **Astronomy Engine** im Browser. |
| `geocron/index.php` | **Astronomy Engine** über das Geocron-Plugin. |
## Zentrale Python-Dateien
| Datei | Aufgabe |
|---|---|
| `public/py/api.py` | Allgemeine astronomische Ereignisse; überwiegend Astronomy Engine. Einzelne Dämmerungsberechnungen verwenden Skyfield. |
| `public/py/ephemeriden_api.py` | Planet-Ephemeriden mit Skyfield + `de421.bsp`; Kleinplaneten und Kometen mit eigenen Bahnelement-Berechnungen. |
| `public/py/astronomical_conversions.py` | Gemeinsame Skyfield-Konfiguration und Zugriff auf `de421.bsp`. |
| `public/py/astronomy.py` | Lokale Python-Version der Astronomy Engine. |
## Standort und Lichtlaufzeit
Wo eine Seite einen Standort benötigt, wird grundsätzlich der persönliche
Standardstandort des angemeldeten Benutzers verwendet. Fehlt dieser, greift
der konfigurierte Fallback-Standort.
Die Verwendung von Skyfield oder Astronomy Engine sagt allein noch nicht, ob
eine Berechnung topozentrisch, geozentrisch oder mit Lichtlaufzeit erfolgt.
Das ist je Ereignis in der jeweiligen Funktion festgelegt.
+1
View File
@@ -819,6 +819,7 @@ require __DIR__ . '/header.php';
<input type="hidden" name="range_value" value="<?= ephemeriden_h($rangeValueInput) ?>">
<input type="hidden" name="range_unit" value="<?= ephemeriden_h($selectedRangeUnit) ?>">
<input type="hidden" name="start_dt" value="<?= ephemeriden_h($startDtInput) ?>">
<input type="hidden" name="end_dt" value="<?= ephemeriden_h($endDtInput) ?>">
<input type="hidden" name="export_format" value="csv">
<button type="submit" class="btn btn-secondary">Export CSV</button>
</form>
+80 -15
View File
@@ -12,6 +12,8 @@ if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
import astronomy
from skyfield import almanac as skyfield_almanac
from astronomical_conversions import skyfield_context, skyfield_wgs84
EPHEMERIS_BODIES = {
@@ -26,6 +28,21 @@ EPHEMERIS_BODIES = {
"Neptune": ("Neptun", astronomy.Body.Neptune),
}
# DE421 enthält die äußeren Planeten als Systemschwerpunkte, nicht als
# separate Planetenzentren. Für Ephemeriden ist das die vorgesehene
# Zielkörperdefinition dieser Datei.
SKYFIELD_EPHEMERIS_TARGETS = {
"Sun": "sun",
"Moon": "moon",
"Mercury": "mercury",
"Venus": "venus",
"Mars": "mars",
"Jupiter": 5,
"Saturn": 6,
"Uranus": 7,
"Neptune": 8,
}
J2000_OBLIQUITY_DEG = 23.439279444444445
GAUSSIAN_GRAVITATIONAL_CONSTANT = 0.01720209895
PARABOLIC_ECCENTRICITY_TOLERANCE = 1.0e-6
@@ -118,6 +135,45 @@ def search_event(
return result
def skyfield_observer(latitude: float, longitude: float, elevation: float):
_timescale, planets = skyfield_context()
return planets["earth"] + skyfield_wgs84.latlon(latitude, longitude, elevation_m=elevation)
def skyfield_planet_state(body_name: str, observer, dt_utc: datetime, timescale=None, planets=None):
if timescale is None or planets is None:
timescale, planets = skyfield_context()
time_value = timescale.from_datetime(dt_utc.astimezone(timezone.utc))
apparent = observer.at(time_value).observe(planets[SKYFIELD_EPHEMERIS_TARGETS[body_name]]).apparent()
ra, dec, _distance = apparent.radec(epoch=timescale.J2000)
altitude, azimuth, _distance = apparent.altaz("standard")
return float(ra.hours), float(dec.degrees), float(azimuth.degrees), float(altitude.degrees)
def search_skyfield_rise_set(observer, body_name: str, local_day_start: datetime, timescale, planets) -> tuple[datetime | None, datetime | None]:
"""Sucht Auf- und Untergang gemeinsam über Skyfields diskrete Ereignissuche."""
target = planets[SKYFIELD_EPHEMERIS_TARGETS[body_name]]
event_function = skyfield_almanac.risings_and_settings(
planets,
target,
observer - planets["earth"],
)
local_day_end = local_day_start + timedelta(days=1)
start_time = timescale.from_datetime(local_day_start.astimezone(timezone.utc))
end_time = timescale.from_datetime(local_day_end.astimezone(timezone.utc))
event_times, event_states = skyfield_almanac.find_discrete(start_time, end_time, event_function)
rise = None
set_ = None
for event_time, state in zip(event_times.utc_datetime(), event_states):
event_dt = event_time.astimezone(timezone.utc)
if bool(state) and rise is None:
rise = event_dt
elif not bool(state) and set_ is None:
set_ = event_dt
return rise, set_
def format_ra_hours(ra_hours: float) -> str:
total_seconds = int(round(float(ra_hours) * 3600.0))
total_seconds %= 24 * 3600
@@ -522,21 +578,28 @@ def action_planet_ephemeris(args: list[str]) -> dict:
label, body = EPHEMERIS_BODIES[body_name]
observer = astronomy.Observer(latitude, longitude, elevation)
skyfield_timescale, skyfield_planets = skyfield_context()
skyfield_topos = skyfield_observer(latitude, longitude, elevation)
def row_builder(current_local: datetime, time_value: astronomy.Time, rise_label: str | None, set_label: str | None) -> dict:
eq = astronomy.Equator(body, time_value, observer, False, True)
horizontal = astronomy.Horizon(time_value, observer, float(eq.ra), float(eq.dec), astronomy.Refraction.Normal)
ra_hours, dec_deg, azimuth_deg, altitude_deg = skyfield_planet_state(
body_name,
skyfield_topos,
current_local.astimezone(timezone.utc),
skyfield_timescale,
skyfield_planets,
)
return {
"object_name": label,
"date_local": current_local.strftime("%d.%m.%Y"),
"time_local": current_local.strftime("%H:%M"),
"local_iso": current_local.isoformat(),
"ra": format_ra_hours(float(eq.ra)),
"ra_decimal_hours": round(float(eq.ra), 8),
"dec": format_dec_deg(float(eq.dec)),
"dec_decimal_deg": round(float(eq.dec), 8),
"azimuth_deg": round(float(horizontal.azimuth), 6),
"altitude_deg": round(float(horizontal.altitude), 6),
"ra": format_ra_hours(ra_hours),
"ra_decimal_hours": round(ra_hours, 8),
"dec": format_dec_deg(dec_deg),
"dec_decimal_deg": round(dec_deg, 8),
"azimuth_deg": round(azimuth_deg, 6),
"altitude_deg": round(altitude_deg, 6),
"rise": rise_label,
"set": set_label,
}
@@ -547,13 +610,15 @@ def action_planet_ephemeris(args: list[str]) -> dict:
except Exception as exc:
fail("Zeitzone ist ungueltig.", extra={"details": str(exc)})
local_day_end = local_day_start + timedelta(days=1)
day_start_time = dt_to_time(local_day_start.astimezone(timezone.utc))
day_end_utc = local_day_end.astimezone(timezone.utc)
rise = search_event(body, astronomy.Direction.Rise, observer, day_start_time, day_end_utc)
set_ = search_event(body, astronomy.Direction.Set, observer, day_start_time, day_end_utc)
rise_label = serialize_event("Aufgang", rise, tz).get("local_time") if rise is not None else None
set_label = serialize_event("Untergang", set_, tz).get("local_time") if set_ is not None else None
rise, set_ = search_skyfield_rise_set(
skyfield_topos,
body_name,
local_day_start,
skyfield_timescale,
skyfield_planets,
)
rise_label = rise.astimezone(tz).strftime("%H:%M") if rise is not None else None
set_label = set_.astimezone(tz).strftime("%H:%M") if set_ is not None else None
return rise_label, set_label
return build_rows_response(