diff --git a/public/monatsvorhersage.php b/public/monatsvorhersage.php index 67b37ba..68358ee 100644 --- a/public/monatsvorhersage.php +++ b/public/monatsvorhersage.php @@ -230,6 +230,7 @@ $goldenHandleEvents = []; $moonPlanetApproaches = []; $moonDeepSkyApproaches = []; $planetBrightStarApproaches = []; +$goldenGatePlanetEvents = []; $mercuryGoodVisibilityEvents = []; $planetParadeEvents = []; $planetConstellationChangeEvents = []; @@ -876,7 +877,7 @@ if (!function_exists('monthForecastBuildNarrativeText')) { $planetObservationEvents = $findEvents( $filteredEvents, - static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['inner_planet_elongation', 'venus_peak_magnitude', 'outer_planet_event', 'planet_conjunction', 'planet_parade', 'mercury_good_visibility'], true), + static fn (array $event): bool => in_array((string) ($event['type'] ?? ''), ['inner_planet_elongation', 'venus_peak_magnitude', 'outer_planet_event', 'planet_conjunction', 'planet_parade', 'mercury_good_visibility', 'golden_gate_of_ecliptic'], true), 5 ); if ($planetObservationEvents !== []) { @@ -1662,6 +1663,26 @@ if (is_array($planetBrightStarDecoded) && ($planetBrightStarDecoded['ok'] ?? fal $planetBrightStarApproaches = is_array($planetBrightStarDecoded['approaches'] ?? null) ? $planetBrightStarDecoded['approaches'] : []; } +$goldenGateOutput = []; +$goldenGateExitCode = 0; +$goldenGateArgs = [ + $pythonScriptPath, + 'golden_gate_of_ecliptic_for_month', + (string) $chartLatitude, + (string) $chartLongitude, + (string) $chartElevation, + (string) $selectedYear, + (string) $selectedMonth, + $chartTimezone, +]; +$goldenGateCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $goldenGateArgs)) . ' 2>&1'; +exec($goldenGateCommand, $goldenGateOutput, $goldenGateExitCode); +$goldenGateRaw = trim(implode("\n", $goldenGateOutput)); +$goldenGateDecoded = json_decode($goldenGateRaw, true); +if (is_array($goldenGateDecoded) && ($goldenGateDecoded['ok'] ?? false)) { + $goldenGatePlanetEvents = is_array($goldenGateDecoded['events'] ?? null) ? $goldenGateDecoded['events'] : []; +} + $planetConjunctionOutput = []; $planetConjunctionExitCode = 0; $planetConjunctionArgs = [ @@ -2099,6 +2120,20 @@ foreach ($planetBrightStarApproaches as $approachEvent) { ]; } +foreach ($goldenGatePlanetEvents as $goldenGateEvent) { + if (!is_array($goldenGateEvent)) { + continue; + } + + $monthEventList[] = [ + 'event' => (string) ($goldenGateEvent['label'] ?? 'Planet im Goldenen Tor der Ekliptik'), + 'date' => (string) ($goldenGateEvent['local_date'] ?? ''), + 'time' => (string) ($goldenGateEvent['local_time'] ?? ''), + 'local_iso' => (string) ($goldenGateEvent['local_iso'] ?? ''), + 'type' => 'golden_gate_of_ecliptic', + ]; +} + foreach ($planetConjunctions as $conjunctionEvent) { if (!is_array($conjunctionEvent)) { continue; diff --git a/public/py/__pycache__/api.cpython-314.pyc b/public/py/__pycache__/api.cpython-314.pyc index 4b12884..44f62a8 100644 Binary files a/public/py/__pycache__/api.cpython-314.pyc and b/public/py/__pycache__/api.cpython-314.pyc differ diff --git a/public/py/api.py b/public/py/api.py index 9e97983..ec16734 100644 --- a/public/py/api.py +++ b/public/py/api.py @@ -414,6 +414,25 @@ PLANETS = [ ("Neptun", astronomy.Body.Neptune), ] +GOLDEN_GATE_PLANETS = [ + ("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"), +] + +# Das "Goldene Tor der Ekliptik" wird hier pragmatisch als Korridor +# zwischen Plejaden und Aldebaran/Hyaden modelliert. +GOLDEN_GATE_PLEIADES_RA_HOURS = 3.7833 +GOLDEN_GATE_PLEIADES_DEC_DEG = 24.1167 +GOLDEN_GATE_HYADES_RA_HOURS = 4.5987 +GOLDEN_GATE_HYADES_DEC_DEG = 16.5093 +GOLDEN_GATE_HALF_WIDTH_DEG = 3.6 +GOLDEN_GATE_MIN_PLANET_ALTITUDE_DEG = 5.0 +GOLDEN_GATE_MAX_SUN_ALTITUDE_DEG = -6.0 +GOLDEN_GATE_MIN_SOLAR_SEPARATION_DEG = 15.0 + EPHEMERIS_BODIES = { "Sun": ("Sonne", astronomy.Body.Sun), "Moon": ("Mond", astronomy.Body.Moon), @@ -2406,6 +2425,203 @@ def planet_fixed_equatorial_separation_deg( return spherical_separation_deg(float(body_eq.ra), float(body_eq.dec), float(ra_hours), float(dec_deg)) +def equatorial_unit_vector(ra_hours: float, dec_deg: float) -> tuple[float, float, float]: + ra_rad = math.radians(ra_hours * 15.0) + dec_rad = math.radians(dec_deg) + cos_dec = math.cos(dec_rad) + return ( + cos_dec * math.cos(ra_rad), + cos_dec * math.sin(ra_rad), + math.sin(dec_rad), + ) + + +def vector_dot(a: tuple[float, float, float], b: tuple[float, float, float]) -> float: + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + + +def vector_cross(a: tuple[float, float, float], b: tuple[float, float, float]) -> tuple[float, float, float]: + return ( + a[1] * b[2] - a[2] * b[1], + a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0], + ) + + +def vector_scale(v: tuple[float, float, float], factor: float) -> tuple[float, float, float]: + return (v[0] * factor, v[1] * factor, v[2] * factor) + + +def vector_add(a: tuple[float, float, float], b: tuple[float, float, float]) -> tuple[float, float, float]: + return (a[0] + b[0], a[1] + b[1], a[2] + b[2]) + + +def vector_normalize(v: tuple[float, float, float]) -> tuple[float, float, float]: + length = math.sqrt(vector_dot(v, v)) + if length <= 0.0: + return (0.0, 0.0, 1.0) + return (v[0] / length, v[1] / length, v[2] / length) + + +def build_golden_gate_geometry() -> dict: + pleiades_vec = equatorial_unit_vector(GOLDEN_GATE_PLEIADES_RA_HOURS, GOLDEN_GATE_PLEIADES_DEC_DEG) + hyades_vec = equatorial_unit_vector(GOLDEN_GATE_HYADES_RA_HOURS, GOLDEN_GATE_HYADES_DEC_DEG) + center_vec = vector_normalize(vector_add(pleiades_vec, hyades_vec)) + north_vec = (0.0, 0.0, 1.0) + east_vec = vector_normalize(vector_cross(north_vec, center_vec)) + north_tangent_vec = vector_normalize(vector_cross(center_vec, east_vec)) + + def project(ra_hours: float, dec_deg: float) -> tuple[float, float]: + vec = equatorial_unit_vector(ra_hours, dec_deg) + return ( + math.degrees(math.asin(max(-1.0, min(1.0, vector_dot(vec, east_vec))))), + math.degrees(math.asin(max(-1.0, min(1.0, vector_dot(vec, north_tangent_vec))))), + ) + + pleiades_xy = project(GOLDEN_GATE_PLEIADES_RA_HOURS, GOLDEN_GATE_PLEIADES_DEC_DEG) + hyades_xy = project(GOLDEN_GATE_HYADES_RA_HOURS, GOLDEN_GATE_HYADES_DEC_DEG) + + return { + "center_vec": center_vec, + "east_vec": east_vec, + "north_vec": north_tangent_vec, + "pleiades_xy": pleiades_xy, + "hyades_xy": hyades_xy, + } + + +GOLDEN_GATE_GEOMETRY = build_golden_gate_geometry() + + +def golden_gate_planet_state( + body: astronomy.Body, + observer: astronomy.Observer, + dt_utc: datetime, +) -> dict: + time_value = dt_to_time(dt_utc) + body_eq = astronomy.Equator(body, time_value, observer, True, True) + planet_vec = equatorial_unit_vector(float(body_eq.ra), float(body_eq.dec)) + + x = math.degrees(math.asin(max(-1.0, min(1.0, vector_dot(planet_vec, GOLDEN_GATE_GEOMETRY["east_vec"]))))) + y = math.degrees(math.asin(max(-1.0, min(1.0, vector_dot(planet_vec, GOLDEN_GATE_GEOMETRY["north_vec"]))))) + + ax, ay = GOLDEN_GATE_GEOMETRY["pleiades_xy"] + bx, by = GOLDEN_GATE_GEOMETRY["hyades_xy"] + sx = bx - ax + sy = by - ay + seg_len_sq = sx * sx + sy * sy + if seg_len_sq <= 0.0: + return { + "inside": False, + "center_distance_deg": 999.0, + "offset_deg": 999.0, + "track_fraction": -1.0, + "entry_margin_deg": 999.0, + } + + px = x - ax + py = y - ay + track_fraction = (px * sx + py * sy) / seg_len_sq + closest_x = ax + track_fraction * sx + closest_y = ay + track_fraction * sy + offset_deg = math.hypot(x - closest_x, y - closest_y) + center_distance_deg = math.hypot(x, y) + seg_len_deg = math.sqrt(seg_len_sq) + before_start_deg = max(0.0, -track_fraction * seg_len_deg) + after_end_deg = max(0.0, (track_fraction - 1.0) * seg_len_deg) + along_excess_deg = max(before_start_deg, after_end_deg) + width_excess_deg = max(0.0, offset_deg - GOLDEN_GATE_HALF_WIDTH_DEG) + inside = 0.0 <= track_fraction <= 1.0 and offset_deg <= GOLDEN_GATE_HALF_WIDTH_DEG + entry_margin_deg = max(width_excess_deg, along_excess_deg) + if inside: + entry_margin_deg = -min( + GOLDEN_GATE_HALF_WIDTH_DEG - offset_deg, + track_fraction * seg_len_deg, + (1.0 - track_fraction) * seg_len_deg, + ) + + return { + "inside": inside, + "center_distance_deg": float(center_distance_deg), + "offset_deg": float(offset_deg), + "track_fraction": float(track_fraction), + "entry_margin_deg": float(entry_margin_deg), + } + + +def golden_gate_visibility_state( + body: astronomy.Body, + observer: astronomy.Observer, + dt_utc: datetime, +) -> dict: + planet_altitude_deg = body_altitude_deg(body, observer, dt_utc) + sun_altitude_deg = body_altitude_deg(astronomy.Body.Sun, observer, dt_utc) + solar_separation_deg = planet_pair_separation_deg(body, astronomy.Body.Sun, observer, dt_utc) + observable = ( + planet_altitude_deg >= GOLDEN_GATE_MIN_PLANET_ALTITUDE_DEG + and sun_altitude_deg <= GOLDEN_GATE_MAX_SUN_ALTITUDE_DEG + and solar_separation_deg >= GOLDEN_GATE_MIN_SOLAR_SEPARATION_DEG + ) + return { + "observable": bool(observable), + "planet_altitude_deg": float(planet_altitude_deg), + "sun_altitude_deg": float(sun_altitude_deg), + "solar_separation_deg": float(solar_separation_deg), + } + + +def refine_golden_gate_offset_minimum( + body: astronomy.Body, + observer: astronomy.Observer, + left_utc: datetime, + right_utc: datetime, +) -> tuple[datetime, dict]: + left = left_utc + right = right_utc + + for _ in range(32): + span = (right - left) / 3 + m1 = left + span + m2 = right - span + f1 = golden_gate_planet_state(body, observer, m1)["offset_deg"] + f2 = golden_gate_planet_state(body, observer, m2)["offset_deg"] + if f1 <= f2: + right = m2 + else: + left = m1 + + best = left + (right - left) / 2 + return best, golden_gate_planet_state(body, observer, best) + + +def refine_golden_gate_observable_minimum( + body: astronomy.Body, + observer: astronomy.Observer, + left_utc: datetime, + right_utc: datetime, +) -> tuple[datetime | None, dict | None, dict | None]: + step = timedelta(minutes=5) + current = left_utc + best_time: datetime | None = None + best_gate_state: dict | None = None + best_visibility_state: dict | None = None + + while current <= right_utc: + gate_state = golden_gate_planet_state(body, observer, current) + visibility_state = golden_gate_visibility_state(body, observer, current) + if bool(gate_state["inside"]) and bool(visibility_state["observable"]): + if best_gate_state is None or float(gate_state["offset_deg"]) < float(best_gate_state["offset_deg"]): + best_time = current + best_gate_state = gate_state + best_visibility_state = visibility_state + current += step + + if best_time is None or best_gate_state is None or best_visibility_state is None: + return None, None, None + + return best_time, best_gate_state, best_visibility_state + + def refine_minimum_separation( body: astronomy.Body, observer: astronomy.Observer, @@ -2989,6 +3205,162 @@ def action_planet_conjunctions_for_month(args: list[str]) -> dict: } +def action_golden_gate_of_ecliptic_for_month(args: list[str]) -> dict: + if len(args) != 6: + fail( + "Aktion golden_gate_of_ecliptic_for_month erwartet 6 Argumente: latitude longitude elevation year month 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]) + 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] + 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) + passages: list[dict] = [] + + for planet_label, planet_body, planet_key in GOLDEN_GATE_PLANETS: + samples: list[tuple[datetime, dict]] = [] + current = utc_start + while current <= utc_end: + samples.append((current, golden_gate_planet_state(planet_body, observer, current))) + current += scan_step + if samples[-1][0] < utc_end: + samples.append((utc_end, golden_gate_planet_state(planet_body, observer, utc_end))) + + segment_start: datetime | None = None + best_sample_time: datetime | None = None + best_sample_state: dict | None = None + + for sample_time, sample_state in samples: + if bool(sample_state["inside"]): + if segment_start is None: + segment_start = sample_time + best_sample_time = sample_time + best_sample_state = sample_state + elif best_sample_state is None or float(sample_state["offset_deg"]) < float(best_sample_state["offset_deg"]): + best_sample_time = sample_time + best_sample_state = sample_state + elif segment_start is not None: + segment_end = sample_time + refine_left = max(utc_start, segment_start - scan_step) + refine_right = min(utc_end, segment_end) + event_time_utc, event_state, visibility_state = refine_golden_gate_observable_minimum( + planet_body, + observer, + refine_left, + refine_right, + ) + if ( + event_time_utc is not None + and event_state is not None + and visibility_state is not None + ): + local_dt = event_time_utc.astimezone(tz) + if local_dt.year == year and local_dt.month == month: + passages.append({ + "planet_key": planet_key, + "planet_label": planet_label, + "label": f"{planet_label} im Goldenen Tor der Ekliptik", + "center_distance_deg": float(event_state["center_distance_deg"]), + "offset_deg": float(event_state["offset_deg"]), + "track_fraction": float(event_state["track_fraction"]), + "planet_altitude_deg": float(visibility_state["planet_altitude_deg"]), + "sun_altitude_deg": float(visibility_state["sun_altitude_deg"]), + "solar_separation_deg": float(visibility_state["solar_separation_deg"]), + "utc_iso": event_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"), + }) + segment_start = None + best_sample_time = None + best_sample_state = None + + if segment_start is not None: + refine_left = max(utc_start, segment_start - scan_step) + refine_right = utc_end + event_time_utc, event_state, visibility_state = refine_golden_gate_observable_minimum( + planet_body, + observer, + refine_left, + refine_right, + ) + if ( + event_time_utc is not None + and event_state is not None + and visibility_state is not None + ): + local_dt = event_time_utc.astimezone(tz) + if local_dt.year == year and local_dt.month == month: + passages.append({ + "planet_key": planet_key, + "planet_label": planet_label, + "label": f"{planet_label} im Goldenen Tor der Ekliptik", + "center_distance_deg": float(event_state["center_distance_deg"]), + "offset_deg": float(event_state["offset_deg"]), + "track_fraction": float(event_state["track_fraction"]), + "planet_altitude_deg": float(visibility_state["planet_altitude_deg"]), + "sun_altitude_deg": float(visibility_state["sun_altitude_deg"]), + "solar_separation_deg": float(visibility_state["solar_separation_deg"]), + "utc_iso": event_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"), + }) + + passages.sort(key=lambda item: item["local_iso"]) + + return { + "ok": True, + "action": "golden_gate_of_ecliptic_for_month", + "selected": { + "year": year, + "month": month, + "timezone": timezone_name, + }, + "gate": { + "pleiades": { + "ra_hours": GOLDEN_GATE_PLEIADES_RA_HOURS, + "dec_deg": GOLDEN_GATE_PLEIADES_DEC_DEG, + }, + "hyades": { + "ra_hours": GOLDEN_GATE_HYADES_RA_HOURS, + "dec_deg": GOLDEN_GATE_HYADES_DEC_DEG, + }, + "half_width_deg": GOLDEN_GATE_HALF_WIDTH_DEG, + "min_planet_altitude_deg": GOLDEN_GATE_MIN_PLANET_ALTITUDE_DEG, + "max_sun_altitude_deg": GOLDEN_GATE_MAX_SUN_ALTITUDE_DEG, + "min_solar_separation_deg": GOLDEN_GATE_MIN_SOLAR_SEPARATION_DEG, + }, + "events": passages, + } + + def eclipse_kind_label(kind: astronomy.EclipseKind) -> str: if kind == astronomy.EclipseKind.Penumbral: return "Halbschatten" @@ -5110,7 +5482,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", "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", "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", "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", "moon_star_occultations_for_month"]}) action = sys.argv[1] args = sys.argv[2:] @@ -5215,6 +5587,11 @@ def main() -> None: print(json.dumps(result, ensure_ascii=True)) return + if action == "golden_gate_of_ecliptic_for_month": + result = action_golden_gate_of_ecliptic_for_month(args) + print(json.dumps(result, ensure_ascii=True)) + return + if action == "planet_conjunctions_for_month": result = action_planet_conjunctions_for_month(args) print(json.dumps(result, ensure_ascii=True)) @@ -5285,7 +5662,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", "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", "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", "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", "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", "moon_star_occultations_for_month"]}) if __name__ == "__main__":