From 0357d957c0019ee92f5ed089449dbb8979b2d0ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sat, 12 Sep 2026 19:35:15 +0200 Subject: [PATCH] Ephemeriden mit Skyfield optimieren und dokumentieren --- BERECHNUNGSGRUNDLAGEN.md | 79 ++++++++++++++++++++++++++++++ public/ephemeriden.php | 1 + public/py/ephemeriden_api.py | 95 ++++++++++++++++++++++++++++++------ 3 files changed, 160 insertions(+), 15 deletions(-) create mode 100644 BERECHNUNGSGRUNDLAGEN.md diff --git a/BERECHNUNGSGRUNDLAGEN.md b/BERECHNUNGSGRUNDLAGEN.md new file mode 100644 index 0000000..f9e11de --- /dev/null +++ b/BERECHNUNGSGRUNDLAGEN.md @@ -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. diff --git a/public/ephemeriden.php b/public/ephemeriden.php index de83a54..c9a8c70 100644 --- a/public/ephemeriden.php +++ b/public/ephemeriden.php @@ -819,6 +819,7 @@ require __DIR__ . '/header.php'; + diff --git a/public/py/ephemeriden_api.py b/public/py/ephemeriden_api.py index e34d287..a09acc6 100644 --- a/public/py/ephemeriden_api.py +++ b/public/py/ephemeriden_api.py @@ -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(