Gute Merkursichtbarkeit rein

This commit is contained in:
2026-04-04 10:58:20 +02:00
parent d147c9cda8
commit ad73f1e5b3
5 changed files with 954 additions and 6 deletions
+307 -2
View File
@@ -262,6 +262,17 @@ def action_planet_rise_set(args: list[str]) -> dict:
}
def body_altitude_deg(
body: astronomy.Body,
observer: astronomy.Observer,
dt_utc: datetime,
) -> float:
time_value = dt_to_time(dt_utc)
eq = astronomy.Equator(body, time_value, observer, True, True)
hor = astronomy.Horizon(time_value, observer, eq.ra, eq.dec, astronomy.Refraction.Normal)
return float(hor.altitude)
def serialize_body_position(
key: str,
label: str,
@@ -293,6 +304,148 @@ def serialize_body_position(
return payload
def action_mercury_good_visibility_for_month(args: list[str]) -> dict:
if len(args) != 10:
fail(
"Aktion mercury_good_visibility_for_month erwartet 10 Argumente: latitude longitude elevation year month timezone sample_offset_minutes min_mercury_alt_deg max_sun_alt_deg min_consecutive_days",
extra={"argv": args},
)
latitude = parse_float(args[0], "Latitude")
longitude = parse_float(args[1], "Longitude")
elevation = parse_float(args[2], "Elevation")
try:
year = int(args[3])
month = int(args[4])
sample_offset_minutes = int(args[6])
min_consecutive_days = int(args[9])
except ValueError as exc:
fail("Jahr, Monat oder Sichtbarkeitsparameter sind ungueltig.", extra={"details": str(exc), "argv": args})
if month < 1 or month > 12:
fail("Monat muss zwischen 1 und 12 liegen.", extra={"month": month})
timezone_name = args[5]
min_mercury_alt_deg = parse_float(args[7], "Minimale Merkurhoehe")
max_sun_alt_deg = parse_float(args[8], "Maximale Sonnenhoehe")
try:
tz = ZoneInfo(timezone_name)
except Exception as exc:
fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)})
observer = astronomy.Observer(latitude, longitude, elevation)
local_start = datetime(year, month, 1, 0, 0, 0, tzinfo=tz)
if month == 12:
local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz)
else:
local_end = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=tz)
daily_checks: list[dict] = []
current_local = local_start
while current_local < local_end:
day_start_utc = current_local.astimezone(timezone.utc)
next_day_utc = (current_local + timedelta(days=1)).astimezone(timezone.utc)
start_time = dt_to_time(day_start_utc)
sunrise = search_event(astronomy.Body.Sun, astronomy.Direction.Rise, observer, start_time, next_day_utc)
sunset = search_event(astronomy.Body.Sun, astronomy.Direction.Set, observer, start_time, next_day_utc)
morning_ok = False
morning_dt_local = None
morning_mercury_alt = None
morning_sun_alt = None
if sunrise is not None:
sunrise_local = time_to_datetime(sunrise).astimezone(tz)
morning_dt_local = sunrise_local - timedelta(minutes=sample_offset_minutes)
morning_dt_utc = morning_dt_local.astimezone(timezone.utc)
morning_mercury_alt = body_altitude_deg(astronomy.Body.Mercury, observer, morning_dt_utc)
morning_sun_alt = body_altitude_deg(astronomy.Body.Sun, observer, morning_dt_utc)
morning_ok = morning_mercury_alt >= min_mercury_alt_deg and morning_sun_alt <= max_sun_alt_deg
evening_ok = False
evening_dt_local = None
evening_mercury_alt = None
evening_sun_alt = None
if sunset is not None:
sunset_local = time_to_datetime(sunset).astimezone(tz)
evening_dt_local = sunset_local + timedelta(minutes=sample_offset_minutes)
evening_dt_utc = evening_dt_local.astimezone(timezone.utc)
evening_mercury_alt = body_altitude_deg(astronomy.Body.Mercury, observer, evening_dt_utc)
evening_sun_alt = body_altitude_deg(astronomy.Body.Sun, observer, evening_dt_utc)
evening_ok = evening_mercury_alt >= min_mercury_alt_deg and evening_sun_alt <= max_sun_alt_deg
daily_checks.append({
"date": current_local.strftime("%d.%m.%Y"),
"morning_ok": morning_ok,
"morning_local": morning_dt_local,
"morning_mercury_alt_deg": morning_mercury_alt,
"morning_sun_alt_deg": morning_sun_alt,
"evening_ok": evening_ok,
"evening_local": evening_dt_local,
"evening_mercury_alt_deg": evening_mercury_alt,
"evening_sun_alt_deg": evening_sun_alt,
})
current_local += timedelta(days=1)
def build_windows(period_key: str, label_text: str) -> list[dict]:
events = []
start_index = None
for index, day in enumerate(daily_checks):
is_ok = bool(day[f"{period_key}_ok"])
if is_ok and start_index is None:
start_index = index
is_last = index == len(daily_checks) - 1
if start_index is not None and (not is_ok or is_last):
end_index = index if (is_ok and is_last) else index - 1
duration_days = end_index - start_index + 1
if duration_days >= min_consecutive_days:
start_day = daily_checks[start_index]
end_day = daily_checks[end_index]
start_local = start_day[f"{period_key}_local"]
if start_local is not None:
range_text = (
start_day["date"]
if start_index == end_index
else f'{start_day["date"]} bis {end_day["date"]}'
)
events.append({
"period": period_key,
"label": f"Gute Merkur-Sichtbarkeit am {label_text} ({range_text})",
"utc_iso": start_local.astimezone(timezone.utc).isoformat().replace("+00:00", "Z"),
"local_iso": start_local.isoformat(),
"local_date": start_local.strftime("%d.%m.%Y"),
"local_time": start_local.strftime("%H:%M"),
"end_local_date": end_day["date"],
"duration_days": duration_days,
})
start_index = None
return events
events = build_windows("morning", "Morgen")
events.extend(build_windows("evening", "Abend"))
events.sort(key=lambda item: item["local_iso"])
return {
"ok": True,
"action": "mercury_good_visibility_for_month",
"selected": {
"year": year,
"month": month,
"timezone": timezone_name,
"sample_offset_minutes": sample_offset_minutes,
"min_mercury_alt_deg": min_mercury_alt_deg,
"max_sun_alt_deg": max_sun_alt_deg,
"min_consecutive_days": min_consecutive_days,
},
"events": events,
}
def action_month_sky_context(args: list[str]) -> dict:
if len(args) != 8:
fail(
@@ -774,6 +927,17 @@ 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))
def moon_fixed_equatorial_separation_deg(
ra_hours: float,
dec_deg: float,
observer: astronomy.Observer,
dt_utc: datetime,
) -> float:
time_value = dt_to_time(dt_utc)
moon_eq = astronomy.Equator(astronomy.Body.Moon, time_value, observer, True, True)
return spherical_separation_deg(float(moon_eq.ra), float(moon_eq.dec), float(ra_hours), float(dec_deg))
def planet_pair_separation_deg(
body_a: astronomy.Body,
body_b: astronomy.Body,
@@ -835,6 +999,31 @@ def refine_planet_pair_minimum_separation(
return best, planet_pair_separation_deg(body_a, body_b, observer, best)
def refine_fixed_target_minimum_separation(
ra_hours: float,
dec_deg: float,
observer: astronomy.Observer,
left_utc: datetime,
right_utc: datetime,
) -> tuple[datetime, float]:
left = left_utc
right = right_utc
for _ in range(32):
span = (right - left) / 3
m1 = left + span
m2 = right - span
f1 = moon_fixed_equatorial_separation_deg(ra_hours, dec_deg, observer, m1)
f2 = moon_fixed_equatorial_separation_deg(ra_hours, dec_deg, observer, m2)
if f1 <= f2:
right = m2
else:
left = m1
best = left + (right - left) / 2
return best, moon_fixed_equatorial_separation_deg(ra_hours, dec_deg, observer, best)
def action_moon_planet_approaches(args: list[str]) -> dict:
if len(args) != 7:
fail(
@@ -946,6 +1135,112 @@ def action_moon_planet_approaches(args: list[str]) -> dict:
}
def action_moon_deep_sky_approaches_for_month(args: list[str]) -> dict:
if len(args) != 7:
fail(
"Aktion moon_deep_sky_approaches_for_month erwartet 7 Argumente: latitude longitude elevation year month timezone max_sep_deg",
extra={"argv": args},
)
latitude = parse_float(args[0], "Latitude")
longitude = parse_float(args[1], "Longitude")
elevation = parse_float(args[2], "Elevation")
try:
year = int(args[3])
month = int(args[4])
except ValueError as exc:
fail("Jahr oder Monat sind ungueltig.", extra={"details": str(exc), "argv": args})
if month < 1 or month > 12:
fail("Monat muss zwischen 1 und 12 liegen.", extra={"month": month})
timezone_name = args[5]
max_sep_deg = parse_float(args[6], "Maximalabstand")
try:
tz = ZoneInfo(timezone_name)
except Exception as exc:
fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)})
observer = astronomy.Observer(latitude, longitude, elevation)
local_start = datetime(year, month, 1, 0, 0, 0, tzinfo=tz)
if month == 12:
local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz)
else:
local_end = datetime(year, month + 1, 1, 0, 0, 0, tzinfo=tz)
utc_start = local_start.astimezone(timezone.utc)
utc_end = local_end.astimezone(timezone.utc)
scan_step = timedelta(hours=1)
coarse_threshold = max_sep_deg + 1.0
target_defs = [
("pleiades", "Plejaden", 3.7833, 24.1167),
("praesepe", "Praesepe", 8.6667, 19.9833),
]
approaches = []
for key, label, ra_hours, dec_deg in target_defs:
samples: list[tuple[datetime, float]] = []
current = utc_start
while current <= utc_end:
samples.append((current, moon_fixed_equatorial_separation_deg(ra_hours, dec_deg, observer, current)))
current += scan_step
if samples[-1][0] < utc_end:
samples.append((utc_end, moon_fixed_equatorial_separation_deg(ra_hours, dec_deg, observer, utc_end)))
seen_ranges: list[tuple[datetime, datetime]] = []
for index in range(1, len(samples) - 1):
curr_t, curr_sep = samples[index]
prev_sep = samples[index - 1][1]
next_sep = samples[index + 1][1]
if curr_sep > coarse_threshold:
continue
if curr_sep > prev_sep or curr_sep > next_sep:
continue
left = max(utc_start, curr_t - scan_step)
right = min(utc_end, curr_t + scan_step)
if any(not (right <= seen_left or left >= seen_right) for seen_left, seen_right in seen_ranges):
continue
min_time_utc, min_sep = refine_fixed_target_minimum_separation(ra_hours, dec_deg, observer, left, right)
local_dt = min_time_utc.astimezone(tz)
if local_dt.year != year or local_dt.month != month:
continue
if min_sep > max_sep_deg:
continue
seen_ranges.append((left, right))
approaches.append({
"target_key": key,
"target_label": label,
"label": f"Mond nahe {label}",
"separation_deg": float(min_sep),
"utc_iso": min_time_utc.isoformat().replace("+00:00", "Z"),
"local_iso": local_dt.isoformat(),
"local_date": local_dt.strftime("%d.%m.%Y"),
"local_time": local_dt.strftime("%H:%M"),
})
approaches.sort(key=lambda item: item["local_iso"])
return {
"ok": True,
"action": "moon_deep_sky_approaches_for_month",
"selected": {
"year": year,
"month": month,
"timezone": timezone_name,
"max_separation_deg": max_sep_deg,
},
"approaches": approaches,
}
def action_planet_conjunctions_for_month(args: list[str]) -> dict:
if len(args) != 7:
fail(
@@ -3135,7 +3430,7 @@ def action_satellite_passes(args: list[str]) -> dict:
def main() -> None:
if len(sys.argv) < 2:
fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "moon_star_occultations_for_month"]})
fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "moon_star_occultations_for_month"]})
action = sys.argv[1]
args = sys.argv[2:]
@@ -3195,6 +3490,11 @@ def main() -> None:
print(json.dumps(result, ensure_ascii=False))
return
if action == "moon_deep_sky_approaches_for_month":
result = action_moon_deep_sky_approaches_for_month(args)
print(json.dumps(result, ensure_ascii=False))
return
if action == "planet_conjunctions_for_month":
result = action_planet_conjunctions_for_month(args)
print(json.dumps(result, ensure_ascii=False))
@@ -3240,12 +3540,17 @@ def main() -> None:
print(json.dumps(result, ensure_ascii=False))
return
if action == "mercury_good_visibility_for_month":
result = action_mercury_good_visibility_for_month(args)
print(json.dumps(result, ensure_ascii=False))
return
if action == "moon_star_occultations_for_month":
result = action_moon_star_occultations_for_month(args)
print(json.dumps(result, ensure_ascii=False))
return
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "moon_star_occultations_for_month"]})
fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "moon_star_occultations_for_month"]})
if __name__ == "__main__":