""" Core ephemeris calculation logic — shared by views and management commands. """ from django.conf import settings as django_settings import swisseph as swe DEFAULT_HOUSE_SYSTEM = 'O' # Porphyry SIGNS = [ 'Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces', ] SIGN_ELEMENT = { 'Aries': 'Fire', 'Leo': 'Fire', 'Sagittarius': 'Fire', 'Taurus': 'Earth', 'Virgo': 'Earth', 'Capricorn': 'Earth', 'Gemini': 'Air', 'Libra': 'Air', 'Aquarius': 'Air', 'Cancer': 'Water', 'Scorpio': 'Water', 'Pisces': 'Water', } ASPECTS = [ ('Conjunction', 0, 8.0), ('Sextile', 60, 6.0), ('Square', 90, 8.0), ('Trine', 120, 8.0), ('Opposition', 180, 10.0), ] PLANET_CODES = { '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, } def set_ephe_path(): ephe_path = getattr(django_settings, 'SWISSEPH_PATH', None) if ephe_path: swe.set_ephe_path(ephe_path) def get_sign(lon): return SIGNS[int(lon // 30) % 12] def get_julian_day(dt): return swe.julday( dt.year, dt.month, dt.day, dt.hour + dt.minute / 60 + dt.second / 3600, ) def get_planet_positions(jd): flag = swe.FLG_SWIEPH | swe.FLG_SPEED planets = {} for name, code in PLANET_CODES.items(): pos, _ = swe.calc_ut(jd, code, flag) degree = pos[0] planets[name] = { 'sign': get_sign(degree), 'degree': degree, 'retrograde': pos[3] < 0, } return planets def get_element_counts(planets): sign_counts = {s: 0 for s in SIGNS} counts = {'Fire': 0, 'Water': 0, 'Earth': 0, 'Air': 0} for data in planets.values(): sign = data['sign'] counts[SIGN_ELEMENT[sign]] += 1 sign_counts[sign] += 1 # Time: highest planet concentration in a single sign, minus 1 counts['Time'] = max(sign_counts.values()) - 1 # Space: longest consecutive run of occupied signs (circular), minus 1 indices = [i for i, s in enumerate(SIGNS) if sign_counts[s] > 0] max_seq = 0 for start in range(len(indices)): seq_len = 1 for offset in range(1, len(indices)): if (indices[start] + offset) % len(SIGNS) in indices: seq_len += 1 else: break max_seq = max(max_seq, seq_len) counts['Space'] = max_seq - 1 return counts def calculate_aspects(planets): """Return a list of aspects between all planet pairs. Each entry: {planet1, planet2, type, angle (actual, rounded), orb (rounded)}. Only the first matching aspect type is reported per pair (aspects are well-separated enough that at most one can apply with standard orbs). """ names = list(planets.keys()) aspects = [] for i, name1 in enumerate(names): for name2 in names[i + 1:]: deg1 = planets[name1]['degree'] deg2 = planets[name2]['degree'] angle = abs(deg1 - deg2) if angle > 180: angle = 360 - angle for aspect_name, target, max_orb in ASPECTS: orb = abs(angle - target) if orb <= max_orb: aspects.append({ 'planet1': name1, 'planet2': name2, 'type': aspect_name, 'angle': round(angle, 2), 'orb': round(orb, 2), }) break return aspects