Ephemeris Backend #
Kerykeion supports two selectable ephemeris backends. All astrological
calculations go through a single adapter module (kerykeion.ephemeris_backend)
that selects the active engine at import time. Kerykeion’s core chart APIs stay
the same when switching, while backend-specific body/date coverage, fixed-star
setup, and some search directions can differ.
Backends #
| Backend | Package | License | Notes |
|---|---|---|---|
| libephemeris (default) | libephemeris |
AGPL-3.0 | Pure Python. Uses NASA JPL DE440/DE441 data via LEB/Skyfield. No C compiler needed. Owned by the Kerykeion project. |
| swisseph | pyswisseph |
AGPL-3.0 | C bindings to the Swiss Ephemeris library by Astrodienst AG. If you use this backend, the Swiss Ephemeris AGPL-3.0 license applies. |
Installation #
# Default (libephemeris only -- no C compiler needed)
pip install "kerykeion"
# Swiss Ephemeris backend (requires C compiler)
pip install "kerykeion[swiss]"
# Both backends and the CLI (all optional extras)
pip install "kerykeion[all]"
Configuration #
All configuration is done through environment variables. No code changes needed.
KERYKEION_BACKEND #
Force a specific backend. When unset, auto-detection tries libephemeris
first, then swisseph.
KERYKEION_BACKEND=swisseph python my_script.py
KERYKEION_BACKEND=libephemeris python my_script.py
KERYKEION_EPHE_PATH #
Override the directory where ephemeris data files are loaded from.
Default: empty string (libephemeris manages its own data internally via
~/.libephemeris/leb/; swisseph falls back to its built-in Moshier
analytical ephemeris).
# For swisseph: point to a directory containing .se1 files
KERYKEION_EPHE_PATH=/path/to/se1/files python my_script.py
# For libephemeris: typically not needed (uses ~/.libephemeris/leb/)
KERYKEION_EPHE_PATH=/path/to/custom/data python my_script.py
Both backends call ephe.set_ephe_path(EPHE_DATA_PATH) at init time.
For swisseph this points to .se1 files; for libephemeris the call is
a no-op (it manages its own data directory internally).
KERYKEION_LEB_MODE #
Only applies when libephemeris is the active backend.
Controls the calculation pipeline. Default: "leb" (mandatory .leb files).
| Value | Behavior |
|---|---|
leb |
Require precomputed .leb binary files. Raises RuntimeError if none found. Fastest (~14x vs Skyfield). This is the default. |
auto |
Use .leb if available, fall back to Skyfield/DE440. |
skyfield |
Always use Skyfield/DE440 (requires local DE440 file or download). |
horizons |
Use NASA JPL Horizons API (requires internet). |
KERYKEION_LEB_MODE=auto python my_script.py
The mode is applied with set_calc_mode() at import time, and in the default
leb mode Kerykeion additionally pins set_network_policy("sealed"). Both
calls override whatever libephemeris read from its own LIBEPHEMERIS_MODE /
LIBEPHEMERIS_NETWORK_POLICY variables, so setting those has no effect on a
Kerykeion process — use KERYKEION_LEB_MODE. reset_ephemeris_session()
re-applies the pinned mode after every reset, since a reset would otherwise
leave the session in auto and silently re-enable the Skyfield fallback.
LIBEPHEMERIS_PRECISION #
Only applies when libephemeris is the active backend.
Selects the ephemeris tier — base, medium or extended — and therefore the
date range that can be computed. Unset, libephemeris auto-detects the widest
tier the installed kernel can serve. A date outside the active range raises
KerykeionException; sealed leb mode does not substitute a lower-precision
source.
LIBEPHEMERIS_PRECISION=extended python my_script.py
Installing .leb files #
.leb files contain precomputed Chebyshev polynomial approximations and
live in ~/.libephemeris/leb/. Download them with:
# doc-snippet: no-run — downloads ephemeris kernels
from libephemeris import download_leb_for_tier
# Every tier installs the SAME 14-body core (Sun-Pluto, Earth,
# Mean/True Node, Mean Apogee). Tiers differ by DATE RANGE, not by
# which bodies the core contains.
download_leb_for_tier("base") # 1850-2150 (DE440s), ~31 MB, bundled
download_leb_for_tier("medium") # 1550-2650 (DE440), ~114 MB
download_leb_for_tier("extended") # -13200 to +17191 (DE441), ~3.1 GB
The authoritative table is libephemeris.list_tiers(), which reports each
tier’s name, kernel file, range and size.
Asteroids (Chiron, Ceres, Pallas, Juno, Vesta), other curated minor
bodies and exotics (centaurs, trans-Neptunians), and lunar apsides are
separate companion groups, available at every tier — they are not
folded into the core by tier. Install them alongside the core with
download_leb2_for_tier, which lives in libephemeris.download rather than at
the package root:
# doc-snippet: no-run — downloads ephemeris kernels
from libephemeris.download import download_leb2_for_tier
download_leb2_for_tier("medium", groups=["core", "asteroids"])
download_leb2_for_tier(tier_name, groups=None, force=False, show_progress=True, quiet=False, activate=True); the group names are core, asteroids, exotics
and apogee (libephemeris.leb_groups.LEB2_GROUPS), and groups=None installs
all of them.
The Hamburg/Uranian points and the White Moon need no files at all: they are
fictitious bodies, always computed from their runtime analytical models
(provenance source="Analytical"), at every tier and for every date.
Architecture #
Application code
|
v
kerykeion.ephemeris_backend <-- single import point
|
+-- ephe = <module> (swisseph OR libephemeris, selected at import)
+-- BACKEND_NAME ("swisseph" or "libephemeris")
+-- EPHE_DATA_PATH (resolved data directory)
|
v
29 consumer modules <-- from kerykeion.ephemeris_backend.backend
import ephe, ephemeris_session
Package Exports #
kerykeion.ephemeris_backend.__all__:
| Name | What it is |
|---|---|
ephe |
The selected backend module itself (libephemeris or swisseph). |
BACKEND_NAME |
"libephemeris" or "swisseph". |
EPHE_DATA_PATH |
Resolved ephemeris data directory. |
EPHEMERIS_LOCK |
The lock every backend call is serialized behind. |
ephemeris_session |
Context manager: takes the lock, applies zodiac/sidereal/perspective/topocentric configuration, yields the iflag, resets and releases on exit. |
reset_ephemeris_session |
Resets per-calculation backend state and re-pins the calc mode. Callers must hold EPHEMERIS_LOCK; never call ephe.close() directly. |
houses_ex2_with_polar_fallback |
House cusps with the polar substitution applied. |
houses_ex2_with_polar_fallback_ex |
Same, also returning the fallback record. |
houses_ring_with_polar_fallback |
Cusp ring variant for the 36-sector Gauquelin grid. |
HouseRing |
Return type of the ring helper. |
POLAR_HOUSES_ERROR_TYPES |
Backend exception types that mean “undefined at this latitude”. |
DEFAULT_SWEPH_DOWNLOAD_DIR |
Where the Swiss Ephemeris data files are looked for. |
The polar helpers are the only supported way to compute houses above the polar circle.
Key design decisions:
-
Direct module re-export, not an adapter class. The overlapping ephemeris API used by Kerykeion is exposed through a module alias with zero overhead. No wrapper, proxy, or method interception is involved; callers that use backend-specific functions must still account for their differences.
-
Selection at import time. The backend is resolved once when
kerykeion.ephemeris_backendis first imported. All subsequentephe.*calls go directly to the backend module with no indirection. -
Environment variables only. No Python API to switch backends at runtime. This keeps the code simple and prevents accidental state corruption mid-calculation. Restart the process to switch.
Detecting the Active Backend #
from kerykeion import BACKEND_NAME
print(BACKEND_NAME) # "libephemeris" or "swisseph"
if BACKEND_NAME == "libephemeris":
print("Using pure-Python backend")
Licensing and Dual-License Strategy #
The backend abstraction gives Kerykeion one calculation surface across two dependencies with different licenses:
- libephemeris is licensed under AGPL-3.0.
- swisseph (Swiss Ephemeris) is licensed under AGPL-3.0 by Astrodienst AG. If you use this backend, the Swiss Ephemeris license terms apply to those components.
Users who install the [swiss] extra do so knowingly and accept the
AGPL-3.0 terms of Astrodienst AG for the Swiss Ephemeris components.
Testing #
Three parallel test suites verify backend correctness:
# Run all core tests with libephemeris (default backend)
poe test:lib
# Run identical core tests with swisseph
poe test:swe
# Cross-backend comparison: same calculations, both engines,
# assert results match within tolerance (requires both installed)
poe test:compare
The test:lib and test:swe suites are identical – same test files,
same assertions. The only difference is the KERYKEION_BACKEND environment
variable.
Golden-file tests do not use a loose numeric tolerance. For SVG baselines
(tests/data/compare_svg_lines.py) the contract has two halves:
- Structure is fatal on both backends. Line count, the count of numbers per line, and each line with its numbers blanked out must match the baseline exactly, or the test fails and names the line.
- Numbers are compared only on
libephemeris, the backend the baselines were generated with, atabs_tol = 1e-4with no relative component. A DMS label printed to the second cannot move by less than 2.78e-4, so any flipped label is rejected. On the other backend the structural assertions still run in full and the test then reports SKIPPED with a reason — not compared is not the same as compared and equal.
Report snapshots (tests/core/test_report.py) apply the same shape: line count
and the non-numeric skeleton must match exactly, and numbers are compared at
abs_tol = 0.01, which absorbs display-rounding flips of the last printed
decimals and nothing more.
Numerical Differences #
Both backends are astronomically valid. Small differences arise from different ephemeris sources and algorithms:
| Metric | Typical delta | Cause |
|---|---|---|
| Planetary longitudes | < 0.02 deg | Swiss Eph vs NASA JPL DE440 |
| House cusps | < 0.01 deg | Negligible |
| Planet speeds | < 0.01 deg/day | Analytical vs finite differences |
| True Node | ~ 6 arcsec | Different osculating element methods |
| Zodiac signs | Agreement in the maintained matrix | A boundary-adjacent delta can change the label |
| Retrograde status | Agreement in the maintained matrix | A near-stationary speed can be boundary-sensitive |
Debug: Backend Source Tracing #
To see which specific backend (LEB, Skyfield, SPK, Horizons, ASSIST,
Keplerian) computed each celestial body, enable DEBUG-level logging on
the "libephemeris" logger:
LIBEPHEMERIS_LOG_LEVEL=DEBUG python my_script.py
This emits log lines like body=0 jd=2448045.9 source=LEB at every
dispatch point. See the libephemeris
testing documentation
for the full list of source values.
When using the Astrologer API, the same information is available via the
opt-in X-Debug-Ephemeris: true HTTP header, which injects an
_ephemeris_debug.backends map into the JSON response. See the API’s
ephemeris debug documentation
for details.