Examples

Moon Phases with Pattern Matching

The example below uses emoji labels in prose — not inside Python strings — to avoid syntax-highlight warnings.

from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from ketu.ephemeris.time import utc_to_julian
from ketu.calculations import long

def lunar_phase(jday):
    """Return (phase_name, elongation, description) for the given Julian Day."""

    # Calculate the Sun-Moon angle
    sun_long = long(jday, 0)
    moon_long = long(jday, 1)

    # Elongation of the Moon relative to the Sun
    elongation = (moon_long - sun_long) % 360

    # Phase matching on elongation range
    match elongation:
        case e if 0 <= e < 22.5:
            return "New Moon", e, "Conjunction"
        case e if 22.5 <= e < 67.5:
            return "Waxing Crescent", e, "Waxing"
        case e if 67.5 <= e < 112.5:
            return "First Quarter", e, "Waxing Quarter"
        case e if 112.5 <= e < 157.5:
            return "Waxing Gibbous", e, "Gibbous"
        case e if 157.5 <= e < 202.5:
            return "Full Moon", e, "Opposition"
        case e if 202.5 <= e < 247.5:
            return "Waning Gibbous", e, "Gibbous"
        case e if 247.5 <= e < 292.5:
            return "Last Quarter", e, "Waning Square"
        case e if 292.5 <= e < 337.5:
            return "Waning Crescent", e, "Balsamic"
        case _:
            return "New Moon", e, "Conjunction"


def lunar_calendar(year, month):
    """Generate a lunar phase calendar for a month."""

    print(f"\n{'='*50}")
    print(f"LUNAR CALENDAR - {month:02d}/{year}")
    print(f"{'='*50}\n")

    tz = ZoneInfo("UTC")

    for day in range(1, 32):
        try:
            dt = datetime(year, month, day, 12, 0, tzinfo=tz)
            jday = utc_to_julian(dt)

            phase, elongation, description = lunar_phase(jday)

            if any(key in phase for key in ["New", "First Quarter",
                                            "Full", "Last Quarter"]):
                print(f"{day:02d}/{month:02d}: {phase} ({elongation:.1f}°)")

        except ValueError:
            break  # End of month


# Example of use
lunar_calendar(2024, 1)

Planetary Transits

from datetime import datetime, timedelta
from dataclasses import dataclass
from zoneinfo import ZoneInfo
from ketu.ephemeris.time import utc_to_julian
from ketu.calculations import long, body_name
from ketu.aspects import get_orb
import ketu.core


@dataclass
class Transit:
    """Represents a planetary transit."""
    planet: str
    aspect: str
    natal_planet: str
    date: datetime
    orb: float
    exact: bool = False


def search_transits(natal_date, transit_date, planets_to_follow=None):
    """Search for transits to natal positions."""

    if planets_to_follow is None:
        planets_to_follow = [0, 1, 2, 3, 4, 5, 6]  # Sun to Saturn

    natal_jday = utc_to_julian(natal_date)
    natal_positions = {i: long(natal_jday, i) for i in planets_to_follow}

    transit_jday = utc_to_julian(transit_date)
    transits = []

    for i_transit in planets_to_follow:
        transit_pos = long(transit_jday, i_transit)

        for natal_i, natal_pos in natal_positions.items():
            diff = abs(transit_pos - natal_pos) % 360
            if diff > 180:
                diff = 360 - diff

            for j, angle in enumerate(ketu.core.aspects["angle"]):
                max_orb = get_orb(i_transit, natal_i, j)
                orb = abs(diff - angle)

                if orb <= max_orb:
                    transit = Transit(
                        planet=body_name(i_transit),
                        aspect=ketu.core.aspects["name"][j].decode(),
                        natal_planet=body_name(natal_i),
                        date=transit_date,
                        orb=orb,
                        exact=(orb < 1.0)
                    )
                    transits.append(transit)

    return transits


# Example
natal = datetime(1990, 5, 15, 14, 30, tzinfo=ZoneInfo("Europe/Paris"))
transit = datetime.now(ZoneInfo("Europe/Paris"))

transits = search_transits(natal, transit)
for t in transits:
    exact = " EXACT!" if t.exact else ''
    print(f"{t.planet} {t.aspect} {t.natal_planet} natal "
          f"(orb: {t.orb:.2f}°){exact}")

Period Analysis

from datetime import datetime, timedelta
from ketu.ephemeris.time import utc_to_julian
from ketu.calculations import long, body_sign, is_retrograde, body_name
from ketu.aspects import calculate_aspects
import ketu
import ketu.core


def analyze_period(start_date, end_date, step_days=1):
    """Analyze aspects over a period."""

    results = {
        "exact_aspects": [],
        "sign_changes": [],
        "retrogrades": [],
        "statistics": {}
    }

    current = start_date
    prev_signs = None
    prev_retros = None

    while current <= end_date:
        jday = utc_to_julian(current)

        signs = [body_sign(long(jday, i))[0] for i in range(10)]
        retros = [is_retrograde(jday, i) for i in range(10)]

        if prev_signs is not None:
            for i, (s1, s2) in enumerate(zip(prev_signs, signs)):
                if s1 != s2:
                    results["sign_changes"].append({
                        "date": current,
                        "planet": body_name(i),
                        "old_sign": ketu.signs[s1],
                        "new_sign": ketu.signs[s2]
                    })

        if prev_retros is not None:
            for i, (r1, r2) in enumerate(zip(prev_retros, retros)):
                if r1 != r2:
                    results["retrogrades"].append({
                        "date": current,
                        "planet": body_name(i),
                        "status": 'Retrograde' if r2 else "Direct"
                    })

        aspects = calculate_aspects(jday)
        for asp in aspects:
            if abs(asp[3]) < 0.5:
                results["exact_aspects"].append({
                    "date": current,
                    "aspect": ketu.core.aspects["name"][asp[2]].decode(),
                    "planet1": body_name(asp[0]),
                    "planet2": body_name(asp[1]),
                    "orb": asp[3]
                })

        prev_signs = signs
        prev_retros = retros
        current += timedelta(days=step_days)

    return results


# Analyze the current month
start = datetime.now().replace(day=1, hour=0, minute=0, second=0, microsecond=0)
end = (start + timedelta(days=32)).replace(day=1) - timedelta(days=1)

analysis = analyze_period(start, end)
print(f"Sign changes: {len(analysis['sign_changes'])}")
print(f"Direction changes: {len(analysis['retrogrades'])}")
print(f"Exact aspects: {len(analysis['exact_aspects'])}")

Full Chart with Synastry (New in v1.2)

from ketu.ephemeris.time import utc_to_julian
from ketu.charts import compute_chart
from ketu.synastry import calculate_synastry
from ketu.calculations import body_name
from datetime import datetime

# Person A: Paris, J2000
jd_a = 2451545.0
chart_a = compute_chart(jd_a, 48.8566, 2.3522, system="placidus")

# Person B: London, one year later
jd_b = 2451910.0
chart_b = compute_chart(jd_b, 51.5074, -0.1278, system="placidus")

print(f"Person A — ASC: {chart_a['asc']:.2f}°  Sun: {chart_a['body_lons'][0]:.2f}°")
print(f"Person B — ASC: {chart_b['asc']:.2f}°  Sun: {chart_b['body_lons'][0]:.2f}°")

# Compute synastry aspects between the two charts
syn = calculate_synastry(chart_a, chart_b)

print(f"\nSynastry aspects ({len(syn)} in orb):")

# calculate_synastry extends the 14-body axis with two angles:
# index 14 = ASC, index 15 = MC. body_name() only knows 0–13, so map the angles.
ANGLE_LABEL = {14: "ASC", 15: "MC"}

def synastry_label(idx):
    return ANGLE_LABEL.get(idx, body_name(idx))

for row in syn[:5]:   # Show first 5
    name_a = synastry_label(int(row["body_a"]))
    name_b = synastry_label(int(row["body_b"]))
    print(f"  {name_a} (A) — {name_b} (B): orb {row['orb']:.2f}°")

Solar Return (New in v1.2)

from ketu.ephemeris.time import utc_to_julian
from ketu.returns import solar_return
from datetime import datetime

# Natal data
natal_jd = 2451545.0       # J2000 — birth Julian Day
natal_lat, natal_lon = 48.8566, 2.3522   # Paris

# Solar return for 2026 at the natal location
sr = solar_return(natal_jd, natal_lat, natal_lon, target_year=2026)

print(f"Solar Return 2026:")
print(f"  ASC:  {sr['asc']:.2f}°")
print(f"  MC:   {sr['mc']:.2f}°")
print(f"  Sun:  {sr['body_lons'][0]:.2f}°")

# Relocated solar return (e.g. London)
sr_london = solar_return(
    natal_jd, natal_lat, natal_lon,
    target_year=2026,
    return_lat=51.5074, return_lon=-0.1278
)
print(f"\nRelocated to London — ASC: {sr_london['asc']:.2f}°")

Next steps

  • Check out the Concepts to understand the theory

  • Refer to the API for technical details

  • Explore Relational Charts for synastry and composite

  • Contribute to the project on GitHub