Stellium Astrology Library - Architecture Documentation¶
WARNING: This document is significantly out of date (last updated November 2025). Many code examples reference deprecated APIs (
ComparisonBuilder, old import paths, removed functions). For current and accurate API usage, refer to:
README.md — Quick start and feature overview
CLAUDE.md — Development instructions and patterns
CONTRIBUTING.md — How to extend Stellium
docs/options_list.md — Complete configuration reference
This document is retained for its architectural concepts (protocols, composability, immutability) which remain accurate, but its code examples should not be copy-pasted.
Version: 1.0 Last Updated: 2025-11-16
Table of Contents¶
Executive Summary¶
Stellium is a modern, protocol-driven Python astrology library built on Swiss Ephemeris for NASA-grade astronomical calculations. The architecture follows three core design principles:
Core Principles¶
Protocols over Inheritance: Uses Python protocols (structural typing) instead of class inheritance, allowing flexible composition without coupling
Immutability: All data models are frozen dataclasses that cannot be modified after creation
Composability: Features are composed through builder patterns and pluggable engines
Key Features¶
Fluent Builder API for chart construction
Multiple house system support (23+ systems)
Comprehensive aspect calculation with customizable orbs
Essential dignity calculations (traditional & modern)
Aspect pattern detection (Grand Trines, T-Squares, Yods, etc.)
Arabic parts and midpoints
SVG chart rendering with layer system
Rich terminal report generation
Synastry and transit comparisons
Full caching system for performance
Directory Structure¶
src/stellium/
├── __init__.py # Main package exports
│
├── core/ # Core abstractions and data models
│ ├── __init__.py
│ ├── models.py # Immutable dataclasses (ChartLocation, CelestialPosition, etc.)
│ ├── protocols.py # Protocol definitions (interfaces)
│ ├── builder.py # ChartBuilder (fluent API)
│ ├── native.py # Native & Notable (input parsing)
│ ├── config.py # Configuration classes
│ ├── registry.py # Celestial object & aspect metadata registries
│ └── comparison.py # Synastry/transit comparison system
│
├── engines/ # Calculation engines
│ ├── __init__.py
│ ├── ephemeris.py # Planet position calculation (Swiss Ephemeris)
│ ├── houses.py # House system engines (23+ systems)
│ ├── aspects.py # Aspect calculation engines
│ ├── orbs.py # Orb calculation strategies
│ ├── dignities.py # Essential dignity calculators
│ └── patterns.py # Aspect pattern detection
│
├── components/ # Optional add-on components
│ ├── __init__.py
│ ├── arabic_parts.py # Arabic parts calculator (25+ lots)
│ ├── midpoints.py # Midpoint calculator
│ └── dignity.py # Dignity component wrapper
│
├── visualization/ # SVG chart rendering
│ ├── __init__.py
│ ├── core.py # ChartRenderer class
│ ├── drawing.py # High-level drawing functions
│ ├── layers.py # Layer system (ZodiacLayer, PlanetLayer, etc.)
│ └── moon_phase.py # Moon phase visualization
│
├── presentation/ # Report generation
│ ├── __init__.py
│ ├── builder.py # ReportBuilder (fluent API)
│ ├── sections.py # Report sections (positions, aspects, etc.)
│ └── renderers.py # Output renderers (Rich, plain text)
│
├── utils/ # Utility modules
│ ├── __init__.py
│ ├── cache.py # Caching system
│ └── cache_utils.py # Cache helpers
│
├── data/ # Data access layer
│ ├── __init__.py
│ └── registry.py # Notable births registry
│
└── cli/ # Command-line interface
├── __init__.py
├── chart.py # Chart CLI commands
├── ephemeris.py # Ephemeris management
└── cache.py # Cache management
tests/ # Test suite
├── test_chart_generation.py # Chart drawing tests
├── test_core_models.py # Data model tests
├── test_chart_builder.py # Builder API tests
├── test_ephemeris_engine.py # Ephemeris calculations
├── test_aspect_engine.py # Aspect detection
├── test_integration.py # End-to-end flows
└── test_notables.py # Notable registry
examples/ # Usage examples
└── usage.py # Basic usage examples
docs/ # Documentation
├── ARCHITECTURE.md # This file
└── planning/ # Feature planning documents
Core Data Models¶
All models are frozen dataclasses (immutable). They live in core/models.py.
Input Models¶
ChartLocation¶
Represents a geographic location for chart calculation.
@dataclass(frozen=True)
class ChartLocation:
latitude: float # -90 to 90 (north positive)
longitude: float # -180 to 180 (east positive)
name: str = "" # Optional location name
timezone: str = "" # Optional timezone identifier
Example:
location = ChartLocation(
latitude=47.6062,
longitude=-122.3321,
name="Seattle, WA",
timezone="America/Los_Angeles"
)
ChartDateTime¶
Represents a date/time for chart calculation. Always stores UTC internally.
@dataclass(frozen=True)
class ChartDateTime:
utc_datetime: dt.datetime # Timezone-aware datetime (UTC)
julian_day: float # Julian day for Swiss Ephemeris
local_datetime: dt.datetime | None = None # Optional local time
Example:
from datetime import datetime, timezone
chart_dt = ChartDateTime(
utc_datetime=datetime(2000, 1, 6, 20, 0, tzinfo=timezone.utc),
julian_day=2451550.3333333,
local_datetime=datetime(2000, 1, 6, 12, 0) # PST
)
Positional Data Models¶
ObjectType (Enum)¶
Categorizes celestial objects.
class ObjectType(Enum):
PLANET = "planet"
ANGLE = "angle" # ASC, MC, DSC, IC
ASTEROID = "asteroid"
POINT = "point" # Lilith, etc.
NODE = "node" # North/South Node
ARABIC_PART = "arabic_part"
MIDPOINT = "midpoint"
FIXED_STAR = "fixed_star"
CelestialPosition¶
The fundamental positional data structure for all celestial objects.
@dataclass(frozen=True)
class CelestialPosition:
# Identity
name: str # "Sun", "Moon", "Ascendant", etc.
object_type: ObjectType
# Positional data (ecliptic coordinates)
longitude: float # 0-360 degrees
latitude: float = 0.0 # Degrees north/south of ecliptic
distance: float = 0.0 # AU from Earth
# Velocity (degrees per day)
speed_longitude: float = 0.0
speed_latitude: float = 0.0
speed_distance: float = 0.0
# Derived fields (auto-calculated in __post_init__)
sign: str = field(init=False) # "Aries", "Taurus", etc.
sign_degree: float = field(init=False) # 0-30 (degree within sign)
is_retrograde: bool = field(init=False) # True if speed_longitude < 0
# Optional phase data (for Moon/planets)
phase: PhaseData | None = None
Key Properties:
sign_position: Human-readable string (e.g., “15°23’ Aries”)sign: Automatically calculated from longitudesign_degree: Automatically calculated from longitudeis_retrograde: Automatically determined from speed_longitude
Example:
sun = CelestialPosition(
name="Sun",
object_type=ObjectType.PLANET,
longitude=285.5, # 15°30' Capricorn
speed_longitude=1.0
)
print(sun.sign) # "Capricorn"
print(sun.sign_degree) # 15.5
print(sun.sign_position) # "15°30' Capricorn"
print(sun.is_retrograde) # False
MidpointPosition¶
Extends CelestialPosition for midpoints between two objects.
@dataclass(frozen=True)
class MidpointPosition(CelestialPosition):
object1: CelestialPosition # First component planet
object2: CelestialPosition # Second component planet
is_indirect: bool = False # True for indirect (far) midpoint
Example:
# Sun/Moon midpoint at 150°
midpoint = MidpointPosition(
name="Sun/Moon",
object_type=ObjectType.MIDPOINT,
longitude=150.0,
object1=sun_position,
object2=moon_position,
is_indirect=False
)
PhaseData¶
Stores phase information for the Moon or planets.
@dataclass(frozen=True)
class PhaseData:
phase_angle: float # 0-360° (angle from Sun)
illuminated_fraction: float # 0.0-1.0 (0% to 100%)
elongation: float # Angular distance from Sun
apparent_diameter: float # arc seconds
apparent_magnitude: float # visual magnitude
geocentric_parallax: float = 0.0
Key Properties:
is_waxing: Whether illumination is increasingphase_name: “New”, “Waxing Crescent”, “First Quarter”, “Waxing Gibbous”, “Full”, “Waning Gibbous”, “Last Quarter”, “Waning Crescent”
Example:
moon_phase = PhaseData(
phase_angle=95.0,
illuminated_fraction=0.42,
elongation=95.0,
apparent_diameter=1800.0,
apparent_magnitude=-10.5
)
print(moon_phase.phase_name) # "Waxing Gibbous"
print(moon_phase.is_waxing) # True
House Data Models¶
HouseCusps¶
Stores house cusps for a specific house system.
@dataclass(frozen=True)
class HouseCusps:
system: str # "Placidus", "Whole Sign", "Koch", etc.
cusps: tuple[float, ...] # 12 cusps as longitudes (0-360°)
Key Methods:
get_cusp(house_number: int) -> float: Get cusp longitude for house 1-12get_sign(house_number: int) -> str: Get zodiac sign on cuspget_description(house_number: int) -> str: Human-readable description
Example:
placidus = HouseCusps(
system="Placidus",
cusps=(15.0, 45.0, 72.0, 105.0, 138.0, 165.0,
195.0, 225.0, 252.0, 285.0, 318.0, 345.0)
)
print(placidus.get_cusp(1)) # 15.0
print(placidus.get_sign(1)) # "Aries"
print(placidus.get_description(1)) # "House 1 (Placidus): 15°00' Aries"
Aspect Data Models¶
Aspect¶
Represents an aspect between two celestial objects.
@dataclass(frozen=True)
class Aspect:
object1: CelestialPosition
object2: CelestialPosition
aspect_name: str # "Conjunction", "Trine", "Square", etc.
aspect_degree: int # 0, 60, 90, 120, 180, etc.
orb: float # Actual orb in degrees (always positive)
is_applying: bool | None = None # True if orb decreasing, False if increasing
Key Properties:
description: Human-readable string (e.g., “Sun Trine Moon (orb: 2.3°)”)
Example:
aspect = Aspect(
object1=sun_position,
object2=moon_position,
aspect_name="Trine",
aspect_degree=120,
orb=2.3,
is_applying=True
)
print(aspect.description) # "Sun Trine Moon (orb: 2.3°, applying)"
ComparisonAspect¶
Extends Aspect for synastry/transit aspects between two different charts.
@dataclass(frozen=True)
class ComparisonAspect(Aspect):
chart1_object: CelestialPosition # Object from chart 1
chart2_object: CelestialPosition # Object from chart 2
AspectPattern¶
Represents a geometric aspect pattern (Grand Trine, T-Square, etc.).
@dataclass(frozen=True)
class AspectPattern:
name: str # "Grand Trine", "T-Square", "Yod", etc.
planets: list[CelestialPosition]
aspects: list[Aspect]
element: str | None = None # "Fire", "Earth", "Air", "Water"
quality: str | None = None # "Cardinal", "Fixed", "Mutable"
Key Properties:
focal_planet: For patterns with an apex (T-Square, Yod)
Example:
grand_trine = AspectPattern(
name="Grand Trine",
planets=[sun, mars, jupiter],
aspects=[sun_trine_mars, mars_trine_jupiter, jupiter_trine_sun],
element="Fire"
)
Chart Output Models¶
CalculatedChart¶
The main output of chart calculation. Contains all calculated data.
@dataclass(frozen=True)
class CalculatedChart:
# Input data
datetime: ChartDateTime
location: ChartLocation
# Calculated data
positions: tuple[CelestialPosition, ...]
house_systems: dict[str, HouseCusps] # {"Placidus": HouseCusps, ...}
house_placements: dict[str, dict[str, int]] # {"Placidus": {"Sun": 10, ...}}
aspects: tuple[Aspect, ...] = ()
metadata: dict[str, Any] = field(default_factory=dict)
calculation_timestamp: dt.datetime = field(default_factory=...)
Key Query Methods:
Object Queries:
chart.get_object(name: str) -> CelestialPosition | None
chart.get_planets() -> list[CelestialPosition]
chart.get_angles() -> list[CelestialPosition] # ASC, MC, DSC, IC, Vertex
chart.get_points() -> list[CelestialPosition] # Lilith, etc.
chart.get_nodes() -> list[CelestialPosition] # North/South Node
House Queries:
chart.get_house(object_name: str, system_name: str = "Placidus") -> int
chart.get_houses(system_name: str = "Placidus") -> HouseCusps
Dignity Queries:
chart.get_dignities(system: str = "traditional") -> dict
chart.get_planet_dignity(planet_name: str, system: str) -> dict
chart.get_mutual_receptions(system: str) -> list
chart.get_accidental_dignities(system: str = "Placidus") -> dict
chart.get_strongest_planet(system: str) -> str # Almuten
chart.get_planet_total_score(planet, essential_system, accidental_system) -> float
Utility Methods:
chart.sect() -> str # "day" or "night"
chart.to_dict() -> dict # JSON-serializable
Example:
# Query objects
sun = chart.get_object("Sun")
print(f"{sun.name} is at {sun.sign_position}")
# Get house placement
sun_house = chart.get_house("Sun", "Placidus")
print(f"Sun is in house {sun_house}")
# Get dignities
sun_dignity = chart.get_planet_dignity("Sun", "traditional")
print(f"Sun dignity: {sun_dignity}")
# Check sect
print(f"Chart sect: {chart.sect()}") # "day" or "night"
Comparison¶
Represents a comparison between two charts (synastry or transits).
@dataclass(frozen=True)
class Comparison:
comparison_type: ComparisonType # SYNASTRY or TRANSIT
chart1: CalculatedChart # Native/inner chart
chart2: CalculatedChart # Partner/transit/outer chart
cross_aspects: tuple[ComparisonAspect, ...]
house_overlays: tuple[HouseOverlay, ...]
chart1_label: str = "Native"
chart2_label: str = "Other"
Example:
synastry = create_synastry(native1, native2)
# Access cross-chart aspects
for aspect in synastry.cross_aspects:
print(f"{aspect.chart1_object.name} {aspect.aspect_name} {aspect.chart2_object.name}")
# Check house overlays
for overlay in synastry.house_overlays:
print(f"{overlay.planet_name} falls in partner's house {overlay.house_number}")
Protocols (Interfaces)¶
Protocols define structural interfaces without requiring inheritance. Any class that matches the protocol’s signature can be used.
All protocols are defined in core/protocols.py.
Calculation Engine Protocols¶
EphemerisEngine¶
Calculates planetary positions.
class EphemerisEngine(Protocol):
def calculate_positions(
self,
datetime: ChartDateTime,
location: ChartLocation,
objects: list[str] | None = None
) -> list[CelestialPosition]:
"""Calculate positions for requested objects."""
...
Implementations:
SwissEphemerisEngine(default, inengines/ephemeris.py)MockEphemerisEngine(for testing)
Example Implementation:
class CustomEphemerisEngine:
def calculate_positions(self, datetime, location, objects=None):
# Your calculation logic here
return [CelestialPosition(...), ...]
# Use it
chart = ChartBuilder.from_native(native).with_ephemeris(CustomEphemerisEngine()).calculate()
HouseSystemEngine¶
Calculates house cusps and angles.
class HouseSystemEngine(Protocol):
@property
def system_name(self) -> str:
"""Name of the house system (e.g., 'Placidus')."""
...
def calculate_house_data(
self,
datetime: ChartDateTime,
location: ChartLocation
) -> tuple[HouseCusps, list[CelestialPosition]]:
"""
Calculate house cusps and angles.
Returns:
(HouseCusps, [ASC, MC, DSC, IC, Vertex])
"""
...
def assign_houses(
self,
positions: list[CelestialPosition],
cusps: HouseCusps
) -> dict[str, int]:
"""
Assign house numbers to positions.
Returns:
{object_name: house_number}
"""
...
Implementations (in engines/houses.py):
PlacidusHouses(default)WholeSignHousesKochHousesEqualHousesPlus 20+ more via Swiss Ephemeris
Example:
chart = (
ChartBuilder.from_native(native)
.with_house_systems([PlacidusHouses(), WholeSignHouses()])
.calculate()
)
# Access different systems
placidus_sun_house = chart.get_house("Sun", "Placidus")
whole_sign_sun_house = chart.get_house("Sun", "Whole Sign")
AspectEngine¶
Finds aspects between celestial objects.
class AspectEngine(Protocol):
def calculate_aspects(
self,
positions: list[CelestialPosition],
orb_engine: OrbEngine
) -> list[Aspect]:
"""Calculate aspects between positions using orb engine."""
...
Implementations (in engines/aspects.py):
ModernAspectEngine: Configurable major/minor aspectsHarmonicAspectEngine: Harmonic aspects (H5, H7, H9, etc.)
Example:
from stellium.engines import ModernAspectEngine
from stellium.core.config import AspectConfig
config = AspectConfig(
aspects=["Conjunction", "Trine", "Square", "Opposition"],
include_angles=True
)
chart = (
ChartBuilder.from_native(native)
.with_aspects(ModernAspectEngine(config))
.calculate()
)
for aspect in chart.aspects:
print(aspect.description)
OrbEngine¶
Determines orb allowances for aspects.
class OrbEngine(Protocol):
def get_orb_allowance(
self,
obj1: CelestialPosition,
obj2: CelestialPosition,
aspect_name: str
) -> float:
"""Return maximum orb in degrees for this aspect."""
...
Implementations (in engines/orbs.py):
SimpleOrbEngine: One orb per aspect typeLuminariesOrbEngine: Wider orbs for Sun/MoonComplexOrbEngine: Cascading priority matrix
Example:
from stellium.engines import LuminariesOrbEngine
orb_engine = LuminariesOrbEngine(
luminary_orbs={"Conjunction": 10.0, "Trine": 8.0},
default_orbs={"Conjunction": 8.0, "Trine": 6.0}
)
chart = (
ChartBuilder.from_native(native)
.with_aspects(ModernAspectEngine())
.with_orbs(orb_engine)
.calculate()
)
Component Protocols¶
ChartComponent¶
Adds new calculated positions to a chart (Arabic parts, midpoints, etc.).
class ChartComponent(Protocol):
@property
def component_name(self) -> str:
"""Name of the component."""
...
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 additional positions.
Returns:
New positions to add to the chart
"""
...
Implementations:
ArabicPartsCalculator(incomponents/arabic_parts.py)MidpointCalculator(incomponents/midpoints.py)DignityComponent(incomponents/dignity.py)AccidentalDignityComponent(incomponents/dignity.py)
Example:
from stellium.components import ArabicPartsCalculator
chart = (
ChartBuilder.from_native(native)
.add_component(ArabicPartsCalculator())
.calculate()
)
# Access calculated Arabic parts
lot_of_fortune = chart.get_object("Lot of Fortune")
print(f"Lot of Fortune: {lot_of_fortune.sign_position}")
ChartAnalyzer¶
Analyzes a chart and stores findings in metadata.
class ChartAnalyzer(Protocol):
@property
def analyzer_name(self) -> str:
"""Name of the analyzer."""
...
@property
def metadata_name(self) -> str:
"""Key to use in chart.metadata."""
...
def analyze(self, chart: CalculatedChart) -> list | dict:
"""
Analyze the chart.
Returns:
Analysis results (stored in chart.metadata[metadata_name])
"""
...
Implementations:
AspectPatternAnalyzer(inengines/patterns.py)
Example:
from stellium.engines import AspectPatternAnalyzer
chart = (
ChartBuilder.from_native(native)
.with_aspects(ModernAspectEngine())
.add_analyzer(AspectPatternAnalyzer())
.calculate()
)
# Access detected patterns
patterns = chart.metadata.get('aspect_patterns', [])
for pattern in patterns:
print(f"Found {pattern.name}: {[p.name for p in pattern.planets]}")
Presentation Protocols¶
ReportSection¶
Generates data for a report section.
class ReportSection(Protocol):
@property
def section_name(self) -> str:
"""Name of the section."""
...
def generate_data(self, chart: CalculatedChart) -> dict[str, Any]:
"""
Generate section data.
Returns:
Dictionary with 'type', 'headers', 'rows', etc.
"""
...
Implementations (in presentation/sections.py):
ChartOverviewSectionPlanetPositionSectionAspectSectionMidpointSection
ReportRenderer¶
Renders report sections to output format.
class ReportRenderer(Protocol):
def render_section(self, section_name: str, section_data: dict) -> str:
"""Render a single section."""
...
def render_report(self, sections: list[tuple[str, dict]]) -> str:
"""Render complete report from sections."""
...
Implementations (in presentation/renderers.py):
RichTableRenderer: Rich terminal outputPlainTextRenderer: Plain text tables
Builder API¶
The Builder API provides a fluent interface for constructing charts.
Native (Input Parser)¶
Native handles flexible input formats and produces clean, immutable data.
Location: core/native.py
class Native:
datetime: ChartDateTime
location: ChartLocation
def __init__(
self,
datetime_input: dt.datetime | ChartDateTime | dict,
location_input: str | ChartLocation | tuple | dict
):
"""
Create a Native from flexible inputs.
datetime_input can be:
- datetime object (naive or aware)
- ChartDateTime object
- dict with datetime info
location_input can be:
- Location name string (geocoded automatically)
- (latitude, longitude) tuple
- ChartLocation object
- dict with lat/lon
"""
Features:
Accepts naive/aware datetimes
Geocodes location names to coordinates
Finds timezones automatically
Converts everything to UTC for calculations
Immutable output (ChartDateTime, ChartLocation)
Examples:
Simple:
from datetime import datetime
from stellium.core.native import Native
# Naive datetime + location name
native = Native(
datetime(2000, 1, 6, 12, 0),
"Seattle, WA"
)
Explicit:
from datetime import datetime, timezone
# Aware datetime + coordinates
native = Native(
datetime(2000, 1, 6, 20, 0, tzinfo=timezone.utc),
(47.6062, -122.3321)
)
With timezone:
import pytz
# Local timezone + location name
pacific = pytz.timezone('America/Los_Angeles')
native = Native(
pacific.localize(datetime(2000, 1, 6, 12, 0)),
"Seattle, WA"
)
Notable (Named Births Registry)¶
Quick access to notable birth charts.
Location: core/native.py
class Notable:
@staticmethod
def get(name: str) -> Native:
"""Get a notable person's birth data by name."""
...
@staticmethod
def list_all() -> list[str]:
"""List all available notable names."""
...
@staticmethod
def search(query: str) -> list[str]:
"""Search for notables by name."""
...
Example:
from stellium.core.native import Notable
# Get Einstein's chart
einstein = Notable.get("Albert Einstein")
# List all notables
all_notables = Notable.list_all()
# Search
physicists = Notable.search("Einstein")
ChartBuilder¶
The main entry point for creating charts. Provides a fluent API.
Location: core/builder.py
class ChartBuilder:
def __init__(self, datetime: ChartDateTime, location: ChartLocation):
"""Create builder from datetime and location."""
...
Factory Methods¶
@classmethod
def from_native(cls, native: Native) -> "ChartBuilder":
"""Create from Native object."""
...
@classmethod
def from_notable(cls, name: str) -> "ChartBuilder":
"""Create from notable person's name."""
...
Configuration Methods (Fluent)¶
# Engines
def with_ephemeris(self, engine: EphemerisEngine) -> "ChartBuilder":
"""Set ephemeris engine."""
...
def with_house_systems(self, engines: list[HouseSystemEngine]) -> "ChartBuilder":
"""Set house system engines (replaces all)."""
...
def add_house_system(self, engine: HouseSystemEngine) -> "ChartBuilder":
"""Add a house system engine."""
...
def with_aspects(self, engine: AspectEngine | None) -> "ChartBuilder":
"""Set aspect engine (None to disable)."""
...
def with_orbs(self, engine: OrbEngine) -> "ChartBuilder":
"""Set orb engine."""
...
# Configuration
def with_config(self, config: CalculationConfig) -> "ChartBuilder":
"""Set calculation config (which objects to calculate)."""
...
# Components & Analyzers
def add_component(self, component: ChartComponent) -> "ChartBuilder":
"""Add a chart component (Arabic parts, midpoints, etc.)."""
...
def add_analyzer(self, analyzer: ChartAnalyzer) -> "ChartBuilder":
"""Add a chart analyzer (pattern detection, etc.)."""
...
# Caching
def with_cache(
self,
cache: Cache | None,
cache_chart: bool = False,
cache_key_prefix: str = ""
) -> "ChartBuilder":
"""Configure caching."""
...
Execution¶
def calculate(self) -> CalculatedChart:
"""
Execute calculation and return immutable chart.
Execution order:
1. Calculate planetary positions (ephemeris)
2. Calculate house systems and angles
3. Assign house placements for all systems
4. Run components (Arabic parts, etc.)
5. Calculate aspects (if engine provided)
6. Run analyzers (patterns, etc.)
7. Return frozen CalculatedChart
"""
...
Default Configuration¶
If you don’t specify engines/config, these defaults are used:
Ephemeris:
SwissEphemerisEngine()Houses:
[PlacidusHouses()]Aspects:
None(no aspects calculated)Orbs:
SimpleOrbEngine()Config:
CalculationConfig()(standard planets)
Usage Examples¶
Simple (using defaults):
from stellium.core.builder import ChartBuilder
chart = ChartBuilder.from_notable("Albert Einstein").calculate()
With aspects:
from stellium.engines import ModernAspectEngine
chart = (
ChartBuilder.from_notable("Albert Einstein")
.with_aspects(ModernAspectEngine())
.calculate()
)
Multiple house systems:
from stellium.engines import PlacidusHouses, WholeSignHouses, KochHouses
chart = (
ChartBuilder.from_native(native)
.with_house_systems([PlacidusHouses(), WholeSignHouses(), KochHouses()])
.calculate()
)
# Access different systems
print(chart.get_house("Sun", "Placidus"))
print(chart.get_house("Sun", "Whole Sign"))
print(chart.get_house("Sun", "Koch"))
With components:
from stellium.components import ArabicPartsCalculator, MidpointCalculator
chart = (
ChartBuilder.from_native(native)
.add_component(ArabicPartsCalculator())
.add_component(MidpointCalculator())
.calculate()
)
# Access calculated data
lot_of_fortune = chart.get_object("Lot of Fortune")
sun_moon_midpoint = chart.get_object("Sun/Moon")
Full customization:
from stellium.core.builder import ChartBuilder
from stellium.core.config import CalculationConfig, AspectConfig
from stellium.engines import (
PlacidusHouses,
WholeSignHouses,
ModernAspectEngine,
LuminariesOrbEngine,
AspectPatternAnalyzer
)
from stellium.components import ArabicPartsCalculator, DignityComponent
# Configure what to calculate
calc_config = CalculationConfig(
include_planets=["Sun", "Moon", "Mercury", "Venus", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"],
include_nodes=True,
include_chiron=True,
include_asteroids=["Ceres", "Pallas", "Juno", "Vesta"]
)
# Configure aspects
aspect_config = AspectConfig(
aspects=["Conjunction", "Sextile", "Square", "Trine", "Opposition"],
include_angles=True,
include_nodes=True
)
# Build chart
chart = (
ChartBuilder.from_native(native)
.with_config(calc_config)
.with_house_systems([PlacidusHouses(), WholeSignHouses()])
.with_aspects(ModernAspectEngine(aspect_config))
.with_orbs(LuminariesOrbEngine())
.add_component(ArabicPartsCalculator())
.add_component(DignityComponent())
.add_analyzer(AspectPatternAnalyzer())
.calculate()
)
Registries¶
Registries provide comprehensive metadata for celestial objects and aspects.
Celestial Object Registry¶
Location: core/registry.py
Stores metadata for all supported celestial objects.
CelestialObjectInfo¶
@dataclass(frozen=True)
class CelestialObjectInfo:
name: str # "Mean Apogee"
display_name: str # "Black Moon Lilith"
object_type: ObjectType
glyph: str # Unicode symbol (☉, ☽, etc.)
glyph_svg_path: str | None # Custom SVG path for rendering
swiss_ephemeris_id: int | None # Swiss Ephemeris object ID
category: str | None # "Centaur", "TNO", "Big Four", etc.
aliases: list[str] # Alternative names
description: str
metadata: dict[str, Any] # Additional data
Registry Contents¶
Traditional Planets (7):
Sun, Moon, Mercury, Venus, Mars, Jupiter, Saturn
Modern Planets (3):
Uranus, Neptune, Pluto
Lunar Nodes:
North Node (True & Mean)
South Node (True & Mean)
Calculated Points:
Mean Apogee (Black Moon Lilith)
True Apogee
Vertex
Big Four Asteroids:
Ceres, Pallas, Juno, Vesta
Centaurs:
Chiron, Pholus, Nessus, Chariklo, Asbolus
Trans-Neptunian Objects (TNOs):
Eris, Sedna, Makemake, Haumea, Quaoar, Orcus
Uranian/Hamburg Planets:
Cupido, Hades, Zeus, Kronos, Apollon, Admetos, Vulcanus, Poseidon
Fixed Stars:
Regulus, Aldebaran, Antares, Fomalhaut (Royal Stars)
Helper Functions¶
def get_object_info(name: str) -> CelestialObjectInfo:
"""Get info by exact name."""
...
def get_by_alias(alias: str) -> CelestialObjectInfo | None:
"""Get info by alias."""
...
def get_all_by_type(object_type: ObjectType) -> list[CelestialObjectInfo]:
"""Get all objects of a type."""
...
def get_all_by_category(category: str) -> list[CelestialObjectInfo]:
"""Get all objects in a category."""
...
def search_objects(query: str) -> list[CelestialObjectInfo]:
"""Search by name/alias/description."""
...
Example:
from stellium.core.registry import get_object_info, get_all_by_type
# Get info
lilith_info = get_object_info("Mean Apogee")
print(f"{lilith_info.display_name}: {lilith_info.glyph}")
# Get all asteroids
asteroids = get_all_by_type(ObjectType.ASTEROID)
for asteroid in asteroids:
print(f"{asteroid.name} ({asteroid.category})")
Aspect Registry¶
Location: core/registry.py
Stores metadata for all aspect types.
AspectInfo¶
@dataclass(frozen=True)
class AspectInfo:
name: str # "Conjunction"
angle: float # 0.0, 60.0, 90.0, 120.0, 180.0, etc.
category: str # "Major", "Minor", "Harmonic"
family: str | None # "Ptolemaic", "Quintile Series", etc.
glyph: str # Unicode symbol (☌, ⚹, □, △, ☍)
color: str # Hex color for rendering
default_orb: float # Default orb in degrees
aliases: list[str]
description: str
metadata: dict[str, Any] # line_width, dash_pattern, etc.
Registry Contents¶
Major Aspects (Ptolemaic):
Conjunction (0°) - orb 8°
Sextile (60°) - orb 6°
Square (90°) - orb 7°
Trine (120°) - orb 8°
Opposition (180°) - orb 8°
Minor Aspects:
Semisextile (30°) - orb 2°
Semisquare (45°) - orb 2°
Sesquisquare (135°) - orb 2°
Quincunx (150°) - orb 3°
Quintile Family (H5):
Quintile (72°) - orb 2°
Biquintile (144°) - orb 2°
Septile Family (H7):
Septile (51.43°) - orb 1°
Biseptile (102.86°) - orb 1°
Triseptile (154.29°) - orb 1°
Novile Family (H9):
Novile (40°) - orb 1°
Binovile (80°) - orb 1°
Quadnovile (160°) - orb 1°
Helper Functions¶
def get_aspect_info(name: str) -> AspectInfo:
"""Get aspect info by name."""
...
def get_aspect_by_alias(alias: str) -> AspectInfo | None:
"""Get aspect info by alias."""
...
def get_aspects_by_category(category: str) -> list[AspectInfo]:
"""Get all aspects in category."""
...
def get_aspects_by_family(family: str) -> list[AspectInfo]:
"""Get all aspects in family."""
...
def search_aspects(query: str) -> list[AspectInfo]:
"""Search aspects."""
...
Example:
from stellium.core.registry import get_aspect_info, get_aspects_by_category
# Get info
trine_info = get_aspect_info("Trine")
print(f"{trine_info.name}: {trine_info.angle}° (orb: {trine_info.default_orb}°)")
print(f"Glyph: {trine_info.glyph}, Color: {trine_info.color}")
# Get all major aspects
major = get_aspects_by_category("Major")
for aspect in major:
print(f"{aspect.name}: {aspect.angle}°")
Calculation Engines¶
Engines perform the core astronomical calculations.
Ephemeris Engine¶
Location: engines/ephemeris.py
SwissEphemerisEngine¶
The default engine using Swiss Ephemeris library.
class SwissEphemerisEngine:
def __init__(self, ephemeris_path: str | None = None):
"""
Initialize with optional custom ephemeris data path.
Args:
ephemeris_path: Path to Swiss Ephemeris data files
(defaults to package data/swisseph/ephe/)
"""
...
Key Methods:
def calculate_positions(
self,
datetime: ChartDateTime,
location: ChartLocation,
objects: list[str] | None = None
) -> list[CelestialPosition]:
"""
Calculate positions for all requested objects.
Args:
datetime: When to calculate
location: Where to calculate (for topocentric)
objects: List of object names (None = use defaults)
Returns:
List of CelestialPosition objects
"""
...
@cached(cache_type="ephemeris", max_age_seconds=86400)
def _calculate_single_position(
self,
julian_day: float,
object_id: int,
object_name: str
) -> CelestialPosition:
"""
Calculate single position (cached).
Uses:
- swe.calc_ut() for position
- swe.pheno_ut() for phase data (Moon/planets)
"""
...
Features:
Caches individual position calculations
Automatically calculates phase data for Moon/planets
Derives South Node from North Node
Uses registry for object metadata
Topocentric positions (location-specific)
Example:
from stellium.engines import SwissEphemerisEngine
# Use custom ephemeris path
engine = SwissEphemerisEngine(ephemeris_path="/path/to/ephe")
chart = (
ChartBuilder.from_native(native)
.with_ephemeris(engine)
.calculate()
)
MockEphemerisEngine¶
For testing without Swiss Ephemeris.
class MockEphemerisEngine:
def __init__(self, positions: dict[str, float]):
"""
Create mock engine with fixed positions.
Args:
positions: {object_name: longitude}
"""
...
Example:
from stellium.engines.ephemeris import MockEphemerisEngine
mock = MockEphemerisEngine({
"Sun": 285.5, # 15° Capricorn
"Moon": 120.0, # 0° Leo
"Mercury": 275.0 # 5° Capricorn
})
chart = ChartBuilder.from_native(native).with_ephemeris(mock).calculate()
House System Engines¶
Location: engines/houses.py
All house engines extend SwissHouseSystemBase.
Base Class¶
class SwissHouseSystemBase:
@cached(cache_type="ephemeris", max_age_seconds=86400)
def _calculate_swiss_houses(
self,
julian_day: float,
latitude: float,
longitude: float,
system_code: bytes
) -> tuple:
"""Call swe.houses() with caching."""
return swe.houses(julian_day, latitude, longitude, system_code)
def calculate_house_data(
self,
datetime: ChartDateTime,
location: ChartLocation
) -> tuple[HouseCusps, list[CelestialPosition]]:
"""
Calculate cusps and angles.
Returns:
(HouseCusps, [ASC, MC, DSC, IC, Vertex])
"""
...
def assign_houses(
self,
positions: list[CelestialPosition],
cusps: HouseCusps
) -> dict[str, int]:
"""
Assign house numbers to positions.
Returns:
{object_name: house_number}
"""
...
Supported Systems¶
Quadrant Systems:
PlacidusHouses(default) - Most popular in Western astrologyKochHouses- Koch housesRegiomontanusHouses- RegiomontanusCampanusHouses- CampanusTopocentricHouses- TopocentricAlcabitiusHouses- Alcabitius
Equal Systems:
EqualHouses- Equal from AscendantEqualMCHouses- Equal from MCVehlowEqualHouses- Vehlow equalWholeSignHouses- Whole sign
Other:
PorphyryHouses- Porphyry (space division)MorinusHouses- Morinus (equatorial)Plus 15+ more via Swiss Ephemeris
Example:
from stellium.engines import PlacidusHouses, WholeSignHouses
chart = (
ChartBuilder.from_native(native)
.with_house_systems([PlacidusHouses(), WholeSignHouses()])
.calculate()
)
# Compare systems
print(f"Placidus: Sun in house {chart.get_house('Sun', 'Placidus')}")
print(f"Whole Sign: Sun in house {chart.get_house('Sun', 'Whole Sign')}")
# Get cusps for each system
placidus_cusps = chart.get_houses("Placidus")
whole_sign_cusps = chart.get_houses("Whole Sign")
Aspect Engines¶
Location: engines/aspects.py
ModernAspectEngine¶
Configurable aspect engine using AspectConfig.
class ModernAspectEngine:
def __init__(self, config: AspectConfig | None = None):
"""
Create aspect engine.
Args:
config: AspectConfig or None (uses defaults)
"""
...
def calculate_aspects(
self,
positions: list[CelestialPosition],
orb_engine: OrbEngine
) -> list[Aspect]:
"""
Find all aspects within orb.
Process:
1. Filter positions by config
2. Check all pairs
3. For each configured aspect:
- Calculate angular distance
- Ask orb_engine for allowance
- Check if within orb
- Determine applying/separating
"""
...
Key Helper:
def _is_applying(
obj1: CelestialPosition,
obj2: CelestialPosition,
aspect_angle: float,
current_distance: float
) -> bool | None:
"""
Determine if aspect is applying or separating.
Uses 1-minute interval to check if orb is decreasing.
Returns:
True: applying (getting tighter)
False: separating (getting wider)
None: unable to determine
"""
...
Example:
from stellium.engines import ModernAspectEngine
from stellium.core.config import AspectConfig
# Custom config
config = AspectConfig(
aspects=["Conjunction", "Trine", "Square", "Opposition"],
include_angles=True,
include_nodes=True,
include_asteroids=False
)
chart = (
ChartBuilder.from_native(native)
.with_aspects(ModernAspectEngine(config))
.calculate()
)
# Check aspects
for aspect in chart.aspects:
applying = "applying" if aspect.is_applying else "separating"
print(f"{aspect.description} ({applying})")
HarmonicAspectEngine¶
Calculates harmonic aspects (septiles, noviles, etc.).
class HarmonicAspectEngine:
def __init__(self, harmonic: int):
"""
Create harmonic aspect engine.
Args:
harmonic: Harmonic number (5, 7, 9, 11, etc.)
Examples:
5: Quintile series (72°, 144°)
7: Septile series (51.43°, 102.86°, 154.29°)
9: Novile series (40°, 80°, 160°)
"""
...
Example:
from stellium.engines import HarmonicAspectEngine
# Septile aspects
septile_engine = HarmonicAspectEngine(harmonic=7)
chart = (
ChartBuilder.from_native(native)
.with_aspects(septile_engine)
.calculate()
)
# Aspects will be H7-1, H7-2, H7-3 (septile, biseptile, triseptile)
Orb Engines¶
Location: engines/orbs.py
SimpleOrbEngine¶
One orb value per aspect type.
class SimpleOrbEngine:
def __init__(
self,
orb_map: dict[str, float] | None = None,
fallback_orb: int = 2
):
"""
Create simple orb engine.
Args:
orb_map: {aspect_name: orb_degrees} or None (uses registry defaults)
fallback_orb: Default for unknown aspects
"""
...
Example:
from stellium.engines import SimpleOrbEngine
orbs = SimpleOrbEngine(
orb_map={
"Conjunction": 10.0,
"Trine": 8.0,
"Square": 7.0,
"Sextile": 6.0,
"Opposition": 8.0
},
fallback_orb=2.0
)
chart = (
ChartBuilder.from_native(native)
.with_aspects(ModernAspectEngine())
.with_orbs(orbs)
.calculate()
)
LuminariesOrbEngine¶
Wider orbs when Sun or Moon is involved.
class LuminariesOrbEngine:
def __init__(
self,
luminary_orbs: dict[str, float] | None = None,
default_orbs: dict[str, float] | None = None,
fallback_orb: float = 2.0
):
"""
Create luminaries orb engine.
Args:
luminary_orbs: Orbs when Sun/Moon involved
default_orbs: Orbs for other planets
fallback_orb: Final fallback
"""
...
Example:
from stellium.engines import LuminariesOrbEngine
orbs = LuminariesOrbEngine(
luminary_orbs={
"Conjunction": 10.0,
"Trine": 8.0,
"Square": 8.0
},
default_orbs={
"Conjunction": 8.0,
"Trine": 6.0,
"Square": 6.0
}
)
# Sun-Moon conjunction: 10° orb
# Mars-Venus conjunction: 8° orb
ComplexOrbEngine¶
Cascading priority matrix with multiple rule types.
class ComplexOrbEngine:
def __init__(self, config: dict[str, Any]):
"""
Create complex orb engine.
Config structure:
{
"by_pair": {
"Moon-Sun": {"Square": 10.0, "default": 8.0},
"Venus-Mars": {"default": 5.0}
},
"by_planet": {
"Sun": {"Conjunction": 10.0, "default": 8.0},
"Moon": {"default": 8.0}
},
"by_aspect": {
"Conjunction": 8.0,
"Trine": 6.0
},
"default": 3.0
}
Priority order:
1. Specific pair + specific aspect
2. Specific pair default
3. Single planet rules (highest priority wins)
4. Aspect default
5. Global default
"""
...
Example:
from stellium.engines import ComplexOrbEngine
config = {
"by_pair": {
"Moon-Sun": {"Square": 10.0, "default": 8.0}
},
"by_planet": {
"Sun": {"default": 8.0},
"Moon": {"default": 8.0}
},
"by_aspect": {
"Square": 7.0,
"Trine": 6.0
},
"default": 3.0
}
orbs = ComplexOrbEngine(config)
# Moon-Sun square: 10.0° (by_pair specific)
# Sun-Mars square: 8.0° (Sun's by_planet default)
# Venus-Mars square: 7.0° (by_aspect)
# Venus-Jupiter quintile: 3.0° (global default)
Dignity Engines¶
Location: engines/dignities.py
TraditionalDignityCalculator¶
Traditional essential dignities scoring.
Scores:
Ruler: +5
Exaltation: +4
Triplicity: +3
Term (Bound): +2
Face (Decan): +1
Detriment: -5
Fall: -4
Peregrine: 0 (no dignity)
class TraditionalDignityCalculator:
def calculate_dignity(
self,
planet: CelestialPosition,
decan_system: str = "triplicity"
) -> dict:
"""
Calculate traditional essential dignities.
Args:
planet: Planet position
decan_system: "triplicity", "chaldean", or "egyptian"
Returns:
{
'ruler': bool,
'exaltation': bool,
'triplicity': bool,
'term': bool,
'face': bool,
'detriment': bool,
'fall': bool,
'score': int,
'state': str # "Ruler", "Exalted", "Detriment", etc.
}
"""
...
Example:
from stellium.engines import TraditionalDignityCalculator
calc = TraditionalDignityCalculator()
sun = chart.get_object("Sun")
dignity = calc.calculate_dignity(sun)
print(f"Sun dignity: {dignity['state']}")
print(f"Score: {dignity['score']}")
ModernDignityCalculator¶
Uses modern rulerships (Uranus rules Aquarius, Neptune rules Pisces, Pluto rules Scorpio).
class ModernDignityCalculator:
def calculate_dignity(
self,
planet: CelestialPosition,
decan_system: str = "triplicity"
) -> dict:
"""Same interface as TraditionalDignityCalculator."""
...
MutualReceptionAnalyzer¶
Finds mutual receptions (planets in each other’s signs).
class MutualReceptionAnalyzer:
def find_mutual_receptions(
self,
positions: list[CelestialPosition],
rulership_system: str = "traditional"
) -> list[dict]:
"""
Find mutual receptions.
Args:
positions: Planet positions
rulership_system: "traditional" or "modern"
Returns:
[
{
'planet1': str,
'planet2': str,
'type': 'rulership' or 'exaltation',
'sign1': str,
'sign2': str
},
...
]
"""
...
Example:
from stellium.engines import MutualReceptionAnalyzer
analyzer = MutualReceptionAnalyzer()
positions = chart.get_planets()
receptions = analyzer.find_mutual_receptions(positions, "traditional")
for reception in receptions:
print(f"{reception['planet1']} in {reception['sign1']} and "
f"{reception['planet2']} in {reception['sign2']} "
f"({reception['type']} reception)")
Pattern Analyzer¶
Location: engines/patterns.py
AspectPatternAnalyzer¶
Detects geometric aspect patterns.
Detected Patterns:
Grand Trine: 3 planets, 3 trines (all same element)
T-Square: Opposition + 2 squares to apex
Yod (Finger of God): 2 sextiles + 2 quincunxes to apex
Grand Cross: 4 planets, 2 oppositions, 4 squares
Stellium: 3+ planets within 8° orb
Mystic Rectangle: 2 oppositions, 4 sextiles/trines
Kite: Grand trine + opposition to one point
class AspectPatternAnalyzer:
@property
def analyzer_name(self) -> str:
return "Aspect Pattern Analyzer"
@property
def metadata_name(self) -> str:
return "aspect_patterns"
def analyze(self, chart: CalculatedChart) -> list[AspectPattern]:
"""
Detect all patterns in chart.
Returns:
List of AspectPattern objects
"""
...
Example:
from stellium.engines import AspectPatternAnalyzer, ModernAspectEngine
chart = (
ChartBuilder.from_native(native)
.with_aspects(ModernAspectEngine())
.add_analyzer(AspectPatternAnalyzer())
.calculate()
)
patterns = chart.metadata.get('aspect_patterns', [])
for pattern in patterns:
planets = ", ".join([p.name for p in pattern.planets])
print(f"\n{pattern.name}")
print(f"Planets: {planets}")
if pattern.element:
print(f"Element: {pattern.element}")
if pattern.focal_planet:
print(f"Focal planet: {pattern.focal_planet.name}")
Components¶
Components add new calculated positions (or metadata) to charts.
Arabic Parts Calculator¶
Location: components/arabic_parts.py
Calculates 25+ Arabic lots (Hermetic lots).
class ArabicPartsCalculator:
@property
def component_name(self) -> str:
return "Arabic Parts"
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 Arabic parts.
Returns:
List of new CelestialPosition objects (type=ARABIC_PART)
"""
...
Features:
Sect-aware formulas (day/night chart detection)
Hermetic core lots
Family and life topic lots
All lots have ObjectType.ARABIC_PART
Calculated Lots:
Core Hermetic Lots:
Lot of Fortune (sect-aware)
Lot of Spirit (sect-aware)
Lot of Eros (sect-aware)
Lot of Necessity (sect-aware)
Lot of Courage (sect-aware)
Lot of Victory (sect-aware)
Family Lots:
Father, Mother, Children, Marriage, Siblings
Life Topic Lots:
Action, Illness, Death, Travel, Friends
Exaltation, Basis, Love, Increase, Goods
Formula Format:
Day: ASC + Point1 - Point2
Night: ASC + Point2 - Point1 (reversed)
Example:
from stellium.components import ArabicPartsCalculator
chart = (
ChartBuilder.from_native(native)
.add_component(ArabicPartsCalculator())
.calculate()
)
# Access lots
lot_of_fortune = chart.get_object("Lot of Fortune")
lot_of_spirit = chart.get_object("Lot of Spirit")
print(f"Lot of Fortune: {lot_of_fortune.sign_position}")
print(f"In house: {chart.get_house('Lot of Fortune')}")
Midpoint Calculator¶
Location: components/midpoints.py
Calculates midpoints between planets.
class MidpointCalculator:
@property
def component_name(self) -> str:
return "Midpoints"
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 all midpoints.
Returns:
List of MidpointPosition objects
"""
...
Features:
Calculates both direct and indirect midpoints
Only for major planets (Sun through Pluto)
Creates MidpointPosition objects
Named as “Planet1/Planet2”
Example:
from stellium.components import MidpointCalculator
chart = (
ChartBuilder.from_native(native)
.add_component(MidpointCalculator())
.calculate()
)
# Access midpoints
sun_moon = chart.get_object("Sun/Moon")
print(f"Sun/Moon midpoint: {sun_moon.sign_position}")
# Get all midpoints
midpoints = [p for p in chart.positions if p.object_type == ObjectType.MIDPOINT]
for mp in midpoints:
print(f"{mp.name}: {mp.sign_position}")
Dignity Components¶
Location: components/dignity.py
DignityComponent¶
Calculates essential dignities and stores in metadata.
class DignityComponent:
def __init__(
self,
traditional: bool = True,
modern: bool = True,
receptions: bool = True,
decan_system: str = "triplicity"
):
"""
Create dignity component.
Args:
traditional: Calculate traditional dignities
modern: Calculate modern dignities
receptions: Find mutual receptions
decan_system: "triplicity", "chaldean", or "egyptian"
"""
...
def calculate(self, ...) -> list[CelestialPosition]:
"""
Calculate dignities (stores in metadata, returns empty list).
Stores in chart.metadata['dignities']:
{
'sect': 'day' | 'night',
'planet_dignities': {
'Sun': {
'traditional': {...},
'modern': {...}
},
...
},
'mutual_receptions': {
'traditional': [...],
'modern': [...]
}
}
"""
...
Example:
from stellium.components import DignityComponent
chart = (
ChartBuilder.from_native(native)
.add_component(DignityComponent())
.calculate()
)
# Access via helper methods
sun_dignity = chart.get_planet_dignity("Sun", "traditional")
print(f"Sun state: {sun_dignity['state']}")
print(f"Sun score: {sun_dignity['score']}")
# Get all dignities
all_dignities = chart.get_dignities("traditional")
# Find mutual receptions
receptions = chart.get_mutual_receptions("traditional")
for rec in receptions:
print(f"{rec['planet1']} and {rec['planet2']}: {rec['type']} reception")
AccidentalDignityComponent¶
Calculates accidental (house-based) dignities.
Scores:
Angular houses (1, 4, 7, 10): +5
Succedent houses (2, 5, 8, 11): +3
Cadent houses (3, 6, 9, 12): +1
House joys (traditional): +2
House Joys:
Mercury: House 1
Moon: House 3
Venus: House 5
Mars: House 6
Sun: House 9
Jupiter: House 11
Saturn: House 12
class AccidentalDignityComponent:
def __init__(
self,
house_system: str = "Placidus",
include_joys: bool = True
):
"""
Create accidental dignity component.
Args:
house_system: Which house system to use
include_joys: Include traditional house joys
"""
...
Example:
from stellium.components import AccidentalDignityComponent
chart = (
ChartBuilder.from_native(native)
.add_component(AccidentalDignityComponent())
.calculate()
)
# Access via helper
acc_dignities = chart.get_accidental_dignities("Placidus")
for planet, data in acc_dignities.items():
print(f"{planet}: house {data['house']}, score {data['score']}")
Visualization System¶
The visualization system renders charts to SVG using a composable layer architecture.
Location: visualization/
Layer Architecture¶
Charts are built by stacking layers from bottom to top. Each layer is independent and composable.
IRenderLayer Protocol¶
class IRenderLayer(Protocol):
def render(
self,
renderer: ChartRenderer,
dwg: svgwrite.Drawing,
chart: CalculatedChart
) -> None:
"""Render this layer onto the SVG drawing."""
...
Available Layers¶
Location: visualization/layers.py
ZodiacLayer: Zodiac wheel with signs and symbolsHouseCuspLayer: House cusp linesAngleLayer: ASC, MC markersPlanetLayer: Planet symbols and degree markersAspectLayer: Aspect lines between planetsMoonPhaseLayer: Moon phase visualization
Example:
from stellium.visualization import ChartRenderer
from stellium.visualization.layers import (
ZodiacLayer,
HouseCuspLayer,
PlanetLayer,
AspectLayer
)
renderer = ChartRenderer(size=800)
dwg = renderer.create_svg_drawing("chart.svg")
# Render layers in order
ZodiacLayer().render(renderer, dwg, chart)
HouseCuspLayer().render(renderer, dwg, chart)
PlanetLayer().render(renderer, dwg, chart)
AspectLayer().render(renderer, dwg, chart)
dwg.save()
ChartRenderer¶
Location: visualization/core.py
The core rendering engine with coordinate helpers.
class ChartRenderer:
def __init__(self, size: int = 800, rotation: float = 0):
"""
Create renderer.
Args:
size: Canvas size in pixels (square)
rotation: Rotation in degrees (0 = Aries at 9 o'clock)
"""
self.size = size
self.rotation = rotation
self.center = size / 2
Key Methods:
def create_svg_drawing(self, filename: str) -> svgwrite.Drawing:
"""Create SVG drawing with viewbox."""
...
def polar_to_cartesian(
self,
degrees: float,
radius: float
) -> tuple[float, float]:
"""
Convert polar to Cartesian coordinates.
Args:
degrees: 0-360 (0 = right, 90 = top)
radius: Distance from center
Returns:
(x, y) coordinates
"""
...
def get_chart_rotation(self, chart: CalculatedChart) -> float:
"""Get rotation to place Ascendant at 9 o'clock."""
...
Example:
from stellium.visualization import ChartRenderer
renderer = ChartRenderer(size=1000, rotation=0)
# Convert degrees to coordinates
x, y = renderer.polar_to_cartesian(degrees=0, radius=200) # Right side
x, y = renderer.polar_to_cartesian(degrees=90, radius=200) # Top
High-Level Drawing Functions¶
Location: visualization/drawing.py
Convenience functions for common chart types.
def draw_chart(
chart: CalculatedChart,
filename: str,
size: int = 800,
house_system: str | None = None
) -> None:
"""
Draw a standard natal chart.
Args:
chart: Calculated chart
filename: Output SVG filename
size: Canvas size
house_system: Which system to use (None = first available)
Renders:
- Zodiac wheel
- House cusps
- Angles (ASC, MC)
- Planets
- Aspects
- Moon phase (if Moon present)
"""
...
def draw_chart_with_multiple_houses(
chart: CalculatedChart,
filename: str,
size: int = 800
) -> None:
"""
Draw chart with overlaid house systems.
Shows multiple house systems in different colors.
"""
...
Example:
from stellium.visualization import draw_chart
chart = ChartBuilder.from_notable("Albert Einstein").calculate()
draw_chart(
chart,
"einstein_chart.svg",
size=1000,
house_system="Placidus"
)
Moon Phase Visualization¶
Location: visualization/moon_phase.py
Renders realistic Moon phase appearance.
def draw_moon_phase(
phase_data: PhaseData,
center_x: float,
center_y: float,
radius: float,
dwg: svgwrite.Drawing
) -> None:
"""
Draw Moon phase.
Args:
phase_data: PhaseData from Moon position
center_x, center_y: Center coordinates
radius: Moon circle radius
dwg: SVG drawing
Renders:
- Full circle (Moon outline)
- Illuminated portion (accurate shape)
"""
...
Example:
from stellium.visualization.moon_phase import draw_moon_phase
moon = chart.get_object("Moon")
if moon and moon.phase:
draw_moon_phase(
moon.phase,
center_x=400,
center_y=400,
radius=30,
dwg=dwg
)
Presentation & Reporting¶
The presentation system generates human-readable reports.
Location: presentation/
ReportBuilder¶
Location: presentation/builder.py
Fluent API for building reports.
class ReportBuilder:
def __init__(self):
"""Create empty report builder."""
...
def from_chart(self, chart: CalculatedChart) -> "ReportBuilder":
"""Set the chart to report on."""
...
Section Methods:
def with_chart_overview(self) -> "ReportBuilder":
"""Add chart overview section."""
...
def with_planet_positions(
self,
include_speed: bool = False
) -> "ReportBuilder":
"""Add planet positions table."""
...
def with_aspects(
self,
mode: str = "major",
orbs: bool = False
) -> "ReportBuilder":
"""
Add aspects section.
Args:
mode: "major", "all"
orbs: Include orb values
"""
...
def with_house_positions(
self,
house_system: str = "Placidus"
) -> "ReportBuilder":
"""Add house positions table."""
...
def with_midpoints(
self,
mode: str = "core"
) -> "ReportBuilder":
"""
Add midpoints section.
Args:
mode: "core" (major planets) or "all"
"""
...
def with_dignities(
self,
system: str = "traditional"
) -> "ReportBuilder":
"""Add essential dignities table."""
...
def with_aspect_patterns(self) -> "ReportBuilder":
"""Add aspect patterns section."""
...
Rendering:
def render(
self,
format: str = "rich_table",
file: str | None = None
) -> str:
"""
Render report.
Args:
format: "rich_table" or "plain_text"
file: Optional file to write to
Returns:
Rendered report string
"""
...
Example:
from stellium.presentation import ReportBuilder
chart = ChartBuilder.from_notable("Albert Einstein").calculate()
report = (
ReportBuilder()
.from_chart(chart)
.with_chart_overview()
.with_planet_positions(include_speed=True)
.with_aspects(mode="major", orbs=True)
.with_dignities(system="traditional")
.render(format="rich_table")
)
print(report)
# Save to file
report = (
ReportBuilder()
.from_chart(chart)
.with_chart_overview()
.with_planet_positions()
.render(format="plain_text", file="report.txt")
)
Report Sections¶
Location: presentation/sections.py
Each section generates structured data.
Section Data Format:
{
"type": "table" | "text" | "key_value",
"headers": [...], # For tables
"rows": [[...], ...], # For tables
"text": "...", # For text blocks
"data": {...} # For key-value pairs
}
Available Sections:
ChartOverviewSection: Date, time, location, chart typePlanetPositionSection: Planet positions tableAspectSection: Aspects tableHousePositionSection: House placements tableMidpointSection: Midpoints tableDignitySection: Essential dignities tableAspectPatternSection: Detected patterns
Report Renderers¶
Location: presentation/renderers.py
RichTableRenderer¶
Beautiful terminal output using Rich library.
class RichTableRenderer:
def render_section(
self,
section_name: str,
section_data: dict
) -> str:
"""Render section with Rich formatting."""
...
def render_report(
self,
sections: list[tuple[str, dict]]
) -> str:
"""Render complete report."""
...
Features:
Color-coded tables
Unicode symbols (glyphs)
Aligned columns
Section headers
PlainTextRenderer¶
Simple text tables for files.
class PlainTextRenderer:
def render_section(
self,
section_name: str,
section_data: dict
) -> str:
"""Render section as plain text."""
...
def render_report(
self,
sections: list[tuple[str, dict]]
) -> str:
"""Render complete report."""
...
Features:
ASCII tables
No color/formatting
File-friendly output
Comparison Charts¶
Synastry and transit comparisons between two charts.
Location: core/comparison.py
ComparisonBuilder¶
class ComparisonBuilder:
@classmethod
def from_natives(
cls,
native1: Native,
native2: Native,
comparison_type: ComparisonType = ComparisonType.SYNASTRY
) -> "ComparisonBuilder":
"""
Create comparison builder.
Args:
native1: Native/inner chart
native2: Partner/transit/outer chart
comparison_type: SYNASTRY or TRANSIT
"""
...
@classmethod
def from_charts(
cls,
chart1: CalculatedChart,
chart2: CalculatedChart,
comparison_type: ComparisonType = ComparisonType.SYNASTRY
) -> "ComparisonBuilder":
"""Create from pre-calculated charts."""
...
Configuration Methods:
def with_aspect_engine(
self,
engine: AspectEngine
) -> "ComparisonBuilder":
"""Set aspect engine for cross-chart aspects."""
...
def with_orb_engine(
self,
engine: OrbEngine
) -> "ComparisonBuilder":
"""Set orb engine."""
...
def with_labels(
self,
chart1_label: str,
chart2_label: str
) -> "ComparisonBuilder":
"""Set chart labels for display."""
...
Execution:
def calculate(self) -> Comparison:
"""
Calculate comparison.
Process:
1. Calculate both charts (if not already calculated)
2. Combine all positions
3. Calculate cross-chart aspects
4. Calculate house overlays
5. Return Comparison object
"""
...
Convenience Functions¶
def create_synastry(
native1: Native,
native2: Native
) -> Comparison:
"""Quick synastry comparison."""
...
def create_transits(
native: Native,
transit_datetime: dt.datetime
) -> Comparison:
"""
Quick transit comparison.
Args:
native: Birth chart
transit_datetime: Transit time (uses birth location)
"""
...
Comparison Data Models¶
ComparisonType (Enum)¶
class ComparisonType(Enum):
SYNASTRY = "synastry"
TRANSIT = "transit"
HouseOverlay¶
@dataclass(frozen=True)
class HouseOverlay:
planet_name: str # Name of planet
planet_owner: str # "chart1" or "chart2"
house_number: int # House it falls in
house_owner: str # "chart1" or "chart2"
cusp_longitude: float
Example:
# "Partner's Venus falls in your 7th house"
overlay = HouseOverlay(
planet_name="Venus",
planet_owner="chart2",
house_number=7,
house_owner="chart1",
cusp_longitude=195.0
)
Usage Examples¶
Synastry:
from stellium.core.native import Native
from stellium.core.comparison import create_synastry
native1 = Native(datetime(1990, 1, 1, 12, 0), "New York, NY")
native2 = Native(datetime(1992, 5, 15, 8, 30), "Los Angeles, CA")
synastry = create_synastry(native1, native2)
# Check cross-aspects
for aspect in synastry.cross_aspects:
print(f"{aspect.chart1_object.name} {aspect.aspect_name} "
f"{aspect.chart2_object.name} (orb: {aspect.orb:.2f}°)")
# Check house overlays
for overlay in synastry.house_overlays:
owner = "Partner's" if overlay.planet_owner == "chart2" else "Your"
house_owner = "your" if overlay.house_owner == "chart1" else "partner's"
print(f"{owner} {overlay.planet_name} falls in {house_owner} house {overlay.house_number}")
Transits:
from datetime import datetime
from stellium.core.native import Native
from stellium.core.comparison import create_transits
# Birth chart
native = Native(datetime(1990, 1, 1, 12, 0), "New York, NY")
# Current transits
transits = create_transits(native, datetime.now())
# Check transit aspects
for aspect in transits.cross_aspects:
print(f"Transit {aspect.chart2_object.name} {aspect.aspect_name} "
f"Natal {aspect.chart1_object.name}")
Custom Configuration:
from stellium.core.comparison import ComparisonBuilder
from stellium.engines import ModernAspectEngine, SimpleOrbEngine
from stellium.core.config import AspectConfig
# Tight orbs for transits
orb_engine = SimpleOrbEngine(
orb_map={
"Conjunction": 1.0,
"Square": 1.0,
"Trine": 1.0,
"Opposition": 1.0
}
)
aspect_config = AspectConfig(
aspects=["Conjunction", "Square", "Opposition"],
include_angles=False
)
comparison = (
ComparisonBuilder.from_natives(native, transit_native)
.with_aspect_engine(ModernAspectEngine(aspect_config))
.with_orb_engine(orb_engine)
.with_labels("Natal", "Transit")
.calculate()
)
Data Flow¶
Understanding how data flows through the system helps with debugging and extension.
Chart Calculation Flow¶
User Input
↓
┌─────────────────────┐
│ Native │ Parses flexible inputs
│ - Geocode location │ Converts to UTC
│ - Find timezone │ Creates immutable data
└─────────────────────┘
↓
┌─────────────────────┐
│ ChartBuilder │ Fluent API
│ .from_native() │ Configuration methods
│ .with_aspects() │ .add_component()
│ .with_houses() │ etc.
└─────────────────────┘
↓
┌─────────────────────┐
│ .calculate() │ Orchestrates calculation
└─────────────────────┘
↓
┌─────────────────────────────────────┐
│ CALCULATION STEPS (in order): │
│ │
│ 1. Ephemeris Engine │
│ → Calculate planetary positions │
│ │
│ 2. House System Engines │
│ → Calculate cusps for each │
│ → Calculate angles (ASC/MC/etc) │
│ │
│ 3. House Assignments │
│ → Assign house # to each planet │
│ → For each house system │
│ │
│ 4. Components (in order added) │
│ → Add Arabic parts │
│ → Add midpoints │
│ → Calculate dignities │
│ → (Each gets current state) │
│ │
│ 5. Aspect Engine (if configured) │
│ → Find aspects between all │
│ → Use orb engine for allowances │
│ → Determine applying/separating │
│ │
│ 6. Analyzers (in order added) │
│ → Detect patterns │
│ → Store in metadata │
│ │
└─────────────────────────────────────┘
↓
┌─────────────────────┐
│ CalculatedChart │ Immutable, frozen
│ (frozen dataclass) │ All data included
└─────────────────────┘
Dependency Graph¶
What Depends on What:
Positions (ephemeris)
│
├─→ House Cusps (houses)
│ │
│ └─→ House Placements
│ │
│ └─→ Components (can use houses)
│ │
│ └─→ Aspects (uses all positions)
│ │
│ └─→ Analyzers (uses complete chart)
│
└─→ Aspects (uses positions)
│
└─→ Analyzers
Key Points:
Positions are calculated first - Everything depends on them
Houses need positions - Can’t calculate without ephemeris
Components run after houses - Get access to house placements
Aspects need all positions - Including component-added ones
Analyzers run last - Have access to complete chart
Configuration Propagation¶
How Configuration Flows:
CalculationConfig
↓
ChartBuilder
↓
Ephemeris Engine
↓
(determines which objects to calculate)
AspectConfig
↓
AspectEngine
↓
(determines which aspects to find)
OrbEngine
↓
AspectEngine.calculate_aspects()
↓
(determines if aspects are within orb)
Extension Points¶
How to add new functionality to Stellium.
Adding New Celestial Objects¶
Use Case: Calculate positions for a new asteroid, fixed star, or calculated point.
Steps:
1. Check if Swiss Ephemeris supports it
Find the Swiss Ephemeris ID (MPC number for asteroids, etc.):
Asteroids: Use MPC number
Fixed stars: Use star name
See: https://www.astro.com/swisseph/
2. Add to Swiss Ephemeris ID mapping
In engines/ephemeris.py:
SWISS_EPHEMERIS_IDS = {
# ... existing objects ...
"YourObject": 12345 # MPC number or Swiss Eph ID
}
3. Add to celestial registry
In core/registry.py:
CELESTIAL_REGISTRY["YourObject"] = CelestialObjectInfo(
name="YourObject",
display_name="Your Object",
object_type=ObjectType.ASTEROID, # or POINT, FIXED_STAR, etc.
glyph="⚷", # Unicode symbol
glyph_svg_path=None, # or custom SVG path
swiss_ephemeris_id=12345,
category="YourCategory",
aliases=["Alias1", "Alias2"],
description="Description of your object",
metadata={}
)
4. Include in calculation config
from stellium.core.config import CalculationConfig
config = CalculationConfig(
include_asteroids=["YourObject"] # or include_points
)
chart = ChartBuilder.from_native(native).with_config(config).calculate()
# Access your object
your_object = chart.get_object("YourObject")
print(f"YourObject: {your_object.sign_position}")
Creating Custom House Systems¶
Use Case: Implement a house system not supported by Swiss Ephemeris.
Steps:
1. Implement the protocol
from stellium.core.protocols import HouseSystemEngine
from stellium.core.models import HouseCusps, CelestialPosition, ObjectType
class CustomHouses:
@property
def system_name(self) -> str:
return "Custom System"
def calculate_house_data(
self,
datetime: ChartDateTime,
location: ChartLocation
) -> tuple[HouseCusps, list[CelestialPosition]]:
"""
Calculate house cusps and angles.
Returns:
(HouseCusps, [ASC, MC, DSC, IC, Vertex])
"""
# Your calculation logic here
cusps = [...] # 12 cusp longitudes
# Calculate angles
asc = CelestialPosition(name="Ascendant", object_type=ObjectType.ANGLE, longitude=cusps[0])
mc = CelestialPosition(name="Midheaven", object_type=ObjectType.ANGLE, longitude=cusps[9])
dsc = CelestialPosition(name="Descendant", object_type=ObjectType.ANGLE, longitude=(cusps[0] + 180) % 360)
ic = CelestialPosition(name="Imum Coeli", object_type=ObjectType.ANGLE, longitude=(cusps[9] + 180) % 360)
vertex = CelestialPosition(name="Vertex", object_type=ObjectType.ANGLE, longitude=...)
return (
HouseCusps("Custom System", tuple(cusps)),
[asc, mc, dsc, ic, vertex]
)
def assign_houses(
self,
positions: list[CelestialPosition],
cusps: HouseCusps
) -> dict[str, int]:
"""
Assign house numbers to positions.
Returns:
{object_name: house_number}
"""
placements = {}
for pos in positions:
# Your assignment logic
house_num = self._find_house(pos.longitude, cusps.cusps)
placements[pos.name] = house_num
return placements
def _find_house(self, longitude: float, cusps: tuple) -> int:
"""Helper to find which house a longitude falls in."""
# Your logic here
return house_number
2. Use it
chart = (
ChartBuilder.from_native(native)
.with_house_systems([CustomHouses()])
.calculate()
)
# Access
sun_house = chart.get_house("Sun", "Custom System")
Creating Custom Aspect Types¶
Use Case: Add new aspect angles (e.g., undecile, quindecile).
Steps:
1. Add to aspect registry
In core/registry.py:
ASPECT_REGISTRY["MyAspect"] = AspectInfo(
name="MyAspect",
angle=77.0, # Your angle
category="Custom",
family="My Family",
glyph="◊", # Unicode symbol
color="#FF5733", # Hex color
default_orb=2.0,
aliases=["Alias"],
description="Description of my aspect",
metadata={
"line_width": 1,
"dash_pattern": [5, 5]
}
)
2. Include in aspect config
from stellium.core.config import AspectConfig
from stellium.engines import ModernAspectEngine
config = AspectConfig(
aspects=["Conjunction", "MyAspect"],
include_angles=True
)
chart = (
ChartBuilder.from_native(native)
.with_aspects(ModernAspectEngine(config))
.calculate()
)
# Aspects will include MyAspect
for aspect in chart.aspects:
if aspect.aspect_name == "MyAspect":
print(f"Found {aspect.description}")
Creating Custom Components¶
Use Case: Add new calculated points (e.g., custom lots, harmonic positions).
Steps:
1. Implement the protocol
from stellium.core.protocols import ChartComponent
from stellium.core.models import CelestialPosition, ObjectType
class MyComponent:
@property
def component_name(self) -> str:
return "My Feature"
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 new positions.
Returns:
List of new CelestialPosition objects
"""
new_positions = []
# Your calculation logic
# Example: Calculate a custom point
sun = next((p for p in positions if p.name == "Sun"), None)
moon = next((p for p in positions if p.name == "Moon"), None)
if sun and moon:
# Custom formula
custom_longitude = (sun.longitude + moon.longitude * 2) % 360
custom_point = CelestialPosition(
name="My Custom Point",
object_type=ObjectType.POINT,
longitude=custom_longitude
)
new_positions.append(custom_point)
return new_positions
2. Use it
chart = (
ChartBuilder.from_native(native)
.add_component(MyComponent())
.calculate()
)
# Access
custom_point = chart.get_object("My Custom Point")
print(f"My Custom Point: {custom_point.sign_position}")
Alternative: Store in Metadata
If your component analyzes rather than calculates positions:
class MyAnalysisComponent:
@property
def component_name(self) -> str:
return "My Analysis"
def calculate(self, ...) -> list[CelestialPosition]:
# Analyze chart
analysis_results = {...}
# Store in metadata (won't work directly from component)
# Better to use ChartAnalyzer protocol for this
return [] # No new positions
Creating Custom Orb Engines¶
Use Case: Implement custom orb allowance logic.
Steps:
1. Implement the protocol
from stellium.core.protocols import OrbEngine
from stellium.core.models import CelestialPosition
class MyOrbEngine:
def get_orb_allowance(
self,
obj1: CelestialPosition,
obj2: CelestialPosition,
aspect_name: str
) -> float:
"""
Calculate orb allowance for this aspect.
Returns:
Maximum orb in degrees
"""
# Your logic here
# Example: Wider orbs in angular houses
# (would need access to house placements - use ComplexOrbEngine pattern)
# Simple example: Based on planet type
if obj1.name == "Sun" or obj2.name == "Sun":
return 10.0
elif obj1.object_type == ObjectType.ANGLE or obj2.object_type == ObjectType.ANGLE:
return 2.0
else:
return 5.0
2. Use it
from stellium.engines import ModernAspectEngine
chart = (
ChartBuilder.from_native(native)
.with_aspects(ModernAspectEngine())
.with_orbs(MyOrbEngine())
.calculate()
)
Creating Custom Analyzers¶
Use Case: Analyze charts and store findings in metadata.
Steps:
1. Implement the protocol
from stellium.core.protocols import ChartAnalyzer
from stellium.core.models import CalculatedChart
class MyAnalyzer:
@property
def analyzer_name(self) -> str:
return "My Analysis"
@property
def metadata_name(self) -> str:
return "my_analysis" # Key in chart.metadata
def analyze(self, chart: CalculatedChart) -> dict:
"""
Analyze the chart.
Returns:
Analysis results (stored in chart.metadata[metadata_name])
"""
findings = {}
# Example: Count retrograde planets
retrogrades = [
p for p in chart.get_planets()
if p.is_retrograde
]
findings['retrograde_count'] = len(retrogrades)
findings['retrograde_planets'] = [p.name for p in retrogrades]
# Example: Check element balance
elements = {"Fire": 0, "Earth": 0, "Air": 0, "Water": 0}
for planet in chart.get_planets():
element = self._get_element(planet.sign)
elements[element] += 1
findings['element_balance'] = elements
return findings
def _get_element(self, sign: str) -> str:
"""Helper to get element from sign."""
fire_signs = ["Aries", "Leo", "Sagittarius"]
earth_signs = ["Taurus", "Virgo", "Capricorn"]
air_signs = ["Gemini", "Libra", "Aquarius"]
water_signs = ["Cancer", "Scorpio", "Pisces"]
if sign in fire_signs:
return "Fire"
elif sign in earth_signs:
return "Earth"
elif sign in air_signs:
return "Air"
else:
return "Water"
2. Use it
chart = (
ChartBuilder.from_native(native)
.add_analyzer(MyAnalyzer())
.calculate()
)
# Access findings
findings = chart.metadata['my_analysis']
print(f"Retrograde planets: {findings['retrograde_planets']}")
print(f"Element balance: {findings['element_balance']}")
Creating Custom Report Sections¶
Use Case: Add custom sections to reports.
Steps:
1. Implement the protocol
from stellium.core.protocols import ReportSection
from stellium.core.models import CalculatedChart
class MySection:
@property
def section_name(self) -> str:
return "My Custom Section"
def generate_data(self, chart: CalculatedChart) -> dict:
"""
Generate section data.
Returns:
{
"type": "table" | "text" | "key_value",
"headers": [...], # for tables
"rows": [...], # for tables
"text": "...", # for text
"data": {...} # for key-value
}
"""
# Example: Table of retrograde planets
retrogrades = [p for p in chart.get_planets() if p.is_retrograde]
return {
"type": "table",
"headers": ["Planet", "Position", "Speed"],
"rows": [
[p.name, p.sign_position, f"{p.speed_longitude:.4f}"]
for p in retrogrades
]
}
2. Use it
from stellium.presentation import ReportBuilder
# Create custom builder
builder = ReportBuilder()
builder.from_chart(chart)
# Add custom section manually
builder._sections.append(MySection())
# Render
report = builder.render()
print(report)
Better: Extend ReportBuilder
class CustomReportBuilder(ReportBuilder):
def with_my_section(self) -> "CustomReportBuilder":
"""Add my custom section."""
self._sections.append(MySection())
return self
# Use it
report = (
CustomReportBuilder()
.from_chart(chart)
.with_chart_overview()
.with_my_section()
.render()
)
Configuration Options¶
Comprehensive configuration reference.
Calculation Configuration¶
Location: core/config.py
CalculationConfig¶
Controls which celestial objects are calculated.
@dataclass
class CalculationConfig:
include_planets: list[str] # Planet names
include_nodes: bool = True # North/South Node
include_chiron: bool = True # Chiron
include_points: list[str] # Calculated points (Lilith, etc.)
include_asteroids: list[str] # Asteroid names
Defaults:
include_planets = [
"Sun", "Moon", "Mercury", "Venus", "Mars",
"Jupiter", "Saturn", "Uranus", "Neptune", "Pluto"
]
include_nodes = True
include_chiron = True
include_points = ["Mean Apogee"] # Black Moon Lilith
include_asteroids = []
Presets:
# Minimal (planets only)
CalculationConfig.minimal()
# Comprehensive (everything)
CalculationConfig.comprehensive()
Example:
from stellium.core.config import CalculationConfig
# Custom config
config = CalculationConfig(
include_planets=["Sun", "Moon", "Mercury", "Venus", "Mars"],
include_nodes=False,
include_chiron=True,
include_points=["Mean Apogee", "True Apogee"],
include_asteroids=["Ceres", "Pallas", "Juno", "Vesta"]
)
chart = ChartBuilder.from_native(native).with_config(config).calculate()
AspectConfig¶
Controls which aspects are calculated.
@dataclass
class AspectConfig:
aspects: list[str] # Aspect names
include_angles: bool = True # Include ASC, MC, etc.
include_nodes: bool = True # Include Nodes
include_asteroids: bool = True # Include asteroids
Defaults:
aspects = [
"Conjunction", "Sextile", "Square", "Trine", "Opposition"
] # Ptolemaic 5
Example:
from stellium.core.config import AspectConfig
# Only major aspects, no asteroids
config = AspectConfig(
aspects=["Conjunction", "Square", "Opposition"],
include_angles=True,
include_nodes=True,
include_asteroids=False
)
Engine Configuration¶
Each engine can be configured independently.
Ephemeris Path¶
from stellium.engines import SwissEphemerisEngine
engine = SwissEphemerisEngine(
ephemeris_path="/custom/path/to/ephe"
)
chart = ChartBuilder.from_native(native).with_ephemeris(engine).calculate()
House Systems¶
from stellium.engines import PlacidusHouses, WholeSignHouses, KochHouses
# Multiple systems
chart = (
ChartBuilder.from_native(native)
.with_house_systems([
PlacidusHouses(),
WholeSignHouses(),
KochHouses()
])
.calculate()
)
# Access each
print(chart.get_house("Sun", "Placidus"))
print(chart.get_house("Sun", "Whole Sign"))
print(chart.get_house("Sun", "Koch"))
Orb Configuration¶
See Orb Engines section for full details.
from stellium.engines import SimpleOrbEngine, LuminariesOrbEngine, ComplexOrbEngine
# Simple
simple = SimpleOrbEngine(
orb_map={"Conjunction": 8.0, "Trine": 6.0},
fallback_orb=2.0
)
# Luminaries
luminaries = LuminariesOrbEngine(
luminary_orbs={"Conjunction": 10.0},
default_orbs={"Conjunction": 8.0}
)
# Complex
complex_config = {
"by_pair": {"Moon-Sun": {"default": 10.0}},
"by_planet": {"Sun": {"default": 8.0}},
"by_aspect": {"Conjunction": 8.0},
"default": 3.0
}
complex_orbs = ComplexOrbEngine(complex_config)
Component Configuration¶
Components have their own configuration options.
Arabic Parts¶
from stellium.components import ArabicPartsCalculator
# Default (all lots)
arabic_parts = ArabicPartsCalculator()
chart = (
ChartBuilder.from_native(native)
.add_component(arabic_parts)
.calculate()
)
Dignities¶
from stellium.components import DignityComponent
dignity = DignityComponent(
traditional=True, # Calculate traditional
modern=True, # Calculate modern
receptions=True, # Find mutual receptions
decan_system="triplicity" # "triplicity", "chaldean", "egyptian"
)
chart = (
ChartBuilder.from_native(native)
.add_component(dignity)
.calculate()
)
Accidental Dignities¶
from stellium.components import AccidentalDignityComponent
acc_dignity = AccidentalDignityComponent(
house_system="Placidus", # Which house system to use
include_joys=True # Include traditional house joys
)
chart = (
ChartBuilder.from_native(native)
.add_component(acc_dignity)
.calculate()
)
Caching Configuration¶
from stellium.utils.cache import Cache
# Custom cache
cache = Cache(
cache_dir="/tmp/my_cache",
max_age_seconds=3600, # 1 hour
enabled=True
)
chart = (
ChartBuilder.from_native(native)
.with_cache(
cache=cache,
cache_chart=True, # Cache entire chart
cache_key_prefix="myapp_" # Prefix for cache keys
)
.calculate()
)
# Disable caching
chart = ChartBuilder.from_native(native).with_cache(cache=None).calculate()
Visualization Configuration¶
from stellium.visualization import ChartRenderer, draw_chart
# Custom renderer
renderer = ChartRenderer(
size=1000, # Canvas size
rotation=0 # Rotation in degrees
)
# High-level function
draw_chart(
chart,
"output.svg",
size=1200,
house_system="Whole Sign"
)
Common Patterns & Conventions¶
Immutability¶
All data models are frozen dataclasses. To “modify”:
from dataclasses import replace
old_pos = chart.get_object("Sun")
new_pos = replace(old_pos, longitude=45.0)
# old_pos is unchanged
Protocol-Based Design¶
No inheritance required. Just match the signature:
# This is valid - no inheritance needed
class MyEngine:
def calculate_positions(self, datetime, location, objects=None):
return [...]
# Use it
chart = ChartBuilder.from_native(native).with_ephemeris(MyEngine()).calculate()
Error Handling¶
Common Exceptions:
ValueError: Invalid input (coordinates out of range, invalid dates)RuntimeError: Calculation failures (Swiss Ephemeris errors)KeyError: Missing data in configs or registries
Example:
try:
chart = ChartBuilder.from_native(native).calculate()
except ValueError as e:
print(f"Invalid input: {e}")
except RuntimeError as e:
print(f"Calculation failed: {e}")
Type Hints¶
Stellium is fully typed. Use mypy for type checking:
mypy src/stellium
Example typed usage:
from stellium.core.native import Native
from stellium.core.models import CalculatedChart
from stellium.core.builder import ChartBuilder
native: Native = Native(datetime(2000, 1, 1), "New York")
builder: ChartBuilder = ChartBuilder.from_native(native)
chart: CalculatedChart = builder.calculate()
Querying Charts¶
Always use helper methods:
# Good
sun = chart.get_object("Sun")
# Bad (can fail if Sun not in positions)
sun = next(p for p in chart.positions if p.name == "Sun")
Safe querying:
sun = chart.get_object("Sun")
if sun:
print(f"Sun: {sun.sign_position}")
else:
print("Sun not calculated")
Working with Houses¶
Always specify system name:
# Good
sun_house = chart.get_house("Sun", "Placidus")
# Bad (uses first available, may not be what you want)
sun_house = chart.get_house("Sun")
Fluent API Style¶
Chain methods for readability:
# Good
chart = (
ChartBuilder.from_native(native)
.with_house_systems([PlacidusHouses(), WholeSignHouses()])
.with_aspects(ModernAspectEngine())
.add_component(ArabicPartsCalculator())
.calculate()
)
# Also OK but less readable
builder = ChartBuilder.from_native(native)
builder = builder.with_house_systems([PlacidusHouses()])
builder = builder.with_aspects(ModernAspectEngine())
chart = builder.calculate()
Avoiding Common Mistakes¶
1. Don’t calculate charts without aspects if you need them:
# Wrong - no aspects calculated
chart = ChartBuilder.from_native(native).calculate()
aspects = chart.aspects # Empty tuple!
# Right
chart = (
ChartBuilder.from_native(native)
.with_aspects(ModernAspectEngine())
.calculate()
)
2. Don’t forget to activate pyenv environment:
# Wrong
python examples/usage.py # May fail!
# Right
source ~/.zshrc && pyenv activate stellium && python examples/usage.py
3. Don’t modify frozen dataclasses:
# Wrong
sun = chart.get_object("Sun")
sun.longitude = 45.0 # ERROR: frozen!
# Right
from dataclasses import replace
sun = chart.get_object("Sun")
new_sun = replace(sun, longitude=45.0)
Quick Reference¶
Key Classes by Module¶
Class |
Module |
Purpose |
|---|---|---|
Core Models |
||
|
|
Geographic location |
|
|
Date/time with Julian day |
|
|
Object position data |
|
|
Midpoint position |
|
|
Moon/planet phase |
|
|
House cusps data |
|
|
Aspect between objects |
|
|
Cross-chart aspect |
|
|
Aspect pattern |
|
|
Complete chart data |
|
|
Synastry/transit comparison |
Builders |
||
|
|
Input parser |
|
|
Notable births registry |
|
|
Chart builder (fluent API) |
|
|
Comparison builder |
|
|
Report builder |
Configuration |
||
|
|
What to calculate |
|
|
Which aspects |
Engines |
||
|
|
Planet positions |
|
|
Placidus houses |
|
|
Whole sign houses |
|
|
Koch houses |
|
|
Equal houses |
|
|
Modern aspects |
|
|
Harmonic aspects |
|
|
Simple orbs |
|
|
Luminary orbs |
|
|
Complex orb rules |
|
|
Traditional dignities |
|
|
Modern dignities |
|
|
Mutual receptions |
|
|
Pattern detection |
Components |
||
|
|
Arabic parts |
|
|
Midpoints |
|
|
Essential dignities |
|
|
Accidental dignities |
Visualization |
||
|
|
SVG renderer |
|
|
Zodiac wheel |
|
|
House cusps |
|
|
Planets |
|
|
Aspect lines |
|
|
High-level drawing |
Common Import Patterns¶
# Essential imports
from datetime import datetime
from stellium.core.native import Native
from stellium.core.builder import ChartBuilder
# Models
from stellium.core.models import (
CalculatedChart,
CelestialPosition,
Aspect,
ObjectType
)
# Configuration
from stellium.core.config import CalculationConfig, AspectConfig
# Engines
from stellium.engines import (
PlacidusHouses,
WholeSignHouses,
ModernAspectEngine,
SimpleOrbEngine
)
# Components
from stellium.components import (
ArabicPartsCalculator,
MidpointCalculator,
DignityComponent
)
# Visualization
from stellium.visualization import draw_chart
# Reporting
from stellium.presentation import ReportBuilder
Quick Usage Examples¶
Minimal chart:
chart = ChartBuilder.from_notable("Albert Einstein").calculate()
With aspects:
from stellium.engines import ModernAspectEngine
chart = (
ChartBuilder.from_notable("Albert Einstein")
.with_aspects(ModernAspectEngine())
.calculate()
)
Full featured:
from stellium.engines import PlacidusHouses, ModernAspectEngine, AspectPatternAnalyzer
from stellium.components import ArabicPartsCalculator, DignityComponent
chart = (
ChartBuilder.from_native(native)
.with_house_systems([PlacidusHouses()])
.with_aspects(ModernAspectEngine())
.add_component(ArabicPartsCalculator())
.add_component(DignityComponent())
.add_analyzer(AspectPatternAnalyzer())
.calculate()
)
Generate report:
from stellium.presentation import ReportBuilder
report = (
ReportBuilder()
.from_chart(chart)
.with_chart_overview()
.with_planet_positions()
.with_aspects()
.render()
)
print(report)
Draw chart:
from stellium.visualization import draw_chart
draw_chart(chart, "output.svg", size=1000)
File Locations¶
Core abstractions: src/stellium/core/
Calculation engines: src/stellium/engines/
Add-on components: src/stellium/components/
Chart rendering: src/stellium/visualization/
Report generation: src/stellium/presentation/
Tests: tests/
Examples: examples/
Documentation: docs/
Appendix: Swiss Ephemeris Setup¶
Stellium requires Swiss Ephemeris data files. These are included in data/swisseph/ephe/.
Default path: src/stellium/data/swisseph/ephe/
Custom path:
from stellium.engines import SwissEphemerisEngine
engine = SwissEphemerisEngine(ephemeris_path="/path/to/ephe")
chart = ChartBuilder.from_native(native).with_ephemeris(engine).calculate()
Environment setup (from CLAUDE.md):
Always activate the pyenv environment before running:
source ~/.zshrc && pyenv activate stellium && python your_script.py
End of Architecture Documentation