Source code for stellium.components.midpoints

"""
Midpoint calculator component.

Midpoints are the halfway point between two celestial objects.
They represent the synthesis or blend of two planetary energies.

In midpoint astrology:
- Direct midpoint: Shortest arc between two points
- Indirect midpoint: Opposite point (180° from direct)

Both are significant, but direct midpoint is more commonly used.
"""

from stellium.core.models import (
    CelestialPosition,
    ChartDateTime,
    ChartLocation,
    HouseCusps,
    MidpointPosition,
    ObjectType,
)


[docs] class MidpointCalculator: """ Calculate midpoints between celestial objects. Midpoints reveal how two planetary energies blend or interact. They're used extensively in Uranian astrology and some modern approaches. """ # Common midpoint pairs used in traditional interpretation DEFAULT_PAIRS = [ # === Core Identity Axes === # These are the "big four" of the chart and their combinations. ("Sun", "Moon"), ("Sun", "ASC"), ("Sun", "MC"), ("Moon", "ASC"), ("Moon", "MC"), ("ASC", "MC"), # === Personality Expression === # How the core identity (Sun/Moon) blends with # thought (Mercury), love/values (Venus), and drive (Mars). ("Sun", "Mercury"), ("Sun", "Venus"), ("Sun", "Mars"), ("Moon", "Mercury"), ("Moon", "Venus"), ("Moon", "Mars"), # === Inner Planet Dynamics === # The key blends for love, communication, and action. ("Mercury", "Venus"), ("Mercury", "Mars"), ("Venus", "Mars"), # === Key Social & Structural Points === # How the personal planets interact with opportunity (Jupiter) # and structure/limitation (Saturn). ("Sun", "Jupiter"), ("Sun", "Saturn"), ("Moon", "Jupiter"), ("Moon", "Saturn"), # These two are classic "action" and "structure" midpoints. ("Mars", "Jupiter"), ("Mars", "Saturn"), # The "great benefics" and "social cycle" midpoints. ("Venus", "Jupiter"), ("Jupiter", "Saturn"), ] def __init__( self, pairs: list[tuple[str, str]] | None = None, calculate_all: bool = False, indirect: bool = False, ) -> None: """ Initialize midpoint calculator. Args: pairs: Specific pairs to calculate (None=use defaults) calculate_all: Calculate all planet pairs (overrides `pairs`) indirect: Also calculate indirect midpoints (180 degrees opposite) """ self._pairs = pairs or self.DEFAULT_PAIRS self._calculate_all = calculate_all self._include_indirect = indirect @property def component_name(self) -> str: return "Midpoints"
[docs] def calculate( self, datetime: ChartDateTime, location: ChartLocation, positions: list[CelestialPosition], house_systems_map: dict[str, HouseCusps], house_placements_map: dict[str, dict[str, int]], ) -> list[CelestialPosition]: """ Calculate midpoints. Args: datetime: Chart datetime (unused) location: Chart location (unused) positions: Already calculated positions house_systems_map: House cusps for house assignment (unused) house_placements_map: (unused) Returns: List of CelestialPosition objects for midpoints """ # Build position lookup pos_dict = {p.name: p for p in positions} # Determine which pairs to calculate if self._calculate_all: # All planet-to-planet and planet-to-node pairs valid_objects = [ p for p in positions if p.object_type in (ObjectType.PLANET, ObjectType.NODE) ] pairs = [ (p1.name, p2.name) for i, p1 in enumerate(valid_objects) for p2 in valid_objects[i + 1 :] ] else: pairs = self._pairs midpoints = [] for obj1_name, obj2_name in pairs: if obj1_name not in pos_dict or obj2_name not in pos_dict: continue obj1 = pos_dict[obj1_name] obj2 = pos_dict[obj2_name] # Calculate direct midpoint direct_mid = self._calculate_direct_midpoint(obj1, obj2) midpoints.append(direct_mid) # Calculate indirect midpoint too if requested if self._include_indirect: indirect_mid = self._calculate_indirect_midpoint(obj1, obj2) midpoints.append(indirect_mid) return midpoints
def _calculate_direct_midpoint( self, obj1: CelestialPosition, obj2: CelestialPosition ) -> MidpointPosition: """ Calculate direct midpoint (shortest arc). Args: obj1: First object obj2: Second object Returns: MidpointPosition for the midpoint """ # Calculate shortest arc midpoint long1, long2 = obj1.longitude, obj2.longitude # Calculate angular distance diff = abs(long2 - long1) if diff <= 180: # Direct arc midpoint_long = (long1 + long2) / 2 else: # Shorter arc goes the other way midpoint_long = ((long1 + long2) / 2 + 180) % 360 # Create midpoint position return MidpointPosition( name=f"Midpoint:{obj1.name}/{obj2.name}", object_type=ObjectType.MIDPOINT, longitude=midpoint_long, object1=obj1, object2=obj2, is_indirect=False, ) def _calculate_indirect_midpoint( self, obj1: CelestialPosition, obj2: CelestialPosition ) -> MidpointPosition: """ Calculate indirect midpoint (opposite of direct). Args: obj1: First object obj2: Second object Returns: MidpointPosition for the indirect midpoint """ # Get direct midpoint direct = self._calculate_direct_midpoint(obj1, obj2) # Indirect is 180° opposite indirect_long = (direct.longitude + 180) % 360 return MidpointPosition( name=f"Midpoint:{obj1.name}/{obj2.name} (indirect)", object_type=ObjectType.MIDPOINT, longitude=indirect_long, object1=obj1, object2=obj2, is_indirect=True, )