Skip to content

Repository files navigation

py-ephemeris ("Taiyin")

Python bindings for Taiyin Ephemeris, the C++ core library.

This repository is a monorepo. It publishes the base py-ephemeris distribution from the root and optional extensions from packages/taiyin-bazi and packages/taiyin-ziwei. They install as separate Python packages while sharing one source-control history.

中文 README · Chinese guides · Accuracy and performance

  • GitHub repository: py-ephemeris
  • PyPI distribution: py-ephemeris
  • Python import package: taiyin
python -m pip install py-ephemeris

This is a beta release. The direct Python API is feature-complete for the 1.0 line; incompatible changes are now avoided unless required to fix a serious correctness or safety issue.

The package is being rebuilt as a direct pybind11 binding over the Taiyin C++ API. Python users will import native extension modules normally; they will not locate or load Taiyin DLLs manually.

The direct binding covers the current Taiyin runtime surface, including custom calculation targets, ayanamsha models, and house systems backed by Python callables.

Quick start

Planetary positions

import taiyin

eph = taiyin.Ephemeris()
ctx = eph.create_context()
instant_ut1 = taiyin.JulianDate.from_double(2460409.25)

mars, mars_flags = ctx.position.at_ut1(
    taiyin.Body.mars,
    instant_ut1,
    flags=(taiyin.PositionFlag.radians,),
)
state, state_flags = ctx.position.state_at_ut1(taiyin.Body.mars, instant_ut1)

print("Mars longitude/latitude/distance:", mars)
print("Mars Cartesian position (AU):", state.position_au)

Ephemeris() finds the DE442-derived data bundled with the wheel automatically. The same position service also provides TT, TDB, UTC, batch, velocity, and acceleration forms.

Windows compiler policy

Published win_amd64 wheels are built with MinGW-w64 GCC, the recommended Windows x64 release toolchain for the Taiyin C++ core. win_arm64 wheels use the native ARM64 llvm-mingw Clang/LLD toolchain. MSVC is kept in the core CI as a compatibility target and is supported on a best-effort basis; it does not decide whether a Windows wheel is released.

New contexts enable light-time, annual aberration, and Sun-only gravitational deflection by default. PositionFlag.speed works with those corrections; use PositionFlag.no_aberr or PositionFlag.no_gdefl to disable one for a call. Custom multi-body deflector lists are also supported. See Positions and observers.

Solar and lunar eclipses

search_start = taiyin.AstroDateTime(2024, 1, 1).to_julian_date()

solar_eclipse, solar_flags = ctx.eclipses.next_solar_at_ut1(search_start)
lunar_eclipse, lunar_flags = ctx.eclipses.next_lunar_at_ut1(search_start)

print("Next solar eclipse:", solar_eclipse.kinds, solar_eclipse.maximum)
print("Next lunar eclipse:", lunar_eclipse.kinds, lunar_eclipse.maximum)
print("Execution flags:", solar_flags | lunar_flags)

The eclipse service also supports contact times, local circumstances, global routes and map products, and observer-specific visibility.

Chinese calendar and Ganzhi

from datetime import datetime, timedelta, timezone

local_time = taiyin.AstroDateTime(2003, 3, 13, 14, 15)  # UTC+08:00
instant_utc = taiyin.JulianDate.from_datetime(
    datetime(
        2003, 3, 13, 14, 15,
        tzinfo=timezone(timedelta(hours=8)),
    )
)

# Gregorian date → Chinese lunar date.
lunar, lunar_flags = ctx.chinese_calendar.from_solar(
    taiyin.SolarDate(2003, 3, 13)
)
print("Lunar date:", lunar)

# The same civil time and astronomical instant → year/month/day/hour pillars.
pillars, pillar_flags = ctx.chinese_calendar.four_pillars(instant_utc, local_time)
print("Four pillars:", pillars)
print("Execution flags:", lunar_flags | pillar_flags)
print("Day NaYin:", ctx.ganzhi.nayin_element(pillars.day))

taiyin includes the Chinese calendar and Ganzhi APIs directly; this part does not need another import or extension module. A timezone-aware Python datetime identifies the physical instant; the bound Chinese-calendar context still controls its own local timezone and day-boundary policy.

context.time.scales_from_utc() applies the context's EOP, leap-second, Delta-T, and TDB policies. TAI, TT, UT1, and TDB values can be converted back with tai_to_utc(), tt_to_utc(), ut1_to_utc(), and tdb_to_utc(); automatic UTC/TAI/TT/TDB-to-UT1 routes are available as matching methods.

Astrology

Sidereal positions, ayanamsha, house systems, precession, and nutation are built into the base taiyin package:

import math

ctx.configuration.set_observer_location(
    taiyin.ObserverLocation(118.582, 37.449, 0.0)
)
degrees = lambda radians: math.degrees(radians) % 360.0

sun, sun_flags = ctx.astrology.sidereal_position_at_ut1(
    taiyin.Body.sun,
    instant_utc,
    ayanamsha=taiyin.Ayanamsha.lahiri,
)
houses, house_flags = ctx.astrology.houses_at_ut1(
    instant_utc, system=taiyin.HouseSystem.porphyry
)
print("Sidereal Sun:", degrees(sun.siderealLongitudeRadians))
print("Ascendant:", degrees(houses.ascendantRadians))
print("House cusps:", [degrees(value) for value in houses.cuspLongitudesRadians])

BaZi extension

Install the separate BaZi distribution before importing taiyin_bazi:

python -m pip install py-ephemeris-bazi
import taiyin_bazi

# BaZi is created from the same Ephemeris runtime and inherits its data setup.
ctx = eph.create_context()
bazi = ctx.bazi()
result, result_flags = bazi.calculate_local(
    local_time,
    gender=taiyin_bazi.BaziGender.male,
)
# Or combine a calendar day with explicit clock fields.
result, result_flags = bazi.calculate_solar_day(
    taiyin.SolarDate(2003, 3, 13), hour=14, minute=15,
    gender=taiyin_bazi.BaziGender.male,
)
year_ten_god = bazi.get_ten_god(
    result.pillars.day.stem_id,
    result.pillars.year.stem_id,
)

print("Four pillars:", result.pillars)
print("Qi-Yun start:", result.qiyun.startCivilTime)
print("Qi-Yun start age:", result.qiyun.startAgeYears)
print("Year-stem Ten God:", year_ten_god)
print("Visible Ten Gods:", result.chart.visibleTenGods)

Gender is needed for the Qi-Yun direction convention, but not for the four pillars or the BaZi chart itself. Other house systems and BaZi options are listed in the API reference.

BaZi can also use local apparent solar time (often called "true solar time") as its chart clock. Pass clock=BaziClock(BaziClockMode.apparentSolar, longitudeRadians) to a high-level factory; it preserves the one physical instant and reports original clockTime and effective chartTime. See the BaZi true-solar-time example.

Ziwei Doushu extension

Ziwei Doushu is a separate optional native package. It shares the calculation context's Chinese-calendar policy and ephemeris data:

python -m pip install py-ephemeris-ziwei
import taiyin_ziwei

ctx = eph.create_context()
ziwei = ctx.ziwei()
chart, chart_flags = ziwei.calculate_local(
    taiyin.AstroDateTime(2003, 3, 13, 14, 15),
    gender=taiyin_ziwei.ZiweiGender.male,
)
# `calculate_solar_day(...)` and `calculate_lunar_day(...)` are also available.

life = chart.palace(taiyin_ziwei.ZiweiPalace.life)
print(chart.anchors.ziwei, [star.key for star in life.stars])

It includes natal charts, independent TOML rule selections, brightness and transformation overlays, decade through hourly flow layers, logical early/late Rat-hour navigation, and finite Tier-1 birth-time reverse lookup. See the Ziwei guide and its runnable example.

Applications can add removable JSON option modules over the selected TOML profile without rebuilding the native library:

ruleset = taiyin_ziwei.ZiweiRuleset().add_module(
    taiyin_ziwei.ZiweiJsonRuleModule(
        label="app-school",
        starsJson='[{"key":"ziwei","rule":{"type":"constant","value":5}}]',
    )
)
selection = taiyin_ziwei.ZiweiOptionSelection(
    placement={"ziwei": "app-school"},
)
ziwei = ctx.ziwei(selection=selection, ruleset=ruleset)
ruleset = ruleset.remove_module("app-school")

Bundled TOML options cannot be overwritten or removed. A module label names all options and new stars contributed by that module; removing the label clears the complete contribution. New star entries are marked by ZiweiStar.isNatal; flow-only stars remain distinguishable.

For a true-solar-time chart, pass an explicit ZiweiClock to calculate_local(), calculate_instant(), or either day factory. The physical instant remains authoritative instead of treating the corrected clock as a second civil timestamp. See the Ziwei true-solar-time example.

Bundled data

Ephemeris() uses the package's own taiyin/data/index.opc automatically. The default data bundle includes a DE442-derived major-body OPM2 product over approximately 1550–2650, selected precise asteroid OPM2 files, compact Saturn/Uranus center-of-body corrections, and approximate Kepler fallback elements. DE441 data are not bundled in the Python wheel; provide them through an explicit data_root or source_paths when needed. A separate optional approximately 30,000-year DE441 data package may be published later; it is not released yet. Users may also add NASA/JPL's original BSP/SPK files directly, including DE441, planetary-satellite, and small-body kernels.

The bundled lite fixed-star table is loaded automatically by Ephemeris() when packaged data are enabled. It contains 2,057 stars and 12,242 aliases, including every HIP star used by Stellarium's Chinese and western-zodiac line figures. It remains available for explicit reload after eph.star_catalog.clear():

from pathlib import Path
import taiyin

eph = taiyin.Ephemeris()
eph.star_catalog.clear()  # optional: reset the process-wide catalog
lite_stars = Path(taiyin.__file__).resolve().parent / "data" / "stars" / "catalogs" / "lite" / "stars-bright-v5.tsc1"
eph.star_catalog.add_tsc1(str(lite_stars))

ctx = eph.create_context()
antares, star_flags = ctx.stars.at_ut1(
    "antares", taiyin.JulianDate.from_double(2460310.5)
)
assert ctx.last_status == 0

See bundled data, the default-data example, and the eclipse/visibility example. Task-oriented documentation is in the feature guides. The public API is documented in docs/api.md.

Start with the getting started guide for runnable planet, star, calendar, and eclipse examples. Optional-extension walkthroughs are available for BaZi and Ziwei Doushu.

Development

Source builds prefer a Taiyin C++ checkout next to this repository. If that checkout is absent—as it normally is when building from an sdist—CMake fetches the public v1.0.0-beta.9 source archive and verifies its pinned SHA-256 before compiling it into the extension. Set TAIYIN_SOURCE_DIR explicitly to develop against another local C++ checkout.

python -m venv .venv
. .venv/bin/activate
python -m pip install -U pip
python -m pip install -e ".[test]" \
  --config-settings=cmake.define.TAIYIN_SOURCE_DIR=../taiyin-ephemeris
TAIYIN_SOURCE_DIR=../taiyin-ephemeris python -m pytest

The wheel includes the same default taiyin/data directory. Ephemeris() uses its valid index.opc automatically and falls back to discovering OPM2, SPK, TKE1, and TKC1 sources below that directory when the index is missing or stale:

import taiyin

eph = taiyin.Ephemeris()
context = eph.create_context()

Pass data_root="/path/to/other/data" to select a separate or extended data set instead.

Optional or user-provided solar-system shards can additionally be supplied as files or directories through source_paths=[...]. The source-tree test suite may deliberately select a DE441 fixture to keep fixed numeric oracles independent of the default package's route priority; those test data are not part of the Python wheel.

Chinese lunar month strings

Traditional month names are normalized in Python and then validated by the configured native calendar during conversion:

import taiyin

context = taiyin.Ephemeris().create_context()
lunar = taiyin.LunarDate.from_string(2003, "九月", 1)
solar, solar_flags = context.chinese_calendar.from_lunar(lunar)

leap_month = taiyin.LunarDate.from_string(2023, "闰二月", 15)
historical = taiyin.LunarDate.from_string(-209, "后九月", 15)

The parser accepts /正月, through 十二, , , 闰五, 后九, 拾贰, and 十三; a trailing is optional. The Python parser only creates a structured LunarDate. Month existence and the actual 29/30-day limit remain native Chinese-calendar responsibilities. Invalid names, absent leap months, and days outside the selected month's length raise ValueError; ephemeris coverage and runtime failures remain runtime errors.