Source code for stellium.engines.dignities

"""Dignity calculation engines.

This module provides comprehensive essential dignity calculations for both
traditional (pre-1781) and modern astrological systems. It evaluates planetary
strength through rulership, exaltation, detriment, fall, triplicity, terms/bounds,
faces/decans, and mutual reception.
"""

from typing import Any

from stellium.core.models import CelestialPosition

DIGNITIES = {
    "Aries": {
        "symbol": "♈︎",
        "element": "Fire",
        "modality": "Cardinal",
        "exaltation_degree": 19,
        "traditional": {
            "ruler": "Mars",
            "exaltation": "Sun",
            "detriment": "Venus",
            "fall": "Saturn",
        },
        "modern": {
            "ruler": "Mars",
            "exaltation": "Sun",
            "detriment": "Venus",
            "fall": "Saturn",
        },
        "decan_trip": ["Mars", "Sun", "Jupiter"],
        "decan_chaldean": ["Mars", "Sun", "Venus"],
        "bound_egypt": {  # key is start of the planet's domicile degrees.
            0: "Jupiter",
            6: "Venus",
            12: "Mercury",
            20: "Mars",
            25: "Saturn",
        },
        "triplicity": {"day": "Sun", "night": "Jupiter", "coop": "Saturn"},
    },
    "Taurus": {
        "symbol": "♉︎",
        "element": "Earth",
        "modality": "Fixed",
        "exaltation_degree": 3,
        "traditional": {
            "ruler": "Venus",
            "exaltation": "Moon",
            "detriment": "Mars",
            "fall": None,
        },
        "modern": {
            "ruler": "Venus",
            "exaltation": "Moon",
            "detriment": "Pluto",
            "fall": "Uranus",
        },
        "decan_trip": ["Venus", "Mercury", "Saturn"],
        "decan_chaldean": ["Mercury", "Moon", "Saturn"],
        "bound_egypt": {  # key is start of the planet's domicile degrees.
            0: "Venus",
            8: "Mercury",
            14: "Jupiter",
            22: "Saturn",
            27: "Mars",
        },
        "triplicity": {"day": "Venus", "night": "Moon", "coop": "Mars"},
    },
    "Gemini": {
        "symbol": "♊︎",
        "element": "Air",
        "modality": "Mutable",
        "exaltation_degree": 3,
        "traditional": {
            "ruler": "Mercury",
            "exaltation": "North Node",
            "detriment": "Jupiter",
            "fall": "South Node",
        },
        "modern": {
            "ruler": "Mercury",
            "exaltation": "Mercury",
            "detriment": "Jupiter",
            "fall": "Venus",
        },
        "decan_trip": ["Mercury", "Venus", "Saturn"],
        "decan_chaldean": ["Jupiter", "Mars", "Sun"],
        "bound_egypt": {  # key is start of the planet's domicile degrees.
            0: "Mercury",
            6: "Jupiter",
            12: "Venus",
            17: "Mars",
            24: "Saturn",
        },
        "triplicity": {"day": "Saturn", "night": "Mercury", "coop": "Jupiter"},
    },
    "Cancer": {
        "symbol": "♋︎",
        "element": "Water",
        "modality": "Cardinal",
        "exaltation_degree": 15,
        "traditional": {
            "ruler": "Moon",
            "exaltation": "Jupiter",
            "detriment": "Saturn",
            "fall": "Mars",
        },
        "modern": {
            "ruler": "Moon",
            "exaltation": "Jupiter",
            "detriment": "Saturn",
            "fall": "Pluto",
        },
        "decan_trip": ["Moon", "Mars", "Jupiter"],
        "decan_chaldean": ["Venus", "Mercury", "Moon"],
        "bound_egypt": {
            0: "Mars",
            7: "Venus",
            13: "Mercury",
            19: "Jupiter",
            26: "Saturn",
        },
        "triplicity": {"day": "Mars", "night": "Venus", "coop": "Moon"},
    },
    "Leo": {
        "symbol": "♌︎",
        "element": "Fire",
        "modality": "Fixed",
        "exaltation_degree": None,
        "traditional": {
            "ruler": "Sun",
            "exaltation": "None",
            "detriment": "Saturn",
            "fall": "None",
        },
        "modern": {
            "ruler": "Sun",
            "exaltation": "Neptune",
            "detriment": "Uranus",
            "fall": "Pluto",
        },
        "decan_trip": ["Sun", "Jupiter", "Mars"],
        "decan_chaldean": ["Saturn", "Jupiter", "Mars"],
        "bound_egypt": {
            0: "Jupiter",
            6: "Venus",
            11: "Saturn",
            18: "Mercury",
            24: "Mars",
        },
        "triplicity": {"day": "Sun", "night": "Jupiter", "coop": "Saturn"},
    },
    "Virgo": {
        "symbol": "♍︎",
        "element": "Earth",
        "modality": "Mutable",
        "exaltation_degree": 15,
        "traditional": {
            "ruler": "Mercury",
            "exaltation": "Mercury",
            "detriment": "Jupiter",
            "fall": "Venus",
        },
        "modern": {
            "ruler": "Mercury",
            "exaltation": "Mercury",
            "detriment": "Neptune",
            "fall": "Venus",
        },
        "decan_trip": ["Mercury", "Saturn", "Venus"],
        "decan_chaldean": ["Sun", "Venus", "Mercury"],
        "bound_egypt": {
            0: "Mercury",
            7: "Venus",
            17: "Jupiter",
            21: "Mars",
            28: "Saturn",
        },
        "triplicity": {"day": "Venus", "night": "Moon", "coop": "Mars"},
    },
    "Libra": {
        "symbol": "♎︎",
        "element": "Air",
        "modality": "Cardinal",
        "exaltation_degree": 21,
        "traditional": {
            "ruler": "Venus",
            "exaltation": "Saturn",
            "detriment": "Mars",
            "fall": "Sun",
        },
        "modern": {
            "ruler": "Venus",
            "exaltation": "Saturn",
            "detriment": "Mars",
            "fall": "Sun",
        },
        "decan_trip": ["Venus", "Saturn", "Jupiter"],
        "decan_chaldean": ["Moon", "Saturn", "Jupiter"],
        "bound_egypt": {
            0: "Saturn",
            6: "Mercury",
            14: "Jupiter",
            21: "Venus",
            28: "Mars",
        },
        "triplicity": {"day": "Saturn", "night": "Mercury", "coop": "Jupiter"},
    },
    "Scorpio": {
        "symbol": "♏︎",
        "element": "Water",
        "modality": "Fixed",
        "exaltation_degree": None,
        "traditional": {
            "ruler": "Mars",
            "exaltation": "None",
            "detriment": "Venus",
            "fall": "Moon",
        },
        "modern": {
            "ruler": "Pluto",
            "exaltation": "Uranus",
            "detriment": "Venus",
            "fall": "Moon",
        },
        "decan_trip": ["Mars", "Sun", "Venus"],
        "decan_chaldean": ["Mars", "Sun", "Venus"],
        "bound_egypt": {
            0: "Mars",
            7: "Venus",
            11: "Mercury",
            19: "Jupiter",
            24: "Saturn",
        },
        "triplicity": {"day": "Mars", "night": "Venus", "coop": "Moon"},
    },
    "Sagittarius": {
        "symbol": "♐︎",
        "element": "Fire",
        "modality": "Mutable",
        "exaltation_degree": 3,
        "traditional": {
            "ruler": "Jupiter",
            "exaltation": "South Node",
            "detriment": "Mercury",
            "fall": "North Node",
        },
        "modern": {
            "ruler": "Jupiter",
            "exaltation": "Venus",
            "detriment": "Mercury",
            "fall": "Ceres",
        },
        "decan_trip": ["Jupiter", "Mars", "Sun"],
        "decan_chaldean": ["Mercury", "Moon", "Saturn"],
        "bound_egypt": {
            0: "Jupiter",
            12: "Venus",
            17: "Mercury",
            21: "Mars",
            26: "Saturn",
        },
        "triplicity": {"day": "Sun", "night": "Jupiter", "coop": "Saturn"},
    },
    "Capricorn": {
        "symbol": "♑︎",
        "element": "Earth",
        "modality": "Cardinal",
        "exaltation_degree": 27,
        "traditional": {
            "ruler": "Saturn",
            "exaltation": "Mars",
            "detriment": "Moon",
            "fall": "Jupiter",
        },
        "modern": {
            "ruler": "Saturn",
            "exaltation": "Mars",
            "detriment": "Moon",
            "fall": "Jupiter",
        },
        "decan_trip": ["Saturn", "Venus", "Mercury"],
        "decan_chaldean": ["Jupiter", "Mars", "Sun"],
        "bound_egypt": {
            0: "Mercury",
            7: "Jupiter",
            14: "Venus",
            22: "Saturn",
            26: "Mars",
        },
        "triplicity": {"day": "Venus", "night": "Moon", "coop": "Mars"},
    },
    "Aquarius": {
        "symbol": "♒︎",
        "element": "Air",
        "modality": "Fixed",
        "exaltation_degree": None,
        "traditional": {
            "ruler": "Saturn",
            "exaltation": "None",
            "detriment": "Sun",
            "fall": "None",
        },
        "modern": {
            "ruler": "Uranus",
            "exaltation": "Pluto",
            "detriment": "Sun",
            "fall": "Neptune",
        },
        "decan_trip": ["Saturn", "Mercury", "Venus"],
        "decan_chaldean": ["Mars", "Sun", "Venus"],
        "bound_egypt": {
            0: "Mercury",
            7: "Venus",
            13: "Jupiter",
            20: "Saturn",
            25: "Mars",
        },
        "triplicity": {"day": "Saturn", "night": "Mercury", "coop": "Jupiter"},
    },
    "Pisces": {
        "symbol": "♓︎",
        "element": "Water",
        "modality": "Mutable",
        "exaltation_degree": 27,
        "traditional": {
            "ruler": "Jupiter",
            "exaltation": "Venus",
            "detriment": "Mercury",
            "fall": "Ceres",
        },
        "modern": {
            "ruler": "Neptune",
            "exaltation": "Venus",
            "detriment": "Mercury",
            "fall": "Ceres",
        },
        "decan_trip": ["Jupiter", "Mars", "Moon"],
        "decan_chaldean": ["Saturn", "Jupiter", "Mars"],
        "bound_egypt": {
            0: "Venus",
            12: "Jupiter",
            16: "Mercury",
            19: "Mars",
            28: "Saturn",
        },
        "triplicity": {"day": "Mars", "night": "Venus", "coop": "Moon"},
    },
}


[docs] class TraditionalDignityCalculator: """ Traditional essential dignities calculator (pre-1781). Uses only the seven traditional planets (Sun through Saturn) and calculates dignities according to classical astrological principles. Scoring system: - Domicile/Rulership: +5 points - Exaltation: +4 points - Triplicity ruler: +3 points - Term/Bound ruler: +2 points - Face/Decan ruler: +1 point - Detriment: -5 points - Fall: -4 points - Peregrine (no dignities): 0 points """ TRADITIONAL_PLANETS = [ "Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", ] def __init__(self, decans: str = "chaldean") -> None: """ Initialize traditional dignity calculator. Args: decans: Can be either "chaldean" or "triplicity". """ if decans not in ("chaldean", "triplicity"): raise ValueError( f"Decans must be either 'chaldean' or 'triplicity', got {decans}" ) self.decans = decans @property def calculator_name(self) -> str: """Name of this calculator""" return "Traditional Essential Dignities"
[docs] def calculate_dignities( self, position: CelestialPosition, sect: str | None = "day" ) -> dict[str, Any]: """Calculate traditional dignities for a position. Args: position: CelestialPosition to analyze sect: Chart sect. Can be "day" or "night" (defaults to day) Returns: Dictionary with comprehensive dignity information including: - dignities: List of dignity types held - score: Total dignity score - details: Breakdown of each dignity - is_peregrine: Whether planet is peregrine (no dignities) - reception_potential: Planets this one could have mutual reception with """ if sect not in ("day", "night"): raise ValueError(f"Chart sect must be 'day' or 'night': Got {sect}") # Only traditional planets have dignities if position.name not in self.TRADITIONAL_PLANETS: return { "planet": position.name, "sign": position.sign, "system": "traditional", "note": "Not a traditional planet - no dignities calculated", } sign_data = DIGNITIES.get(position.sign, {}) trad_data = sign_data.get("traditional", {}) dignities = [] score = 0 details = {} # Rulership (+5) if trad_data.get("ruler") == position.name: dignities.append("domicile") score += 5 details["domicile"] = { "value": 5, "description": f"{position.name} rules {position.sign}", } # 2. EXALTATION (+4) exaltation = trad_data.get("exaltation") if exaltation == position.name: dignities.append("exaltation") score += 4 exalt_degree = sign_data.get("exaltation_degree") # Bonus if within 5° of exact exaltation degree exact_bonus = 0 if exalt_degree is not None: distance = abs(position.sign_degree - exalt_degree) if distance <= 5: exact_bonus = 1 score += 1 dignities.append("exaltation_exact") details["exaltation"] = { "value": 4 + exact_bonus, "description": f"{position.name} exalted in {position.sign}", "exact_degree": exalt_degree, "distance_from_exact": abs(position.sign_degree - exalt_degree) if exalt_degree else None, } # TRIPLICITY (+3) triplicity_data = sign_data.get("triplicity", {}) triplicity_ruler = triplicity_data.get(sect) participating_ruler = triplicity_data.get("coop") if triplicity_ruler == position.name: dignities.append("triplicity_ruler") score += 3 details["triplicity"] = { "value": 3, "description": f"{position.name} is {sect} triplicity ruler of {sign_data.get('element')} signs", "element": sign_data.get("element"), } elif participating_ruler == position.name: dignities.append("participating_ruler") score += 2 details["triplicity"] = { "value": 2, "description": f"{position.name} is participating triplicity ruler of {sign_data.get('element')} signs", "element": sign_data.get("element"), } # TERMS/BOUNDS (+2) bounds = sign_data.get("bound_egypt", {}) term_ruler = self._find_bound_ruler(position.sign_degree, bounds) if term_ruler == position.name: dignities.append("term") score += 2 details["term"] = { "value": 2, "description": f"{position.name} rules the term/bound at {position.sign_degree:.1f}° {position.sign}", } # FACE/DECAN (+1) decan_key = f"decan_{self.decans}" decans = sign_data.get(decan_key, []) decan_index = int( position.sign_degree // 10 ) # 0-9° = 0, 10-19° = 1, 20-29° = 2 if 0 <= decan_index < len(decans): decan_ruler = decans[decan_index] if decan_ruler == position.name: dignities.append("decan") score += 1 details["decan"] = { "value": 1, "description": f"{position.name} rules the {self.decans.title()} decan at {position.sign_degree:.1f}° {position.sign}", "decan_number": decan_index + 1, } # DETRIMENT (-5) detriment = trad_data.get("detriment") if detriment == position.name: dignities.append("detriment") score -= 5 details["detriment"] = { "value": -5, "description": f"{position.name} in detriment in {position.sign}", } # FALL (-4) fall = trad_data.get("fall") if fall == position.name: dignities.append("fall") score -= 4 details["fall"] = { "value": -4, "description": f"{position.name} in fall in {position.sign}", } # PEREGRINE (0) positive_dignities = [d for d in dignities if d not in ["detriment", "fall"]] is_peregrine = len(positive_dignities) == 0 if is_peregrine: dignities.append("peregrine") details["peregrine"] = { "value": 0, "description": f"{position.name} is peregrine (no essential dignities) in {position.sign}", } # MUTUAL RECEPTION POTENTIAL # Check which planets this one could have mutual reception with reception_potential = self._check_reception_potential(position, sign_data) return { "planet": position.name, "sign": position.sign, "degree": position.sign_degree, "dignities": dignities, "system": "traditional", "score": score, "details": details, "is_peregrine": is_peregrine, "receiption_potential": reception_potential, "interpretation": self._interpret_score(score, dignities), }
def _find_bound_ruler( self, sign_degree: float, bounds: dict[int, str] ) -> str | None: """Find which planet rules the term/bound at a given degree.""" sorted_bounds = sorted(bounds.items()) for i, (start_degree, ruler) in enumerate(sorted_bounds): # Find the end of this bound if i < len(sorted_bounds) - 1: end_degree = sorted_bounds[i + 1][0] else: end_degree = 30 # End of sign if start_degree <= sign_degree < end_degree: return ruler return None def _check_reception_potential( self, position: CelestialPosition, sign_data: dict ) -> dict[str, list[str]]: """ Check for mutual reception potential. A planet can have mutual reception by: - Rulership: Two planets in each other's domiciles - Exaltation: Two planets in each other's exaltation signs - Mixed: One planet in the other's domicile while that planet is in the first's exaltation """ potential = { "by_domicile": [], "by_exaltation": [], "mixed": [], } traditional_data = sign_data.get("traditional", {}) # Find which sign(s) this planet rules # my_domicile_signs = [ # sign # for sign, data in DIGNITIES.items() # if data.get("traditional", {}).get("ruler") == position.name # ] # # Find which sign(s) this planet is exalted in # my_exaltation_signs = [ # sign # for sign, data in DIGNITIES.items() # if data.get("traditional", {}).get("exaltation") == position.name # ] # Who rules/is exalted in my current sign? current_ruler = traditional_data.get("ruler") current_exaltation = traditional_data.get("exaltation") if current_ruler and current_ruler in self.TRADITIONAL_PLANETS: potential["by_domicile"].append(current_ruler) if current_exaltation and current_exaltation in self.TRADITIONAL_PLANETS: potential["by_exaltation"].append(current_exaltation) # Check for mixed reception if current_exaltation != current_ruler: potential["mixed"].append(current_exaltation) return potential def _interpret_score(self, score: int, dignities: list[str]) -> str: """Provide a human-readable interpretation of the dignity score.""" if "peregrine" in dignities: return "Peregrine - planet lacks essential dignity and may be weakened" elif score >= 5: return "Very strong - planet has major essential dignity" elif score >= 3: return "Strong - planet has significant dignity" elif score >= 1: return "Moderately dignified - planet has minor dignity" elif score == 0: return "Neutral - no significant dignity or debility" elif score >= -3: return "Moderately challenged - planet has minor debility" else: return "Significantly challenged - planet has major debility"
[docs] class ModernDignityCalculator: """ Modern essential dignities calculator (post-1781). Includes outer planets (Uranus, Neptune, Pluto) and uses modern rulership assignments. The scoring system is adapted for modern rulerships while maintaining traditional dignity principles. Scoring system: - Domicile/Rulership: +5 points (modern ruler), +3 points (traditional ruler) - Exaltation: +4 points - Triplicity ruler: +3 points - Term/Bound ruler: +2 points - Face/Decan ruler: +1 point - Detriment: -5 points (modern), -3 points (traditional) - Fall: -4 points """ MODERN_PLANETS = [ "Sun", "Moon", "Mercury", "Venus", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", "Pluto", ] def __init__(self, decans: str = "chaldean") -> None: """ Initialize modern dignity calculator. Args: decans: Can be either "chaldean" or "triplicity". """ if decans not in ("chaldean", "triplicity"): raise ValueError( f"Decans must be either 'chaldean' or 'triplicity', got {decans}" ) self.decans = decans @property def calculator_name(self) -> str: """Name of this calculator.""" return "Modern Essential Dignities"
[docs] def calculate_dignities( self, position: CelestialPosition, sect: str = "day", ) -> dict[str, Any]: """ Calculate modern dignities for a position. Args: position: CelestialPosition to analyze is_day_chart: Whether this is a day chart (Sun above horizon). Affects triplicity ruler selection. Returns: Dictionary with comprehensive dignity information. """ if sect not in ("day", "night"): raise ValueError(f"Chart sect must be 'day' or 'night': Got {sect}") sign_data = DIGNITIES.get(position.sign, {}) modern_data = sign_data.get("modern", {}) traditional_data = sign_data.get("traditional", {}) dignities = [] score = 0 details = {} # DOMICILE/RULERSHIP modern_ruler = modern_data.get("ruler") traditional_ruler = traditional_data.get("ruler") if modern_ruler == position.name: dignities.append("domicile_modern") score += 5 details["domicile"] = { "value": 5, "description": f"{position.name} rules {position.sign} (modern rulership)", "type": "modern", } elif traditional_ruler == position.name and traditional_ruler != modern_ruler: # Traditional ruler gets partial credit in signs with modern rulers dignities.append("domicile_traditional") score += 3 details["domicile"] = { "value": 3, "description": f"{position.name} rules {position.sign} (traditional co-ruler)", "type": "traditional", } # EXALTATION (+4) exaltation = modern_data.get("exaltation") if exaltation == position.name: dignities.append("exaltation") score += 4 exalt_degree = sign_data.get("exaltation_degree") # Bonus if within 5 degrees of exact exaltation degree exact_bonus = 0 if exalt_degree is not None: distance = abs(position.sign_degree - exalt_degree) if distance <= 5: exact_bonus = 1 score += 1 dignities.append("exaltation_exact") details["exaltation"] = { "value": 4 + exact_bonus, "description": f"{position.name} exalted in {position.sign}", "exact_degree": exalt_degree, "distance_from_exact": abs(position.sign_degree - exalt_degree) if exalt_degree else None, } # TRIPLICITY (+3) - Uses traditional triplicity rulers triplicity_data = sign_data.get("triplicity", {}) triplicity_ruler = triplicity_data.get(sect) participating_ruler = triplicity_data.get("coop") if triplicity_ruler == position.name: dignities.append("triplicity_ruler") score += 3 details["triplicity"] = { "value": 3, "description": f"{position.name} is {sect} triplicity ruler of {sign_data.get('element')} signs", "element": sign_data.get("element"), } elif participating_ruler == position.name: dignities.append("triplicity_participating") score += 2 details["triplicity"] = { "value": 2, "description": f"{position.name} is participating triplicity ruler of {sign_data.get('element')} signs", "element": sign_data.get("element"), } # TERM/BOUND (+2) - Only for traditional planets if position.name in TraditionalDignityCalculator.TRADITIONAL_PLANETS: bounds = sign_data.get("bound_egypt", {}) term_ruler = self._find_bound_ruler(position.sign_degree, bounds) if term_ruler == position.name: dignities.append("term") score += 2 details["term"] = { "value": 2, "description": f"{position.name} rules the term/bound at {position.sign_degree:.1f}° {position.sign}", } # FACE/DECAN (+1) - Only for traditional planets if position.name in TraditionalDignityCalculator.TRADITIONAL_PLANETS: decan_key = f"decan_{self.decans}" decans = sign_data.get(decan_key, []) decan_index = int(position.sign_degree // 10) if 0 <= decan_index < len(decans): decan_ruler = decans[decan_index] if decan_ruler == position.name: dignities.append("face") score += 1 details["face"] = { "value": 1, "description": f"{position.name} rules the {self.decans.title()} decan at {position.sign_degree:.1f}° {position.sign}", "decan_number": decan_index + 1, } # DETRIMENT modern_detriment = modern_data.get("detriment") traditional_detriment = traditional_data.get("detriment") if modern_detriment == position.name: dignities.append("detriment_modern") score -= 5 details["detriment"] = { "value": -5, "description": f"{position.name} in detriment in {position.sign} (modern)", "type": "modern", } elif ( traditional_detriment == position.name and traditional_detriment != modern_detriment ): dignities.append("detriment_traditional") score -= 3 details["detriment"] = { "value": -3, "description": f"{position.name} in detriment in {position.sign} (traditional)", "type": "traditional", } # 7. FALL modern_fall = modern_data.get("fall") if modern_fall == position.name: dignities.append("fall") score -= 4 details["fall"] = { "value": -4, "description": f"{position.name} in fall in {position.sign}", } # 8. PEREGRINE positive_dignities = [ d for d in dignities if not d.startswith(("detriment", "fall")) ] is_peregrine = len(positive_dignities) == 0 if is_peregrine: dignities.append("peregrine") details["peregrine"] = { "value": 0, "description": f"{position.name} is peregrine (no essential dignities) in {position.sign}", } # 9. MODERN CONSIDERATIONS # Outer planets get a dignity bonus for being in their traditional domains outer_planet_affinity = self._check_outer_planet_affinity(position) if outer_planet_affinity: dignities.append("generational_affinity") score += 1 details["generational_affinity"] = outer_planet_affinity return { "planet": position.name, "sign": position.sign, "degree": position.sign_degree, "system": "modern", "dignities": dignities, "score": score, "details": details, "is_peregrine": is_peregrine, "interpretation": self._interpret_score(score, dignities, position.name), }
def _find_bound_ruler(self, degree: float, bounds: dict[int, str]) -> str | None: """Find which planet rules the term/bound at a given degree.""" sorted_bounds = sorted(bounds.items()) for i, (start_degree, ruler) in enumerate(sorted_bounds): if i < len(sorted_bounds) - 1: end_degree = sorted_bounds[i + 1][0] else: end_degree = 30 if start_degree <= degree < end_degree: return ruler return None def _check_outer_planet_affinity( self, position: CelestialPosition ) -> dict[str, Any] | None: """ Check if outer planets have affinity with sign characteristics. Modern interpretation: outer planets express well in signs that share their archetypal qualities, even without formal dignity. """ affinities = { "Uranus": { "signs": ["Aquarius", "Gemini", "Libra"], # Air signs "reason": "Air signs support Uranian innovation and intellectual freedom", }, "Neptune": { "signs": ["Pisces", "Cancer", "Scorpio"], # Water signs "reason": "Water signs support Neptunian sensitivity and transcendence", }, "Pluto": { "signs": ["Scorpio", "Capricorn"], # Transformative signs "reason": "These signs support Plutonian depth and transformation", }, } if position.name in affinities: affinity_data = affinities[position.name] if position.sign in affinity_data["signs"]: return { "value": 1, "description": affinity_data["reason"], } return None def _interpret_score( self, score: int, dignities: list[str], planet_name: str ) -> str: """Provide a human-readable interpretation of the dignity score.""" is_outer = planet_name in ["Uranus", "Neptune", "Pluto"] if "peregrine" in dignities: if is_outer: return "Peregrine - outer planet operates through generational themes" return "Peregrine - planet lacks essential dignity and may be weakened" elif score >= 5: return "Very strong - planet has major essential dignity" elif score >= 3: return "Strong - planet has significant dignity" elif score >= 1: return "Moderately dignified - planet has minor dignity" elif score == 0: return "Neutral - no significant dignity or debility" elif score >= -3: return "Moderately challenged - planet has minor debility" else: return "Significantly challenged - planet has major debility"
[docs] class MutualReceptionAnalyzer: """ Analyze mutual reception between planets in a chart. Mutual reception occurs when two planets are each in a sign ruled or exalted by the other. This creates a special bond and can modify the expression of both planets. """ def __init__(self, system: str = "traditional"): """ Initialize mutual reception analyzer. Args: system: Can be "modern" or "traditional" """ if system not in ("modern", "traditional"): raise ValueError( f"Mutual reception system must be 'modern' or 'traditional': got {system}" ) self.system = system
[docs] def find_mutual_receptions( self, positions: list[CelestialPosition] ) -> list[dict[str, Any]]: """ Find all mutual receptions in a set of positions. Args: positions: List of CelestialPosition objects to analyze Returns: List of mutual reception dictionaries with details """ receptions = [] # Check each pair of planets for i, pos1 in enumerate(positions): for pos2 in positions[i + 1 :]: # Check mutual reception by domicile sign1_data = DIGNITIES.get(pos1.sign, {}) sign2_data = DIGNITIES.get(pos2.sign, {}) ruler1 = sign1_data.get(self.system, {}).get("ruler") ruler2 = sign2_data.get(self.system, {}).get("ruler") if ruler1 == pos2.name and ruler2 == pos1.name: receptions.append( { "type": "mutual_reception_domicile", "planet1": pos1.name, "planet2": pos2.name, "planet1_sign": pos1.sign, "planet2_sign": pos2.sign, "strength": "strong", "description": f"{pos1.name} in {pos1.sign} and {pos2.name} in {pos2.sign} are in mutual reception by domicile", } ) # Check mutual reception by exaltation exalt1 = sign1_data.get(self.system, {}).get("exaltation") exalt2 = sign2_data.get(self.system, {}).get("exaltation") if exalt1 == pos2.name and exalt2 == pos1.name: receptions.append( { "type": "mutual_reception_exaltation", "planet1": pos1.name, "planet2": pos2.name, "planet1_sign": pos1.sign, "planet2_sign": pos2.sign, "strength": "moderate", "description": f"{pos1.name} and {pos2.name} are in mutual reception by exaltation", } ) # Check mixed reception (one by domicile, one by exaltation) if (ruler1 == pos2.name and exalt2 == pos1.name) or ( ruler2 == pos1.name and exalt1 == pos2.name ): receptions.append( { "type": "mutual_reception_mixed", "planet1": pos1.name, "planet2": pos2.name, "planet1_sign": pos1.sign, "planet2_sign": pos2.sign, "strength": "moderate", "description": f"{pos1.name} and {pos2.name} are in mixed mutual reception", } ) return receptions