"""Meteor shower data calculations.""" import typing from datetime import datetime, timedelta import ephem # type: ignore MeteorShower = dict[str, typing.Any] # Meteor shower definitions with parent comet orbital elements METEOR_SHOWERS = { "Quadrantids": { "name": "Quadrantids", "radiant_ra": "15h20m", # Right ascension at peak "radiant_dec": "+49.5°", # Declination at peak "peak_solar_longitude": 283.16, # Solar longitude at peak "activity_start": 283.16 - 10, # Activity period "activity_end": 283.16 + 10, "rate_max": 120, "rate_min": 50, "parent_body": "2003 EH1", "visibility": "Northern Hemisphere", "description": "The year kicks off with the Quadrantids, known for their brief but intense peak lasting only about 4 hours.", "velocity_kms": 41, }, "Lyrids": { "name": "Lyrids", "radiant_ra": "18h04m", "radiant_dec": "+32.32°", "peak_solar_longitude": 32.32, "activity_start": 32.32 - 10, "activity_end": 32.32 + 10, "rate_max": 18, "rate_min": 10, "parent_body": "C/1861 G1 (Thatcher)", "visibility": "Both hemispheres", "description": "The Lyrids are one of the oldest recorded meteor showers, with observations dating back 2,700 years.", "velocity_kms": 49, }, "Eta Aquariids": { "name": "Eta Aquariids", "radiant_ra": "22h32m", "radiant_dec": "-1.0°", "peak_solar_longitude": 45.5, "activity_start": 45.5 - 15, "activity_end": 45.5 + 15, "rate_max": 60, "rate_min": 30, "parent_body": "1P/Halley", "visibility": "Southern Hemisphere (best)", "description": "Created by debris from Halley's Comet, these meteors are fast and often leave glowing trails.", "velocity_kms": 66, }, "Perseids": { "name": "Perseids", "radiant_ra": "03h04m", "radiant_dec": "+58.0°", "peak_solar_longitude": 140.0, "activity_start": 140.0 - 15, "activity_end": 140.0 + 15, "rate_max": 100, "rate_min": 50, "parent_body": "109P/Swift-Tuttle", "visibility": "Northern Hemisphere", "description": "One of the most popular meteor showers, viewing conditions vary by year based on moon phase.", "velocity_kms": 59, }, "Orionids": { "name": "Orionids", "radiant_ra": "06h20m", "radiant_dec": "+16.0°", "peak_solar_longitude": 208.0, "activity_start": 208.0 - 15, "activity_end": 208.0 + 15, "rate_max": 25, "rate_min": 15, "parent_body": "1P/Halley", "visibility": "Both hemispheres", "description": "Another shower created by Halley's Comet debris, known for their speed and brightness.", "velocity_kms": 66, }, "Geminids": { "name": "Geminids", "radiant_ra": "07h28m", "radiant_dec": "+32.0°", "peak_solar_longitude": 262.2, "activity_start": 262.2 - 10, "activity_end": 262.2 + 10, "rate_max": 120, "rate_min": 60, "parent_body": "3200 Phaethon", "visibility": "Both hemispheres", "description": "The best shower of most years with the highest rates. Unusual for being caused by an asteroid rather than a comet.", "velocity_kms": 35, }, "Ursids": { "name": "Ursids", "radiant_ra": "14h28m", "radiant_dec": "+75.0°", "peak_solar_longitude": 270.7, "activity_start": 270.7 - 5, "activity_end": 270.7 + 5, "rate_max": 10, "rate_min": 5, "parent_body": "8P/Tuttle", "visibility": "Northern Hemisphere", "description": "A minor shower that closes out the year, best viewed from dark locations away from city lights.", "velocity_kms": 33, }, } def calculate_solar_longitude_date(year: int, target_longitude: float) -> datetime: """Calculate the date when the Sun reaches a specific longitude for a given year.""" # Start from beginning of year start_date = datetime(year, 1, 1) # Use PyEphem to calculate solar longitude observer = ephem.Observer() observer.lat = "0" # Equator observer.lon = "0" # Greenwich observer.date = start_date sun = ephem.Sun(observer) # Search for the date when solar longitude matches target # Solar longitude 0° = Spring Equinox (around March 20) # We need to find when sun reaches the target longitude # Approximate: start search from reasonable date based on longitude if target_longitude < 90: # Spring (Mar-Jun) search_start = datetime(year, 3, 1) elif target_longitude < 180: # Summer (Jun-Sep) search_start = datetime(year, 6, 1) elif target_longitude < 270: # Fall (Sep-Dec) search_start = datetime(year, 9, 1) else: # Winter (Dec-Mar) search_start = datetime(year, 12, 1) observer.date = search_start # Search within a reasonable range (±60 days) for day_offset in range(-60, 61): test_date = search_start + timedelta(days=day_offset) observer.date = test_date sun.compute(observer) # Convert ecliptic longitude to degrees sun_longitude = float(sun.hlon) * 180 / ephem.pi # Check if we're close to target longitude (within 0.5 degrees) if abs(sun_longitude - target_longitude) < 0.5: return test_date # Fallback: return approximation based on solar longitude # Rough approximation: solar longitude increases ~1° per day days_from_equinox = target_longitude equinox_date = datetime(year, 3, 20) # Approximate spring equinox return equinox_date + timedelta(days=days_from_equinox) def calculate_moon_phase(date_obj: datetime) -> tuple[float, str]: """Calculate moon phase for a given date.""" observer = ephem.Observer() observer.date = date_obj moon = ephem.Moon(observer) moon.compute(observer) # Moon phase (0 = new moon, 0.5 = full moon, 1 = new moon again) phase = moon.phase / 100.0 # Determine moon phase name and viewing quality if phase < 0.1: phase_name = "New Moon" viewing_quality = "excellent" elif phase < 0.3: phase_name = "Waxing Crescent" viewing_quality = "good" elif phase < 0.7: phase_name = "First Quarter" if phase < 0.5 else "Waxing Gibbous" viewing_quality = "moderate" elif phase < 0.9: phase_name = "Full Moon" viewing_quality = "poor" else: phase_name = "Waning Crescent" viewing_quality = "good" return phase, f"{phase_name} ({viewing_quality} viewing)" def calculate_meteor_shower_data(year: int) -> list[MeteorShower]: """Calculate meteor shower data for a given year using astronomical calculations.""" meteor_data = [] for shower_id, shower_info in METEOR_SHOWERS.items(): # Calculate peak date based on solar longitude peak_longitude = shower_info["peak_solar_longitude"] assert isinstance(peak_longitude, (int, float)) peak_date = calculate_solar_longitude_date(year, peak_longitude) # Calculate activity period start_longitude = shower_info["activity_start"] assert isinstance(start_longitude, (int, float)) activity_start = calculate_solar_longitude_date(year, start_longitude) end_longitude = shower_info["activity_end"] assert isinstance(end_longitude, (int, float)) activity_end = calculate_solar_longitude_date(year, end_longitude) # Calculate moon phase at peak moon_illumination, moon_phase_desc = calculate_moon_phase(peak_date) # Format dates peak_formatted = peak_date.strftime("%B %d") if peak_date.day != (peak_date + timedelta(days=1)).day: peak_formatted += f"-{(peak_date + timedelta(days=1)).strftime('%d')}" active_formatted = ( f"{activity_start.strftime('%B %d')} - {activity_end.strftime('%B %d')}" ) # Determine viewing quality based on moon phase viewing_quality = ( "excellent" if moon_illumination < 0.3 else ( "good" if moon_illumination < 0.7 else "moderate" if moon_illumination < 0.9 else "poor" ) ) meteor_shower = { "name": shower_info["name"], "peak": peak_formatted, "active": active_formatted, "rate": f"{shower_info['rate_min']}-{shower_info['rate_max']} meteors per hour", "radiant": str(shower_info["radiant_ra"]).split("h")[0] + "h " + str(shower_info["radiant_dec"]), "moon_phase": moon_phase_desc, "visibility": shower_info["visibility"], "description": shower_info["description"], "peak_date": peak_date.strftime("%Y-%m-%d"), "start_date": activity_start.strftime("%Y-%m-%d"), "end_date": activity_end.strftime("%Y-%m-%d"), "rate_min": shower_info["rate_min"], "rate_max": shower_info["rate_max"], "moon_illumination": moon_illumination, "viewing_quality": viewing_quality, "parent_body": shower_info["parent_body"], "velocity_kms": shower_info["velocity_kms"], } meteor_data.append(meteor_shower) # Sort by peak date meteor_data.sort(key=lambda x: datetime.strptime(str(x["peak_date"]), "%Y-%m-%d")) return meteor_data def get_meteor_data(year: int | None = None) -> list[MeteorShower]: """Get meteor shower data for a specific year using astronomical calculations.""" if year is None: year = datetime.now().year return calculate_meteor_shower_data(year)