Mondbedeckungen von Planeten als Kontakte ausgeben

This commit is contained in:
2026-09-12 10:40:35 +02:00
parent dd430bcfd1
commit b5c7248a64
2 changed files with 137 additions and 3 deletions
+16 -2
View File
@@ -1022,6 +1022,19 @@ if (!function_exists('monthForecastBuildNarrativeText')) {
$sepStr = ' in nur ' . $sepMatch[1] . ' Abstand'; $sepStr = ' in nur ' . $sepMatch[1] . ' Abstand';
} }
$apSd = $shortDate($apDate !== '' ? $apDate : $date); $apSd = $shortDate($apDate !== '' ? $apDate : $date);
$eventKind = (string) ($ap['event_kind'] ?? '');
if ($eventKind !== '' && str_starts_with($apLabel, 'Mond bedeckt ')) {
$target = substr($apLabel, strlen('Mond bedeckt '));
$apTime = trim((string) ($ap['time'] ?? ''));
$timeClause = $apTime !== '' ? ' um ' . $apTime . ' Uhr' : '';
$moonSentences[] = match ($eventKind) {
'start' => 'Am ' . $apSd . $timeClause . ' beginnt die Bedeckung von ' . $target . ' durch den Mond.',
'maximum' => 'Am ' . $apSd . $timeClause . ' erreicht die Bedeckung von ' . $target . ' durch den Mond ihr Maximum.',
'end' => 'Am ' . $apSd . $timeClause . ' endet die Bedeckung von ' . $target . ' durch den Mond.',
default => 'Am ' . $apSd . $timeClause . ' bedeckt der Mond ' . $target . '.',
};
continue;
}
if (str_starts_with($apLabel, 'Mond nahe ')) { if (str_starts_with($apLabel, 'Mond nahe ')) {
$target = substr($apLabel, strlen('Mond nahe ')); $target = substr($apLabel, strlen('Mond nahe '));
$moonSentences[] = 'Am ' . $apSd . ' zieht der Mond' . $sepStr . ' an ' . $target . ' vorbei.'; $moonSentences[] = 'Am ' . $apSd . ' zieht der Mond' . $sepStr . ' an ' . $target . ' vorbei.';
@@ -2451,7 +2464,8 @@ foreach ($moonPlanetApproaches as $approachEvent) {
continue; continue;
} }
$distanceText = isset($approachEvent['separation_deg']) $eventKind = (string) ($approachEvent['event_kind'] ?? '');
$distanceText = $eventKind === '' && isset($approachEvent['separation_deg'])
? number_format((float) $approachEvent['separation_deg'], 2, ',', '') . ' Grad' ? number_format((float) $approachEvent['separation_deg'], 2, ',', '') . ' Grad'
: ''; : '';
@@ -2460,7 +2474,7 @@ foreach ($moonPlanetApproaches as $approachEvent) {
'date' => (string) ($approachEvent['local_date'] ?? ''), 'date' => (string) ($approachEvent['local_date'] ?? ''),
'time' => (string) ($approachEvent['local_time'] ?? ''), 'time' => (string) ($approachEvent['local_time'] ?? ''),
'local_iso' => (string) ($approachEvent['local_iso'] ?? ''), 'local_iso' => (string) ($approachEvent['local_iso'] ?? ''),
'type' => 'moon_planet', 'type' => $eventKind !== '' ? 'moon_occultation' : 'moon_planet',
]; ];
} }
+121 -1
View File
@@ -18,6 +18,15 @@ from skyfield import almanac
MOON_RADIUS_KM = 1737.4 MOON_RADIUS_KM = 1737.4
SYNODIC_MONTH = 29.530588853 SYNODIC_MONTH = 29.530588853
PLANET_RADIUS_KM = {
astronomy.Body.Mercury: 2439.7,
astronomy.Body.Venus: 6051.8,
astronomy.Body.Mars: 3396.2,
astronomy.Body.Jupiter: astronomy.JUPITER_EQUATORIAL_RADIUS_KM,
astronomy.Body.Saturn: 60268.0,
astronomy.Body.Uranus: 25559.0,
astronomy.Body.Neptune: 24764.0,
}
STAR_BODIES = [ STAR_BODIES = [
astronomy.Body.Star1, astronomy.Body.Star1,
astronomy.Body.Star2, astronomy.Body.Star2,
@@ -67,6 +76,30 @@ def dt_to_time(dt_utc: datetime) -> astronomy.Time:
) )
def jupiter_emission_datetime(observation_dt: datetime) -> datetime:
"""Berechnet den Emissionszeitpunkt für eine Beobachtung von der Erde."""
observation_dt = observation_dt.astimezone(timezone.utc)
emission_dt = observation_dt
# Die Jupiterentfernung wird am zunächst geschätzten Emissionszeitpunkt
# neu bestimmt. Zwei Durchläufe reichen für die Lichtlaufzeit im
# Jupiter-System deutlich aus.
for _ in range(2):
geo = astronomy.GeoVector(
astronomy.Body.Jupiter,
dt_to_time(emission_dt),
True,
)
distance_au = math.sqrt(
(float(geo.x) * float(geo.x))
+ (float(geo.y) * float(geo.y))
+ (float(geo.z) * float(geo.z))
)
emission_dt = observation_dt - timedelta(days=distance_au / astronomy.C_AUDAY)
return emission_dt
def time_to_datetime(time_value: astronomy.Time) -> datetime: def time_to_datetime(time_value: astronomy.Time) -> datetime:
year, month, day, hour, minute, second = time_value.Calendar() year, month, day, hour, minute, second = time_value.Calendar()
second_int = int(second) second_int = int(second)
@@ -2874,6 +2907,65 @@ def moon_planet_separation_deg(
return spherical_separation_deg(float(moon_eq.ra), float(moon_eq.dec), float(body_eq.ra), float(body_eq.dec)) return spherical_separation_deg(float(moon_eq.ra), float(moon_eq.dec), float(body_eq.ra), float(body_eq.dec))
def moon_planet_occultation_margin_deg(
body: astronomy.Body,
observer: astronomy.Observer,
dt_utc: datetime,
) -> float:
"""Negativ bedeutet Überlappung der scheinbaren Mond- und Planetenscheiben."""
time_value = dt_to_time(dt_utc)
moon_eq = astronomy.Equator(astronomy.Body.Moon, time_value, observer, True, True)
body_eq = astronomy.Equator(body, time_value, observer, True, True)
moon_radius = moon_angular_radius_deg(float(moon_eq.dist))
planet_radius_km = PLANET_RADIUS_KM[body]
planet_ratio = planet_radius_km / (float(body_eq.dist) * astronomy.KM_PER_AU)
planet_radius = math.degrees(math.asin(max(-1.0, min(1.0, planet_ratio))))
separation = spherical_separation_deg(
float(moon_eq.ra), float(moon_eq.dec), float(body_eq.ra), float(body_eq.dec)
)
return separation - moon_radius - planet_radius
def refine_occultation_contact(
body: astronomy.Body,
observer: astronomy.Observer,
center_utc: datetime,
direction: int,
) -> datetime | None:
"""Findet den Scheibenkontakt vor oder nach der größten Bedeckung."""
step = timedelta(minutes=5)
inner = center_utc
inner_margin = moon_planet_occultation_margin_deg(body, observer, inner)
if inner_margin > 0.0:
return None
outer = inner
for _ in range(288): # maximal 24 Stunden vom Maximum entfernt suchen
outer = outer + (step if direction > 0 else -step)
outer_margin = moon_planet_occultation_margin_deg(body, observer, outer)
if outer_margin >= 0.0:
left, right = (inner, outer) if direction > 0 else (outer, inner)
for _ in range(40):
middle = left + (right - left) / 2
middle_margin = moon_planet_occultation_margin_deg(body, observer, middle)
if direction > 0:
if middle_margin < 0.0:
left = middle
else:
right = middle
else:
if middle_margin < 0.0:
right = middle
else:
left = middle
return right if direction > 0 else left
inner = outer
inner_margin = outer_margin
return None
def moon_fixed_equatorial_separation_deg( def moon_fixed_equatorial_separation_deg(
ra_hours: float, ra_hours: float,
dec_deg: float, dec_deg: float,
@@ -3291,6 +3383,30 @@ def action_moon_planet_approaches(args: list[str]) -> dict:
continue continue
seen_ranges.append((left, right)) seen_ranges.append((left, right))
occultation_margin = moon_planet_occultation_margin_deg(body, observer, min_time_utc)
if occultation_margin <= 0.0:
contact_times = [
("start", refine_occultation_contact(body, observer, min_time_utc, -1), "Beginn der Bedeckung"),
("maximum", min_time_utc, "Größte Bedeckung"),
("end", refine_occultation_contact(body, observer, min_time_utc, 1), "Ende der Bedeckung"),
]
if all(contact_time is not None for _, contact_time, _ in contact_times):
for event_kind, contact_time, event_label in contact_times:
assert contact_time is not None
contact_local = contact_time.astimezone(tz)
approaches.append({
"planet_key": key,
"planet_label": label,
"label": f"Mond bedeckt {label} – {event_label}",
"event_kind": event_kind,
"separation_deg": float(min_sep),
"utc_iso": contact_time.isoformat().replace("+00:00", "Z"),
"local_iso": contact_local.isoformat(),
"local_date": contact_local.strftime("%d.%m.%Y"),
"local_time": contact_local.strftime("%H:%M"),
})
continue
approaches.append({ approaches.append({
"planet_key": key, "planet_key": key,
"planet_label": label, "planet_label": label,
@@ -4644,7 +4760,11 @@ def action_jupiter_moons_one_side_for_month(args: list[str]) -> dict:
moon_keys = ["io", "europa", "ganymede", "callisto"] moon_keys = ["io", "europa", "ganymede", "callisto"]
def classify_side(dt_utc: datetime) -> tuple[str | None, str | None, dict[str, float], dict[str, float]]: def classify_side(dt_utc: datetime) -> tuple[str | None, str | None, dict[str, float], dict[str, float]]:
time_value = dt_to_time(dt_utc) # Die ausgegebenen Ereigniszeiten bleiben Beobachtungszeiten in UTC.
# Für die tatsächliche Stellung der Monde rechnen wir auf den
# Emissionszeitpunkt des Jupiter-Lichts zurück.
emission_dt = jupiter_emission_datetime(dt_utc)
time_value = dt_to_time(emission_dt)
rotation_eqj_to_ecl = astronomy.Rotation_EQJ_ECL() rotation_eqj_to_ecl = astronomy.Rotation_EQJ_ECL()
jupiter_geo = astronomy.GeoVector(astronomy.Body.Jupiter, time_value, True) jupiter_geo = astronomy.GeoVector(astronomy.Body.Jupiter, time_value, True)
jupiter_helio = astronomy.HelioVector(astronomy.Body.Jupiter, time_value) jupiter_helio = astronomy.HelioVector(astronomy.Body.Jupiter, time_value)