Documentation · Quick start · vs iztro · Accuracy · 中文文档
x-iztro is a Zi Wei Dou Shu (紫微斗数, Chinese "Purple Star" astrology) chart engine: a Rust core with native Python and Go bindings. It turns a birth date and hour into a complete, semantically labeled chart — twelve palaces, ~100 stars with brightness and transformations, six horoscope levels, 64 named patterns, swappable reading texts. Every piece carries a translated name plus a stable language-independent key, and one call renders any of it as structured text an LLM can read.
Casting a chart is arithmetic, not interpretation. A language model gets some of the arithmetic right and quietly gets the rest wrong, and from the output you cannot tell which. x-iztro does the arithmetic deterministically, then hands the model the part it is actually good at: reading the chart.
from x_iztro import Astro
astro = Astro()
# time_index 2 = Tiger hour (03:00-05:00); 0 = early Rat hour ... 12 = late Rat hour
chart = astro.by_solar("2000-8-16", 2, "female", language="en-US")
print(chart.to_text())# Natal Chart 2000-8-16 Tiger hour female
## Basic Info
- Solar: 2000-8-16 · Lunar: 二〇〇〇年七月十七 · Hour: Tiger hour (03:00~05:00)
- Pillars: geng chen - jia shen - bing woo - geng yin · Zodiac: dragon · Sign: leo
- Five Elements Class: wood 3rd · Soul Star: rebel · Body Star: scholar
- Soul Palace: woo · Body Palace: xu (career) · Original Palace: chen (spouse)
- Birth-Year Mutagen: sun [A]→children, general [B]→wealth, moon [C]→friends, fortunate [D]→health
## Palace Overview
| Palace | Major Stars | Minor Stars | Decadal |
|---|---|---|---|
| **soul** woo | emperor([+3]) | artist([-3]) | 3-12 |
| siblings si | advisor([-1]) | — | 13-22 |
| spouse chen [Original Palace] | marshal([+3]) | helper, impulsive([-3]) | 23-32 |
(the other nine rows omitted)
## Patterns
- **Empress and Minister Facing the Palace** (soul): empress([+3]), minister([+3])
## Palaces
### soul (ren woo) · Decadal 3-12
- Major Stars: emperor([+3])
- Minor Stars: artist([-3])
- Adjective Stars: refined, lucky, intercepted, instigated, considery(Y)
- Trine & Opposite: Opposite surface · Trine wealth, career
- Stem ren Flying: sage [A]→children, emperor [B]→soul, officer [C]→career, general [D]→wealth
- Twelve Gods: Changsheng·weak, Boshi·dragon, Suiqian·downcast, Jiangqian·disastery
- Age Fortune Years: 5, 17, 29, 41, 53, 65, 77, 89, 101, 113
... (the other eleven palaces)
Legend: ([+3]) is brightness on a −3…+3 scale; [A]–[D] are the four
transformations (四化, "mutagens" in iztro's vocabulary) and →children names
the palace the transformed star sits in. The lunar date stays in Chinese
numerals by design. The same chart renders in six
languages; the underlying objects are typed, and chart.to_json() emits every
key JS iztro emits, with the same values, plus documented extension keys.
Chart math is ported from the JavaScript iztro and held identical to it; the layers an AI pipeline needs on top are new.
| iztro v2.6.1 (JS) | x-iztro | |
|---|---|---|
| Chart, twelve palaces (body and Original palace included), six horoscope levels | ✅ | ✅ field-for-field identical, 716,314 golden cases |
| Palace queries, surrounded palaces (三方四正), flying stars | ✅ | ✅ |
| Serialized output | JSON.stringify |
every iztro key, key for key, plus documented extension keys |
| Pattern judgement (64 rules) | — | ✅ |
| Knowledge packs (reading texts, school attributes) | — | ✅ |
| Reverse lookup (BaZi pillars / chart features → birth dates) | — | ✅ |
| Semantic text projection (to_text) | — | ✅ |
| Calling languages | JS / TS | Rust / Python / Go |
| Configuration | global singleton | passed per chart, no global state |
| Invalid input | throws | typed errors with machine-readable codes; never panics on invalid input |
On JS or in the browser, use iztro — it is the original. On a backend, or when the chart is headed for an LLM, use x-iztro.
Rust — MSRV 1.88 (checked by a dedicated CI job), four direct dependencies
(21 crates in the full tree). The core forbids unsafe via a crate-level
#![deny(unsafe_code)]; only the FFI/wasm boundary modules are exempt.
[dependencies]
x-iztro = "0.3"Python — 3.10+, zero runtime dependencies, typed API (dataclasses + StrEnum). Prebuilt abi3 wheels for Linux x86_64/aarch64 (manylinux), macOS universal2 and Windows x64 — no Rust toolchain needed there. Other targets (musl/Alpine, 32-bit) fall back to the sdist and compile with one.
pip install x-iztroGo — no cgo: the Rust core ships inside the module as a ~1.5 MB WebAssembly blob run by wazero, a pure-Go runtime. Cross-compilation works as usual.
go get github.com/x-haose/x-iztro/go/iztroPython
from x_iztro import Astro
from x_iztro.enums import MajorStar, Mutagen, PalaceName
# time_index 2 = Tiger hour; output defaults to zh-CN, pass language="en-US" for English
chart = Astro().by_solar("2000-8-16", 2, "female")
print(chart.chinese_date, chart.soul, chart.five_elements_class)
# Predicates take language-independent keys, so they answer the same
# no matter which language the chart was rendered in.
soul = chart.palace(PalaceName.SOUL)
print(soul.has([MajorStar.ZIWEI]), soul.has_mutagen(Mutagen.LU))
horoscope = chart.horoscope("2024-1-1", 0)
print(horoscope.yearly.heavenly_stem, horoscope.yearly.earthly_branch)Rust
use x_iztro::{by_solar, Config, Gender, IztroError, Language};
fn main() -> Result<(), IztroError> {
let chart = by_solar(
"2000-8-16", // solar birth date
2, // hour index: 0 = early Rat ... 12 = late Rat
// (the Rat hour straddles midnight, hence two indices)
Gender::Female,
true, // fix_leap: split leap months at the midpoint
Language::ZhCN,
Config::default(), // boundaries and school; defaults match JS iztro
)?;
println!("{} / {}", chart.lunar_date, chart.chinese_date);
// These fields are language-independent keys; Debug prints the variant name.
// Use translate_star(chart.soul, lang) etc. for display text.
println!("{:?} {:?}", chart.soul, chart.five_elements_class);
let horoscope = chart.horoscope("2024-1-1", 0)?;
println!("{:?}", horoscope.yearly.mutagen);
Ok(())
}Go
package main
import (
"fmt"
"log"
"github.com/x-haose/x-iztro/go/iztro"
)
func main() {
// args: date, hour index (2 = Tiger hour), gender, fixLeap, language,
// config (nil = defaults, same as JS iztro). Context variants: BySolarContext.
chart, err := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageZhCN, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(chart.ChineseDate, chart.Soul, chart.FiveElementsClass)
soul := chart.Palace(iztro.PalaceSoul)
fmt.Println(soul.Has(iztro.StarZiweiMaj), soul.HasMutagen(iztro.MutagenLu))
horoscope, err := chart.Horoscope("2024-1-1", 0)
if err != nil {
log.Fatal(err)
}
fmt.Println(horoscope.Yearly.HeavenlyStem, horoscope.Yearly.EarthlyBranch)
}All three bindings call the same Rust core, so the same birth data yields the same answers everywhere — that equivalence is itself under test.
iztro computes the chart. x-iztro also derives what the tradition reads into it — the semantic layers an AI pipeline (or an app) needs on top of raw star positions.
chart = astro.by_solar("1985-5-3", 9, "male", language="en-US")
for hit in chart.patterns():
print(hit.name, "|", hit.palace_name, "|", hit.variant, "|", hit.broken)General and Wolf Together | surface | None | False
Empress and Minister Facing the Palace | soul | soul_empty | False
Marshal, Rebel and Wolf | surface | None | False
Money and Horse Galloping Together | soul | surround | False
Officer and Helper Flanking Life | soul | None | False
Literary Nobility and Brilliance | surface | None | False
Literary Stars Facing Life | soul | None | True
Literary Stars in Hidden Support | soul | opposite | False
Literary Stars in Hidden Support | soul | surround | False
Not a lookup table: each hit carries the palace it formed in, the reading
variant that matched, a broken-pattern flag, and the stars that triggered it —
auditable, and language-independent via hit.key. The surface hits sit in
the travel palace because that is where this chart's body palace is:
soul-or-body patterns record the palace they actually formed in. The two
Hidden Support lines are one pattern forming two distinct ways, told apart by
variant. Natal charts and horoscope views share one rule set. How the rules
are evaluated, plus the full table of 64 patterns:
documentation.
from x_iztro import solar_dates_by_bazi
# every solar birth moment in 1900-2100 with the pillars
# geng-chen jia-shen bing-wu geng-yin (庚辰 甲申 丙午 庚寅)
for c in solar_dates_by_bazi(
("gengHeavenly", "chenEarthly"), ("jiaHeavenly", "shenEarthly"),
("bingHeavenly", "wuEarthly"), ("gengHeavenly", "yinEarthly"),
):
print(c.solar_date, c.time_index)1940-8-31 2
2000-8-16 2
2060-8-1 2
A set of pillars recurs roughly every 60 years, so multiple hits are the normal case; the pillars are interpreted under the chart configuration's boundary conventions.
from x_iztro import reverse_chart, ReverseCriteria, StarPosition
# soul palace in 午, body palace in 戌, wood-3rd class,
# Ziwei in the soul palace, birth-year Lu on the Sun
result = reverse_chart(ReverseCriteria(
soul_branch="wuEarthly",
body_branch="xuEarthly",
five_elements_class="wood3rd",
stars=[StarPosition("ziweiMaj", "wuEarthly")],
mutagens=("taiyangMaj", None, None, None),
year_range=(1995, 2005),
))
for c in result.candidates:
print(c.solar_date, c.time_index)2000-2-11 8
2000-2-19 8
2000-2-21 8
2000-8-6 2
2000-8-14 2
2000-8-16 2
The criteria are exactly this README's sample chart — its own birth moment
(2000-8-16, Tiger hour) is among the candidates. Both entry points work by
pruned enumeration, then re-cast every surviving candidate in full, so each
result provably charts back to the target. Constrained queries answer in
milliseconds; sweeps across the whole 200-year range are typically sub-second,
and overly loose criteria stop at a candidate limit with a truncated flag.
Details: documentation.
The core only judges facts; how to interpret them differs by school (a lineage of tradition). So reading texts and school-specific star attributes live in a swappable JSON pack, not in the code. A default pack ships inside — 107 stars, 64 patterns, 12 palaces, 4 transformations, 49 glossary entries, adapted from iztro-docs (MIT, Sylar Long) — and any entry can be replaced with an overlay pack. It doubles as a ready-made RAG corpus, in Chinese: the built-in pack is zh-CN only (note the quote below stays Chinese even on an English chart), so English products bring an overlay.
from x_iztro import KnowledgePack
pack = KnowledgePack.builtin()
chart = astro.by_solar("2000-8-16", 2, "female", language="en-US")
for hit in chart.patterns():
print(hit.name, "|", pack.pattern(hit.key).quotes[0])Empress and Minister Facing the Palace | 府相朝垣命必荣
Pack format, merge rules, and how to write an overlay: documentation.
chart.to_text()projects the whole chart into Markdown (basic info, a palace overview table, patterns, the twelve palaces in detail);chart.horoscope("2024-10-1", 0).to_text()does the same for the horoscope at a date; palaces, surrounded palaces and pattern hits each have their own to_text. The format is deterministic with a stable field order, and the text snapshots guarding it are part of the test suite.chart.to_text(knowledge=True)inlines readings picked by chart next to the facts (each palace's star readings after that palace, pattern readings after the pattern list, mutagen notes at the end), sourced from the bundled knowledge pack or your own pack merged from overlays — the core assembles, it holds no opinion of its own. Rust:to_text_with(&TextOptions::new().knowledge(&pack)); Go:ToTextWith(iztro.TextOptions{Knowledge: iztro.BuiltinKnowledge()}).- A full natal-chart text is ~3.4k characters in zh-CN and ~6.8k in en-US — on the order of 2–3k tokens, varying by model.
- Even for an English-facing product, consider feeding the model the zh-CN
chart: models know the Chinese star names, while the English names are
iztro's own gloss vocabulary (
considery,dissipated) that models mostly don't recognize. Feed the Chinese chart and ask for answers in English. Rationale and wiring, including exposing the library as a tool call: LLM guide. No MCP server yet — the tool-call recipe there covers the same integration. - The documentation site is itself LLM-readable:
/llms.txt,/llms-full.txt, and any docs page with.mdappended.
Every number is checked against the JavaScript iztro v2.6.1 (version-pinned) with zero tolerance for differences. That is a reproducibility standard, not a claim that any one school is the only correct one — schools genuinely differ, which is what the configuration switches and custom transformation/brightness tables are for. And you can re-run the whole comparison yourself:
cargo test # every layer except Tier 3, about a minute
cargo test --release --test golden_tier3 -- --ignored # Tier 3 in full: 586,430 charts, ~70 s716,314 golden cases in nine layers ("60 years" below means 1984–2043, one sexagenary cycle):
| Layer | Cases | Coverage |
|---|---|---|
| Tier 1 | 1,560 | 60 years × 13 hours × both genders, every field compared individually |
| Tier 2 | 37,440 | 60 years × the 1st and 15th of each month × 13 hours × both genders |
| Tier 3 | 586,430 | every day of 60 years × 13 hours × both genders, plus a second fix_leap pass on leap-month days; hashed |
| Edge years | 46,228 | 1583–1983 and 2044–2100 sampled, where leap months and tables strain |
| Horoscope | 5,760 | 360 charts × 16 target dates, all six horoscope levels, every field |
| Variants | 14,268 | lunar-date charts across leap months, Zhongzhou school, all six languages |
| Config | 9,696 | each boundary switch at its non-default value, chart and horoscope layers |
| Astro type | 12,488 | the heaven / earth / human chart perspectives |
| 1602 window | 2,444 | the 1602 leap-month correction window, day by day, hashed |
On top of that: the serialization contract is compared key-by-key against JS
JSON.stringify; translation reverse-lookup is checked against iztro's kot
(its key-of-translation helper) on 1,559 entries; and the three bindings are
cross-checked so the same birth data yields the same answers in Rust, Python
and Go.
One example of what zero tolerance means. The Rust lunar-calendar dependency has a self-contradictory month table for the year 1602 (a 31-day second month), which made one date panic outright and 28 more days silently produce wrong charts. x-iztro adds a correction layer at its single lunar-table entry point, with the true values cross-confirmed against three independent sources — lunar-typescript, the Shou-Xing almanac (sxtwl) and the Korea Astronomy and Space Science Institute's tables — and the window locked by 2,444 golden cases. A full-domain scan of every date from 1583 to 9999 (~6.1 million charts) found no second window of the same kind. The layer probes the dependency at runtime and disables itself once upstream is fixed.
- No global state — configuration and language travel with each call, so charts under different schools or locales never interfere.
- Invalid input is an error, never a panic — date format and existence are
validated, solar years 1583–9999, hour index 0–12. Rust returns
Err(IztroError), Python raisesx_iztro.IztroError(aValueErrorwith a.code), Go returns anerrormatchable witherrors.Is, and the C FFI yields error JSON. Every failure carries a machine-readable category. - Python — ~0.5 ms per chart (Apple Silicon dev machine). Charts are
frozen dataclasses, safe to share across threads, but the compute holds the
GIL — scale with processes, not threads. Serialize with
to_dict()/to_json(), notdataclasses.asdict()(charts hold back-references). - Go — concurrency-safe: a pool of wasm instances (capped at
GOMAXPROCS, one linear memory each) serves goroutines in parallel. First call compiles the embedded wasm (~100–200 ms, or 20–30 ms with the on-disk cache underos.UserCacheDir(); without one, each process recompiles). Steady state is ~0.5 ms per chart — reproduce withgo test -benchingo/iztro— andWarmupmoves the cold start to boot time. - Rust — MSRV 1.88; direct dependencies are
serde,serde_json,lunar_rustandchrono(clock only, forhoroscope_now). - Pre-1.0 stability — the serialized JSON contract, the to_text format and the error codes are snapshot-guarded by the test suite; breaking changes are called out explicitly in the CHANGELOG.
Six switches, passed explicitly per chart — there is no global state.
| Switch | Values | Default | Effect |
|---|---|---|---|
year_divide |
normal / exact |
normal |
Year boundary: lunar new year, or 立春 (Lichun, the solar term that begins spring) |
horoscope_divide |
normal / exact |
normal |
Horoscope boundary: 1st of the month, or solar term |
age_divide |
normal / birthday |
normal |
Nominal age: increments at new year, or on the birthday |
day_divide |
forward / current |
forward |
Late Rat hour belongs to the next day, or the current one |
algorithm |
default / zhongzhou |
default |
School of placement rules |
astro_type |
heaven / earth / human |
heaven |
Chart perspective — a Zhongzhou-school distinction |
Defaults match JS iztro exactly. Custom 四化 (transformation) and brightness
tables can be supplied alongside them. Chart language is a per-call parameter:
zh-CN (default), zh-TW, en-US, ja-JP, ko-KR, vi-VN.
https://ziwei.x-haose.com — the documentation site, in English and Chinese: a guide that starts from zero, the Zi Wei concepts behind the data model, and per-language API references where every function, type and method has its own entry with real output and edge-case notes. New to Zi Wei Dou Shu itself? Start at the concepts.
Rust API docs are also on docs.rs/x-iztro; inline
doc comments are currently Chinese, the English API reference lives on the
docs site. Runnable projects for all three languages are under examples/.
Only needed when changing the Rust core.
cargo build --release
# Python bindings
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 maturin develop --features python
# Go bindings: rebuild and refresh the embedded wasm
cargo build --release --target wasm32-wasip1
cp target/wasm32-wasip1/release/x_iztro.wasm go/iztro/Golden test data is generated from the JS iztro package:
cd tests/golden && npm ci && npm run gen:all.
Chart math ported from iztro; default knowledge-pack texts adapted from iztro-docs (both MIT). iztro's author, SylarLong, maintains an introduction to Zi Wei Dou Shu at iztro.com.
MIT
