from datetime import datetime, timedelta import swisseph as swe from typing import Dict, Any, List, Tuple import math class ChartCalculator: """Enhanced astrological calculations with modern planets and aspects""" def __init__(self): # Initialize Swiss Ephemeris swe.set_ephe_path() self.planets = { 'Sun': swe.SUN, 'Moon': swe.MOON, 'Mercury': swe.MERCURY, 'Venus': swe.VENUS, 'Mars': swe.MARS, 'Jupiter': swe.JUPITER, 'Saturn': swe.SATURN, 'Uranus': swe.URANUS, 'Neptune': swe.NEPTUNE, 'Pluto': swe.PLUTO, 'Chiron': swe.CHIRON, 'North Node': swe.MEAN_NODE } self.zodiac_signs = [ 'Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces' ] self.elements = { 'Fire': ['Aries', 'Leo', 'Sagittarius'], 'Earth': ['Taurus', 'Virgo', 'Capricorn'], 'Air': ['Gemini', 'Libra', 'Aquarius'], 'Water': ['Cancer', 'Scorpio', 'Pisces'] } self.qualities = { 'Cardinal': ['Aries', 'Cancer', 'Libra', 'Capricorn'], 'Fixed': ['Taurus', 'Leo', 'Scorpio', 'Aquarius'], 'Mutable': ['Gemini', 'Virgo', 'Sagittarius', 'Pisces'] } self.aspect_types = { 0: {'name': 'Conjunction', 'orb': 8, 'nature': 'Neutral'}, 60: {'name': 'Sextile', 'orb': 6, 'nature': 'Harmonious'}, 90: {'name': 'Square', 'orb': 8, 'nature': 'Challenging'}, 120: {'name': 'Trine', 'orb': 8, 'nature': 'Harmonious'}, 180: {'name': 'Opposition', 'orb': 8, 'nature': 'Challenging'} } def _normalize_longitude(self, lon: float) -> float: """Normalize longitude to 0-360 range""" return lon % 360 def _get_zodiac_sign(self, longitude: float) -> str: """Get zodiac sign from longitude""" sign_num = int(self._normalize_longitude(longitude) / 30) return self.zodiac_signs[sign_num] def _get_sign_element(self, sign: str) -> str: """Get the element of a zodiac sign""" for element, signs in self.elements.items(): if sign in signs: return element return "Unknown" def _get_sign_quality(self, sign: str) -> str: """Get the quality (modality) of a zodiac sign""" for quality, signs in self.qualities.items(): if sign in signs: return quality return "Unknown" def calculate_birth_chart(self, birth_datetime: datetime, latitude: float, longitude: float) -> Dict[str, Any]: """Calculate comprehensive birth chart including modern planets""" try: # Convert to Julian day julian_day = swe.julday( birth_datetime.year, birth_datetime.month, birth_datetime.day, birth_datetime.hour + birth_datetime.minute/60.0 ) # Calculate houses (Placidus system) houses = swe.houses(julian_day, latitude, longitude)[0] ascendant = houses[0] midheaven = houses[9] # Calculate planetary positions positions = {} for planet_name, planet_id in self.planets.items(): try: result = swe.calc_ut(julian_day, planet_id) lon = self._normalize_longitude(result[0]) sign = self._get_zodiac_sign(lon) positions[planet_name] = { 'longitude': lon, 'latitude': result[1], 'distance': result[2], 'speed': result[3], # Negative means retrograde 'sign': sign, 'element': self._get_sign_element(sign), 'quality': self._get_sign_quality(sign), 'retrograde': result[3] < 0 } # Calculate house placement for i in range(12): house_start = self._normalize_longitude(houses[i]) house_end = self._normalize_longitude(houses[(i + 1) % 12]) if (house_start <= lon < house_end) or \ (house_start > house_end and (lon >= house_start or lon < house_end)): positions[planet_name]['house'] = i + 1 break except Exception as e: positions[planet_name] = {'error': str(e)} # Calculate aspects aspects = self._calculate_aspects(positions) # Calculate element and modality balances element_balance = self._calculate_element_balance(positions) modality_balance = self._calculate_modality_balance(positions) return { 'planets': positions, 'houses': { 'ascendant': { 'longitude': ascendant, 'sign': self._get_zodiac_sign(ascendant) }, 'midheaven': { 'longitude': midheaven, 'sign': self._get_zodiac_sign(midheaven) }, 'cusps': [self._normalize_longitude(h) for h in houses] }, 'aspects': aspects, 'patterns': { 'elements': element_balance, 'modalities': modality_balance } } except Exception as e: return {'error': f"Failed to calculate birth chart: {str(e)}"} def _calculate_aspects(self, positions: Dict[str, Any]) -> List[Dict[str, Any]]: """Calculate planetary aspects""" aspects = [] planet_list = list(positions.keys()) for i in range(len(planet_list)): for j in range(i + 1, len(planet_list)): planet1 = planet_list[i] planet2 = planet_list[j] if 'error' in positions[planet1] or 'error' in positions[planet2]: continue lon1 = positions[planet1]['longitude'] lon2 = positions[planet2]['longitude'] # Calculate angular separation diff = abs(lon1 - lon2) if diff > 180: diff = 360 - diff # Check for aspects for angle, aspect_info in self.aspect_types.items(): orb = aspect_info['orb'] if abs(diff - angle) <= orb: aspects.append({ 'planet1': planet1, 'planet2': planet2, 'aspect': aspect_info['name'], 'orb': round(abs(diff - angle), 2), 'nature': aspect_info['nature'], 'applying': positions[planet1].get('speed', 0) > positions[planet2].get('speed', 0) }) return aspects def _calculate_element_balance(self, positions: Dict[str, Any]) -> Dict[str, int]: """Calculate the distribution of elements""" balance = {'Fire': 0, 'Earth': 0, 'Air': 0, 'Water': 0} for planet_data in positions.values(): if 'element' in planet_data: balance[planet_data['element']] += 1 return balance def _calculate_modality_balance(self, positions: Dict[str, Any]) -> Dict[str, int]: """Calculate the distribution of modalities""" balance = {'Cardinal': 0, 'Fixed': 0, 'Mutable': 0} for planet_data in positions.values(): if 'quality' in planet_data: balance[planet_data['quality']] += 1 return balance def get_chart_analysis(self, chart_data: Dict[str, Any]) -> Dict[str, Any]: """Generate a comprehensive chart analysis""" if 'error' in chart_data: return {'error': chart_data['error']} analysis = { 'dominant_element': max(chart_data['patterns']['elements'].items(), key=lambda x: x[1])[0], 'dominant_modality': max(chart_data['patterns']['modalities'].items(), key=lambda x: x[1])[0], 'retrograde_planets': [ planet for planet, data in chart_data['planets'].items() if data.get('retrograde', False) ], 'aspect_patterns': { 'harmonious': len([a for a in chart_data['aspects'] if a['nature'] == 'Harmonious']), 'challenging': len([a for a in chart_data['aspects'] if a['nature'] == 'Challenging']), 'neutral': len([a for a in chart_data['aspects'] if a['nature'] == 'Neutral']) } } return analysis