diff --git a/BERECHNUNGSGRUNDLAGEN.md b/BERECHNUNGSGRUNDLAGEN.md index f9e11de..2fc1279 100644 --- a/BERECHNUNGSGRUNDLAGEN.md +++ b/BERECHNUNGSGRUNDLAGEN.md @@ -49,6 +49,7 @@ Nicht jede Berechnung ist Skyfield oder Astronomy Engine: | `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. | +| `mondplanetenbedeckungen.php` | **Astronomy Engine** über die lokale Python-API. Durchsucht ein ganzes Jahr nach Mond-Planeten-Bedeckungen (Merkur bis Neptun) für den Standardstandort; berücksichtigt die scheinbare Scheibengröße von Mond und Planet. | | `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. | diff --git a/public/auth_helpers.php b/public/auth_helpers.php index e20df23..011edd2 100644 --- a/public/auth_helpers.php +++ b/public/auth_helpers.php @@ -134,6 +134,7 @@ function manageablePublicPages(): array 'sonnenfinsternis.php' => 'Sonnenfinsternis', 'bedeckung.php' => 'Bedeckungsphänomene', 'sternbedeckungen.php' => 'Mond-Stern-Bedeckungen', + 'mondplanetenbedeckungen.php' => 'Mond-Planeten-Bedeckungen', 'monatsvorhersage.php' => 'Monatsvorhersage', 'moonyear.php' => 'Mondphasen-Jahresübersicht', 'wochenvorhersage.php' => 'Wochenvorhersage', diff --git a/public/css/style.css b/public/css/style.css index c771b43..ff7a58a 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -4676,6 +4676,12 @@ body.solarsystem-page:not(.jupitersystem-page) .solarsystem-stage-wrap { box-shadow: inset 0 0 0 1px rgba(143, 230, 141, 0.22); } +.occult-visibility-below { + background: rgba(136, 148, 173, 0.16); + color: #9aa6c2; + box-shadow: inset 0 0 0 1px rgba(154, 166, 194, 0.22); +} + .occult-detail-card { overflow: hidden; } diff --git a/public/header.php b/public/header.php index 30d4528..020555b 100644 --- a/public/header.php +++ b/public/header.php @@ -146,6 +146,7 @@ $menuGroups = [ ['href' => 'sonnenfinsternis.php', 'label' => 'Sonnenfinsternis'], ['href' => 'bedeckung.php', 'label' => 'Bedeckungsphänomene'], ['href' => 'sternbedeckungen.php', 'label' => 'Mond-Stern-Bedeckungen'], + ['href' => 'mondplanetenbedeckungen.php', 'label' => 'Mond-Planeten-Bedeckungen'], ], ], [ diff --git a/public/mondplanetenbedeckungen.php b/public/mondplanetenbedeckungen.php new file mode 100644 index 0000000..d259273 --- /dev/null +++ b/public/mondplanetenbedeckungen.php @@ -0,0 +1,1284 @@ +&1'; + $output = []; + $resultCode = 0; + exec($command, $output, $resultCode); + + $joined = trim(implode("\n", $output)); + $debugOutput = $output; + + if ($resultCode !== 0) { + $lastError = $joined !== '' ? $joined : ('Fehlercode ' . $resultCode . ' bei ' . $pythonBinary); + continue; + } + + $decoded = json_decode($joined, true); + if (!is_array($decoded)) { + $lastError = $joined !== '' ? 'Python-Ausgabe war kein gültiges JSON.' : 'Python lieferte keine Ausgabe zurück.'; + continue; + } + + if (!($decoded['ok'] ?? false)) { + return [ + 'ok' => false, + 'error' => (string)($decoded['error'] ?? 'Unbekannter Python-Fehler'), + 'debug' => $joined, + ]; + } + + return ['ok' => true, 'data' => $decoded]; + } + + return [ + 'ok' => false, + 'error' => $lastError, + 'debug' => implode("\n", $debugOutput), + ]; +} + +$defaultLocation = null; +$pdo = null; +$pageError = null; +$pageLocation = null; +$detailMapStars = []; +$constellationLines = []; +$detailMapCatalogLimit = 8.0; +$isAjax = isset($_POST['ajax']) && (string)$_POST['ajax'] === '1'; + +try { + $config = require __DIR__ . '/../config/database.php'; + $dsn = sprintf( + 'mysql:host=%s;dbname=%s;charset=%s', + $config['host'], + $config['dbname'], + $config['charset'] ?? 'utf8mb4' + ); + + $pdo = new PDO( + $dsn, + $config['user'], + $config['pass'], + [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ] + ); + + if ($loggedIn && isset($_SESSION['user_id'])) { + $stmtLocation = $pdo->prepare( + 'SELECT id, name, latitude, longitude, elevation, timezone + FROM app_user_locations + WHERE user_id = ? AND is_default = 1 + LIMIT 1' + ); + $stmtLocation->execute([(int)$_SESSION['user_id']]); + $defaultLocation = $stmtLocation->fetch() ?: null; + } +} catch (Throwable $exception) { + $pageError = $exception->getMessage(); +} + +if (!empty($defaultLocation) && is_array($defaultLocation)) { + $pageLocation = [ + 'name' => (string)($defaultLocation['name'] ?? ''), + 'latitude' => isset($defaultLocation['latitude']) ? (float)$defaultLocation['latitude'] : null, + 'longitude' => isset($defaultLocation['longitude']) ? (float)$defaultLocation['longitude'] : null, + 'elevation' => isset($defaultLocation['elevation']) && $defaultLocation['elevation'] !== null ? (float)$defaultLocation['elevation'] : 0.0, + 'timezone' => (string)($defaultLocation['timezone'] ?? 'Europe/Berlin'), + ]; +} + +$timezoneName = $pageLocation['timezone'] ?? 'Europe/Berlin'; +try { + $timezone = new DateTimeZone($timezoneName); +} catch (Throwable $exception) { + $timezoneName = 'Europe/Berlin'; + $timezone = new DateTimeZone($timezoneName); +} + +$todayYear = (int)(new DateTimeImmutable('now', $timezone))->format('Y'); +$selectedYear = isset($_POST['year']) && preg_match('/^\d{4}$/', (string)$_POST['year']) + ? (int)$_POST['year'] + : $todayYear; +$selectedYear = max(1900, min(2100, $selectedYear)); + +if (!$isAjax && $pageError === null && $pdo instanceof PDO && $pageLocation !== null && $pageLocation['latitude'] !== null && $pageLocation['longitude'] !== null) { + try { + $stmtDetailMap = $pdo->query( + "SELECT `hip`, `proper`, `bf`, `con`, `mag`, `ra`, `dec`, `dist` + FROM `star_hipparcos` + WHERE `mag` IS NOT NULL + AND `ra` IS NOT NULL + AND `dec` IS NOT NULL + AND `mag` <= 8.0 + ORDER BY `mag` ASC, `proper` ASC, `bf` ASC, `hip` ASC" + ); + + foreach ($stmtDetailMap->fetchAll() as $row) { + $proper = trim((string)($row['proper'] ?? '')); + $bf = trim((string)($row['bf'] ?? '')); + $hip = isset($row['hip']) ? (int)$row['hip'] : 0; + $label = $proper !== '' ? $proper : ($bf !== '' ? $bf : ($hip > 0 ? 'HIP ' . $hip : 'Unbenannter Stern')); + + $detailMapStars[] = [ + 'hip' => $hip, + 'label' => $label, + 'constellation' => trim((string)($row['con'] ?? '')), + 'mag' => isset($row['mag']) ? (float)$row['mag'] : null, + 'ra' => isset($row['ra']) ? (float)$row['ra'] : null, + 'dec' => isset($row['dec']) ? (float)$row['dec'] : null, + 'distLy' => isset($row['dist']) && $row['dist'] !== null && (float)$row['dist'] > 0.0 + ? (float)$row['dist'] * 3.26156 + : 1000.0, + ]; + } + + $stmtLines = $pdo->query( + "SELECT + cl.`id` AS line_id, + clp.`point_index`, + sh.`ra` AS ra_hours, + sh.`dec` AS dec_deg + FROM `constellation_lines` cl + INNER JOIN `constellation_line_points` clp + ON clp.`line_id` = cl.`id` + INNER JOIN `star_hipparcos` sh + ON sh.`hip` = clp.`hip_number` + WHERE sh.`ra` IS NOT NULL + AND sh.`dec` IS NOT NULL + ORDER BY cl.`id`, clp.`point_index`" + ); + + foreach ($stmtLines->fetchAll() as $row) { + $lineId = (int)$row['line_id']; + $constellationLines[$lineId][] = [ + 'ra' => isset($row['ra_hours']) ? (float)$row['ra_hours'] : null, + 'dec' => isset($row['dec_deg']) ? (float)$row['dec_deg'] : null, + ]; + } + } catch (Throwable $exception) { + $pageError = $exception->getMessage(); + $detailMapStars = []; + $constellationLines = []; + } +} + +if ($isAjax) { + header('Content-Type: application/json; charset=utf-8'); + + if ($pageLocation === null || $pageLocation['latitude'] === null || $pageLocation['longitude'] === null) { + echo json_encode([ + 'ok' => false, + 'error' => 'Kein Standard-Standort gesetzt. Bitte zuerst in den Einstellungen einen Standard-Standort hinterlegen.', + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + + if ($pageError !== null || !($pdo instanceof PDO)) { + echo json_encode([ + 'ok' => false, + 'error' => $pageError ?? 'Die Datenbank ist nicht verfuegbar.', + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + + try { + $pythonResult = runPythonApi( + __DIR__ . '/py/api.py', + 'moon_planet_occultations_for_year', + [ + (string)$pageLocation['latitude'], + (string)$pageLocation['longitude'], + (string)$pageLocation['elevation'], + (string)$selectedYear, + $timezoneName, + ] + ); + + if (!$pythonResult['ok']) { + echo json_encode([ + 'ok' => false, + 'error' => $pythonResult['error'] ?? 'Unbekannter Python-Fehler', + 'debug' => $pythonResult['debug'] ?? null, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } + + echo json_encode([ + 'ok' => true, + 'results' => $pythonResult['data']['results'] ?? [], + 'selected_year' => $selectedYear, + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } catch (Throwable $exception) { + echo json_encode([ + 'ok' => false, + 'error' => $exception->getMessage(), + ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + exit; + } +} + +require __DIR__ . '/header.php'; + +if (!empty($currentLocation) && is_array($currentLocation)) { + $pageLocation = [ + 'name' => (string)($currentLocation['name'] ?? ''), + 'latitude' => isset($currentLocation['latitude']) ? (float)$currentLocation['latitude'] : null, + 'longitude' => isset($currentLocation['longitude']) ? (float)$currentLocation['longitude'] : null, + 'elevation' => isset($currentLocation['elevation']) && $currentLocation['elevation'] !== null ? (float)$currentLocation['elevation'] : 0.0, + 'timezone' => (string)($currentLocation['timezone'] ?? 'Europe/Berlin'), + ]; +} +?> + + + +
+ +
+

Kein Standard-Standort gesetzt

+

+ Fuer die Berechnung wird der Standard-Standort aus der Datenbank verwendet. + Bitte wähle in den Einstellungen einen Standard-Standort aus. +

+
+ +
+

Datenbank nicht verfuegbar

+

Die Sterndaten konnten nicht geladen werden.

+

Technischer Hinweis:

+
+ +
+
+
+ Standort + + ° / ° + +
+
+ Berechnung +
+ + + + + + + +
+
+
+ +
+ +
+
+

Bereit zur Berechnung

+

+ Wähle ein Jahr und starte die Auswertung mit dem Button. + Mond-Planeten-Bedeckungen sind an einem festen Standort selten + – es ist normal, wenn in manchen Jahren keine gefunden werden. +

+
+
+ + + +
+ + + + + + diff --git a/public/py/api.py b/public/py/api.py index 5f93493..b55f458 100644 --- a/public/py/api.py +++ b/public/py/api.py @@ -5844,6 +5844,154 @@ def action_moon_star_occultations(args: list[str]) -> dict: } +PLANET_OCCULTATION_DEFS = [ + ("Merkur", astronomy.Body.Mercury, "mercury"), + ("Venus", astronomy.Body.Venus, "venus"), + ("Mars", astronomy.Body.Mars, "mars"), + ("Jupiter", astronomy.Body.Jupiter, "jupiter"), + ("Saturn", astronomy.Body.Saturn, "saturn"), + ("Uranus", astronomy.Body.Uranus, "uranus"), + ("Neptun", astronomy.Body.Neptune, "neptune"), +] + + +def compute_moon_planet_occultations_for_range( + observer: astronomy.Observer, + utc_start: datetime, + utc_end: datetime, + tz: ZoneInfo, +) -> list[dict]: + scan_step = timedelta(hours=1) + coarse_threshold_deg = 3.0 + results: list[dict] = [] + + for label, body, key in PLANET_OCCULTATION_DEFS: + samples: list[tuple[datetime, float]] = [] + current = utc_start + while current <= utc_end: + samples.append((current, moon_planet_separation_deg(body, observer, current))) + current += scan_step + if samples[-1][0] < utc_end: + samples.append((utc_end, moon_planet_separation_deg(body, observer, utc_end))) + + seen_ranges: list[tuple[datetime, datetime]] = [] + for index in range(1, len(samples) - 1): + prev_t, prev_sep = samples[index - 1] + curr_t, curr_sep = samples[index] + next_t, next_sep = samples[index + 1] + + if curr_sep > coarse_threshold_deg: + 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_minimum_separation(body, observer, left, right) + seen_ranges.append((left, right)) + + overlap_fraction = moon_planet_overlap_fraction(body, observer, min_time_utc) + if overlap_fraction <= 0.0: + continue + + time_value = dt_to_time(min_time_utc) + moon_eq = astronomy.Equator(astronomy.Body.Moon, time_value, observer, True, True) + planet_eq = astronomy.Equator(body, time_value, observer, True, True) + sun_eq = astronomy.Equator(astronomy.Body.Sun, time_value, observer, True, True) + moon_hor = astronomy.Horizon(time_value, observer, moon_eq.ra, moon_eq.dec, astronomy.Refraction.Normal) + planet_hor = astronomy.Horizon(time_value, observer, planet_eq.ra, planet_eq.dec, astronomy.Refraction.Normal) + sun_hor = astronomy.Horizon(time_value, observer, sun_eq.ra, sun_eq.dec, astronomy.Refraction.Normal) + + below_horizon = float(moon_hor.altitude) <= 0.0 or float(planet_hor.altitude) <= 0.0 + + ingress_time = refine_occultation_contact(body, observer, min_time_utc, -1) + egress_time = refine_occultation_contact(body, observer, min_time_utc, 1) + + duration_seconds = None + if ingress_time is not None and egress_time is not None: + duration_seconds = int(round((egress_time - ingress_time).total_seconds())) + + moon_radius_deg = moon_angular_radius_deg(float(moon_eq.dist)) + planet_radius_deg = planet_angular_radius_deg(body, float(planet_eq.dist)) + + results.append({ + "planet_key": key, + "planet_label": label, + "ingress": serialize_occultation_time(ingress_time, tz), + "maximum": serialize_occultation_time(min_time_utc, tz), + "egress": serialize_occultation_time(egress_time, tz), + "partial_start": ingress_time is None, + "partial_end": egress_time is None, + "duration_seconds": duration_seconds, + "separation_arcmin": float(min_sep) * 60.0, + "moon_radius_arcmin": moon_radius_deg * 60.0, + "planet_radius_arcsec": planet_radius_deg * 3600.0, + "overlap_fraction": float(overlap_fraction), + "is_total": overlap_fraction >= 0.999, + "moon_alt_deg": float(moon_hor.altitude), + "planet_alt_deg": float(planet_hor.altitude), + "sun_alt_deg": float(sun_hor.altitude), + "below_horizon": below_horizon, + "visibility": {"key": "below_horizon", "label": "Unter Horizont"} if below_horizon + else classify_visibility(float(sun_hor.altitude)), + }) + + results.sort( + key=lambda item: item["maximum"]["utc_iso"] if item["maximum"] else "9999-99-99T99:99:99Z" + ) + return results + + +def action_moon_planet_occultations_for_year(args: list[str]) -> dict: + if len(args) != 5: + fail( + "Aktion moon_planet_occultations_for_year erwartet 5 Argumente: latitude longitude elevation year timezone", + 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]) + except ValueError as exc: + fail("Jahr ist ungueltig.", extra={"details": str(exc), "argv": args}) + + timezone_name = args[4] + 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, 1, 1, 0, 0, 0, tzinfo=tz) + local_end = datetime(year + 1, 1, 1, 0, 0, 0, tzinfo=tz) + utc_start = local_start.astimezone(timezone.utc) + utc_end = local_end.astimezone(timezone.utc) + + results = compute_moon_planet_occultations_for_range(observer, utc_start, utc_end, tz) + + return { + "ok": True, + "action": "moon_planet_occultations_for_year", + "selected": { + "year": year, + "timezone": timezone_name, + }, + "observer": { + "latitude": latitude, + "longitude": longitude, + "elevation": elevation, + }, + "results": results, + } + + # ── satellite_passes ────────────────────────────────────────────────────────── import math as _math @@ -6415,7 +6563,7 @@ def action_barycenter(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", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "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_bright_star_approaches_for_month", "golden_gate_of_ecliptic_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", "planet_parades_for_month", "planet_constellation_changes_for_month", "sun_constellation_changes_for_year", "sun_constellation_offset_statistics", "moon_star_occultations_for_month"]}) + fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "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_bright_star_approaches_for_month", "golden_gate_of_ecliptic_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", "planet_parades_for_month", "planet_constellation_changes_for_month", "sun_constellation_changes_for_year", "sun_constellation_offset_statistics", "moon_star_occultations_for_month", "moon_planet_occultations_for_year"]}) action = sys.argv[1] args = sys.argv[2:] @@ -6605,6 +6753,11 @@ def main() -> None: print(json.dumps(result, ensure_ascii=True)) return + if action == "moon_planet_occultations_for_year": + result = action_moon_planet_occultations_for_year(args) + print(json.dumps(result, ensure_ascii=True)) + return + if action == "lunar_eclipses_for_year": result = action_lunar_eclipses_for_year(args) print(json.dumps(result, ensure_ascii=True)) @@ -6615,7 +6768,7 @@ def main() -> None: print(json.dumps(result, ensure_ascii=True)) return - fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "twilight_chart", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "planet_visibility_chart", "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_bright_star_approaches_for_month", "golden_gate_of_ecliptic_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", "planet_parades_for_month", "planet_constellation_changes_for_month", "sun_constellation_changes_for_year", "sun_constellation_offset_statistics", "moon_star_occultations_for_month"]}) + fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "astronomical_conversions", "twilight_chart", "comet_brightnesses", "favorite_comet_events_for_month", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "planet_ephemeris", "planet_visibility_chart", "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_bright_star_approaches_for_month", "golden_gate_of_ecliptic_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", "planet_parades_for_month", "planet_constellation_changes_for_month", "sun_constellation_changes_for_year", "sun_constellation_offset_statistics", "moon_star_occultations_for_month", "moon_planet_occultations_for_year"]}) if __name__ == "__main__":