Metadata-Version: 2.5
Name: kerykeion
Version: 6.0.0a93
Summary: A Python library for astrological calculations, including natal charts, houses, planetary aspects, and SVG chart generation.
Project-URL: Homepage, https://www.kerykeion.net/
Project-URL: Repository, https://github.com/g-battaglia/kerykeion
Project-URL: Documentation, https://www.kerykeion.net/content/docs/
Project-URL: Changelog, https://github.com/g-battaglia/kerykeion/blob/alpha/v6/CHANGELOG.md
Project-URL: Issues, https://github.com/g-battaglia/kerykeion/issues
Author-email: Giacomo Battaglia <kerykeion.astrology@gmail.com>
License: AGPL-3.0
License-File: AUTHORS
License-File: LICENSE
License-File: NOTICE
Keywords: astrology,astrology calculations,astrology calculator,astrology library,astrology transits,astronomical algorithms,birth chart,ephemeris,houses of astrology,natal chart,planetary aspects,svg charts,synastry,zodiac,zodiac signs
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Religion
Classifier: Topic :: Scientific/Engineering :: Astronomy
Classifier: Topic :: Scientific/Engineering :: Visualization
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: libephemeris==3.2.1
Requires-Dist: pydantic>=2.5
Requires-Dist: requests-cache>=1.2.1
Requires-Dist: requests>=2.33.0
Requires-Dist: simple-ascii-tables>=1.0.0
Requires-Dist: svg-polish>=1.0.0
Requires-Dist: typing-extensions>=4.12.2
Requires-Dist: tzdata>=2025.2
Provides-Extra: all
Requires-Dist: kerykeion-cli; extra == 'all'
Requires-Dist: pyswisseph>=2.10.3.1; extra == 'all'
Provides-Extra: cli
Requires-Dist: kerykeion-cli; extra == 'cli'
Provides-Extra: swiss
Requires-Dist: pyswisseph>=2.10.3.1; extra == 'swiss'
Description-Content-Type: text/markdown

<h1 align="center">Kerykeion</h1>

<div align="center">
    <img src="https://img.shields.io/github/stars/g-battaglia/kerykeion.svg?logo=github" alt="stars">
    <img src="https://img.shields.io/github/forks/g-battaglia/kerykeion.svg?logo=github" alt="forks">
</div>
<div align="center">
    <img src="https://static.pepy.tech/badge/kerykeion/month" alt="PyPI Downloads">
    <img src="https://static.pepy.tech/badge/kerykeion/week" alt="PyPI Downloads">
    <img src="https://static.pepy.tech/personalized-badge/kerykeion?period=total&units=INTERNATIONAL_SYSTEM&left_color=GREY&right_color=BLUE&left_text=downloads/total" alt="PyPI Downloads">
</div>
<div align="center">
    <img src="https://img.shields.io/pypi/v/kerykeion?label=pypi%20package" alt="Package version">
    <img src="https://img.shields.io/pypi/pyversions/kerykeion.svg" alt="Supported Python versions">
</div>
<p align="center">⭐ Like this project? Star it on GitHub and help it grow! ⭐</p>

<p align="center">
  <img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/docs/charts/modern_default_natal.svg" width="540" alt="John Lennon - Natal Chart">
</p>

Kerykeion is a Python library for astrology. It computes planetary and house positions, detects aspects, and generates SVG charts: birth, synastry, transit, solar and lunar return, progression and composite charts. You can also customize which planets to include in your calculations.

The main goal of this project is to offer a clean, data-driven approach to astrology, making it accessible and programmable.

Kerykeion also integrates seamlessly with LLM and AI applications.

## **Web API**

If you want to use Kerykeion in a web application or for commercial or _closed-source_ purposes, you can try the dedicated web API:

**[AstrologerAPI](https://rapidapi.com/gbattaglia/api/astrologer/pricing)**

It is [open source](https://github.com/g-battaglia/Astrologer-API) and directly supports this project.

## Table of Contents

- [**Web API**](#web-api)
- [Table of Contents](#table-of-contents)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Basic Usage](#basic-usage)
- [Generate a SVG Chart](#generate-a-svg-chart)
  - [Birth Chart](#birth-chart)
  - [External Birth Chart](#external-birth-chart)
  - [Synastry Chart](#synastry-chart)
  - [Transit Chart](#transit-chart)
  - [Solar Return Chart (Dual Wheel)](#solar-return-chart-dual-wheel)
  - [Solar Return Chart (Single Wheel)](#solar-return-chart-single-wheel)
  - [Lunar Return Chart](#lunar-return-chart)
  - [Composite Chart](#composite-chart)
- [Wheel Only Charts](#wheel-only-charts)
  - [Birth Chart](#birth-chart-1)
  - [Wheel Only Birth Chart (External)](#wheel-only-birth-chart-external)
  - [Synastry Chart](#synastry-chart-1)
- [Output Options](#output-options)
  - [Change the Output Directory](#change-the-output-directory)
  - [Change Language](#change-language)
  - [Minified SVG](#minified-svg)
  - [SVG without CSS Variables](#svg-without-css-variables)
  - [Grid Only SVG](#grid-only-svg)
  - [Machine-readable point metadata](#machine-readable-point-metadata)
- [Chart Options](#chart-options)
- [Classic Chart Style](#classic-chart-style)
  - [Classic Birth Chart](#classic-birth-chart)
  - [Classic Synastry Chart](#classic-synastry-chart)
  - [Classic Transit Chart](#classic-transit-chart)
  - [Classic Wheel Only](#classic-wheel-only)
- [Report Generator](#report-generator)
  - [Quick Examples](#quick-examples)
  - [Section Access](#section-access)
- [AI Context Serializer](#ai-context-serializer)
  - [Quick Example](#quick-example)
- [Example: Retrieving Aspects](#example-retrieving-aspects)
- [Relationship Score](#relationship-score)
- [House Comparison (Synastry Overlay)](#house-comparison-synastry-overlay)
- [Element & Quality Distribution Strategies](#element--quality-distribution-strategies)
- [Ayanamsa (Sidereal Modes)](#ayanamsa-sidereal-modes)
- [House Systems](#house-systems)
- [Perspective Type](#perspective-type)
- [Themes](#themes)
- [Alternative Initialization](#alternative-initialization)
- [Arabic Parts (Lots)](#arabic-parts-lots)
- [Lunar Nodes (Rahu & Ketu)](#lunar-nodes-rahu--ketu)
- [Fixed Stars](#fixed-stars)
- [JSON Support](#json-support)
- [Moon Phase Details](#moon-phase-details)
- [Timing Factories](#timing-factories)
  - [Sun Times](#sun-times)
  - [Planetary Hours](#planetary-hours)
  - [Void of Course Moon](#void-of-course-moon)
  - [Lunation Finder](#lunation-finder)
  - [Retrograde Stations](#retrograde-stations)
  - [Sign Ingresses](#sign-ingresses)
  - [Mundane Aspects](#mundane-aspects)
- [V6 Advanced Features](#v6-advanced-features)
  - [Uranian / Hamburg School Planets](#uranian--hamburg-school-planets)
  - [Essential Dignities](#essential-dignities)
  - [Vedic Nakshatras](#vedic-nakshatras)
  - [Eclipse Search](#eclipse-search)
  - [Planetary Phenomena](#planetary-phenomena)
  - [Planetary Nodes & Apsides](#planetary-nodes--apsides)
  - [Heliacal Risings & Settings](#heliacal-risings--settings)
  - [Occultation Search](#occultation-search)
  - [Davison Composite Chart](#davison-composite-chart)
  - [Relocated Charts](#relocated-charts)
  - [Motion State & Stations](#motion-state--stations)
  - [Declination & Out-of-Bounds Detection](#declination--out-of-bounds-detection)
  - [Barycentric & Planetocentric Perspectives](#barycentric--planetocentric-perspectives)
  - [Nutation Model](#nutation-model)
  - [Dynamic Fixed Star Discovery](#dynamic-fixed-star-discovery)
  - [Gauquelin Sectors](#gauquelin-sectors)
  - [Local Space (Azimuth & Altitude)](#local-space-azimuth--altitude)
  - [Lilith Variants & Priapus Points](#lilith-variants--priapus-points)
  - [Transit Exactness & Refinement](#transit-exactness--refinement)
  - [Primary Directions (Placidus Semi-Arc)](#primary-directions-placidus-semi-arc)
  - [Secondary Progressions (Day-for-a-Year)](#secondary-progressions-day-for-a-year)
  - [Solar Arc Directions](#solar-arc-directions)
  - [Midpoints (Cosmobiology / 90° Dial)](#midpoints-cosmobiology--90-dial)
  - [Astro-Cartography (ACG)](#astro-cartography-acg)
  - [Chart Dominants](#chart-dominants)
  - [Zodiacal Releasing (Aphesis)](#zodiacal-releasing-aphesis)
  - [Profections (Annual)](#profections-annual)
  - [Firdaria (Planetary Periods)](#firdaria-planetary-periods)
  - [Mutual Receptions](#mutual-receptions)
  - [Horary Indicators](#horary-indicators)
- [Command Line Interface](#command-line-interface)
- [Documentation](#documentation)
- [Projects built with Kerykeion](#projects-built-with-kerykeion)
- [Development](#development)
- [Using the Swiss Ephemeris Backend (Optional)](#using-the-swiss-ephemeris-backend-optional)
- [AI Agent Skill](#ai-agent-skill)
- [Integrating Kerykeion into Your Project](#integrating-kerykeion-into-your-project)
- [License](#license)
- [Contributing](#contributing)
- [Citations](#citations)

## Installation

Kerykeion requires **Python 3.12** or higher.

```bash
pip3 install kerykeion
```

For more installation options and environment setup, see the [Getting Started guide](https://www.kerykeion.net/content/docs/).

> **Note — supported date range.** The default ephemeris data bundled with a
> fresh install covers the years **1850–2150** (JPL DE440s; upper bound exclusive, so through 2149-12-31). Charts outside
> that range raise a `KerykeionException` until you install a wider data tier:
>
> ```text
> import libephemeris
> libephemeris.download_leb_for_tier("medium")    # 1550–2650 (upper bound exclusive: through 2649-12-31)
> libephemeris.download_leb_for_tier("extended")  # full range, incl. BCE dates
> ```

## Quick Start

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

subject = AstrologicalSubjectFactory.from_birth_data(
    name="Example Person",
    year=1990, month=7, day=15,
    hour=10, minute=30,
    lng=12.4964,
    lat=41.9028,
    tz_str="Europe/Rome",
    online=False,
)

chart_data = ChartDataFactory.create_natal_chart_data(subject)
chart_drawer = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)

chart_drawer.save_svg(output_path=output_dir, filename="example-natal")
print("Chart saved to", (output_dir / "example-natal.svg").resolve())
```

This script shows the recommended workflow:

1. Create an astrological subject via `AstrologicalSubjectFactory` with explicit coordinates and timezone (offline mode).
2. Build a `ChartDataModel` through `ChartDataFactory`.
3. Render the SVG via `ChartDrawer`, saving it to a controlled folder (`charts_output`).

Use the same pattern for synastry, composite, transit, or return charts by swapping the factory method.

**📖 More examples: [kerykeion.net/examples](https://www.kerykeion.net/content/examples/)**

## Basic Usage

Below is a simple example illustrating the creation of an astrological subject and retrieving astrological details:

```python
from kerykeion import AstrologicalSubjectFactory

# Create an instance of the AstrologicalSubjectFactory class.
# Arguments: Name, year, month, day, hour, minutes, city, nation
john = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Retrieve information about the Sun:
print(john.sun.model_dump_json())
# > {"name":"Sun","quality":"Cardinal","element":"Air","sign":"Lib","sign_num":6,"position":16.2678943501,"abs_pos":196.2678943501,"emoji":"♎️","point_type":"AstrologicalPoint","house":"Sixth_House","retrograde":false,"speed":0.9884815716,"motion_state":"average","declination":-6.3988858578, ...}
# (further fields: ecliptic_latitude, magnitude, source, precision_class, ephemeris_coverage_*_jd, the dignity fields (decan_*, term_ruler, essential_dignity, dignity_score), nakshatra*, gauquelin_sector, azimuth, altitude_above_horizon, is_out_of_bounds — the opt-in ones are null until their calculate_* flag is on)

# Retrieve information about the first house:
print(john.first_house.model_dump_json())
# > {"name":"First_House","quality":"Cardinal","element":"Fire","sign":"Ari","sign_num":0,"position":19.7235186504,"abs_pos":19.7235186504,"emoji":"♈️","point_type":"House","house":null,"retrograde":null,"speed":886.5070945745,"motion_state":null,"declination":null, ...}
# (a house is a KerykeionPointModel too: the fields a cusp has no value for are null)

# Retrieve the element of the Moon sign:
print(john.moon.element)
# > 'Air'

# Whether the Sun stood above the horizon (day chart) or below it (night chart).
# Computed from the Sun's true geometric altitude — independent of the chart's
# zodiac and perspective, so the value holds for sidereal and heliocentric
# charts and at polar latitudes. Note the chart and report still omit the line
# for a heliocentric chart: the value is about the moment, but the drawing has
# no Sun on it to point at — the Sun is the centre body there and is excluded.
print(john.is_diurnal)
# > False
```

> **Working offline:** pass `online=False` and specify `lng`, `lat`, and `tz_str` as shown above.  
> **Working online:** set `online=True` and provide `city`, `nation`, and a valid GeoNames username. Register for free at [geonames.org](https://www.geonames.org/login). You can set the username via the `KERYKEION_GEONAMES_USERNAME` environment variable or the `geonames_username` parameter.

**📖 Full factory documentation: [AstrologicalSubjectFactory](https://www.kerykeion.net/content/docs/astrological_subject_factory)**

**To avoid GeoNames, provide longitude, latitude, and timezone and set `online=False`:**

```python
from kerykeion import AstrologicalSubjectFactory

john = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    city="Liverpool",
    nation="GB",
    lng=-2.9833,  # Longitude for Liverpool
    lat=53.4000,  # Latitude for Liverpool
    tz_str="Europe/London",  # Timezone for Liverpool
    online=False,
)
```

## Generate a SVG Chart

All chart-rendering examples below create a local `charts_output/` folder so the tests can write without touching your home directory. Feel free to change the path when integrating into your own projects.

To generate a chart, use the `ChartDataFactory` to pre-compute chart data, then `ChartDrawer` to create the visualization. This two-step process ensures clean separation between astrological calculations and chart rendering.

**📖 Chart generation docs: [Charts Documentation](https://www.kerykeion.net/content/docs/charts)**

The info panel in the bottom-left corner also reports the chart's **diurnality** — `Diurnal` when the Sun stood above the horizon, `Nocturnal` below it. `show_diurnality=False` leaves it out; [Chart Options](#chart-options) lists the charts on which the line is omitted because it has no referent.

**Tip:**
The optimized way to open the generated SVG files is with a web browser (e.g., Chrome, Firefox).
To improve compatibility across different applications, you can use the `remove_css_variables` parameter when generating the SVG. This will inline all styles and eliminate CSS variables, resulting in an SVG that is more broadly supported.

### Birth Chart

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subject
john = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute chart data
chart_data = ChartDataFactory.create_natal_chart_data(john)

# Step 3: Create visualization
birth_chart_svg = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
birth_chart_svg.save_svg(output_path=output_dir, filename="john-lennon-natal")
```

The SVG file is saved under `charts_output/john-lennon-natal.svg`.

**📖 More birth chart examples: [Birth Chart Guide](https://www.kerykeion.net/content/examples/birth-chart)**

![John Lennon Birth Chart](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Natal%20Chart%20-%20Modern.svg)

### External Birth Chart

An "external" birth chart places the zodiac wheel on the outer ring, offering an alternative visualization style. `external_view` is a classic-style feature (the default modern style ignores it and logs a warning), so pass `style="classic"` explicitly:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subject
birth_chart = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute chart data for external natal chart
chart_data = ChartDataFactory.create_natal_chart_data(birth_chart)

# Step 3: Create visualization with external_view=True (classic style only)
birth_chart_svg = ChartDrawer(chart_data=chart_data, external_view=True)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
birth_chart_svg.save_svg(output_path=output_dir, filename="john-lennon-natal-external", style="classic")
```

![John Lennon External Birth Chart](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20ExternalNatal%20-%20Natal%20Chart%20-%20Classic.svg)

### Synastry Chart

Synastry charts overlay two individuals' planetary positions to analyze relationship compatibility:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subjects
first = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)
second = AstrologicalSubjectFactory.from_birth_data(
    "Paul McCartney", 1942, 6, 18, 15, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute synastry chart data
chart_data = ChartDataFactory.create_synastry_chart_data(first, second)

# Step 3: Create visualization
synastry_chart = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
synastry_chart.save_svg(output_path=output_dir, filename="lennon-mccartney-synastry")
```

**📖 Synastry chart guide: [Synastry Chart Examples](https://www.kerykeion.net/content/examples/synastry-chart)**

![John Lennon and Paul McCartney Synastry](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Synastry%20Chart%20-%20Modern.svg)

### Transit Chart

Transit charts compare current planetary positions against a natal chart:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subjects
transit = AstrologicalSubjectFactory.from_birth_data(
    "Transit", 2025, 6, 8, 8, 45,
    lng=-84.3880,
    lat=33.7490,
    tz_str="America/New_York",
    online=False,
)
subject = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute transit chart data
chart_data = ChartDataFactory.create_transit_chart_data(subject, transit)

# Step 3: Create visualization
transit_chart = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
transit_chart.save_svg(output_path=output_dir, filename="john-lennon-transit")
```

**📖 Transit chart guide: [Transit Chart Examples](https://www.kerykeion.net/content/examples/transit-chart)**

![John Lennon Transit Chart](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Transit%20Chart%20-%20Modern.svg)

### Solar Return Chart (Dual Wheel)

Solar returns calculate the exact moment the Sun returns to its natal position each year:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, PlanetaryReturnFactory, ChartDataFactory, ChartDrawer

# Step 1: Create natal subject
john = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Calculate Solar Return subject (offline example with manual coordinates)
return_factory = PlanetaryReturnFactory(
    john,
    lng=-2.9833,
    lat=53.4000,
    tz_str="Europe/London",
    online=False
)
solar_return_subject = return_factory.next_return_from_date(1964, 10, 1, return_type="Solar")

# Step 3: Pre-compute return chart data (dual wheel: natal + solar return)
chart_data = ChartDataFactory.create_return_chart_data(john, solar_return_subject)

# Step 4: Create visualization
solar_return_chart = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
solar_return_chart.save_svg(output_path=output_dir, filename="john-lennon-solar-return-dual")
```

**📖 Return chart guide: [Dual Return Chart Examples](https://www.kerykeion.net/content/examples/dual-return-chart)**

![John Lennon Solar Return Chart (Dual Wheel)](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20DualReturnChart%20Chart%20-%20Solar%20Return%20-%20Modern.svg)

### Solar Return Chart (Single Wheel)

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, PlanetaryReturnFactory, ChartDataFactory, ChartDrawer

# Step 1: Create natal subject
john = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Calculate Solar Return subject (offline example with manual coordinates)
return_factory = PlanetaryReturnFactory(
    john,
    lng=-2.9833,
    lat=53.4000,
    tz_str="Europe/London",
    online=False
)
solar_return_subject = return_factory.next_return_from_date(1964, 10, 1, return_type="Solar")

# Step 3: Build a single-wheel return chart
chart_data = ChartDataFactory.create_single_wheel_return_chart_data(solar_return_subject)

# Step 4: Create visualization
single_wheel_chart = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
single_wheel_chart.save_svg(output_path=output_dir, filename="john-lennon-solar-return-single")
```

**📖 Planetary return factory docs: [PlanetaryReturnFactory](https://www.kerykeion.net/content/docs/planetary_return_factory)**

Return instants are reported to the whole second, and a reported instant can be handed back as the seed of the next search: `next_return_from_iso_formatted_time(reported, "Solar")` gives the *following* return, never the same one again, and `backwards=True` from the same seed gives the previous one. The same holds for lunar returns, heliocentric returns and lunar-node crossings.

```python
from kerykeion import AstrologicalSubjectFactory, PlanetaryReturnFactory

john = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30, lng=-2.9833, lat=53.4, tz_str="Europe/London", online=False,
)
returns = PlanetaryReturnFactory(john, lng=-2.9833, lat=53.4, tz_str="Europe/London", online=False)

first = returns.next_return_from_iso_formatted_time("2022-06-01T00:00:00Z", "Solar")
following = returns.next_return_from_iso_formatted_time(first.iso_formatted_utc_datetime, "Solar")
print(first.iso_formatted_utc_datetime, following.iso_formatted_utc_datetime)
# 2022-10-09T14:12:26+00:00 2023-10-09T20:04:25+00:00
```

![John Lennon Solar Return Chart (Single Wheel)](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20Solar%20Return%20-%20SingleReturnChart%20Chart%20-%20Modern.svg)

### Lunar Return Chart

Lunar returns calculate when the Moon returns to its natal position (approximately monthly):

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, PlanetaryReturnFactory, ChartDataFactory, ChartDrawer

# Step 1: Create natal subject
john = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Calculate Lunar Return subject
return_factory = PlanetaryReturnFactory(
    john,
    lng=-2.9833,
    lat=53.4000,
    tz_str="Europe/London",
    online=False
)
lunar_return_subject = return_factory.next_return_from_date(1964, 1, 1, return_type="Lunar")

# Step 3: Build a dual wheel (natal + lunar return)
lunar_return_chart_data = ChartDataFactory.create_return_chart_data(john, lunar_return_subject)
dual_wheel_chart = ChartDrawer(chart_data=lunar_return_chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
dual_wheel_chart.save_svg(output_path=output_dir, filename="john-lennon-lunar-return-dual")

# Optional: create a single-wheel lunar return
single_wheel_data = ChartDataFactory.create_single_wheel_return_chart_data(lunar_return_subject)
single_wheel_chart = ChartDrawer(chart_data=single_wheel_data)
single_wheel_chart.save_svg(output_path=output_dir, filename="john-lennon-lunar-return-single")
```

![John Lennon Lunar Return Chart (Dual Wheel)](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20DualReturnChart%20Chart%20-%20Lunar%20Return%20-%20Modern.svg)

![John Lennon Lunar Return Chart (Single Wheel)](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20Lunar%20Return%20-%20SingleReturnChart%20Chart%20-%20Modern.svg)

### Composite Chart

Composite charts create a single chart from two individuals' midpoints to represent the relationship entity:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, CompositeSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subjects (offline configuration)
angelina = AstrologicalSubjectFactory.from_birth_data(
    "Angelina Jolie", 1975, 6, 4, 9, 9,
    lng=-118.2437,
    lat=34.0522,
    tz_str="America/Los_Angeles",
    online=False,
)

brad = AstrologicalSubjectFactory.from_birth_data(
    "Brad Pitt", 1963, 12, 18, 6, 31,
    lng=-96.7069,
    lat=35.3273,
    tz_str="America/Chicago",
    online=False,
)

# Step 2: Create composite subject. `house_anchor` picks the angle the composite
# house ring is anchored on — "auto" (default), "ascendant" or "midheaven"; the
# model reports the choice in composite_model.house_anchor and .house_frame.
factory = CompositeSubjectFactory(angelina, brad, house_anchor="auto")
composite_model = factory.get_midpoint_composite_subject_model()

# Step 3: Pre-compute composite chart data
chart_data = ChartDataFactory.create_composite_chart_data(composite_model)

# Step 4: Create visualization
composite_chart = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
composite_chart.save_svg(output_path=output_dir, filename="jolie-pitt-composite")
```

**📖 Composite factory docs: [CompositeSubjectFactory](https://www.kerykeion.net/content/docs/composite_subject_factory)**

![Angelina Jolie and Brad Pitt Composite Chart](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/Angelina%20Jolie%20and%20Brad%20Pitt%20Composite%20Chart%20-%20Composite%20Chart%20-%20Modern.svg)

## Wheel Only Charts

For _all_ the charts, you can generate a wheel-only chart by using the method `save_wheel_only_svg_file()`:

**📖 Minimalist charts guide: [Wheel Only & Aspect Grid Charts](https://www.kerykeion.net/content/examples/minimalist-charts-and-aspect-table)**

### Birth Chart

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subject
birth_chart = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute chart data
chart_data = ChartDataFactory.create_natal_chart_data(birth_chart)

# Step 3: Create visualization
birth_chart_svg = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
birth_chart_svg.save_wheel_only_svg_file(output_path=output_dir, filename="john-lennon-natal-wheel")
```

![John Lennon — Natal Chart (Wheel Only)](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Natal%20Chart%20-%20Modern%20Wheel%20Only.svg)

### Wheel Only Birth Chart (External)

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subject
birth_chart = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute external natal chart data
chart_data = ChartDataFactory.create_natal_chart_data(birth_chart)

# Step 3: Create visualization (external wheel view, classic style only)
birth_chart_svg = ChartDrawer(chart_data=chart_data, external_view=True)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
birth_chart_svg.save_wheel_only_svg_file(
    output_path=output_dir, filename="john-lennon-natal-wheel-external", style="classic"
)
```

![John Lennon — Natal Chart (External Wheel Only)](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Wheel%20External%20Only%20-%20ExternalNatal%20Chart%20-%20Classic%20Wheel%20Only.svg)

### Synastry Chart

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subjects
first = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)
second = AstrologicalSubjectFactory.from_birth_data(
    "Paul McCartney", 1942, 6, 18, 15, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute synastry chart data
chart_data = ChartDataFactory.create_synastry_chart_data(first, second)

# Step 3: Create visualization
synastry_chart = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
synastry_chart.save_wheel_only_svg_file(output_path=output_dir, filename="lennon-mccartney-synastry-wheel")
```

![John Lennon and Paul McCartney Synastry](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Synastry%20Chart%20-%20Modern%20Wheel%20Only.svg)

## Output Options

Every `ChartDrawer` — whatever the chart type or style — can be pointed at a folder, translated, minified, made self-contained, or reduced to its aspect grid.

### Change the Output Directory

To save the SVG file in a custom location, specify the `output_path` parameter in `save_svg()`:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subjects
first = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)
second = AstrologicalSubjectFactory.from_birth_data(
    "Paul McCartney", 1942, 6, 18, 15, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute synastry chart data
chart_data = ChartDataFactory.create_synastry_chart_data(first, second)

# Step 3: Create visualization with custom output directory
synastry_chart = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
synastry_chart.save_svg(output_path=output_dir)
print("Saved to", (output_dir / f"{synastry_chart.first_obj.name} - Synastry Chart - Modern.svg").resolve())
```

### Change Language

You can switch chart language by passing `chart_language` to the `ChartDrawer` class:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subject
birth_chart = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute chart data
chart_data = ChartDataFactory.create_natal_chart_data(birth_chart)

# Step 3: Create visualization with Italian language
birth_chart_svg = ChartDrawer(
    chart_data=chart_data,
    chart_language="IT"  # Change to Italian
)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
birth_chart_svg.save_svg(output_path=output_dir, filename="john-lennon-natal-it")
```

You can also provide custom labels (or introduce a brand-new language) by passing
a dictionary to `language_pack`. Only the keys you supply are merged on top of the
built-in strings:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

birth_chart = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)
chart_data = ChartDataFactory.create_natal_chart_data(birth_chart)

custom_labels = {
    "PT": {
        "info": "Informações",
        "celestial_points": {"Sun": "Sol", "Moon": "Lua"},
    }
}

custom_chart = ChartDrawer(
    chart_data=chart_data,
    chart_language="PT",
    language_pack=custom_labels["PT"],
)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
custom_chart.save_svg(output_path=output_dir, filename="john-lennon-natal-pt")
```

**📖 Language configuration guide: [Chart Language Settings](https://www.kerykeion.net/content/examples/chart-language)**

The available languages are:

- EN (English)
- FR (French)
- PT (Portuguese)
- ES (Spanish)
- TR (Turkish)
- RU (Russian)
- IT (Italian)
- CN (Chinese)
- DE (German)
- HI (Hindi)

### Minified SVG

To generate a minified SVG, set `minify=True` in the `save_svg()` method:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subject
birth_chart = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute chart data
chart_data = ChartDataFactory.create_natal_chart_data(birth_chart)

# Step 3: Create visualization
birth_chart_svg = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
birth_chart_svg.save_svg(
    output_path=output_dir,
    filename="john-lennon-natal-minified",
    minify=True,
)
```

### SVG without CSS Variables

To generate an SVG without CSS variables, set `remove_css_variables=True` in the `save_svg()` method:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subject
birth_chart = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute chart data
chart_data = ChartDataFactory.create_natal_chart_data(birth_chart)

# Step 3: Create visualization
birth_chart_svg = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
birth_chart_svg.save_svg(
    output_path=output_dir,
    filename="john-lennon-natal-no-css-variables",
    remove_css_variables=True,
)
```

This will inline all styles and eliminate CSS variables, resulting in an SVG that is more broadly supported.

### Grid Only SVG

It's possible to generate a grid-only SVG, useful for creating a custom layout. To do this, use the `save_aspect_grid_only_svg_file()` method:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subjects
birth_chart = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)
second = AstrologicalSubjectFactory.from_birth_data(
    "Paul McCartney", 1942, 6, 18, 15, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute synastry chart data
chart_data = ChartDataFactory.create_synastry_chart_data(birth_chart, second)

# Step 3: Create visualization with dark theme
aspect_grid_chart = ChartDrawer(chart_data=chart_data, theme="dark")

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
aspect_grid_chart.save_aspect_grid_only_svg_file(output_path=output_dir, filename="lennon-mccartney-aspect-grid")
```

![John Lennon — Aspect Grid](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Aspect%20Grid%20Only%20-%20Natal%20Chart%20-%20Aspect%20Grid%20Only.svg)

### Machine-readable point metadata

Rendered `<g kr:node="ChartPoint">` elements expose stable `kr:` attributes for
interactive consumers. On every dual wheel (Transit, Synastry, Dual Return,
and Progression), `kr:house` remains the point owner's house and
`kr:horoscope` identifies that owner ring. `kr:projectedhouse` gives the same
point's house in the other subject's cusp system, while
`kr:projectedhoroscope` identifies that target ring. The reciprocal metadata is
available in classic/modern, full/wheel-only output even when house-comparison
data or tables are disabled.

Each point also carries the physical state the model computed for it —
`kr:motionstate`, `kr:speed`, `kr:declination`, `kr:oob` when the body is out of
bounds, plus `kr:magnitude`, `kr:nearpoint` and `kr:orb` on fixed stars — and the
chart-level analyses it takes part in: `kr:angularity` (one attribute listing
every angle the point stands on, as `Ascendant:0.8991 Medium_Coeli:4.3156`,
closest first) and `kr:stellium`. None of these are gated by a
rendering option; the opt-in marks in [Chart Options](#chart-options) only decide
whether a reader sees them drawn. An attribute is **absent** when the model does not carry the value, so
silence means "this chart does not compute it" rather than zero or false — a
heliocentric chart states no motion state, a midpoint composite none at all.
Attribute names are lowercase letters with no separators (`motionstate`, not
`motion_state`), because consumers rewrite the namespace with a general pattern
and a name carrying an underscore would be dropped silently. See the
[charts documentation](https://www.kerykeion.net/content/docs/charts) for the
full table.

## Chart Options

`ChartDrawer` takes its options at construction. `style` and `glyph_size` can also be overridden per render (`save_svg(style="classic")`); every other option is constructor-only. All of them apply to the full chart and to the wheel-only output alike.

**Style.** Since v6 the **modern** concentric-ring layout is the default. The traditional **classic** wheel remains fully supported: `ChartDrawer(chart_data=..., style="classic")` or `save_svg(style="classic")`. Available `style` values: `"modern"` (default) and `"classic"`. Both styles work with all three themes — see [Themes](#themes) — or with no theme at all.

Default filenames spell the style out: `save_svg()` writes `"{name} - {chart type} Chart - Modern.svg"`, and with `style="classic"` it writes `"... - Classic.svg"` (wheel-only output uses `" - Modern Wheel Only"` / `" - Classic Wheel Only"`).

**Every chart, both styles:**

| Parameter | Type | Default | Description |
|---|---|---|---|
| `theme` | `str \| None` | `"classic"` | `"classic"`, `"dark"` or `"black-and-white"`; `None` ships the CSS variables unthemed, for your own stylesheet. See [Themes](#themes) |
| `chart_language` | `str` | `"EN"` | One of the ten chart languages; a `language_pack` dict supplies custom labels. See [Change Language](#change-language) |
| `custom_title` | `str \| None` | `None` | Replace the generated chart title |
| `transparent_background` | `bool` | `False` | Leave the page unpainted instead of filling it with the theme's paper colour. The wheel-only output paints its background too, so set this to lay the wheel over your own page |
| `auto_size` | `bool` | `True` | Size the page to its contents; `padding` (default `20`, in px) is the margin left around them |
| `show_diurnality` | `bool` | `True` | Print the chart's diurnality in the bottom-left info panel — `Diurnal` when the Sun stood above the horizon, `Nocturnal` below it. Two-wheel charts report both wheels (`Natal Nocturnal · Transit Diurnal`; a synastry names the two subjects). The line is omitted where it has no referent: any chart not cast from the Earth (a heliocentric chart has no Sun to point at; a Marscentric or Selenocentric one draws a Sun that is not the one measured), a midpoint composite (no single sky) and a solar arc direction (it keeps the nativity's instant). With `False` the panel keeps exactly the spacing it had before the line existed |

**Modern-only keyword arguments** (ignored by the classic style):

| Parameter | Type | Default | Description |
|---|---|---|---|
| `show_zodiac_background_ring` | `bool` | `True` | Draw colored zodiac wedges as the outer zodiac annulus around the cusp ring |
| `glyph_size` | `str` | `"medium"` | Size of the planet cluster — glyph, degrees, sign, minutes and ℞. `"medium"` is the default drawing; `"small"` is the same cluster at 90%; `"large"` draws the planet glyph at the classic style's own size (24px on a single wheel, 19.2px on a dual wheel, at the default page with the zodiac background ring on). On a single wheel the degrees and sign grow with the large glyph; on the dual rings only the glyph grows and the reading keeps the medium size. Overridable per render: `save_svg(glyph_size="large")` |

<table>
  <tr>
    <td align="center"><strong>small</strong></td>
    <td align="center"><strong>medium</strong> (default)</td>
    <td align="center"><strong>large</strong></td>
  </tr>
  <tr>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Natal%20Chart%20-%20Modern%20Small.svg" width="250" alt="Modern natal chart with the small glyph size"></td>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Natal%20Chart%20-%20Modern.svg" width="250" alt="Modern natal chart with the default medium glyph size"></td>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Natal%20Chart%20-%20Modern%20Large.svg" width="250" alt="Modern natal chart with the large glyph size"></td>
  </tr>
</table>

**Dual-chart keyword arguments** (Synastry, Transit, Dual Return, Progression — a composite is a single wheel):

| Parameter | Type | Default | Description |
|---|---|---|---|
| `double_chart_aspect_grid_type` | `str` | `"list"` | Aspect grid layout: `"list"` (compact vertical list) or `"table"` (traditional cross-reference grid) |
| `show_house_position_comparison` | `bool` | `True` | Draw the house-comparison grid: where the second subject's points fall in the first subject's houses |
| `show_cusp_position_comparison` | `bool` | `False` | Draw the cusp-comparison grid: where the second subject's house cusps fall in the first subject's houses |

**Classic-only constructor arguments** (ignored by the modern style, which logs a warning when they are set):

| Parameter | Type | Default | Description |
|---|---|---|---|
| `external_view` | `bool` | `False` | Place planets outside the zodiac ring (Natal charts only) |
| `show_degree_indicators` | `bool` | `True` | Show degree indicators on planets |
| `show_aspect_icons` | `bool` | `True` | Show aspect icons on aspect lines |

**Opt-in marks** (constructor only, both styles). Six facts the chart data already carries and the wheel did not show. Every one defaults to `False`, so no chart gains a mark it was not asked for, and every one is silent where it has no referent — a chart with no station, data that never computed a score, a tropical zodiac, a house system that was honoured:

| Parameter | Type | Default | Description |
|---|---|---|---|
| `show_motion_state` | `bool` | `False` | Mark a planet at a station: `SR` where the retrograde phase opens, `SD` where it closes. Modern recolours the cluster and uses the row that holds `RX`; classic writes the two letters at the foot of the glyph, where its `℞` sits |
| `show_out_of_bounds` | `bool` | `False` | Badge out-of-bounds planets `OOB` in the point tables (in the Gauquelin grid, off the declination column) |
| `show_aspect_movement` | `bool` | `False` | Dash the separating aspect lines; applying ones stay solid |
| `show_relationship_score` | `bool` | `False` | Print the synastry relationship score in the info panel. Needs a score on the chart data, which `create_synastry_chart_data` computes by default |
| `show_ayanamsa_value` | `bool` | `False` | Append the ayanamsa offset in degrees and minutes to the zodiac line of a sidereal chart |
| `show_polar_fallback_note` | `bool` | `False` | Mark the domification line when the requested house system was undefined at that latitude and another stood in for it |

```python
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# 25 August 1990: Mercury is at a station, Uranus is out of bounds.
station = AstrologicalSubjectFactory.from_birth_data(
    "Mercury Station", 1990, 8, 25, 12, 0,
    lng=-0.1276, lat=51.5074, tz_str="Europe/London",
    online=False, suppress_geonames_warning=True,
)
drawer = ChartDrawer(
    ChartDataFactory.create_natal_chart_data(station),
    show_motion_state=True,
    show_out_of_bounds=True,
    show_aspect_movement=True,
)
svg = drawer.generate_svg_string()

print(station.mercury.motion_state)     # stationary_retrograde
print(station.uranus.is_out_of_bounds)  # True
```

`examples/svg_extended_example.py` renders all six, each on a subject that has its referent.

Rendered, with every option on. A mark draws nothing where there is nothing to
mark, so no single chart shows the set — these four between them carry every
referent, and none of them claims something its own sky does not have:

<table>
  <tr>
    <td align="center"><b>Stations, out-of-bounds and separating aspects</b><br><sub>Mercury at its August 1990 station; Uranus past the obliquity</sub></td>
    <td align="center"><b>The same sky, classic</b><br><sub>SR at the foot of the glyph, where ℞ sits</sub></td>
  </tr>
  <tr>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/docs/charts/marks_wheel_modern.svg" width="380" alt="Modern wheel with station, out-of-bounds and separating-aspect marks"></td>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/docs/charts/marks_wheel_classic.svg" width="380" alt="Classic wheel with station, out-of-bounds and separating-aspect marks"></td>
  </tr>
  <tr>
    <td align="center"><b>Ayanamsa offset</b><br><sub>Sidereal Lahiri — the offset next to the mode name</sub></td>
    <td align="center"><b>Polar fallback note</b><br><sub>Placidus undefined at 78°N, so the line says what drew the cusps</sub></td>
  </tr>
  <tr>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/docs/charts/marks_sidereal_classic.svg" width="380" alt="Sidereal chart showing the ayanamsa offset in degrees"></td>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/docs/charts/marks_polar_classic.svg" width="380" alt="Polar chart admitting the house-system substitution"></td>
  </tr>
  <tr>
    <td align="center" colspan="2"><b>Relationship score</b><br><sub>The synastry score and its band, in a panel row that was empty</sub></td>
  </tr>
  <tr>
    <td colspan="2"><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/docs/charts/marks_synastry_classic.svg" width="770" alt="Synastry chart printing the relationship score"></td>
  </tr>
</table>

## Classic Chart Style

Select the classic wheel with `style="classic"` (constructor or per render); its own options are listed under [Chart Options](#chart-options). The examples below render the same charts as above in the classic style.

### Classic Birth Chart

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

john = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

chart_data = ChartDataFactory.create_natal_chart_data(john)
chart = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
chart.save_svg(output_path=output_dir, filename="john-lennon-classic", style="classic")
```

![John Lennon Classic Birth Chart](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Natal%20Chart%20-%20Classic.svg)

### Classic Synastry Chart

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

john = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)
paul = AstrologicalSubjectFactory.from_birth_data(
    "Paul McCartney", 1942, 6, 18, 15, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

chart_data = ChartDataFactory.create_synastry_chart_data(john, paul)
chart = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
chart.save_svg(output_path=output_dir, filename="lennon-mccartney-synastry-classic", style="classic")
```

![John Lennon Classic Synastry Chart](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Synastry%20Chart%20-%20Classic.svg)

### Classic Transit Chart

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

john = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

transit = AstrologicalSubjectFactory.from_birth_data(
    "Transit", 2025, 3, 4, 12, 0,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

chart_data = ChartDataFactory.create_transit_chart_data(john, transit)
chart = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
chart.save_svg(output_path=output_dir, filename="lennon-transit-classic", style="classic")
```

![John Lennon Classic Transit Chart](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Transit%20Chart%20-%20Classic.svg)

### Classic Wheel Only

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

john = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

chart_data = ChartDataFactory.create_natal_chart_data(john)
chart = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
chart.save_wheel_only_svg_file(
    output_path=output_dir,
    filename="john-lennon-classic-wheel",
    style="classic",
)
```

![John Lennon Classic Wheel Only](https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/tests/data/svg/John%20Lennon%20-%20Wheel%20Only%20-%20Natal%20Chart%20-%20Classic%20Wheel%20Only.svg)

**📖 Modern chart examples: [Modern Charts Guide](https://www.kerykeion.net/content/examples/modern-charts)**


## Report Generator

`ReportGenerator` mirrors the chart-type dispatch of `ChartDrawer`. It accepts raw `AstrologicalSubjectModel` instances as well as any `ChartDataModel` produced by `ChartDataFactory`—including natal, composite, synastry, transit, and planetary return charts—and renders the appropriate textual report automatically.

**📖 Full report documentation: [Report Generator Guide](https://www.kerykeion.net/content/docs/report)**

### Quick Examples

```python
from kerykeion import ReportGenerator, AstrologicalSubjectFactory, ChartDataFactory

# Subject-only report
subject = AstrologicalSubjectFactory.from_birth_data(
    "Sample Natal", 1990, 7, 21, 14, 45,
    lng=12.4964,
    lat=41.9028,
    tz_str="Europe/Rome",
    online=False,
)
ReportGenerator(subject).print_report(include_aspects=False)

# Single-chart data (elements, qualities, aspects enabled)
natal_data = ChartDataFactory.create_natal_chart_data(subject)
ReportGenerator(natal_data).print_report(max_aspects=10)

# Dual-chart data (synastry, transit, dual return, …)
partner = AstrologicalSubjectFactory.from_birth_data(
    "Sample Partner", 1992, 11, 5, 9, 30,
    lng=12.4964,
    lat=41.9028,
    tz_str="Europe/Rome",
    online=False,
)
synastry_data = ChartDataFactory.create_synastry_chart_data(subject, partner)
ReportGenerator(synastry_data).print_report(max_aspects=12)
```

Each report contains:

- A chart-aware title summarising the subject(s) and chart type
- Birth/event metadata and configuration settings
- Celestial points with sign, position, **daily motion**, **motion state** (including the two named stations, SR and SD), **declination**, retrograde flag, and house (an **out-of-bounds** column appears when a point actually is)
- Arabic Parts, fixed stars (with **constellation**) and midpoints in tables of their own, when active
- House cusp tables for every subject involved
- Essential dignities, nakshatras and Gauquelin sectors, when the chart computed them
- Lunar phase details when available
- Chart diurnality (Sun above or below the horizon), when it applies
- Element/quality distributions, **angularities** and **stelliums**, and active configuration summaries (for chart data)
- Aspect listings tailored for single or dual charts, with symbols for type and movement
- Dual-chart extras such as house comparisons and relationship scores (when provided by the data)

Technique results are accepted directly, each rendering its own report:
`ProfectionsModel`, `FirdariaModel`, `HoraryIndicatorsModel`,
`MutualReceptionsModel`, `DominantsModel` and `ZodiacalReleasingModel`.

```python
from kerykeion import AstrologicalSubjectFactory, FirdariaFactory, ReportGenerator

subject = AstrologicalSubjectFactory.from_birth_data("Jane", 1990, 6, 15, 12, 0, lng=12.4964, lat=41.9028, tz_str="Europe/Rome", online=False)
ReportGenerator(FirdariaFactory.from_subject(subject, target_date="2026-06-04")).print_report()
```

### Section Access

All section helpers remain available for targeted output:

```python
from kerykeion import ReportGenerator, AstrologicalSubjectFactory, ChartDataFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Sample Natal", 1990, 7, 21, 14, 45,
    lng=12.4964,
    lat=41.9028,
    tz_str="Europe/Rome",
    online=False,
)
natal_data = ChartDataFactory.create_natal_chart_data(subject)

report = ReportGenerator(natal_data)
sections = report.generate_report(max_aspects=5).split("\n\n")
for section in sections[:3]:
    print(section)
```

**📖 Report examples: [Report Examples](https://www.kerykeion.net/content/examples/report)**

## AI Context Serializer

The `context_serializer` module transforms Kerykeion data models into precise, non-qualitative XML optimized for LLM consumption. It provides the essential "ground truth" data needed for AI agents to generate accurate astrological interpretations.

**📖 Full context serializer docs: [Context Serializer Guide](https://www.kerykeion.net/content/docs/context_serializer)**

### Quick Example

```python
from kerykeion import AstrologicalSubjectFactory, to_context

# Create a subject
subject = AstrologicalSubjectFactory.from_birth_data(
    "John Doe", 1990, 1, 1, 12, 0,
    city="London",
    nation="GB",
    lng=-0.1278,
    lat=51.5074,
    tz_str="Europe/London",
    online=False,
)

# Generate AI-ready context
context = to_context(subject)
print(context)
```

**Output:**

```xml
<chart name="John Doe">
  <birth_data date="1990-01-01 12:00" city="London" nation="GB" lat="51.51" lng="-0.13" lng_dir="W" tz="Europe/London" />
  <config zodiac="Tropical" house_system="Placidus" perspective="Apparent Geocentric" />
  <planets>
    <point name="Sun" position="10.81" sign="Capricorn" element="Earth" quality="Cardinal" ... />
    <point name="Moon" position="3.27" sign="Pisces" element="Water" quality="Mutable" ... />
    ...
  </planets>
  <houses>...</houses>
  <lunar_phase name="Waxing Crescent" phase="5" degrees_between="52.45" emoji="🌒" />
</chart>
```

**Key Features:**

- **XML Output:** Well-formed XML with semantic tags, proper escaping, and optional field omission.
- **Standardized Output:** Consistent format for Natal, Synastry, Composite, and Return charts.
- **Non-Qualitative:** Provides raw data (positions, aspects) without interpretive bias.
- **Prompt-Ready:** Designed to be injected directly into system prompts.

## Example: Retrieving Aspects

Kerykeion provides a unified `AspectsFactory` class for calculating astrological aspects within single charts or between two charts:

```python
from kerykeion import AspectsFactory, AstrologicalSubjectFactory

# Create astrological subjects
jack = AstrologicalSubjectFactory.from_birth_data(
    "Jack", 1990, 6, 15, 15, 15,
    lng=12.4964,
    lat=41.9028,
    tz_str="Europe/Rome",
    online=False,
)
jane = AstrologicalSubjectFactory.from_birth_data(
    "Jane", 1991, 10, 25, 21, 0,
    lng=12.4964,
    lat=41.9028,
    tz_str="Europe/Rome",
    online=False,
)

# For single chart aspects (natal, return, composite, etc.)
single_chart_result = AspectsFactory.single_chart_aspects(jack)
print(f"Found {len(single_chart_result.aspects)} aspects in Jack's chart")
print(single_chart_result.aspects[0])

# For dual chart aspects (synastry, transits, comparisons, etc.)
dual_chart_result = AspectsFactory.dual_chart_aspects(jack, jane)
print(f"Found {len(dual_chart_result.aspects)} aspects between Jack and Jane's charts")
print(dual_chart_result.aspects[0])

# Each AspectModel includes:
# - p1_name, p2_name: Planet/point names
# - p1_owner, p2_owner: Subject name string (e.g., "Jack", "Jane")
# - aspect: Aspect type (conjunction, trine, square, etc.)
# - orbit: Actual orb in degrees
# - aspect_degrees: Exact degrees for the aspect (0, 60, 90, 120, 180, etc.)
# - diff: Absolute angular difference between the two points
# - p1_abs_pos, p2_abs_pos: Absolute ecliptic positions
# - p1_speed, p2_speed: Daily speed of each point
# - aspect_movement: "Applying", "Separating", or "Static"
```

**📖 Aspects documentation: [Aspects Factory Guide](https://www.kerykeion.net/content/docs/aspects)**

**Advanced Usage with Custom Settings:**

```python
from kerykeion import AstrologicalSubjectFactory, AspectsFactory

subject = AstrologicalSubjectFactory.from_birth_data("Jane", 1990, 6, 15, 12, 0, lng=12.4964, lat=41.9028, tz_str="Europe/Rome", online=False)

# Custom aspect set with explicit per-aspect orbs (a list of {name, orb} dicts):
custom_aspects = [
    {"name": "conjunction", "orb": 3},
    {"name": "opposition", "orb": 3},
    {"name": "trine", "orb": 3},
    {"name": "square", "orb": 3},
    {"name": "sextile", "orb": 2},
]
aspects = AspectsFactory.single_chart_aspects(subject, active_aspects=custom_aspects)

# Tighten the orb when an angle (Asc/MC) is involved, or widen specific points:
aspects = AspectsFactory.single_chart_aspects(
    subject,
    axis_orb_limit=2.0,
    point_orb_adjustments={"Sun": 10.0, "Moon": 10.0},
)
```

**📖 Configuration options: [Settings Documentation](https://www.kerykeion.net/content/docs/settings)**

## Relationship Score

Kerykeion can calculate a relationship compatibility score based on synastry aspects, using the method of the Italian astrologer **Ciro Discepolo**:

```python
from kerykeion import AstrologicalSubjectFactory
from kerykeion.relationship_score.factory import RelationshipScoreFactory

# Create two subjects
person1 = AstrologicalSubjectFactory.from_birth_data(
    "Alice", 1990, 3, 15, 14, 30,
    lng=12.4964,
    lat=41.9028,
    tz_str="Europe/Rome",
    online=False,
)
person2 = AstrologicalSubjectFactory.from_birth_data(
    "Bob", 1988, 7, 22, 9, 0,
    lng=12.4964,
    lat=41.9028,
    tz_str="Europe/Rome",
    online=False,
)

# Calculate relationship score
score_factory = RelationshipScoreFactory(person1, person2)
result = score_factory.get_relationship_score()

print(f"Compatibility Score: {result.score_value}")
print(f"Description: {result.score_description}")
```

**📖 Relationship score guide: [Relationship Score Examples](https://www.kerykeion.net/content/examples/relationship-score)**

**📖 Factory documentation: [RelationshipScoreFactory](https://www.kerykeion.net/content/docs/relationship_score_factory)**

## House Comparison (Synastry Overlay)

`HouseComparisonFactory` performs a bidirectional house overlay: it reports where each subject's points fall within the *other* subject's houses — a core synastry technique. It also accepts planetary-return subjects.

```python
from kerykeion import AstrologicalSubjectFactory, HouseComparisonFactory

person_a = AstrologicalSubjectFactory.from_birth_data("Person A", 1990, 5, 15, 10, 30, lng=12.4964, lat=41.9028, tz_str="Europe/Rome", online=False)
person_b = AstrologicalSubjectFactory.from_birth_data("Person B", 1992, 8, 23, 14, 45, lng=9.19, lat=45.4642, tz_str="Europe/Rome", online=False)

comparison = HouseComparisonFactory(person_a, person_b).get_house_comparison()
for placement in comparison.first_points_in_second_houses:
    print(placement)  # A's points located in B's houses
# comparison.second_points_in_first_houses -> B's points in A's houses
```

## Element & Quality Distribution Strategies

`ChartDataFactory` offers two strategies for calculating element and modality totals. The default `"weighted"` mode leans on a curated map that emphasises core factors (for example `sun`, `moon`, and `ascendant` weight 2.0, angles such as `medium_coeli` 1.5, personal planets 1.5, social planets 1.0, outers 0.5, and minor bodies 0.3–0.8). Provide `distribution_method="pure_count"` when you want every active point to contribute equally.

You can refine the weighting without rebuilding the dictionary: pass lowercase point names to `custom_distribution_weights` and use `"__default__"` to override the fallback value applied to entries that are not listed explicitly.

```python
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Sample", 1986, 4, 12, 8, 45,
    lng=11.3426,
    lat=44.4949,
    tz_str="Europe/Rome",
    online=False,
)

# Equal weighting: every active point counts once
pure_data = ChartDataFactory.create_natal_chart_data(
    subject,
    distribution_method="pure_count",
)

# Custom emphasis: boost the Sun, soften everything else
weighted_data = ChartDataFactory.create_natal_chart_data(
    subject,
    distribution_method="weighted",
    custom_distribution_weights={
        "sun": 3.0,
        "__default__": 0.75,
    },
)

print(pure_data.element_distribution.fire)
print(weighted_data.element_distribution.fire)
```

All convenience helpers (`create_synastry_chart_data`, `create_transit_chart_data`, returns, and composites) forward the same keyword-only parameters, so you can keep a consistent weighting scheme across every chart type.

**📖 Element/quality distribution guide: [Distribution Documentation](https://www.kerykeion.net/content/docs/element_quality_distribution)**

## Ayanamsa (Sidereal Modes)

By default, the zodiac type is **Tropical**. To use **Sidereal**, specify the sidereal mode:

```python
from kerykeion import AstrologicalSubjectFactory

johnny = AstrologicalSubjectFactory.from_birth_data(
    "Johnny Depp", 1963, 6, 9, 0, 0,
    lng=-87.1112,
    lat=37.7719,
    tz_str="America/Chicago",
    online=False,
    zodiac_type="Sidereal",
    sidereal_mode="LAHIRI"
)

# The ayanamsa offset (degrees) is available on sidereal charts:
print(johnny.ayanamsa_value)  # e.g. 23.34
```

Kerykeion supports **47 named sidereal modes** plus a **USER** mode for custom ayanamsa definitions (48 total). Mode families include Indian/Vedic (Lahiri, Krishnamurti, Raman, Aryabhata, Suryasiddhanta, True Citra/Pushya/Revati, ...), Western sidereal (Fagan-Bradley, DeLuce, Hipparchos, ...), Babylonian (Kugler, Huber, Britton, ...), galactic alignment, and astronomical reference frames (J2000, J1900, B1950).

**Custom ayanamsa (USER mode):**

```python
from kerykeion import AstrologicalSubjectFactory

custom = AstrologicalSubjectFactory.from_birth_data(
    "Custom Ayanamsa", 2000, 1, 1, 0, 0,
    lng=0.0, lat=51.5, tz_str="Etc/GMT", online=False,
    zodiac_type="Sidereal",
    sidereal_mode="USER",
    custom_ayanamsa_t0=2451545.0,      # J2000.0 reference epoch
    custom_ayanamsa_ayan_t0=23.5,       # ayanamsa offset at epoch (degrees)
)
```

**📖 Sidereal mode examples: [Sidereal Modes Guide](https://www.kerykeion.net/content/examples/sidereal-modes/)**

**📖 Full list of supported sidereal modes: [SiderealMode Schema](https://www.kerykeion.net/content/docs/schemas#siderealmode)**

## House Systems

By default, houses are calculated using **Placidus**. Configure a different house system as follows:

```python
from kerykeion import AstrologicalSubjectFactory

johnny = AstrologicalSubjectFactory.from_birth_data(
    "Johnny Depp", 1963, 6, 9, 0, 0,
    lng=-87.1112,
    lat=37.7719,
    tz_str="America/Chicago",
    online=False,
    houses_system_identifier="M"
)
```

**📖 House system examples: [House Systems Guide](https://www.kerykeion.net/content/examples/houses-systems/)**

**📖 Full list of supported house systems: [HouseSystemIdentifier Schema](https://www.kerykeion.net/content/docs/schemas#housesystemidentifier)**

All house systems available in the ephemeris backend are supported, including Gauquelin Sectors (see [Gauquelin Sectors](#gauquelin-sectors) below).

Some systems crowd several cusps onto one longitude at extreme latitudes (Sunshine at 74° N puts the second through the sixth cusp on one degree). `subject.coincident_house_cusps` lists those groups of house numbers — the houses between them have no width, so no point can ever be in them — and is empty for every chart whose twelve cusps are twelve distinct points, which is every ordinary chart. Inside the polar circle a house system that is undefined there is substituted (Porphyry by default) and the substitution is recorded in `subject.polar_house_fallbacks`; `show_polar_fallback_note=True` prints it on the chart.

## Perspective Type

By default, Kerykeion uses the **Apparent Geocentric** perspective (the most standard in astrology). Other perspectives (e.g., **Heliocentric**) can be set this way:

```python
from kerykeion import AstrologicalSubjectFactory

johnny = AstrologicalSubjectFactory.from_birth_data(
    "Johnny Depp", 1963, 6, 9, 0, 0,
    lng=-87.1112,
    lat=37.7719,
    tz_str="America/Chicago",
    online=False,
    perspective_type="Heliocentric"
)
```

**📖 Perspective type examples: [Perspective Type Guide](https://www.kerykeion.net/content/examples/perspective-type/)**

**📖 Full list of supported perspective types: [PerspectiveType Schema](https://www.kerykeion.net/content/docs/schemas#perspectivetype)**

## Themes

<table>
  <tr>
    <td></td>
    <td align="center"><strong>Classic</strong></td>
    <td align="center"><strong>Dark</strong></td>
    <td align="center"><strong>Black & White</strong></td>
  </tr>
  <tr>
    <td align="center"><strong>Modern Style</strong> (default)</td>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/docs/charts/modern_classic_natal.svg" width="220" alt="Modern Classic Natal Chart"></td>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/docs/charts/modern_dark_natal.svg" width="220" alt="Modern Dark Natal Chart"></td>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/docs/charts/modern_black_and_white_natal.svg" width="220" alt="Modern Black and White Natal Chart"></td>
  </tr>
  <tr>
    <td align="center"><strong>Classic Style</strong></td>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/docs/charts/classic_default_natal.svg" width="220" alt="Classic Natal Chart"></td>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/docs/charts/classic_dark_natal.svg" width="220" alt="Dark Natal Chart"></td>
    <td><img src="https://raw.githubusercontent.com/g-battaglia/kerykeion/refs/heads/alpha/v6/docs/charts/classic_black_and_white_natal.svg" width="220" alt="Black and White Natal Chart"></td>
  </tr>
</table>

Kerykeion ships three chart themes — **Classic** (the default: light, with the rainbow zodiac band), **Dark**, and **Black & White** (for monochrome printing) — plus the option of no theme at all. Each works in both **modern** (the default style) and **classic** chart styles: the theme picks the palette, the style picks the wheel layout. All three meet WCAG AAA on the text a reader reads.

Each theme offers a distinct visual style, allowing you to choose the one that best suits your preferences or presentation needs. If you prefer more control over the appearance, you can opt not to set any theme, making it easier to customize the chart by overriding the default CSS variables.

**📖 Theming guide with all examples: [Theming Documentation](https://www.kerykeion.net/content/examples/theming)**

The Black & White theme renders glyphs, rings, and aspects in solid black on light backgrounds, designed for crisp B/W prints (PDF or paper) without sacrificing legibility.

Here's an example of how to set the theme:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subject
dark_theme_subject = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon - Dark Theme", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute chart data
chart_data = ChartDataFactory.create_natal_chart_data(dark_theme_subject)

# Step 3: Create visualization with the dark theme
dark_theme_natal_chart = ChartDrawer(chart_data=chart_data, theme="dark")

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
dark_theme_natal_chart.save_svg(output_path=output_dir, filename="john-lennon-natal-dark")
```

## Alternative Initialization

Create an `AstrologicalSubjectModel` from a UTC ISO 8601 string:

```python
from kerykeion import AstrologicalSubjectFactory

subject = AstrologicalSubjectFactory.from_iso_utc_time(
    name="Johnny Depp",
    iso_utc_time="1963-06-09T05:00:00Z",
    city="Owensboro",
    nation="US",
    lng=-87.1112,
    lat=37.7719,
    tz_str="America/Chicago",
    online=False,
)

print(subject.iso_formatted_local_datetime)
```

If you prefer automatic geocoding, set `online=True` and provide your GeoNames credentials via `geonames_username`.

**📖 All initialization options: [AstrologicalSubjectFactory Documentation](https://www.kerykeion.net/content/docs/astrological_subject_factory)**

## Arabic Parts (Lots)

Kerykeion computes the four classical Arabic Parts (Hellenistic *Lots*) as activatable points. Add them to `active_points` to include them in a chart and its aspects:

- **`Pars_Fortunae`** — Part of Fortune (Lot of Fortune; sect-aware: day = Asc + Moon − Sun, night = Asc + Sun − Moon)
- **`Pars_Spiritus`** — Part of Spirit
- **`Pars_Amoris`** — Part of Eros / Love
- **`Pars_Fidei`** — Part of Faith / Necessity

```python
from kerykeion import AstrologicalSubjectFactory
from kerykeion.settings.config_constants import DEFAULT_ACTIVE_POINTS

subject = AstrologicalSubjectFactory.from_birth_data(
    "Jane", 1990, 6, 15, 12, 0, lng=12.4964, lat=41.9028, tz_str="Europe/Rome", online=False,
    active_points=DEFAULT_ACTIVE_POINTS + ["Pars_Fortunae", "Pars_Spiritus", "Pars_Amoris", "Pars_Fidei"],
)
print(subject.pars_fortunae.sign, subject.pars_fortunae.position)
```

## Lunar Nodes (Rahu & Ketu)

Kerykeion supports both **True** and **Mean** Lunar Nodes:

- **True North Lunar Node**: `"True_North_Lunar_Node"`
- **True South Lunar Node**: `"True_South_Lunar_Node"`
- **Mean North Lunar Node**: `"Mean_North_Lunar_Node"`
- **Mean South Lunar Node**: `"Mean_South_Lunar_Node"`

By default only the **True North** node is active in charts and aspect calculations (`True_North_Lunar_Node` is in the default active points; `True_South_Lunar_Node` is not). To include the South node or the Mean nodes, pass the `active_points` parameter to the `ChartDataFactory` methods.

**📖 ChartDataFactory documentation: [ChartDataFactory Guide](https://www.kerykeion.net/content/docs/chart_data_factory)**

Example:

```python
from pathlib import Path
from kerykeion import AstrologicalSubjectFactory, ChartDataFactory, ChartDrawer

# Step 1: Create subject
subject = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833,
    lat=53.4,
    tz_str="Europe/London",
    online=False,
)

# Step 2: Pre-compute chart data with custom active points including the Mean nodes
chart_data = ChartDataFactory.create_natal_chart_data(
    subject,
    active_points=[
        "Sun",
        "Moon",
        "Mercury",
        "Venus",
        "Mars",
        "Jupiter",
        "Saturn",
        "Uranus",
        "Neptune",
        "Pluto",
        "Mean_North_Lunar_Node",
        "Mean_South_Lunar_Node",
        "True_North_Lunar_Node",
        "True_South_Lunar_Node",
        "Ascendant",
        "Medium_Coeli",
        "Descendant",
        "Imum_Coeli"
    ]
)

# Step 3: Create visualization
chart = ChartDrawer(chart_data=chart_data)

output_dir = Path("charts_output")
output_dir.mkdir(exist_ok=True)
chart.save_svg(output_path=output_dir, filename="john-lennon-mean-nodes")
```

## Fixed Stars

The default libephemeris backend exposes a **1,447-star catalog**. Kerykeion's
`DEFAULT_FIXED_STARS` preset selects 23 commonly used stars: all 15 Behenian
stars plus 8 additional bright stars, including the 4 Royal Stars. You may pass
any catalog name, not only preset members. Each computed star provides ecliptic
longitude, daily motion (`speed`), equatorial `declination`, and apparent visual
`magnitude`.

Fixed stars are **opt-in**: pass the names you want to `active_fixed_stars` when building the subject. Stars requested this way are computed into `subject.fixed_stars` and participate automatically in chart rendering and aspect calculations.

```python
from kerykeion import AstrologicalSubjectFactory
from kerykeion import ChartDataFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "John Lennon", 1940, 10, 9, 18, 30,
    lng=-2.9833, lat=53.4, tz_str="Europe/London", online=False,
    active_fixed_stars=["Sirius", "Regulus", "Aldebaran", "Antares", "Fomalhaut"],
)

# Access fixed star data
sirius = subject.find_fixed_star("Sirius")
print(sirius.abs_pos)        # Ecliptic longitude
print(sirius.magnitude)      # -1.46
print(sirius.declination)    # Equatorial declination

# The requested stars are rendered and aspected automatically
chart_data = ChartDataFactory.create_natal_chart_data(subject)
```

The 23-star preset contains: Regulus, Spica, Aldebaran, Antares, Sirius,
Fomalhaut, Algol, Betelgeuse, Canopus, Procyon, Arcturus, Pollux, Deneb, Altair,
Rigel, Achernar, Capella, Vega, Alcyone, Alphecca, Algorab, Deneb_Algedi, and
Alkaid. Discover other names with `FixedStarCatalog.list_all()` or
`FixedStarCatalog.find()` from `kerykeion.fixed_stars`.

**📖 Full active points list: [Active Points Documentation](https://www.kerykeion.net/content/docs/active_points)**

## JSON Support

You can serialize the astrological subject (the base data used throughout the library) to JSON:

```python
from kerykeion import AstrologicalSubjectFactory

johnny = AstrologicalSubjectFactory.from_birth_data(
    "Johnny Depp", 1963, 6, 9, 0, 0,
    lng=-87.1112,
    lat=37.7719,
    tz_str="America/Chicago",
    online=False,
)

print(johnny.model_dump_json(indent=2))
```

**📖 Data models and schemas: [Schemas Documentation](https://www.kerykeion.net/content/docs/schemas)**

## Moon Phase Details

The `MoonPhaseDetailsFactory` generates a rich lunar phase context from any astrological subject — including illumination, upcoming major phases, next eclipses (solar and lunar), sunrise/sunset, moonrise/moonset, and apparent solar position. All timings use the ephemeris backend for ~1 second precision. `moonrise` and `moonset` are ISO-8601 strings in the subject's local zone, with the same instants as Unix timestamps beside them; either is `None` on the roughly one civil day in thirty that has no such event.

```python
from kerykeion import AstrologicalSubjectFactory, MoonPhaseDetailsFactory, ReportGenerator

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 2025, 4, 1, 7, 51,
    lng=-0.1276, lat=51.5074, tz_str="Europe/London",
    online=False,
)

overview = MoonPhaseDetailsFactory.from_subject(subject)

print(f"Phase: {overview.moon.phase_name} {overview.moon.emoji}")
print(f"Illumination: {overview.moon.illumination}")
print(f"Stage: {overview.moon.stage}")

if overview.moon.detailed and overview.moon.detailed.upcoming_phases:
    fm = overview.moon.detailed.upcoming_phases.full_moon
    if fm and fm.next:
        print(f"Next Full Moon: {fm.next.datestamp}")

# Generate a formatted ASCII report
ReportGenerator(overview).print_report()
```

**Report output (truncated):**

```text
=====================================================
Moon Phase Overview — Tue, 01 Apr 2025 06:51:00 +0000
=====================================================

+Moon Summary------+--------------------+
| Field            | Value              |
+------------------+--------------------+
| Phase Name       | Waxing Crescent 🌒 |
| Major Phase      | New Moon           |
| Stage            | Waxing             |
| Illumination     | 12%                |
| Age (days)       | 3                  |
| Age (exact days) | 2.8286             |
| Lunar Cycle      | 11.068%            |
| Sun Sign         | Ari                |
| Moon Sign        | Tau                |
+------------------+--------------------+

+Illumination Details-------+
| Field            | Value  |
+------------------+--------+
| Percentage       | 12.0%  |
| Visible Fraction | 0.1161 |
| Phase Angle      | 39.85° |
+------------------+--------+

+Upcoming Phases+---------------------------------+---------------------------------+
| Phase         | Last                            | Next                            |
+---------------+---------------------------------+---------------------------------+
| New Moon      | Sat, 29 Mar 2025 10:57:49 +0000 | Sun, 27 Apr 2025 19:31:09 +0000 |
| First Quarter | ...                             | ...                             |
| Full Moon     | ...                             | ...                             |
| Last Quarter  | ...                             | ...                             |
+---------------+---------------------------------+---------------------------------+

...
```

You can also get the full model as JSON:

```python
from kerykeion import AstrologicalSubjectFactory, MoonPhaseDetailsFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 2025, 4, 1, 6, 51,
    lng=-0.1276, lat=51.5074, tz_str="Etc/GMT", online=False,
)
overview = MoonPhaseDetailsFactory.from_subject(subject)
print(overview.model_dump_json(exclude_none=True, indent=2))
```

**JSON output (truncated):**

```json
{
  "timestamp": 1743490260,
  "datestamp": "Tue, 01 Apr 2025 06:51:00 +0000",
  "sun": {
    "sunrise": "2025-04-01T05:35:33.380154Z",
    "sunset": "2025-04-01T18:34:03.841539Z",
    "solar_noon": "2025-04-01T12:04:48.610846Z",
    "day_length": "PT12H58M30.461385S",
    "next_solar_eclipse": { "type": "Partial Solar Eclipse", "...": "..." }
  },
  "moon": {
    "phase_name": "Waxing Crescent",
    "major_phase": "New Moon",
    "stage": "waxing",
    "illumination": "12%",
    "emoji": "🌒",
    "moonrise": "2025-04-01T06:29:49.367603+00:00",
    "moonrise_timestamp": 1743488989,
    "moonset": "2025-04-01T23:37:11.433132+00:00",
    "moonset_timestamp": 1743550631,
    "next_lunar_eclipse": { "type": "Total Lunar Eclipse", "...": "..." },
    "detailed": { "upcoming_phases": { "...": "..." }, "illumination_details": { "...": "..." } }
  },
  "location": { "latitude": "51.5074", "longitude": "-0.1276" }
}
```

**📖 Full documentation: [Moon Phase Details Factory](https://www.kerykeion.net/content/docs/moon_phase_details_factory)**

**📖 Examples: [Moon Phase Details Examples](https://www.kerykeion.net/content/examples/moon-phase-details)**

## Timing Factories

Seven lightweight factories that work directly from dates and locations — no `AstrologicalSubjectModel` is built. `SunTimesFactory` and `PlanetaryHoursFactory` return timezone-aware UTC datetimes; the event finders return ISO-8601 UTC strings with the Julian Day beside them.

### Sun Times

`SunTimesFactory` returns sunrise / sunset / solar-noon / day-length for a civil date at a location, with polar day/night detection.

**What the numbers mean.** Sunrise and sunset are the moment the Sun's apparent **upper limb** meets the level sea horizon, with the real semidiameter and standard refraction (the Sun's centre is near −0.83° at the event). Solar noon is the **meridian transit** — the instant the Sun is highest — not the midpoint of sunrise and sunset, so it is reported on polar days too. The full conventions are in the [Sun Times docs](https://www.kerykeion.net/content/docs/sun_times_factory).

**Sunrise is not `is_diurnal`.** `AstrologicalSubjectModel.is_diurnal` tests the Sun's **geometric centre** against the true horizon — no disc, no atmosphere — because that is the question a chart is cast from. Sunrise counts the Sun as risen as soon as its upper edge shows through the air, which happens earlier. The gap is real and grows towards the poles: about **3.3 min at the equator, 4.4 min at Rome, 8.2 min at Reykjavík and 10 min at Tromsø**. Both answers are right to their own question, and neither should ever be derived from the other.

```python
from kerykeion import SunTimesFactory

sun = SunTimesFactory.from_date(
    2026, 5, 28, latitude=41.9028, longitude=12.4964, tz_str="Europe/Rome"
)
print(sun.sunrise, sun.sunset, sun.day_length)
```

### Planetary Hours

`PlanetaryHoursFactory` computes the 24 unequal Chaldean planetary hours (twelve day + twelve night) for the planetary day containing a moment. Moments before sunrise resolve to the previous planetary day.

```python
from kerykeion import PlanetaryHoursFactory

hours = PlanetaryHoursFactory.from_datetime(
    2026, 5, 28, 11, 30, latitude=41.9028, longitude=12.4964, tz_str="Europe/Rome"
)
print(hours.day_ruler, hours.current_ruler)
for hour in hours.hours[:3]:
    print(hour.index, hour.ruler, hour.start, hour.end)
```

### Void of Course Moon

`VoidOfCourseMoonFactory` resolves the classical void-of-course Moon — the last exact Ptolemaic aspect to a traditional planet before sign ingress. It is geocentric (no location needed) and supports both the tropical and sidereal zodiacs.

```python
from kerykeion import VoidOfCourseMoonFactory

voc = VoidOfCourseMoonFactory.from_datetime(2026, 6, 1, 9, 0, tz_str="Europe/Rome")
print(voc.is_void_of_course, voc.moon_sign, "->", voc.next_sign)
print(voc.last_aspect, voc.next_aspect, voc.ingress)
```

### Lunation Finder

`LunationFinderFactory` finds the New, First-Quarter, Full and Last-Quarter Moons in a date range (ISO dates, treated as UTC). It is geocentric — no location needed.

```python
from kerykeion import LunationFinderFactory

result = LunationFinderFactory.from_iso_range("2026-01-01", "2026-12-31")
for lunation in result.lunations:
    print(lunation.iso_utc, lunation.phase)  # phase: new / first_quarter / full / last_quarter

# Only full moons:
fulls = LunationFinderFactory.from_iso_range("2026-01-01", "2026-12-31", phases=["full"])
```

### Retrograde Stations

`RetrogradeStationFactory` finds planetary stations (retrograde and direct turning points) in a date range (Mercury–Pluto by default).

```python
from kerykeion import RetrogradeStationFactory

result = RetrogradeStationFactory.from_iso_range("2026-01-01", "2026-12-31")
for station in result.stations:
    print(station.iso_utc, station.planet, station.station_type)  # station_type: 'SR' (turns retrograde) / 'SD' (turns direct)
```

`retrograde_periods_from_iso_range` turns the stations into spans — the intervals during which a planet is retrograde. A retrograde station opens a span and a direct station closes it; the range edges clip, so a planet already retrograde on the first day is reported from that day with `start_clipped=True`, and one still retrograde on the last day with `end_clipped=True`. `"Chiron"` is accepted opt-in by both finders; the Sun and Moon are rejected.

```python
from kerykeion import RetrogradeStationFactory

periods = RetrogradeStationFactory.retrograde_periods_from_iso_range("2026-01-01", "2026-12-31")
for period in periods.periods:
    print(period.planet, period.start, "->", period.end, period.start_clipped, period.end_clipped)
# Mercury 2026-02-26T06:48:10Z -> 2026-03-20T19:32:50Z False False
# Mercury 2026-06-29T17:35:55Z -> 2026-07-23T22:57:51Z False False
# ...

# Opt in to Chiron (Mercury..Pluto is the default set):
with_chiron = RetrogradeStationFactory.retrograde_periods_from_iso_range(
    "2026-01-01", "2026-12-31", planets=["Mercury", "Chiron"]
)
```

### Sign Ingresses

`SignIngressFactory` finds the moments planets cross from one zodiac sign into the next (Sun–Pluto by default; pass `planets=["Moon"]` to include the fast-moving Moon).

```python
from kerykeion import SignIngressFactory

result = SignIngressFactory.from_iso_range("2026-01-01", "2026-12-31")
for ingress in result.ingresses:
    print(ingress.iso_utc, ingress.planet, "->", ingress.sign)
```

`sign_periods_from_iso_range` answers the other question — *which sign is each planet in, and for how long*: per planet, the contiguous stays that cover the whole range. The first stay opens at the range start (`start_clipped=True`), each ingress hands over to the next stay (the `end` of one is the `start` of the next, to the same instant), and the last stay closes at the range end (`end_clipped=True`). A sidereal request yields sidereal stays with sidereal ingress instants. The Moon is opt-in here too.

```python
from kerykeion import SignIngressFactory

stays = SignIngressFactory.sign_periods_from_iso_range("2026-03-01", "2026-03-31", planets=["Sun", "Mercury"])
for stay in stays.periods:
    print(stay.planet, stay.sign, stay.start, "->", stay.end, stay.start_clipped, stay.end_clipped)
# Sun Pis 2026-03-01T00:00:00Z -> 2026-03-20T14:45:58Z True False
# Sun Ari 2026-03-20T14:45:58Z -> 2026-04-01T00:00:00Z False True
# Mercury Pis 2026-03-01T00:00:00Z -> 2026-04-01T00:00:00Z True True
```

### Mundane Aspects

`MundaneAspectFactory` finds the exact moments two moving bodies form an aspect in the sky — no natal chart involved. Points are the Moon through Pluto, Chiron and the lunar nodes; aspects are the longitude aspects of the default set. ISO inputs without an offset are treated as UTC.

```python
from kerykeion import MundaneAspectFactory

result = MundaneAspectFactory.from_iso_range(
    "2026-01-01", "2026-01-31",
    points=["Sun", "Moon", "Mars", "Jupiter"], aspects=["conjunction", "opposition"],
)
for hit in result.aspects[:3]:
    print(hit.iso_utc, hit.point_a, hit.aspect, hit.point_b, hit.point_a_sign, hit.point_b_sign)
# 2026-01-03T10:02:55Z Moon opposition Sun Can Cap
```

## V6 Advanced Features

Kerykeion v6 adds a suite of advanced astronomical and astrological calculation modules. Every module below is opt-in: nothing here changes the output of a chart that does not ask for it. (Code written for v5 needs the [migration guide](https://www.kerykeion.net/content/docs/migration) — several classes were renamed and some defaults changed.)

### Uranian / Hamburg School Planets

Eight hypothetical planets used in Uranian astrology: Cupido, Hades, Zeus, Kronos, Apollon, Admetos, Vulkanus, Poseidon.

```python
from kerykeion import AstrologicalSubjectFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
    active_points=["Sun", "Moon", "Cupido", "Hades", "Zeus", "Kronos",
                   "Apollon", "Admetos", "Vulkanus", "Poseidon"],
)
print(f"Cupido: {subject.cupido.abs_pos:.4f}")
print(f"Poseidon: {subject.poseidon.abs_pos:.4f}")
```

### Essential Dignities

Ptolemaic essential dignities (Domicile, Exaltation, Detriment, Fall, Term, Peregrine) for any planet.

```python
from kerykeion import AstrologicalSubjectFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
    calculate_dignities=True,
)
print(f"Sun dignity: {subject.sun.essential_dignity}")
```

### Vedic Nakshatras

Lunar mansions with pada and Vimsottari Dasha lord.

The nakshatras divide the *sidereal* zodiac. A sidereal chart supplies those
longitudes itself; on a tropical chart they are rotated by `nakshatra_ayanamsa`
(default `"LAHIRI"`) for the 27-fold division only, so the chart stays tropical
and still names the nakshatra a Jyotish chart would name. Pass
`nakshatra_ayanamsa=None` to divide the tropical longitudes as they are (the uncorrected values, with a warning).

```python
from kerykeion import AstrologicalSubjectFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
    calculate_nakshatra=True,
)
print(f"Moon nakshatra: {subject.moon.nakshatra}, pada: {subject.moon.nakshatra_pada}")
print(f"Rotated by {subject.nakshatra_ayanamsa}: {subject.nakshatra_ayanamsa_value:.4f} deg")
```

### Eclipse Search

Find upcoming solar and lunar eclipses globally or from a specific location.

```python
from kerykeion import EclipseFactory

result = EclipseFactory.search_global(start_year=2025, count=3)
for ecl in result.solar_eclipses:
    print(f"Solar: {ecl.datestamp} ({ecl.type})")
for ecl in result.lunar_eclipses:
    print(f"Lunar: {ecl.datestamp} ({ecl.type})")
```

**📖 Full documentation: [Eclipse Factory](https://www.kerykeion.net/content/docs/eclipse_factory)**

### Planetary Phenomena

Phase angle, elongation, apparent magnitude, morning/evening star status, and the
solar phase.

`solar_phase` names the classical condition near the Sun — `"cazimi"`,
`"combust"`, `"under_the_beams"` or `"free"` — read off the elongation against
the collection's `solar_phase_thresholds` (0.2833° / 8.5° / 17° by default, and
replaceable, since the schools disagree). `is_morning_star` / `is_evening_star`
are a different question and purely geometric: which side of the Sun the planet
stands on, with no visibility threshold at all.

```python
from kerykeion import PlanetaryPhenomenaFactory, AstrologicalSubjectFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 2000, 1, 1, 12, 0,
    lng=0, lat=0, tz_str="Etc/GMT", online=False,
)
phenom = PlanetaryPhenomenaFactory.from_subject(subject)
venus = next(p for p in phenom.phenomena if p.name == "Venus")
print(f"Venus elongation: {venus.elongation:.2f}, magnitude: {venus.apparent_magnitude:.2f}")
print(f"Solar phase: {venus.solar_phase}")
```

**📖 Full documentation: [Planetary Phenomena Factory](https://www.kerykeion.net/content/docs/planetary_phenomena_factory)**

### Planetary Nodes & Apsides

Ascending/descending node and the orbit's periapsis/apoapsis.

The apsides carry two names for the same two points. `periapsis`/`apoapsis` are
generic and always correct; `perihelion`/`aphelion` are deprecated — they name
the Sun, which is right for the planets and wrong for the Moon, which goes round
the Earth. `apsis_kind` says which reading applies (`"geocentric"` for the Moon
alone, whose apoapsis is to the decimal the Black Moon Lilith).

```python
from kerykeion import PlanetaryNodesFactory

nodes = PlanetaryNodesFactory.from_julian_day(2451545.0, planets=["Mars", "Jupiter"])
for entry in nodes.nodes:
    print(f"{entry.planet_name}: ascending node {entry.ascending_node.abs_pos:.2f}")
    print(f"  apoapsis {entry.apoapsis.abs_pos:.2f} ({entry.apsis_kind})")
```

**📖 Full documentation: [Planetary Nodes & Apsides](https://www.kerykeion.net/content/docs/planetary_nodes_factory)**

### Heliacal Risings & Settings

Find when a planet first becomes visible or disappears in twilight.

```python
from kerykeion import HeliacalFactory

factory = HeliacalFactory()
from kerykeion.ephemeris_backend import ephe
jd = ephe.julday(2025, 1, 1, 0.0)
event = factory.next_heliacal_rising(jd, "Venus", geopos=(12.5, 41.9, 0))
print(f"Venus heliacal rising: {event.datestamp}")
```

**📖 Full documentation: [Heliacal Risings & Settings](https://www.kerykeion.net/content/docs/heliacal_factory)**

### Occultation Search

Find lunar occultations of planets.

```python
from kerykeion import OccultationFactory
from kerykeion.ephemeris_backend import ephe

factory = OccultationFactory()
jd = ephe.julday(2025, 1, 1, 0.0)
events = factory.search_global(jd, ephe.VENUS, count=3)
for occ in events:
    print(f"{occ.planet_name} occultation: {occ.datestamp} ({occ.type})")
```

**📖 Full documentation: [Occultation Factory](https://www.kerykeion.net/content/docs/occultation_factory)**

### Davison Composite Chart

Time-space midpoint composite method (in addition to the existing midpoint composite).

```python
from kerykeion import AstrologicalSubjectFactory, CompositeSubjectFactory

s1 = AstrologicalSubjectFactory.from_birth_data("A", 1990, 3, 15, 10, 0,
    lng=12.5, lat=41.9, tz_str="Europe/Rome", online=False)
s2 = AstrologicalSubjectFactory.from_birth_data("B", 1992, 7, 20, 14, 30,
    lng=-73.9, lat=40.7, tz_str="America/New_York", online=False)

factory = CompositeSubjectFactory(s1, s2)
davison = factory.get_davison_composite_subject_model()
print(f"Davison Sun: {davison.sun.abs_pos:.4f}")
```

### Relocated Charts

Keep planetary positions, recalculate houses for a new location.

```python
from kerykeion import AstrologicalSubjectFactory, RelocatedChartFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
)
relocated = RelocatedChartFactory.relocate(subject, new_lng=139.69, new_lat=35.69, new_city="Tokyo")
print(f"Original ASC: {subject.first_house.abs_pos:.2f}")
print(f"Tokyo ASC: {relocated.first_house.abs_pos:.2f}")
```

**📖 Full documentation: [Relocated Chart Factory](https://www.kerykeion.net/content/docs/relocated_chart_factory)**

### Motion State & Stations

Every planet carries a `motion_state`: `"fast"`, `"average"`, `"slow"`,
`"retrograde"`, or one of the three stationary values. The stationary band
brackets zero speed on both sides and is tested before the sign, so a planet
edging backwards at a hundredth of its mean motion reports a station rather than
a plain retrograde. Which station it is comes from the trend, not the sign —
both stations are approached from one side of zero and left on the other — so
the factory samples the speed again a day later: falling through the band opens
the retrograde phase (`"stationary_retrograde"`, SR), rising through it closes
the phase (`"stationary_direct"`, SD). Where no second sample is available the
generic `"stationary"` stands.

```python
from kerykeion import AstrologicalSubjectFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Mercury Station", 1990, 8, 25, 12, 0,
    lng=-0.1276, lat=51.5074, tz_str="Europe/London", online=False,
)
print(f"Mercury: {subject.mercury.motion_state} at {subject.mercury.speed:.5f}°/day")
# Mercury: stationary_retrograde at 0.01237°/day
```

`ChartDrawer(..., show_motion_state=True)` draws these as SR/SD on the wheel;
`RetrogradeStationFactory` finds the instants of the stations themselves.

### Declination & Out-of-Bounds Detection

Equatorial declination and OOB detection for all celestial points.

```python
from kerykeion import AstrologicalSubjectFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
)
print(f"Sun declination: {subject.sun.declination:.4f}")
print(f"Sun OOB: {subject.sun.is_out_of_bounds}")
```

### Barycentric & Planetocentric Perspectives

Solar system barycenter or any planet as the observer origin.

```python
from kerykeion import AstrologicalSubjectFactory

bary = AstrologicalSubjectFactory.from_birth_data(
    "Bary", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
    perspective_type="Barycentric",
)
print(f"Barycentric Sun: {bary.sun.abs_pos:.4f}")
```

### Nutation Model

True/mean obliquity and nutation in longitude/obliquity.

```python
from kerykeion import AstrologicalSubjectFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 2000, 1, 1, 12, 0,
    lng=0, lat=0, tz_str="Etc/GMT", online=False,
    calculate_nutation=True,
)
print(f"True obliquity: {subject.nutation.true_obliquity:.4f}")
print(f"Nutation in longitude: {subject.nutation.nutation_longitude:.6f}")
```

### Dynamic Fixed Star Discovery

Auto-discover fixed stars near natal planet positions.

```python
from kerykeion import AstrologicalSubjectFactory, FixedStarDiscoveryFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
)
stars = FixedStarDiscoveryFactory.find_prominent_stars(subject, orb=2.0)
for star in stars:
    print(f"{star.name} at {star.longitude:.2f} (mag {star.magnitude:.1f})")
```

**📖 Full documentation: [Fixed Star Discovery](https://www.kerykeion.net/content/docs/fixed_star_discovery_factory)**

### Gauquelin Sectors

36-sector system for statistical astrology research.

```python
from kerykeion import AstrologicalSubjectFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
    calculate_gauquelin=True,
)
print(f"Sun Gauquelin sector: {subject.sun.gauquelin_sector:.2f}")
print(f"Mars Gauquelin sector: {subject.mars.gauquelin_sector:.2f}")
```

### Local Space (Azimuth & Altitude)

Horizon coordinates for all celestial points.

```python
from kerykeion import AstrologicalSubjectFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
    calculate_local_space=True,
)
print(f"Sun azimuth: {subject.sun.azimuth:.2f}, altitude: {subject.sun.altitude_above_horizon:.2f}")
```

### Lilith Variants & Priapus Points

Interpolated Lilith, Mean Priapus, and True Priapus (anti-Lilith points).

```python
from kerykeion import AstrologicalSubjectFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
    active_points=["Sun", "Moon", "Mean_Lilith", "True_Lilith",
                   "Interpolated_Lilith", "Mean_Priapus", "True_Priapus"],
)
print(f"Interpolated Lilith: {subject.interpolated_lilith.abs_pos:.4f}")
print(f"Mean Priapus: {subject.mean_priapus.abs_pos:.4f}")
```

### Transit Exactness & Refinement

Bisection refinement for sub-step precision on exact transit moments.

```python
from datetime import datetime
from kerykeion import AstrologicalSubjectFactory, TransitsTimeRangeFactory, EphemerisDataFactory

natal = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
)

# Generate ephemeris points for the transit period. A 4-hour step keeps the
# sampling finer than half the Moon's in-orb window; the default 1-day step is
# coarse enough that the factory logs a sub-sampling warning for the Moon.
ephemeris = EphemerisDataFactory(
    start_datetime=datetime(2025, 6, 1),
    end_datetime=datetime(2025, 7, 1),
    step_type="hours", step=4,
    lng=11.25, lat=43.77, tz_str="Europe/Rome",
)
points = ephemeris.get_ephemeris_data_as_astrological_subjects()

factory = TransitsTimeRangeFactory(natal, points)
events = factory.get_transit_events(refine_exact_moments=True)
for ev in events.events[:3]:
    print(f"{ev.p1_name} {ev.aspect} {ev.p2_name}: {ev.exact_moment} (orb {ev.min_orb:.4f})")
```

### Primary Directions (Placidus Semi-Arc)

Classical predictive technique with Ptolemy and Naibod rate keys.

```python
from kerykeion import AstrologicalSubjectFactory, PrimaryDirectionsFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
)
directions = PrimaryDirectionsFactory.compute(subject, max_years=30)
for d in directions[:5]:
    print(f"{d.promissor} {d.aspect} {d.significator}: {d.direction_years:.1f} years")
```

**📖 Full documentation: [Primary Directions](https://www.kerykeion.net/content/docs/primary_directions_factory)**

### Secondary Progressions (Day-for-a-Year)

The day-for-a-year technique maps each day after birth to one year of life.
The progressed chart is a real ephemeris snapshot, returned as a standard
`AstrologicalSubjectModel` — so every downstream tool (aspects, dignities,
chart drawer) works transparently.

```python
from pathlib import Path

from kerykeion import AstrologicalSubjectFactory, SecondaryProgressionFactory, ChartDataFactory, ChartDrawer

natal = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
)
progressed = SecondaryProgressionFactory.compute(natal, target_year=2026)

# Inspect progressed positions
print(f"Progressed Sun: {progressed.sun.sign} {progressed.sun.position:.2f}°")
print(f"Progressed Moon: {progressed.moon.sign} {progressed.moon.position:.2f}°")

# Generate a biwheel SVG (natal inner ring, progressed outer ring)
data = ChartDataFactory.create_progression_chart_data(natal, progressed)
drawer = ChartDrawer(data)
output_dir = Path("charts_output")
output_dir.mkdir(parents=True, exist_ok=True)
drawer.save_svg(output_path=output_dir, filename="progression-biwheel")
```

**📖 Full documentation: [Secondary Progressions](https://www.kerykeion.net/content/docs/secondary_progressions_factory)**

The biwheel shows the natal chart on the inner ring and progressed positions
on the outer ring. Astrologers read it by looking for contacts between the
two rings: when a progressed planet (outer) reaches a conjunction, square,
or trine to a natal planet (inner), it signals a symbolic theme active for
roughly one year. Sign ingresses (a progressed planet changing zodiac sign)
mark longer-term shifts in how that planetary energy is expressed.

### Solar Arc Directions

Solar arc takes the progressed Sun's forward motion and applies it
uniformly to every natal point. The result is a structured model with
directed positions and directed-to-natal aspect contacts.

```python
from kerykeion import AstrologicalSubjectFactory, SolarArcFactory

natal = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
)
result = SolarArcFactory.compute(natal, target_year=2026)

print(f"Solar arc: {result.solar_arc:.2f}°")
for dp in result.directed_points[:5]:
    ingress = " (sign changed)" if dp.sign_changed else ""
    print(f"  {dp.name}: {dp.directed_sign} {dp.directed_position:.2f}°{ingress}")

for asp in result.directed_to_natal_aspects[:5]:
    print(f"  {asp.directed_point} {asp.aspect} {asp.natal_point} (orb {asp.orb:.2f}°)")
```

**📖 Full documentation: [Solar Arc Directions](https://www.kerykeion.net/content/docs/solar_arc_factory)**

### Midpoints (Cosmobiology / 90° Dial)

Computes every pairwise midpoint of the active points, with the 90° dial
position used by cosmobiology and Uranian astrology, plus optional
aspect-to-midpoint detection (third-point activations).

```python
from kerykeion import AstrologicalSubjectFactory, MidpointFactory

natal = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
)
midpoints = MidpointFactory.compute(natal, aspect_orb=1.0)

for m in midpoints[:5]:
    activations = ", ".join(
        f"{a.point_name} {a.aspect} ({a.orb:.2f}°)" for a in m.aspects_to_midpoint
    )
    print(f"{m.point_a}/{m.point_b}: {m.midpoint_sign} {m.midpoint_position:.2f}° "
          f"(90° dial: {m.midpoint_modulus_90:.2f}°)"
          f"{' — activated by: ' + activations if activations else ''}")
```

**📖 Full documentation: [Midpoints](https://www.kerykeion.net/content/docs/midpoint_factory)**

### Astro-Cartography (ACG)

Compute MC, IC, ASC, DSC planetary lines on the world map.

```python
from kerykeion import AstrologicalSubjectFactory, AstroCartographyFactory

subject = AstrologicalSubjectFactory.from_birth_data(
    "Example", 1985, 4, 15, 8, 30,
    lng=11.25, lat=43.77, tz_str="Europe/Rome", online=False,
)
lines = AstroCartographyFactory.compute(subject)
for line in lines[:5]:
    print(f"{line.planet} {line.line_type}: {len(line.points)} points")
```

**📖 Full documentation: [Astro-Cartography](https://www.kerykeion.net/content/docs/astro_cartography_factory)**

### Chart Dominants

`DominantsFactory` computes a chart's dominant planet / sign / element / quality using a chosen scoring school: `"modern"` (default), `"almuten_figuris"` (the traditional "Lord of the Geniture"), or `"elemental"`. Custom schools (the `DominantStrategy` protocol) and per-point `custom_weights` are also supported.

```python
from kerykeion import AstrologicalSubjectFactory, DominantsFactory

subject = AstrologicalSubjectFactory.from_birth_data("John Lennon", 1940, 10, 9, 18, 30, lng=-2.9833, lat=53.4, tz_str="Europe/London", online=False)

dominants = DominantsFactory.from_subject(subject, strategy="modern")
print(dominants.dominant_planet, dominants.dominant_sign)
print(dominants.dominant_element, dominants.dominant_quality)

# Traditional Almuten Figuris, with a per-rule audit trail:
almuten = DominantsFactory.from_subject(subject, strategy="almuten_figuris", include_score_breakdown=True)
```

### Zodiacal Releasing (Aphesis)

`ZodiacalReleasingFactory` computes the Hellenistic time-lord technique of zodiacal releasing (aphesis) from the Lot of Fortune or Spirit, unfolding nested periods (levels L1–L4) with the "loosing of the bond" jumps and peak/angular markers. Requires a known birth time.

```python
from kerykeion import AstrologicalSubjectFactory, ZodiacalReleasingFactory

subject = AstrologicalSubjectFactory.from_birth_data("Jane", 1990, 6, 15, 12, 0, lng=12.4964, lat=41.9028, tz_str="Europe/Rome", online=False)
zr = ZodiacalReleasingFactory.from_subject(subject, lot="fortune", levels=2, target_date="2026-06-04")
print(zr.lot_sign, len(zr.periods), "top-level periods")
```

### Profections (Annual)

`ProfectionsFactory` computes annual profections — a traditional timing technique where each year of life activates one house (cycling every 12 years). The sign on the cusp determines the Lord of the Year. Respects the subject's house system and supports BCE births.

```python
from kerykeion import AstrologicalSubjectFactory, ProfectionsFactory

subject = AstrologicalSubjectFactory.from_birth_data("Jane", 1990, 6, 15, 12, 0, lng=12.4964, lat=41.9028, tz_str="Europe/Rome", online=False)
profections = ProfectionsFactory.from_subject(subject, target_date="2026-06-04")
print(f"Age {profections.current.age}: house {profections.current.house}, lord {profections.current.lord}")
```

### Firdaria (Planetary Periods)

`FirdariaFactory` computes the Persian time-lord sequence that divides life into planetary periods. Day charts begin with the Sun (10 years), night charts with the Moon (9 years). Each major period is subdivided among the seven classical planets. All date arithmetic uses Julian Days, so BCE births are supported.

```python
from kerykeion import AstrologicalSubjectFactory, FirdariaFactory

subject = AstrologicalSubjectFactory.from_birth_data("Jane", 1990, 6, 15, 12, 0, lng=12.4964, lat=41.9028, tz_str="Europe/Rome", online=False)
firdaria = FirdariaFactory.from_subject(subject, target_date="2026-06-04")
print(f"Current lord: {firdaria.current.lord}" if firdaria.current else "No current period")
```

### Mutual Receptions

`MutualReceptionsFactory` detects domicile and exaltation mutual receptions among the seven classical planets (Sun through Saturn). A reception is found when two planets each occupy a sign ruled by the other.

```python
from kerykeion import AstrologicalSubjectFactory, MutualReceptionsFactory

subject = AstrologicalSubjectFactory.from_birth_data("Jane", 1990, 6, 15, 12, 0, lng=12.4964, lat=41.9028, tz_str="Europe/Rome", online=False)
receptions = MutualReceptionsFactory.from_subject(subject)
for r in receptions.receptions:
    print(f"{r.first_planet} ↔ {r.second_planet} ({r.reception_type})")
```

### Horary Indicators

`HoraryIndicatorsFactory` assembles horary significators (querent/quesited via classical rulership), considerations before judgment (Ascendant degree, Saturn placement, Moon void-of-course), and mutual receptions for a question chart.

```python
from kerykeion import AstrologicalSubjectFactory, HoraryIndicatorsFactory

subject = AstrologicalSubjectFactory.from_birth_data("Question", 2026, 6, 4, 15, 30, lng=12.4964, lat=41.9028, tz_str="Europe/Rome", online=False)
indicators = HoraryIndicatorsFactory.from_subject(subject)
print(f"Querent ruler: {indicators.querent.ruler}, Quesited ruler: {indicators.quesited.ruler}")
```

## Command Line Interface

Kerykeion ships a command-line interface that exposes the whole library —
every chart type, analytical technique, sky event and factory — without
writing any Python. It lives in its own package, `kerykeion-cli`, which the
`cli` extra installs; the interface is built on the standard library alone, so
nothing third-party comes with it (`kerykeion[all]` also pulls the Swiss
Ephemeris backend):

```bash
pip3 install "kerykeion[cli]"          # the library plus the command
uv tool install kerykeion-cli          # or as a standalone tool (pipx works too)
```

A plain `pip install kerykeion` installs the library alone, with no command.

Save a subject profile once, then reuse it across every command:

```console
$ kerykeion subject save john --name "John Lennon" --date 1940-10-09 --time 18:30 \
      --lat 53.4 --lng -2.9833 --tz Europe/London --offline
$ kerykeion natal -s john                       # on a terminal → ASCII report
$ kerykeion natal -s john -f svg -o /tmp/john.svg
```

In a pipeline the default format flips to JSON, so `jq` works with no extra flag:

```console
$ kerykeion natal -s john | jq -r .sun.sign
Lib
```

The command tree covers charts (`natal`, `synastry`, `transit`, `composite`,
`return`, `progression`), analyses (`aspects`, `dominants`, `moon`,
`relationship-score`), analytical techniques (`technique profections`,
`technique solar-arc`, …), astronomical events (`sky eclipses`, `sky mundane`,
`sky occultations`, …), time series (`ephemeris`, `transits`), subject profiles
(`subject …`) and a guarded dispatcher (`call`) that reaches any public factory
method:

```console
$ kerykeion call ProfectionsFactory.from_subject -s john -f json
$ kerykeion call --list
```

`call` only dispatches to names in `kerykeion.__all__`, so `kerykeion call
os.system` is refused by design.

SVG output is fully controllable — themes, ten chart languages, styles,
wheel-only or aspect-grid-only variants, and a JSON settings file for palettes:

```console
$ kerykeion natal -s john -f svg -o /tmp/john.svg --theme dark --chart-language IT
```

And the CLI describes itself, so no table of valid values has to be memorised:

```console
$ kerykeion info literals SiderealMode    # what --sidereal-mode accepts
$ kerykeion status --check                # check the install; exits 6 if broken
```

**📖 Full CLI reference: [Command Line Interface](https://www.kerykeion.net/content/docs/cli)**

## Documentation

- **Main Website**: [kerykeion.net](https://www.kerykeion.net)
- **Getting Started**: [kerykeion.net/docs](https://www.kerykeion.net/content/docs/)
- **Examples Gallery**: [kerykeion.net/examples](https://www.kerykeion.net/content/examples/)
- **API Reference**: [kerykeion.net/pydocs](https://www.kerykeion.net/pydocs/)
- **Astrologer API Docs**: [kerykeion.net/astrologer-api](https://www.kerykeion.net/content/docs/astrologer-api)
- **Migration Guide (v4/v5 to v6)**: [Migration Guide](https://www.kerykeion.net/content/docs/migration)

## Projects built with Kerykeion

**[AstrologerStudio](https://www.astrologerstudio.com/)** is a cloud-based astrology app built on top of Kerykeion.

## Development

Clone the repository or download the ZIP via the GitHub interface.

```bash
git clone https://github.com/g-battaglia/kerykeion.git
cd kerykeion
uv sync --dev
uv run poe test:core   # the core suite; `uv run poe check` runs every gate
```

The development workflow — test tiers, golden baselines, documentation gates (`poe docs:check`, `poe docs:snippets`), release steps — is described in [DEVELOPMENT.md](https://github.com/g-battaglia/kerykeion/blob/alpha/v6/DEVELOPMENT.md); the test suite in [TEST.md](https://github.com/g-battaglia/kerykeion/blob/alpha/v6/TEST.md). Every gate runs locally through `poe`; there is no CI.

## Using the Swiss Ephemeris Backend (Optional)

Kerykeion uses **libephemeris** by default (no external data files needed). If you want to use the Swiss Ephemeris C backend instead, install the optional extra and run the setup utility:

```bash
pip install kerykeion[swiss]
python -m kerykeion.swisseph_setup
```

The setup utility downloads the required data files from the official Swiss Ephemeris repository (AGPL-3.0, Astrodienst AG) and asks for license confirmation. Then set the environment variables:

```bash
export KERYKEION_BACKEND=swisseph
export KERYKEION_EPHE_PATH=~/.kerykeion/sweph
```

For the full configuration guide, see [Swiss Ephemeris Configuration](https://github.com/g-battaglia/kerykeion/blob/alpha/v6/site/docs/swisseph_configuration.md).

> **Fixed stars on swisseph**: the fixed-star catalog file `sefstars.txt` is
> required for any fixed-star feature when using the swisseph backend, and is
> not bundled with kerykeion (Swiss Ephemeris license belongs to Astrodienst).
> The setup utility above downloads it automatically; for the manual procedure
> and a diagnostic warning reference, see the
> [Fixed Stars Catalog section of the configuration guide](https://github.com/g-battaglia/kerykeion/blob/alpha/v6/site/docs/swisseph_configuration.md#fixed-stars-catalog-sefstarstxt).

## AI Agent Skill

Kerykeion ships a cross-platform [Agent Skill](https://agentskills.io/) that teaches AI coding agents the real v6 API — factories, chart types, backends, sidereal modes, predictive and traditional techniques — so generated code stops guessing method names. It works with any skills-aware agent (Claude Code, Cursor, Codex, Copilot, Gemini CLI, and others).

> **During the v6 alpha, install from the `alpha/v6` branch.** The skill is not part of the PyPI package, and the registry command below resolves this repository's default branch — which still carries the v5-era skill. Until v6 is merged to the default branch, clone the branch and copy the folder.

```bash
git clone --branch alpha/v6 --depth 1 https://github.com/g-battaglia/kerykeion.git
cd kerykeion

# Claude Code
cp -r skills/kerykeion /path/to/your-project/.claude/skills/kerykeion

# Codex
cp -r skills/kerykeion /path/to/your-project/.agents/skills/kerykeion

# Generic agentskills.io layout (Cursor and others)
cp -r skills/kerykeion /path/to/your-project/skills/kerykeion
```

Once v6 is the default branch, [skills.sh](https://skills.sh/) installs it in one step:

```bash
npx skills add g-battaglia/kerykeion
```

Prefer a single-file guide? The package also ships `kerykeion/llms.txt`, a self-contained AI-agent reference installed with the library. For turning chart *data* into LLM input at runtime, see [AI Context Serializer](#ai-context-serializer).

## Integrating Kerykeion into Your Project

If you would like to incorporate Kerykeion's astrological features into your application, please reach out via [email](mailto:kerykeion.astrology@gmail.com?subject=Integration%20Request). Whether you need custom features, support, or specialized consulting, I am happy to discuss potential collaborations.

For commercial or closed-source applications, consider using the paid [Astrologer API (RapidAPI plans & pricing)](https://www.kerykeion.net/astrologer-api/subscribe) which provides REST endpoints for all Kerykeion functionality.

## License

This project is covered under the AGPL-3.0 License. For detailed information, please see the [LICENSE](https://github.com/g-battaglia/kerykeion/blob/alpha/v6/LICENSE) file. If you have questions, feel free to contact me at [kerykeion.astrology@gmail.com](mailto:kerykeion.astrology@gmail.com?subject=Kerykeion).

As a rule of thumb, if you use this library in a project, you should open-source that project under a compatible license. Alternatively, if you wish to keep your source closed, consider using the paid [Astrologer API](https://www.kerykeion.net/astrologer-api/subscribe), which is AGPL-3.0 compliant and also helps support the project.

Since the Astrologer API is an external third-party service, using it does _not_ require your code to be open-source.

The dual-licensing model (AGPL-3.0 for open source, a commercial license on request) is described in [LICENSING.md](https://github.com/g-battaglia/kerykeion/blob/alpha/v6/LICENSING.md) and [COMMERCIAL-LICENSE.md](https://github.com/g-battaglia/kerykeion/blob/alpha/v6/COMMERCIAL-LICENSE.md); both are marked as drafts.

_This is not legal advice — see the [LICENSE](https://github.com/g-battaglia/kerykeion/blob/alpha/v6/LICENSE) file and consult legal counsel for guidance._

## Contributing

Contributions are welcome! Feel free to submit pull requests or report issues.

By submitting a contribution, you agree to assign the copyright of that contribution to the maintainer. The project stays openly available under the AGPL for everyone, while the re-licensing option helps sustain future development. Your authorship remains acknowledged in the commit history and release notes.

## Citations

If using Kerykeion in published or academic work, please cite as follows:

```
Battaglia, G. (2025). Kerykeion: A Python Library for Astrological Calculations and Chart Generation.
https://github.com/g-battaglia/kerykeion
```
