# Documentation (/en/docs)
A Zi Wei Dou Shu chart engine, field-for-field identical to JS iztro, plus pattern judgement, knowledge packs and reverse birth-date lookup — with LLM-ready text output. A Rust core, callable from Rust, Python and Go.
Zi Wei Dou Shu — Chinese "Purple Star" astrology — charts a life from a birth date and hour.
This library turns that birth moment into a complete chart, and into text a language model can
read in one call — **let the library get the chart right; let the AI do the reading**.
One call produces the text below — the full basic info and the first palace, with the other eleven
palaces following in the same shape. Paste it into any language model and start asking questions:
```text
=== Basic Info ===
Gender: female
Solar Date: 2000-8-16
Lunar Date: 二〇〇〇年七月十七
Chinese Date: geng chen - jia shen - bing woo - geng yin
Time: Tiger hour (03:00~05:00)
Zodiac Sign: leo
Zodiac Animal: dragon
Soul Palace Branch: woo
Body Palace Branch: xu
Soul Star: rebel
Body Star: scholar
Five Elements Class: wood 3rd
Birth-Year Mutagen: sunA, generalB, moonC, fortunateD
=== Palaces ===
--- wealth ---
Stem-Branch: wuyin
Decadal: 43-52
Age Fortune Years: 9, 21, 33, 45, 57, 69, 81, 93, 105, 117
Twelve Gods: dissipated, gossip, sorrowing, varied
Major Stars: general([+1])[B], minister([+3])
Minor Stars: horse
Adjective Stars: considery, senior, ageless, psychic, gourmet, gloomy, upset
… (the other eleven palaces)
```
`Lunar Date` is the only field that stays in Chinese in an English chart — the lunar date is
rendered with Chinese numerals (`二〇〇〇年七月十七` = the 17th day of the 7th lunar month, 2000).
`Chinese Date` is the four pillars in iztro's own romanization — close to pinyin, but note 午
renders as `woo` to avoid clashing with 戊 `wu`. Bracket notation: `([+3])` is brightness on a
-3…+3 scale, `[A]`/`[B]`/`[C]`/`[D]` are the four mutagens.
Whether a chart is *correct* has one hard standard here: **zero field-level divergence from JS
[iztro](https://github.com/SylarLong/iztro) v2.5.8**, held by 716,314 golden test cases — see
[Accuracy](/en/docs/guide/about/accuracy). Defaults match iztro exactly; the Zhongzhou school and
every boundary convention are [switchable](/en/docs/guide/guides/config), because parity with iztro
is an engineering standard, not a claim that any one school is the only correct one.
## Where to start [#where-to-start]
## Three things iztro doesn't have [#three-things-iztro-doesnt-have]
These are the semantic layers above the raw chart — the part an AI pipeline actually consumes —
and upstream iztro has no equivalent API for any of them:
## Find your path [#find-your-path]
* **A practitioner, not a programmer** → [Using it without writing code](/en/docs/guide/guides/for-non-developers)
* **Backend / AI application engineer** → [Getting started](/en/docs/guide/getting-started), then the [LLM guide](/en/docs/guide/guides/llm)
* **New to Zi Wei Dou Shu** → [the concepts](/en/docs/guide/concepts), starting from stems, branches and the twelve palaces
## Three programming languages, one result [#three-programming-languages-one-result]
All three bindings call the same Rust core, so charts come out identical field for field.
The predicate methods are built on language-independent keys, so one analysis rule — written in
Rust, Python or Go — yields the same answer on a chart rendered in any output language.
```rust
use x_iztro::*;
let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?;
let soul = chart.palace(Palace::Soul).unwrap();
println!("{}", soul.has(&[StarKey::ZiweiMaj]));
```
```python
from x_iztro import Astro
chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US")
soul = chart.palace("soulPalace")
print(soul.has(["ziweiMaj"]))
```
```go
chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil)
soul := chart.Palace(iztro.PalaceSoul)
fmt.Println(soul.Has(iztro.StarZiweiMaj))
```
All three report the same answer — the Soul palace of this chart holds Ziwei, the Emperor star
(Python prints `True`; Rust and Go print `true`). All three use the language-independent key
`ziweiMaj`: render the same chart in English or Japanese and the answer doesn't change.
# Introduction (/en/docs/guide)
Turn a birth date and hour into a complete Zi Wei Dou Shu chart, and into text a language model can read. A Rust core, callable from Rust, Python and Go, matching JS iztro field for field.
*For: developers · Zi Wei enthusiasts · product and decision makers*
Turn a birth date and hour into a complete Zi Wei Dou Shu chart, and into text a language model can
read in one call — **let the library get the chart right, let the AI do the reading**.
One call produces this text. Paste it into any language model and start asking questions:
```text
=== Basic Info ===
Gender: female
Solar Date: 2000-8-16
Lunar Date: 二〇〇〇年七月十七
Chinese Date: geng chen - jia shen - bing woo - geng yin
Time: Tiger hour (03:00~05:00)
Zodiac Sign: leo
Zodiac Animal: dragon
Soul Palace Branch: woo
Body Palace Branch: xu
Soul Star: rebel
Body Star: scholar
Five Elements Class: wood 3rd
Birth-Year Mutagen: sunA, generalB, moonC, fortunateD
=== Palaces ===
--- wealth ---
Stem-Branch: wuyin
Decadal: 43-52
Age Fortune Years: 9, 21, 33, 45, 57, 69, 81, 93, 105, 117
Twelve Gods: dissipated, gossip, sorrowing, varied
Major Stars: general([+1])[B], minister([+3])
Minor Stars: horse
Adjective Stars: considery, senior, ageless, psychic, gourmet, gloomy, upset
(the other eleven palaces omitted)
```
Whether a chart is *correct* has one hard standard here: **zero field-level divergence from JS
[iztro](https://github.com/SylarLong/iztro) v2.5.8**, held by 716,314 golden test cases.
The core is written in Rust and exposed to higher-level languages through three bindings:
## The problem it solves [#the-problem-it-solves]
Charting Zi Wei Dou Shu looks like table lookup, but it drags in a chain of easily botched calendar
and school-of-thought details: handling leap months in the lunar calendar, whether the late Zi hour
belongs to today or tomorrow, whether the year's stem and branch turn over on lunar New Year or at
the Beginning of Spring (立春, the solar term around 4 February), how nominal age (虚岁, the East Asian
reckoning that starts at 1 on the day of birth) increments, and how mutagen tables differ between
schools. Decide any one of them differently and you are no longer
looking at the same chart.
The most complete open-source implementation in the community is
[iztro](https://github.com/SylarLong/iztro), written in JavaScript — but it only runs on a JS
runtime. x-iztro ports that logic to Rust in full, so servers, data-analysis scripts, command-line
tools and mobile apps can all work from the same charts.
x-iztro is a port of iztro v2.5.8, not a reinvention. Wherever iztro has a feature or a data table,
the two must agree field for field — a line held by 716,314 golden test cases. See
[Accuracy](/en/docs/guide/about/accuracy).
## Features [#features]
### One call to text an AI can read [#one-call-to-text-an-ai-can-read]
The semantic text projection (to\_text) is built in: project a chart or a horoscope into the
natural-language text shown above and hand it straight to a language model, with no need to assemble
a chart description yourself. See [Semantic text](/en/docs/guide/guides/to-text).
### Complete charts and horoscopes [#complete-charts-and-horoscopes]
Natal chart, decadal, age, childhood, yearly, monthly, daily and hourly scopes: palaces, stars,
mutagens and surrounded palaces are all reachable across six levels. Yearly adjective stars, the
twelve Sui-qian gods, the twelve Jiang-qian gods, the twelve Boshi gods and the twelve Changsheng
gods are all present.
### Configurable schools and boundaries [#configurable-schools-and-boundaries]
The six switches on `Config` cover every point practitioners actually disagree on: the year
boundary, the horoscope boundary, the nominal-age boundary, where the late Zi hour belongs, the
algorithm school (default / Zhongzhou) and the charting perspective (heaven / earth / human plate).
The mutagen and brightness tables can also be replaced wholesale. The defaults match JS iztro;
details on [Config in depth](/en/docs/guide/guides/config).
### Six chart languages [#six-chart-languages]
Simplified Chinese, Traditional Chinese, English, Japanese, Korean and Vietnamese. Switching the
chart language only swaps the translation — the computed result is unaffected.
### A Chinese chart and an English chart answer the same [#a-chinese-chart-and-an-english-chart-answer-the-same]
For developers
Star and palace names read differently in each chart language, but every entity also carries a
stable key. The Python enums and Go constants are built on those keys, so a check like "does the
Soul palace hold Ziwei?" is written the same way and yields the same answer on a chart in any chart
language. See [The key contract](/en/docs/guide/guides/keys).
## One minute in [#one-minute-in]
```python
from x_iztro import Astro
astro = Astro()
# 2 = hour index; index 2 is the Tiger hour, 03:00-05:00.
# language defaults to "zh-CN"; pass "en-US" for an English chart.
chart = astro.by_solar("2000-8-16", 2, "female", language="en-US")
soul = chart.palace("soulPalace") # the Soul palace
print(chart.five_elements_class, chart.soul, chart.body)
print(soul.name, soul.heavenly_stem + soul.earthly_branch)
print([s.name for s in soul.major_stars])
```
```text
wood 3rd rebel scholar
soul renwoo
['emperor']
```
Full installation and examples for all three programming languages are on
[Getting started](/en/docs/guide/getting-started).
## Where to read next [#where-to-read-next]
# Overview (/en/docs/guide/getting-started)
The inputs a chart needs, what each parameter accepts, and how to install for each of the three programming languages.
*For: everyone. The parameter table is readable without writing code*
All three bindings share one Rust core, so parameter meanings and chart results are identical —
only the spelling differs. Get clear on what you need to supply, then pick your programming
language.
## The inputs you need [#the-inputs-you-need]
Whatever the programming language, charting starts from these parameters. The first three are
required; the last three have defaults.
| Parameter | Meaning | Accepts |
| --------------------------- | ----------------------- | -------------------------------------------------------------------------------- |
| `solar_date` / `lunar_date` | Date of birth | `"YYYY-M-D"`, e.g. `"2000-8-16"`. Gregorian range 1583–9999 |
| `time_index` | Hour of birth | Integer 0–12, see the table below |
| `gender` | Gender | `"male"` / `"female"` (a `Gender` enum in Rust) |
| `fix_leap` | Correct for leap months | Boolean, defaults to `true` |
| `language` | Chart language | `"zh-CN"` (default), `"zh-TW"`, `"en-US"`, `"ja-JP"`, `"ko-KR"`, `"vi-VN"` |
| `config` | Boundaries and school | See [Config in depth](/en/docs/guide/guides/config); omit for the iztro defaults |
Omit `language` and you get a Simplified Chinese chart. For English output pass the exact string
`"en-US"` — every example on the English pages does. The chart language only changes the text of
names; it never changes which star lands in which palace.
Charting needs only three things: date of birth, hour of birth, gender. Write them out in the
formats above and hand them over — one call on their side produces the chart. For what the library
can do and how it is typically used, see
[Using it without writing code](/en/docs/guide/guides/for-non-developers).
### Hour index [#hour-index]
Zi Wei Dou Shu divides the day into twelve double-hours, and splits the Zi hour into an early and a
late segment — hence 13 index values, 0 through 12.
| Index | Hour | Time | Index | Hour | Time |
| ----- | -------------- | ----------- | ----- | ------------- | ----------- |
| 0 | Early Zi (Rat) | 00:00–01:00 | 7 | Wei (Goat) | 13:00–15:00 |
| 1 | Chou (Ox) | 01:00–03:00 | 8 | Shen (Monkey) | 15:00–17:00 |
| 2 | Yin (Tiger) | 03:00–05:00 | 9 | You (Rooster) | 17:00–19:00 |
| 3 | Mao (Rabbit) | 05:00–07:00 | 10 | Xu (Dog) | 19:00–21:00 |
| 4 | Chen (Dragon) | 07:00–09:00 | 11 | Hai (Pig) | 21:00–23:00 |
| 5 | Si (Snake) | 09:00–11:00 | 12 | Late Zi (Rat) | 23:00–24:00 |
| 6 | Woo (Horse) | 11:00–13:00 | | | |
Someone born between 23:00 and 24:00 uses index `12`, not `0`. The two indexes produce different
charts: under the default configuration the late Zi hour takes its day pillar from the **following**
day, while the early Zi hour takes it from the current day. The behaviour is controlled by the
`day_divide` switch — see
[Config in depth](/en/docs/guide/guides/config#late-zi-hour-attribution-day_divide).
### About `fix_leap` [#about-fix_leap]
A leap month in the lunar calendar has no month pillar of its own, so charting has to decide whether
its days count towards the preceding or the following month. With `fix_leap = true` (the default)
iztro's correction applies: the first half of the leap month counts as the current month, the second
half as the next. Set it to `false` and the whole leap month counts as the current month.
Only people born in a leap month are affected; otherwise the parameter does nothing.
## Pick a programming language [#pick-a-programming-language]
## Input validation [#input-validation]
For developers
Dates and hour indexes are validated up front in the **core**, so all three programming languages
sit behind the same line of defence. Invalid input never panics; it is reported the way each
language expects:
| Programming language | Behaviour |
| -------------------- | ---------------------------------------------------------------------- |
| Rust | Returns `Err(IztroError)`; `.code()` gives a machine-readable category |
| Python | Raises `IztroError` (a subclass of `ValueError`) with the same `.code` |
| Go | Returns `*iztro.Error`, matchable against sentinels with `errors.Is` |
| C FFI | Returns `{"error":"...","code":"..."}` as JSON |
The core validates date format and real existence, the Gregorian range 1583–9999, and hour index
0–12. Gender, chart language and configuration switches — the parameters passed as strings — are
validated in the **binding layer** as they are parsed; in Rust they are enums to begin with, so
there is no invalid value to reject.
See [Error handling](/en/docs/guide/guides/errors).
# Rust (/en/docs/guide/getting-started/rust)
Install the x-iztro crate, produce your first chart, and see how enums and translation functions divide the work.
*For: developers*
## Installation [#installation]
```bash
cargo add x-iztro
```
Or in `Cargo.toml`:
```toml
[dependencies]
x-iztro = "0.3"
```
The crate is named `x-iztro`; in code the library is `x_iztro`. No C dependencies, pure Rust build.
## Charting [#charting]
```rust
use x_iztro::{by_solar, IztroError};
use x_iztro::data::types::*;
fn main() -> Result<(), IztroError> {
let astrolabe = by_solar(
"2000-8-16", // Gregorian date of birth
2, // hour index: the Yin (Tiger) hour, 03:00-05:00
Gender::Female, // gender
true, // fix_leap: correct for leap months
Language::EnUS, // chart language; Language::ZhCN is the default elsewhere
Config::default(), // boundaries and school, same defaults as JS iztro
)?;
println!("Solar: {}", astrolabe.solar_date);
println!("Lunar: {}", astrolabe.lunar_date);
println!("Pillars: {}", astrolabe.chinese_date);
println!("Hour: {} ({})", astrolabe.time, astrolabe.time_range);
Ok(())
}
```
```text
Solar: 2000-8-16
Lunar: 二〇〇〇年七月十七
Pillars: geng chen - jia shen - bing woo - geng yin
Hour: Tiger hour (03:00~05:00)
```
`lunar_date` is the one field that stays in Chinese in an English chart: the lunar date is written
with Chinese numerals, and `二〇〇〇年七月十七` is the 17th day of the 7th lunar month, 2000.
`chinese_date` is the four pillars romanized in pinyin — `geng chen` is 庚辰, `bing woo` is 丙午.
Pass `Language::ZhCN` instead and both come out in Chinese.
Chart from a lunar date with [`by_lunar`](/en/docs/rust/astro#by_lunar): where `by_solar` takes
`fix_leap`, this takes a three-way `LeapMonth` (`NotLeap` / `Leap` / `LeapFixed` — leap month with
days after the 15th treated as the next month), so one argument says how the leap month is handled
and there is no pair of booleans to swap:
```rust
by_lunar("2000-7-17", 2, Gender::Female, LeapMonth::NotLeap, Language::EnUS, Config::default())?;
```
## Next [#next]
The example above only touches the charting entry point. The full API — locating the twelve palaces,
star predicates, flying stars, horoscopes, the star-placement module, data tables and translation —
lives under **[Rust API](/en/docs/rust)**, where every function, type and method has its own entry
with real run output and edge-case notes.
# Python (/en/docs/guide/getting-started/python)
Install with pip, use the typed dataclass API, and write chart-language-independent checks with enums.
*For: developers*
## Installation [#installation]
```bash
pip install x-iztro
```
Requires Python 3.10 or later. The wheel contains a native extension compiled by PyO3 (abi3), so
there are no runtime dependencies at all — no pydantic, and no Rust toolchain on the machine.
Only needed when you are changing the Rust side:
```bash
pip install maturin
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 maturin develop --features python
```
## Charting [#charting]
```python
from x_iztro import Astro
astro = Astro()
# language defaults to "zh-CN"; pass "en-US" for an English chart
chart = astro.by_solar("2000-8-16", 2, "female", language="en-US")
print(chart.solar_date) # Gregorian date
print(chart.lunar_date) # lunar date
print(chart.chinese_date) # the four pillars
print(chart.time, chart.time_range)
print(chart.sign, chart.zodiac) # zodiac sign, zodiac animal
print(chart.soul, chart.body) # soul star, body star
print(chart.five_elements_class) # Five Elements class
```
```text
2000-8-16
二〇〇〇年七月十七
geng chen - jia shen - bing woo - geng yin
Tiger hour 03:00~05:00
leo dragon
rebel scholar
wood 3rd
```
`lunar_date` is the one field that stays in Chinese in an English chart: the lunar date is written
with Chinese numerals, and `二〇〇〇年七月十七` is the 17th day of the 7th lunar month, 2000.
`chinese_date` is the four pillars romanized in pinyin — `geng chen` is 庚辰, `bing woo` is 丙午.
`solar_date` echoes the input string verbatim, without zero padding — pass `"2000-08-16"` and you
get `"2000-08-16"` back. For a structured date use `chart.raw_dates`.
Chart from a lunar date with `by_lunar`, which takes one extra argument, `is_leap_month`.
Everything after `gender` (`is_leap_month`, `fix_leap`, `language`, `config`) is keyword-only —
two adjacent booleans passed positionally can be swapped without an error:
```python
chart = astro.by_lunar("2000-7-17", 2, "female", is_leap_month=False, language="en-US")
```
The returned `Astrolabe` is a dataclass with annotated fields, so editors autocomplete it. Every
text field is already translated into the chart language.
## Next [#next]
The example above only touches the charting entry point. The full API — locating the twelve palaces,
star predicates, flying stars, horoscopes, the star-placement module, data tables and translation —
lives under **[Python API](/en/docs/python)**, where every function, class and method has its own
entry with real run output and edge-case notes.
# Go (/en/docs/guide/getting-started/go)
go get and go — embedded WebAssembly, no cgo, cross-compilation preserved.
*For: developers*
## Installation [#installation]
```bash
go get github.com/x-haose/x-iztro/go/iztro
```
The package embeds a WebAssembly module compiled from the core library (`wasm32-wasip1`) and calls
into it through [wazero](https://wazero.io), a runtime implemented in pure Go.
Which means: **no cgo, no Rust toolchain on the machine, and cross-compilation still works**.
wazero's compiler backend covers amd64 and arm64 only; other architectures fall back to the
interpreter — slower, same results.
A single wasm instance cannot be used concurrently, so the package keeps an instance pool (capped at
`GOMAXPROCS`). Calls from multiple goroutines are not serialized against each other and run in
genuine parallel.
The first call has to compile the wasm module. The compiled artifact is cached on disk (under
`os.UserCacheDir()`), so only the very first run costs \~200ms; after that the first call in each
process costs \~30ms. If you want a service's first request to take the hot path, call
`iztro.Warmup(ctx)` once at startup. On the hot path a chart — including JSON encoding, decoding and
memory copies — is on the order of 0.5ms.
## Charting [#charting]
```go
package main
import (
"fmt"
"log"
"github.com/x-haose/x-iztro/go/iztro"
)
func main() {
// the fifth argument is the chart language; "zh-CN" is what the other
// pages default to, "en-US" gives an English chart
chart, err := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(chart.SolarDate) // Gregorian date
fmt.Println(chart.LunarDate) // lunar date
fmt.Println(chart.ChineseDate) // the four pillars
fmt.Println(chart.Time, chart.TimeRange)
fmt.Println(chart.Sign, chart.Zodiac)
fmt.Println(chart.Soul, chart.Body) // soul star, body star
fmt.Println(chart.FiveElementsClass)
}
```
```text
2000-8-16
二〇〇〇年七月十七
geng chen - jia shen - bing woo - geng yin
Tiger hour 03:00~05:00
leo dragon
rebel scholar
wood 3rd
```
`LunarDate` is the one field that stays in Chinese in an English chart: the lunar date is written
with Chinese numerals, and `二〇〇〇年七月十七` is the 17th day of the 7th lunar month, 2000.
`ChineseDate` is the four pillars romanized in pinyin — `geng chen` is 庚辰, `bing woo` is 丙午.
The last argument is a `*Config`; pass `nil` for the defaults. `gender` and `language` are the named
types `iztro.Gender` / `iztro.Language` (`iztro.GenderFemale`, `iztro.LanguageEnUS`; string literals
still work). Chart from a lunar date with `ByLunar`: where `BySolar` takes `fixLeap`, this takes a
three-way `iztro.LeapMonth` (`NotLeapMonth` / `LeapMonthKeep` / `LeapMonthFixed`), so one argument
says how the leap month is handled:
```go
iztro.ByLunar("2000-7-17", 2, iztro.GenderFemale, iztro.NotLeapMonth, iztro.LanguageEnUS, nil)
```
Every entry point has a `*Context` variant (`BySolarContext`, `ByLunarContext` and so on), where
`ctx` cancels the wait for a pooled instance.
## Error handling [#error-handling]
Failures always come back as `*iztro.Error` carrying a machine-readable `Code`; match them by
category with `errors.Is`:
```go
_, err := iztro.BySolar("2000-13-1", 2, iztro.GenderMale, true, iztro.LanguageEnUS, nil)
if errors.Is(err, iztro.ErrInvalidDate) {
var e *iztro.Error
errors.As(err, &e)
fmt.Println(e.Code, e.Message)
}
```
```text
invalid_date invalid solar date '2000-13-1': month must be within 1-12
```
See [Error handling](/en/docs/guide/guides/errors).
## Next [#next]
The example above only touches the charting entry point. The full API — locating the twelve palaces,
star predicates, flying stars, horoscopes, the star-placement module, data tables and translation —
lives under **[Go API](/en/docs/go)**, where every exported function, type and method has its own
entry with real run output and edge-case notes.
# What a chart is made of (/en/docs/guide/concepts)
The minimum Zi Wei Dou Shu you need, readable without code — what a chart is, what parts it has, and what each part decides.
*For: everyone. No code needed to read this; each page ends with a "In code" section*
This chapter does not teach you how to *interpret* a chart. It explains **what a chart is made of**
— read it and you will know what each of those nouns in the output is talking about.
## What charting does [#what-charting-does]
Charting takes four inputs: the **date** of birth, the **hour** of birth, the **gender**, and a set
of **configuration** switches that pin down school and boundary choices.
The output is a chart of fixed shape: **twelve palaces**, each with its own stem and branch, its own
palace name, and however many **stars** land in it. This chart never changes for the rest of a life;
it is called the **natal chart**.
On top of the natal chart, **horoscopes** are projected forward in time: which palaces and which
mutagens govern this decade (the decadal), this year, this month, this day, this hour. Horoscopes
change with the date you query.
```
date of birth + hour + gender + config
│
├─→ natal chart (twelve palaces + stars + mutagens) ← fixed for life
│
└─→ horoscope (decadal/age/yearly/monthly/daily/hourly) ← varies with the target date
```
## Four concepts to get straight first [#four-concepts-to-get-straight-first]
### Stems and branches [#stems-and-branches]
Ten heavenly stems (jia, yi, bing, ding, wu, ji, geng, xin, ren, gui) and twelve earthly branches
(zi, chou, yin, mao, chen, si, woo, wei, shen, you, xu, hai) are paired off in a cycle of sixty and
used to number years, months, days and hours.
The four stem-branch pairs for a moment of birth are the **four pillars** — the
`geng chen - jia shen - bing woo - geng yin` you see in the chart output.
Each of the twelve palaces also carries a heavenly stem of its own (the palace stem), which is what
the flying mutagens are derived from.
### Soul palace and Body palace [#soul-palace-and-body-palace]
The **Soul palace** is the origin of the whole chart, located from the lunar month and hour of
birth. The twelve palace names are laid out starting from it.
The **Body palace** is not a thirteenth palace: it is one of the twelve, additionally marked, and it
indicates where effort is applied in later life.
### Five Elements class [#five-elements-class]
Derived from the stem and branch of the Soul palace. It is one of five values: water 2nd, wood 3rd,
metal 4th, earth 5th, fire 6th. The number (2 to 6) is used twice further on:
1. **Placing Ziwei**: the lunar day is divided by the class number to fix which palace Ziwei falls
in — and the positions of all fourteen major stars unfold from that one step.
2. **Fixing the starting age of the decadals**: water 2nd starts at nominal age 2, fire 6th at 6,
and each palace governs ten years from there.
**Nominal age** (虚岁) is East Asian age reckoning: you are 1 at birth and gain a year at the turn of
the year, not on your birthday. Every age quoted in a chart — decadal ranges, age-fortune years — is
a nominal age, so it runs one or two ahead of the age on your passport.
### Mutagens [#mutagens]
Each heavenly stem carries an assignment of four mutagens — Wealth (禄), Power (权), Status (科),
Trouble (忌) — pointing at four specific stars. When charting, the birth year's stem stamps its
mutagens onto the corresponding stars; each horoscope level then has mutagens of its own.
This is the main source of dynamic information in Zi Wei Dou Shu. See
[Mutagens and flying stars](/en/docs/guide/concepts/mutagen).
## How the chart is laid out [#how-the-chart-is-laid-out]
A Zi Wei chart is twelve cells arranged in a ring, each cell owned by one **earthly branch**. The
twelve palaces in the output are stored in a fixed order, and **slot 0 is the Yin palace**:
| Slot | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| ------ | --- | --- | ---- | -- | --- | --- | ---- | --- | -- | --- | -- | ---- |
| Branch | yin | mao | chen | si | woo | wei | shen | you | xu | hai | zi | chou |
The slot order follows the **branches**, not the palace names. The Soul palace can land in any one
of the twelve cells, and slot 0 is not necessarily the Soul palace. Look palaces up by name; never
hard-code a slot index.
The twelve **palace names** (Soul, Parents, Spirit, …) are what charting computes: starting from the
cell holding the Soul palace, they are laid out counter-clockwise. So "which cell holds the Soul
palace" differs from chart to chart — which is exactly what the first step of charting decides. See
[The twelve palaces](/en/docs/guide/concepts/palaces).
## The layers of information on a chart [#the-layers-of-information-on-a-chart]
For the type and meaning of every field, see the [Data model](/en/docs/guide/data-model).
## Read on [#read-on]
# Stems, branches and elements (/en/docs/guide/concepts/stems-branches)
What heavenly stems, earthly branches, the five elements and yin/yang each decide, where palace stems come from, and how soul and body stars are looked up.
*For: everyone. Code is at the end of the page*
Charting rests on stems and branches from beginning to end. Once this page makes sense, every
two-character noun on a chart has somewhere to sit.
## Ten stems and twelve branches [#ten-stems-and-twelve-branches]
The ten heavenly stems: jia, yi, bing, ding, wu, ji, geng, xin, ren, gui (甲乙丙丁戊己庚辛壬癸).
The twelve earthly branches: zi, chou, yin, mao, chen, si, woo, wei, shen, you, xu, hai
(子丑寅卯辰巳午未申酉戌亥).
The two are paired off in order and cycle; the least common multiple of 10 and 12 is 60, so one
round is the **sexagenary cycle**. Year, month, day and hour each get a pair, and together they are
the **four pillars**:
```text
geng chen jia shen bing woo geng yin
year month day hour
```
The branch woo (午) is romanized `woo` in this library so that it does not collide with the stem wu
(戊). In keys they are further apart still: the branch is `wuEarthly`, the stem is `wuHeavenly`. When
you write code, use the keys and the collision never arises; the romanized spellings appear only in
prose and in English chart output.
Two different things share a spelling in English. **Yin** (寅) is the third earthly branch — the
Tiger, hour index 2 — and it is a *yang* branch. **Yin** (阴) is the negative pole of yin/yang. This
page uses "the Yin branch" for the former and lower-case "yin" for the latter.
## What each pillar decides [#what-each-pillar-decides]
The four pillars are not parallel decoration; each drives a different part of charting:
| Pillar | Drives |
| --------- | ------------------------------------------------------------------------------------------------------------------------------- |
| **Year** | Birth-year mutagens, soul and body stars, the stems of all twelve palaces, decadal direction, every year-derived star |
| **Month** | Display only; the month itself (not the month pillar) fixes the Soul palace and the month-derived stars such as Zuofu and Youbi |
| **Day** | Display only; the lunar day fixes where Ziwei starts and the day-derived stars such as Santai and Bazuo |
| **Hour** | Display only; the hour itself fixes the Soul palace, the Body palace and the hour-derived stars such as Wenchang and Wenqu |
The reason the year turnover point (lunar New Year or the Beginning of Spring — 立春, the solar term
around 4 February) is a configuration switch at all is that the year's stem and branch pull the most
along with them: change it and the mutagens, the soul and body stars, and the palace stems all move.
See [Config in depth](/en/docs/guide/guides/config).
## Yin and yang [#yin-and-yang]
Stems and branches each carry a polarity, alternating by ordinal parity: jia, bing, wu, geng, ren
are yang and yi, ding, ji, xin, gui are yin; zi, yin, chen, woo, shen, xu are yang and chou, mao,
si, wei, you, hai are yin.
The sexagenary cycle only ever pairs a stem with a branch of matching polarity (jiazi, yichou, …),
so **the stem and the branch of any pair always have the same polarity** — "the polarity of the year
stem" and "the polarity of the year branch" can never disagree. x-iztro always decides from the
**year branch**.
Polarity does exactly one job in charting, but it is a consequential one — **it sets direction**:
| Use | Rule |
| -------------------------- | ------------------------------------------------------------------------ |
| Decadal direction | Same polarity for gender and year branch → forward; different → backward |
| The twelve Changsheng gods | As above |
| The twelve Boshi gods | As above |
Gender has a polarity too: male is yang, female is yin. So the mnemonic "yang man and yin woman go
forward, yin man and yang woman go backward" is about exactly these three things. Age fortune is not
among them — its direction depends on gender alone, see
[The twelve palaces](/en/docs/guide/concepts/palaces#age-fortune).
## The five elements [#the-five-elements]
Metal, wood, water, fire, earth. Every heavenly stem and every earthly branch belongs to one of
them.
The names look alike; the roles do not overlap at all:
* **Five elements**: an attribute of a single stem or branch, reference information.
* **Five Elements class**: derived from the stem and branch of the **Soul palace** — water 2nd,
wood 3rd, metal 4th, earth 5th, fire 6th — and it decides where Ziwei starts and at what age the
decadals begin.
Don't mix them up in a predicate: the first is the `fiveElements` field of a stem or branch, the
second is `five_elements_class` on the astrolabe.
## Clashes [#clashes]
Earthly branches sit opposite one another; six positions apart is a clash: zi–woo, chou–wei,
yin–shen, mao–you, chen–xu, si–hai.
This is precisely where the **opposite palace** comes from — with the twelve palaces in a ring, a
palace and the one six positions away hold clashing branches, which is why the opposite palace has
the most direct influence. Heavenly stems clash too (jia–geng, yi–xin, bing–ren, ding–gui); wu and
ji sit at the centre and clash with nothing.
## Where palace stems and branches come from [#where-palace-stems-and-branches-come-from]
The branches of the twelve palaces are **fixed**: slot 0 is always the Yin palace and the last slot
is always the Chou palace, without exception.
The stems are derived from the year stem by the **Five Tigers rule** (五虎遁): fix the stem of the
Yin palace first, then run the remaining eleven forward in order. (It is called that because it
starts at the Yin palace, the palace of the Tiger.) Take the chart used throughout these pages: the
birth-year stem is geng, and a geng year starts the Yin palace at wu, giving these twelve palaces:
```text
wuyin jimao gengchen xinsi renwoo guiwei jiashen yiyou bingxu dinghai wuzi jichou
```
Palace stems are not ornamental — a palace stem decides which four mutagen stars that palace
**flies out**, and that is the starting point of every flying-star predicate. See
[Mutagens and flying stars](/en/docs/guide/concepts/mutagen).
There is a companion **Five Rats rule** (五鼠遁), which derives the stem of the Zi hour from the day
stem and is used to fix the hour pillar. (Zi is the branch of the Rat, hence the name.)
## Soul star and body star [#soul-star-and-body-star]
Each earthly branch maps to one soul star and one body star, by table lookup. The two are looked up
from different things:
| | Looked up from | When the Soul palace is rearranged |
| ---------------------------- | -------------------------- | ---------------------------------- |
| Soul star (default school) | The **Soul palace branch** | Changes |
| Soul star (Zhongzhou school) | The **birth-year branch** | Unchanged |
| Body star | The **birth-year branch** | Unchanged |
Under `algorithm = zhongzhou` the soul star is looked up from the birth-year branch, so rearranging
the chart onto another Soul palace (`rearranged`, or switching between the heaven / earth / human
plate) no longer moves it. Under the default school the soul star follows the Soul palace. See
[Config in depth](/en/docs/guide/guides/config#algorithm-school-algorithm).
## In code [#in-code]
Stems, branches, elements, the Five Tigers and Five Rats rules, and the soul and body stars are all
exposed as data tables; there is no need to copy the tables yourself.
```python
from x_iztro import data
data.heavenly_stems()["jiaHeavenly"].five_elements # 木 (wood)
data.earthly_branches()["ziEarthly"].yin_yang # 阳 (yang)
data.earthly_branches()["ziEarthly"].crash # wuEarthly (zi clashes with woo)
data.heavenly_stems()["wuHeavenly"].crash # None (wu clashes with nothing)
data.constants().tiger_rule["jiaHeavenly"] # bingHeavenly (a jia year starts the Yin palace at bing)
data.constants().rat_rule["jiaHeavenly"] # jiaHeavenly
data.constants().five_elements_class # {'earth5th': 5, ... 'wood3rd': 3}
zi = data.earthly_branches()["ziEarthly"]
zi.soul # tanlangMaj — soul star
zi.body # huoxingMin — body star
```
`five_elements` and `yin_yang` come back as the raw Chinese characters (`木`, `阳`) whatever the
chart language, because they are table values rather than chart output. Everything else in these
tables is a key, which is language-independent by construction.
The language-independent keys for the four pillars live under `raw_dates.chinese_date` as
`yearly_keys` / `monthly_keys` / `daily_keys` / `hourly_keys`; the top-level `chinese_date` is the
display string.
| Concept | Data table | Constant |
| --------------------------- | ------------------------- | --------------------------------- |
| Heavenly stem info | `data.heavenly_stems()` | — |
| Earthly branch info | `data.earthly_branches()` | — |
| Five Tigers rule | — | `constants().tiger_rule` |
| Five Rats rule | — | `constants().rat_rule` |
| Gender polarity | — | `constants().gender` |
| Five Elements class numbers | — | `constants().five_elements_class` |
Field-by-field notes are on the data table page for
[Rust](/en/docs/rust/data), [Python](/en/docs/python/data) and [Go](/en/docs/go/data).
# The twelve palaces (/en/docs/guide/concepts/palaces)
What each of the twelve palace names covers, how the Body palace and Original palace are decided, what palace stems are for, and how decadals and age fortune hang off palaces.
*For: everyone. Code is at the end of the page*
## The twelve palace names [#the-twelve-palace-names]
The twelve palaces cover twelve domains of a life. Starting from the **Soul palace**, they are laid
out **counter-clockwise** around the chart in a fixed order:
| Order | Name on the chart | Common aliases | Roughly covers |
| ----- | ----------------- | --------------------------------- | ---------------------------------------------------- |
| 1 | soul (命宫) | Life palace, Self palace | Nature and the overall thrust; the core of the chart |
| 2 | parents (父母) | Parents palace, Appearance palace | Parents, elders, superiors, patronage |
| 3 | spirit (福德) | Fortune palace, Blessings palace | Inner life, interests, good fortune |
| 4 | property (田宅) | Property palace, Estate palace | Real estate, the domestic environment |
| 5 | career (官禄) | Career palace, Officials palace | Career, occupation, study |
| 6 | friends (仆役) | Friends palace, Servants palace | Friends, colleagues, subordinates |
| 7 | surface (迁移) | Travel palace, Migration palace | Going out, movement, encounters with the outside |
| 8 | health (疾厄) | Health palace, Illness palace | Body, illness |
| 9 | wealth (财帛) | Wealth palace, Finance palace | Money, income |
| 10 | children (子女) | Children palace | Children, juniors, creativity |
| 11 | spouse (夫妻) | Spouse palace, Marriage palace | Spouse, intimate relationships |
| 12 | siblings (兄弟) | Siblings palace | Brothers and sisters, peers |
In an English chart the palace name is exactly the single word in the first column — `soul`,
`friends`, `surface` — with no "palace" suffix. (In a Chinese chart the same is true, with 命宫 the
one exception that keeps its 宫.) The aliases column records how different traditions talk; those
strings never appear in the output. For predicates, use the keys — see the end of this page.
"The order of the palace names" and "where a palace sits on the chart" are two different things. The
name order is always the cycle in the table above, but which branch cell holds the Soul palace is
computed, and the other eleven follow it. So a slot in the twelve-palace array corresponds to a
branch, not to a position in the name order.
## How the Soul palace is located [#how-the-soul-palace-is-located]
The Soul palace is located from the **lunar month** and the **hour** of birth together: start at the
Yin palace as the first lunar month and count forward to the month of birth, then from that palace
count backward from the Zi hour to the hour of birth. This position sets the shape of the whole
chart and is the starting point for the Five Elements class and the decadals.
## The Body palace [#the-body-palace]
The Body palace uses the same month and hour data but counts the hour forward, and always lands on
one of the twelve palaces. It is not an independent thirteenth palace, just a flag added to one of
the twelve.
Traditionally the Soul palace speaks to innate nature, the Body palace to acquired effort and the
direction of the second half of life.
## The Original palace [#the-original-palace]
The Original palace is the palace whose **stem equals the birth-year stem**, excluding the Zi and
Chou palaces. It marks where the chart "comes from" and is an important starting point in the
flying-star school.
A chart **always has exactly one** Original palace. The reason lies in how palace stems are laid
out:
The twelve palace stems run forward from the Yin palace by the Five Tigers rule — ten stems across
twelve palaces — so only the first two (Yin, Mao) repeat at the end (Zi, Chou). That makes exactly
two palaces whose stem equals the year stem, and they are necessarily a pair of "Yin or Mao" with
"Zi or Chou". Excluding Zi and Chou leaves exactly one.
## Palace stems and branches [#palace-stems-and-branches]
Every palace has a stem-branch pair of its own:
* **Palace branch**: fixed by the palace's slot on the chart (slot 0 is Yin, see
[What a chart is made of](/en/docs/guide/concepts#how-the-chart-is-laid-out)), unchanging for
life.
* **Palace stem**: derived from the birth-year stem by the Five Tigers rule.
Palace stems exist for **flying mutagens**: a palace's stem decides which four mutagen stars it
"flies out", and relationships between palaces are judged from that. See
[Mutagens and flying stars](/en/docs/guide/concepts/mutagen#flying-stars).
## Decadals [#decadals]
A decadal is a ten-year stretch of fortune, one per palace. The starting nominal age is set by the
[Five Elements class](/en/docs/guide/concepts#five-elements-class):
| Five Elements class | Class number | Starting nominal age | First decadal range |
| ------------------- | ------------ | -------------------- | ------------------- |
| water 2nd | 2 | 2 | 2–11 |
| wood 3rd | 3 | 3 | 3–12 |
| metal 4th | 4 | 4 | 4–13 |
| earth 5th | 5 | 5 | 5–14 |
| fire 6th | 6 | 6 | 6–15 |
Direction is decided by **gender polarity against year-branch polarity**: same → forward, different
→ backward, which is the mnemonic "yang man and yin woman go forward, yin man and yang woman go
backward". (The year stem and the year branch always share a polarity, so phrasing the rule in terms
of the year stem says the same thing — see
[Yin and yang](/en/docs/guide/concepts/stems-branches#yin-and-yang).)
All ages here are **nominal ages** (虚岁), the East Asian reckoning in which you are 1 at birth and
gain a year at the turn of the year rather than on your birthday.
The years between birth and the starting nominal age belong to no decadal. That stretch is derived
as the **childhood scope**, returned by the horoscope API. See
[Horoscopes](/en/docs/guide/concepts/horoscope#the-childhood-scope).
## Age fortune [#age-fortune]
Age fortune is a one-year-per-step track running in parallel with the decadals: one palace per year,
so a given palace comes round every twelve years.
Its rules **differ** from the decadals — the two are independent:
* **The origin** is set by the trine group of the year branch (yin/woo/xu years start at the Chen
palace, shen/zi/chen years at Xu, si/you/chou years at Wei, hai/mao/wei years at Chou).
* **Direction depends on gender alone**: male forward, female backward, regardless of year-branch
polarity.
## In code [#in-code]
Look a palace up by name, then ask what is in it:
```rust
let soul = astrolabe.palace(Palace::Soul).unwrap();
soul.has(&[StarKey::ZiweiMaj]); // holds all of these stars?
soul.has_one_of(&[StarKey::ZiweiMaj, StarKey::TianfuMaj]); // holds any one of them?
soul.has_mutagen(Mutagen::Lu); // holds a Wealth mutagen?
soul.is_empty(); // empty palace (no major stars)?
soul.is_body_palace; // is it the Body palace
soul.is_original_palace; // is it the Original palace
soul.decadal.range; // the decadal range this palace governs, (3, 12)
soul.ages; // nominal ages at which age fortune passes through
```
```python
from x_iztro.enums import PalaceName, MajorStar, Mutagen
soul = chart.palace(PalaceName.SOUL)
soul.has([MajorStar.ZIWEI])
soul.has_one_of([MajorStar.ZIWEI, MajorStar.TIANFU])
soul.has_mutagen(Mutagen.LU)
soul.is_empty()
soul.is_body_palace
soul.is_original_palace
soul.decadal.range # (3, 12)
soul.ages # [5, 17, 29, 41, 53, 65, 77, 89, 101, 113]
```
```go
soul := chart.Palace(iztro.PalaceSoul)
soul.Has(iztro.StarZiweiMaj)
soul.HasOneOf(iztro.StarZiweiMaj, iztro.StarTianfuMaj)
soul.HasMutagen(iztro.MutagenLu)
soul.IsEmpty()
soul.IsBodyPalace
soul.IsOriginalPalace
soul.Decadal.Range
soul.Ages
```
"Empty palace" means no major stars, not that the palace is bare — minor and adjective stars are
usually still there. An empty palace is read by borrowing the stars of its opposite palace, which is
one of the reasons [surrounded palaces](/en/docs/guide/concepts/surrounded) exist.
### Language-independent keys for palace names [#language-independent-keys-for-palace-names]
Predicate on keys; never match the palace name text — switch the chart language and a branch that
matches text fails silently.
| Palace | Key |
| -------- | ---------------- |
| soul | `soulPalace` |
| parents | `parentsPalace` |
| spirit | `spiritPalace` |
| property | `propertyPalace` |
| career | `careerPalace` |
| friends | `friendsPalace` |
| surface | `surfacePalace` |
| health | `healthPalace` |
| wealth | `wealthPalace` |
| children | `childrenPalace` |
| spouse | `spousePalace` |
| siblings | `siblingsPalace` |
There are two further keys that can only be used for lookup and never appear as a palace name:
`bodyPalace` (the Body palace) and `originalPalace` (the Original palace). Pass either to the palace
lookup method to get the palace carrying that flag — on this chart they resolve to `career` and
`spouse` respectively.
The values of Python's `PalaceName` enum and Go's `Palace*` constants are exactly the keys above,
and they hold on a chart in any chart language. See
[The key contract](/en/docs/guide/guides/keys).
# Stars (/en/docs/guide/concepts/stars)
What each of the three star groups holds, the eight star types, how to read brightness and mutagen marks, the four groups of twelve gods, and a star-name table.
*For: everyone. Code and the key tables are at the end of the page*
Stars on a palace are stored in three groups, and four further groups of "twelve gods" hang off each
palace as single values.
## Major stars [#major-stars]
The fourteen major stars are the skeleton of a reading. They are placed by the rules of two series,
Ziwei's and Tianfu's. Some palaces end up with two of them, some with none at all — the latter is an
**empty palace**.
| Ziwei series (six) | Tianfu series (eight) |
| ------------------ | --------------------- |
| Ziwei | Tianfu |
| Tianji | Taiyin |
| Taiyang | Tanlang |
| Wuqu | Jumen |
| Tiantong | Tianxiang |
| Lianzhen | Tianliang |
| | Qisha |
| | Pojun |
The empty-palace predicate is asking whether exactly this group is empty.
## Minor stars [#minor-stars]
Fourteen minor stars, in four classes by character:
| Class | Members |
| ------------------------- | -------------------------------------------------- |
| Soft (the six auspicious) | Zuofu, Youbi, Wenchang, Wenqu, Tiankui, Tianyue |
| Tough (the six malefics) | Qingyang, Tuoluo, Huoxing, Lingxing, Dikong, Dijie |
| Lucun | Lucun |
| Tianma | Tianma |
In the traditional division they are neither purely auspicious nor purely malefic: Lucun governs
wealth but fears the void stars, Tianma governs movement and change but wants Lucun in sight.
Predicates often need to handle them separately from the six soft and six tough stars, so each takes
a class of its own — filter by class and you never have to hard-code a star name.
## Adjective stars [#adjective-stars]
Dozens of auxiliary stars, grouped by origin into year-stem, year-branch, month, day and hour
families, each placed by a different rule. They split into three types:
| Type | Count | Members |
| ------------------------- | ----- | -------------------------------------------------------------------------- |
| Peach-blossom stars | 4 | Hongluan `hongluan`, Tianxi `tianxi`, Tianyao `tianyao`, Xianchi `xianchi` |
| Helper stars | 2 | Jieshen `jieshen`, Nianjie `nianjie` |
| All other adjective stars | 32 | See below |
The 32 remaining adjective stars of the default school, grouped by character:
| Group | Members |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Rank and support | Santai `santai`, Bazuo `bazuo`, Enguang `engguang`, Tiangui `tiangui`, Taifu `taifu`, Fenggao `fenggao`, Longchi `longchi`, Fengge `fengge`, Tianguan `tianguan`, Tianfu `tianfu` |
| Talent and shelter | Tiancai `tiancai`, Tianshou `tianshou`, Tianwu `tianwu`, Tianchu `tianchu`, Tiande `tiande`, Yuede `yuede`, Huagai `huagai` |
| The void family | Tiankong `tiankong`, Xunkong `xunkong`, Jielu `jielu`, Kongwang `kongwang` |
| Punishment and isolation | Tianxing `tianxing`, Guchen `guchen`, Guasu `guasu`, Posui `posui`, Feilian `feilian`, Yinsha `yinsha`, Tianku `tianku`, Tianxu `tianxu`, Tianyue `tianyue` |
| Injury and messenger | Tianshang `tianshang`, Tianshi `tianshi` |
"Peach blossom" (桃花) is the traditional metaphor for romance and attraction. These four stars bear
on romance, appeal to the opposite sex and personal magnetism, and they are the first group a reader
looks at for relationships. Within the four, Hongluan and Tianxi lean towards committed romance and
celebration; Tianyao and Xianchi lean towards desire and socialising.
The default school places **Jielu** (截路) and **Kongwang** (空亡) as two separate stars; do not
collapse them into one "Jiekong". The Zhongzhou school (`algorithm = zhongzhou`) instead places
**Jiekong** (截空), **Jiesha** (劫煞), **Dahao** (大耗) and **Longde** (龙德), and does not place
Jielu or Kongwang — taking the adjective-star total from 32 to 34. See
[Config in depth](/en/docs/guide/guides/config#algorithm-school-algorithm).
When you ask whether a palace holds a given mutagen, only the major and minor stars are examined —
**adjective stars are not** — matching iztro's behaviour.
## Stars come in eight types [#stars-come-in-eight-types]
The three groups above together cover every type exactly once:
| Type key | Which group it appears in | In one line |
| ----------- | ------------------------- | --------------------------------------------------- |
| `major` | Major stars | The fourteen major stars; the skeleton of the chart |
| `soft` | Minor stars | The six auspicious |
| `tough` | Minor stars | The six malefics |
| `lucun` | Minor stars | Lucun, a class of its own |
| `tianma` | Minor stars | Tianma, a class of its own |
| `flower` | Adjective stars | Peach-blossom stars |
| `helper` | Adjective stars | Helper stars |
| `adjective` | Adjective stars | Everything else |
For a question like "does this palace see a malefic?", filtering by type is the sturdy way — star
names change with the chart language, types do not.
## Brightness [#brightness]
Brightness describes how strong a star is in the branch position it landed on, on a seven-step
scale:
| en-US output | zh-CN output | Full name | Strength |
| ------------ | ------------ | ----------------------------- | --------- |
| `[+3]` | 庙 | miaowang (temple-flourishing) | Strongest |
| `[+2]` | 旺 | wangxiang (flourishing) | |
| `[+1]` | 得 | dedi (well-placed) | |
| `[0]` | 利 | liyi (advantaged) | |
| `[-1]` | 平 | pinghe (neutral) | Neutral |
| `[-2]` | 不 | budedi (poorly placed) | |
| `[-3]` | 陷 | luoxian (fallen) | Weakest |
The same star has different brightness in different palaces, fixed by a star-against-branch lookup
table. Adjective stars usually have no brightness at all.
The English, Korean and other non-Chinese vocabularies have no brightness translations, so the
output is the mark `[+3]` (miaowang) through `[-3]` (luoxian) shown above. Predicate on
`brightnessKey` (`miao`, `wang`, `de`, `li`, `ping`, `bu`, `xian`), never on the text — see
[Multilingual output](/en/docs/guide/guides/i18n).
## The four groups of twelve gods [#the-four-groups-of-twelve-gods]
Besides the three star groups, every palace carries four **single-valued** fields, each coming from
its own cycle of twelve gods. Each group's twelve members fill the twelve palaces, exactly one per
palace.
The Python enums and Go constants for these four are listed on the
[enum listings](/en/docs/python/data#enum-listings).
### The twelve Changsheng gods [#the-twelve-changsheng-gods]
Placed from the **Five Elements class together with gender and year-branch polarity**. They describe
the twelve stages of a thing coming into being, declining and starting over.
| Key | Chinese | en-US |
| ------------ | ------- | ----------- |
| `changsheng` | 长生 | born |
| `muyu` | 沐浴 | infancy |
| `guandai` | 冠带 | adolescence |
| `linguan` | 临官 | adulthood |
| `diwang` | 帝旺 | prime |
| `shuai` | 衰 | weak |
| `bing` | 病 | sick |
| `si` | 死 | dead |
| `mu` | 墓 | buried |
| `jue` | 绝 | dissipated |
| `tai` | 胎 | embryo |
| `yang` | 养 | molding |
### The twelve Boshi gods [#the-twelve-boshi-gods]
Placed from **Lucun's position together with gender and year-branch polarity**. Weighted towards
talent, wealth and disputes.
| Key | Chinese | en-US |
| ---------- | ------- | ---------- |
| `boshi` | 博士 | doctor |
| `lishi` | 力士 | sumo |
| `qinglong` | 青龙 | dragon |
| `xiaohao` | 小耗 | consumer |
| `jiangjun` | 将军 | general |
| `zhoushu` | 奏书 | book |
| `faylian` | 飞廉 | gossip |
| `xishen` | 喜神 | happiness |
| `bingfu` | 病符 | illness |
| `dahao` | 大耗 | wastrel |
| `fubing` | 伏兵 | ambush |
| `guanfu` | 官府 | government |
### The twelve Sui-qian gods [#the-twelve-sui-qian-gods]
**Sui-qian** (岁前, "ahead of the year") is placed from the **year branch**, always running forward.
Weighted towards the auspicious and inauspicious events of a single year.
| Key | Chinese | en-US |
| --------- | ------- | --------- |
| `suijian` | 岁建 | initial |
| `huiqi` | 晦气 | unlucky |
| `sangmen` | 丧门 | downcast |
| `guansuo` | 贯索 | tied |
| `gwanfu` | 官符 | official |
| `xiaohao` | 小耗 | consumer |
| `dahao` | 大耗 | wastrel |
| `longde` | 龙德 | virtuous |
| `baihu` | 白虎 | sinister |
| `tiande` | 天德 | blessed |
| `diaoke` | 吊客 | sorrowing |
| `bingfu` | 病符 | illness |
The Zhongzhou school replaces **Dahao** in this group with **Suipo** (`suipo`, 岁破), which shares the
English rendering `wastrel`.
### The twelve Jiang-qian gods [#the-twelve-jiang-qian-gods]
**Jiang-qian** (将前, "ahead of the general") is placed from the **year branch's trine group**, always
running forward. Weighted towards movement, travel and obstruction by other people.
| Key | Chinese | en-US |
| ----------- | ------- | ---------- |
| `jiangxing` | 将星 | capable |
| `panan` | 攀鞍 | admired |
| `suiyi` | 岁驿 | varied |
| `xiishen` | 息神 | listless |
| `huagai` | 华盖 | religious |
| `jiesha` | 劫煞 | robbed |
| `zhaisha` | 灾煞 | disastery |
| `tiansha` | 天煞 | condemned |
| `zhibei` | 指背 | insidious |
| `xianchi` | 咸池 | passionate |
| `yuesha` | 月煞 | hapless |
| `wangshen` | 亡神 | perished |
`faylian` (飞廉), `gwanfu` (官符), `xiishen` (息神) and `zhaisha` (灾煞) do not match their pinyin.
They keep iztro's original vocabulary keys, which exist to separate them from the homophonous
`feilian` (蜚廉, an adjective star), `guanfu` (官府, a Boshi god) and `xishen` (喜神, a Boshi god).
Copy the keys; do not spell them out from pinyin yourself.
`disastery` and `considery` are not words. They come verbatim from iztro's en-US vocabulary and
x-iztro reproduces them exactly, because matching iztro field for field outranks fixing its
spelling. Treat `name` as display text only and never parse it.
Horoscope levels carry their own Sui-qian and Jiang-qian gods, placed afresh from that level's year
branch. They are a different thing from the four groups on the natal chart. See
[Horoscopes](/en/docs/guide/concepts/horoscope).
## In code [#in-code]
### Walking the stars of one palace [#walking-the-stars-of-one-palace]
```python
chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US")
soul = chart.palace("soulPalace")
for s in soul.major_stars + soul.minor_stars + soul.adjective_stars:
print(s.key, s.name, s.type, s.brightness, s.mutagen)
```
```text
ziweiMaj emperor major [+3] None
wenquMin artist soft [-3] None
fengge refined adjective None None
tianfu lucky adjective None None
jielu intercepted adjective None None
feilian instigated adjective None None
nianjie considery(Y) helper None None
```
`nianjie`'s en-US rendering is literally `considery(Y)`, and it appears that way on the **natal**
chart too, where `scope` is `origin`. The suffix is not a scope marker — iztro's vocabulary bakes it
into the string to distinguish 年解 (`nianjie`) from 解神 (`jieshen`, `considery`), and x-iztro copies
the vocabulary verbatim. One more reason to branch on `key`, never on `name`.
Fields on a single star:
| Field | Meaning |
| ------------------------------ | ----------------------------------------------------------------- |
| `name` | Star name, translated into the chart language |
| `key` | Language-independent key, e.g. `ziweiMaj` |
| `type` | One of the eight types |
| `scope` | Which level it belongs to: `origin` natal, `decadal`, `yearly`, … |
| `brightness` / `brightnessKey` | Brightness; only some stars have one |
| `mutagen` / `mutagenKey` | Mutagen mark; only stars that were mutated have one |
### The four groups of twelve gods [#the-four-groups-of-twelve-gods-1]
```python
for p in chart.palaces:
print(p.name, p.changsheng12, p.boshi12, p.suiqian12, p.jiangqian12)
```
Each field also has a `*_key` variant (`changsheng12_key` and so on); predicate on that.
### Finding where a star is [#finding-where-a-star-is]
```rust
if let Some(star) = astrolabe.star(StarKey::ZiweiMaj) {
// StarRef derefs to Star, and can also give the palace it sits in
println!("{:?} {:?}", star.brightness, star.palace().name);
}
```
```python
from x_iztro.enums import MajorStar
star = chart.star(MajorStar.ZIWEI) # the star alone
star, palace = chart.star_in_palace(MajorStar.ZIWEI) # the star and its palace
```
```go
star, palace := chart.Star(iztro.StarZiweiMaj)
```
The lookup walks all three groups, so major, minor and adjective stars are all findable; a miss
returns an empty value.
### Star-name table [#star-name-table]
The same star reads differently in each chart language while the key stays put. The English
vocabulary comes from iztro's own word list: it is interpretive rather than transliterated, and it
is *not* the rendering conventional in English-language Zi Wei writing. It is for display only —
always predicate on the key.
| Pinyin | Key | zh-CN | en-US |
| --------- | -------------- | ----- | --------- |
| Ziwei | `ziweiMaj` | 紫微 | emperor |
| Tianji | `tianjiMaj` | 天机 | advisor |
| Taiyang | `taiyangMaj` | 太阳 | sun |
| Wuqu | `wuquMaj` | 武曲 | general |
| Tiantong | `tiantongMaj` | 天同 | fortunate |
| Lianzhen | `lianzhenMaj` | 廉贞 | judge |
| Tianfu | `tianfuMaj` | 天府 | empress |
| Taiyin | `taiyinMaj` | 太阴 | moon |
| Tanlang | `tanlangMaj` | 贪狼 | wolf |
| Jumen | `jumenMaj` | 巨门 | advocator |
| Tianxiang | `tianxiangMaj` | 天相 | minister |
| Tianliang | `tianliangMaj` | 天梁 | sage |
| Qisha | `qishaMaj` | 七杀 | marshal |
| Pojun | `pojunMaj` | 破军 | rebel |
| Zuofu | `zuofuMin` | 左辅 | officer |
| Youbi | `youbiMin` | 右弼 | helper |
| Wenchang | `wenchangMin` | 文昌 | scholar |
| Wenqu | `wenquMin` | 文曲 | artist |
| Tiankui | `tiankuiMin` | 天魁 | assistant |
| Tianyue | `tianyueMin` | 天钺 | aide |
| Qingyang | `qingyangMin` | 擎羊 | driven |
| Tuoluo | `tuoluoMin` | 陀罗 | tangled |
| Huoxing | `huoxingMin` | 火星 | impulsive |
| Lingxing | `lingxingMin` | 铃星 | spark |
| Dikong | `dikongMin` | 地空 | ideologue |
| Dijie | `dijieMin` | 地劫 | fickle |
| Lucun | `lucunMin` | 禄存 | money |
| Tianma | `tianmaMin` | 天马 | horse |
Note the collision `helper`: it is the en-US name of the star Youbi (右弼) *and* the name of the
`helper` star type. They are unrelated — one is a `name`, the other a `type`.
Keys for the adjective stars and the twelve gods are in the sections above. To convert between any
key and any language, use the translation and reverse-lookup functions — see
[Multilingual output](/en/docs/guide/guides/i18n#converting-between-keys-and-names).
# Mutagens and flying stars (/en/docs/guide/concepts/mutagen)
Where the four mutagens come from, the full ten-stem mutagen table, and how self-mutagens and flying predicates work.
*For: everyone. Code and the full method table are at the end of the page*
Mutagens carry the most important dynamic information in Zi Wei Dou Shu. On a single chart they wire
the static stars into a directed web of relationships.
## The four mutagens [#the-four-mutagens]
| Mutagen | Key | en-US mark | Usually read as |
| ----------- | ----------- | ---------- | ------------------------------------- |
| Hua Lu 化禄 | `sihuaLu` | `A` | Flow, gain, the origin of an affinity |
| Hua Quan 化权 | `sihuaQuan` | `B` | Control, expansion, force |
| Hua Ke 化科 | `sihuaKe` | `C` | Reputation, benefactors, mitigation |
| Hua Ji 化忌 | `sihuaJi` | `D` | Obstruction, fixation, volatility |
The en-US vocabulary has no words for the four mutagens, so `mutagen` comes back as `A`, `B`, `C`,
`D` in the order Lu, Quan, Ke, Ji. Predicate on `mutagenKey`, never on the letter.
## Mutagens come from the heavenly stem [#mutagens-come-from-the-heavenly-stem]
Each heavenly stem assigns four fixed stars to Lu, Quan, Ke and Ji respectively. It is a lookup
table:
| Stem | Hua Lu | Hua Quan | Hua Ke | Hua Ji |
| ------ | --------- | --------- | --------- | -------- |
| jia 甲 | Lianzhen | Pojun | Wuqu | Taiyang |
| yi 乙 | Tianji | Tianliang | Ziwei | Taiyin |
| bing 丙 | Tiantong | Tianji | Wenchang | Lianzhen |
| ding 丁 | Taiyin | Tiantong | Tianji | Jumen |
| wu 戊 | Tanlang | Taiyin | Youbi | Tianji |
| ji 己 | Wuqu | Tanlang | Tianliang | Wenqu |
| geng 庚 | Taiyang | Wuqu | Taiyin | Tiantong |
| xin 辛 | Jumen | Taiyang | Wenqu | Wenchang |
| ren 壬 | Tianliang | Ziwei | Zuofu | Wuqu |
| gui 癸 | Pojun | Jumen | Taiyin | Tanlang |
Traditions differ on which star takes Hua Ke under the geng stem — Taiyin, Tianfu and Tiantong have
all been argued for. x-iztro uses **Taiyin**, matching JS iztro.
The `algorithm` switch **does not change the mutagen table**: the Zhongzhou school and the default
school use the same one. To adopt a different reading, replace the table through Config's custom
mutagen tables, which swap a stem's four assignments wholesale — see
[Config in depth](/en/docs/guide/guides/config#custom-mutagen-and-brightness-tables).
## Natal mutagens [#natal-mutagens]
When charting, the **birth-year stem** is looked up in the table above and the mutagen marks are
stamped onto the corresponding stars. A chart carries exactly four natal mutagens — the mutable
stars include Wenchang, Wenqu, Zuofu and Youbi alongside the fourteen major stars, and all four of
those minor stars are always present, so the count never falls short.
## Horoscope mutagens [#horoscope-mutagens]
Beyond the natal set, every horoscope level has mutagens of its own: the decadal uses its decadal
palace stem, the yearly level uses the year's stem, and so on. They stack onto the same chart and
are the main handle for reading a horoscope.
The four returned stars are always in the order **Lu, Quan, Ke, Ji**.
## Flying stars [#flying-stars]
Palaces have heavenly stems too. Look a palace stem up in the mutagen table and you get the four
stars that palace **flies out**; then see which palaces those four stars sit in. That is a **flying
star**, and it is how the working relationships between palaces are described.
## Self-mutagens [#self-mutagens]
When a mutagen star flown out by a palace's own stem lands inside that same palace, it is a
**self-mutagen**.
In a reading, a self-mutagen means force being spent or leaking inside the palace itself — a
different character from flying into another palace.
## In code [#in-code]
### Reading the natal mutagens [#reading-the-natal-mutagens]
```python
chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US")
for p in chart.palaces:
for s in p.major_stars + p.minor_stars:
if s.mutagen:
print(f"{p.name} — {s.name} takes {s.mutagen}")
```
```text
wealth — general takes B
children — sun takes A
friends — moon takes C
health — fortunate takes D
```
### Reading horoscope mutagens [#reading-horoscope-mutagens]
```python
h = chart.horoscope("2024-10-1", 0)
print(h.decadal.mutagen) # the four stars of the decadal mutagens
print(h.yearly.mutagen) # the four stars of the yearly mutagens
```
```text
['sun', 'general', 'moon', 'fortunate']
['judge', 'rebel', 'general', 'sun']
```
For predicates use the key form `h.yearly.mutagen_star_keys` — the mutated stars' star keys, independent of translation.
### Asking whether a palace holds a mutagen [#asking-whether-a-palace-holds-a-mutagen]
```python
soul.has_mutagen(Mutagen.LU)
soul.not_have_mutagen(Mutagen.JI)
```
### Flying-star predicates [#flying-star-predicates]
```rust
let wealth = astrolabe.palace(Palace::Wealth).unwrap();
// does the Ji flown out by the Wealth palace's stem land in the Soul palace?
wealth.flies_to(Palace::Soul, &[Mutagen::Ji]);
```
```python
soul.flies_one_of_to(PalaceName.WEALTH, [Mutagen.LU, Mutagen.QUAN])
soul.not_fly_to(0, Mutagen.JI)
places = soul.mutaged_places() # which palace each of Lu/Quan/Ke/Ji flew into; length 4
```
### Self-mutagen predicates [#self-mutagen-predicates]
```rust
soul.self_mutaged(&[Mutagen::Lu]); // does the Soul palace self-mutate Lu
soul.self_mutaged_one_of(&[]); // any self-mutagen at all
soul.not_self_mutaged(&[]); // none of the four
```
Passing an empty list to `self_mutaged_one_of` or `not_self_mutaged` checks all four mutagens; pass
a subset to check only those.
### The full method list [#the-full-method-list]
| Method | Does |
| ----------------------------- | ---------------------------------------------------------------- |
| `has_mutagen(m)` | Does this palace hold the given mutagen |
| `not_have_mutagen(m)` | Does this palace lack the given mutagen |
| `mutagen_stars(ms)` | The stars this palace's stem puts on the given mutagen positions |
| `flies_to(target, ms)` | Do **all** the given mutagen stars land in the target palace |
| `flies_one_of_to(target, ms)` | Does **any one** of them land there |
| `not_fly_to(target, ms)` | Does **none** of them land there |
| `self_mutaged(ms)` | Does this palace self-mutate the given mutagens |
| `self_mutaged_one_of(ms?)` | Does it have any self-mutagen |
| `not_self_mutaged(ms?)` | Does it lack all the given self-mutagens |
| `mutaged_places()` | The palaces the four mutagen stars sit in |
A palace also carries a `mutagen_star_keys` field, giving directly the keys of the four stars its
stem mutates, in the order Lu, Quan, Ke, Ji; it follows a custom mutagen table when one is set.
This reproduces iztro's behaviour and is consistent across all three programming languages: with an
empty mutagen list, `flies_to` returns `false` while `flies_one_of_to` and `not_fly_to` return
`true`.
For mutagen predicates across the surrounded palaces, see
[Surrounded palaces](/en/docs/guide/concepts/surrounded).
# Surrounded palaces (/en/docs/guide/concepts/surrounded)
Why a palace is never read alone, which four palaces make up the surrounded set, and how to get them in each of the three programming languages.
*For: everyone. Code and the predicate table are at the end of the page*
## Why it exists [#why-it-exists]
Reading a palace on its own loses half the information. The convention in Zi Wei Dou Shu is that any
palace is read together with its **opposite palace** and its two **trine palaces**. Those four
together are the **surrounded palaces** (三方四正).
The most immediate reason is the empty palace — when a palace holds no major star, tradition says to
"borrow the stars of the opposite palace". Even when it is not empty, malefics and mutagens anywhere
in the surrounded set bear on the reading of the palace at the centre.
## Which four palaces [#which-four-palaces]
Taking the palace in question as the reference slot, the other three are at fixed offsets:
| Member | Slot | Notes |
| --------------- | ------- | ------------------------------------------ |
| Target | `i` | The palace being read |
| Opposite | `i + 6` | Directly across; the most direct influence |
| Career position | `i + 4` | One of the two trines |
| Wealth position | `i + 8` | One of the two trines |
Slots are taken modulo 12. On the chart these four positions form a triangle plus a diagonal: 三方
("three directions") is the trine of three palaces, 四正 ("four squared") is those plus the opposite,
four in all.
```
i+4 (career position)
/ \
/ \
i ──────── i+6 (opposite)
\ /
\ /
i+8 (wealth position)
```
"Career position" and "wealth position" are names **relative to the target palace**, not the Career
and Wealth palaces on the chart. They coincide only when the target is the Soul palace; for any
other target, only the positional relationship is the same.
## In code [#in-code]
All three programming languages accept either a palace index or a palace name:
```rust
let sp = astrolabe.surrounded_palaces(Palace::Soul).unwrap();
println!("{:?}", sp.opposite.name);
```
```python
sp = chart.surrounded_palaces(PalaceName.SOUL)
sp = chart.surrounded_palaces(soul.index)
```
```go
sp := chart.SurroundedPalaces(iztro.PalaceSoul) // by name
sp = chart.SurroundedPalacesByIndex(soul.Index) // by index
```
The four members are `target`, `opposite`, `career` (the career position) and `wealth` (the wealth
position). On the chart used throughout these pages, taking the Soul palace as the target, they
resolve to `soul`, `surface`, `career` and `wealth`.
### Predicates [#predicates]
The surrounded-palace predicates share names with the single-palace ones, but check the union of the
four palaces. They are identical across the three programming languages:
| Method | Does |
| --------------------- | --------------------------------------------------------- |
| `have(stars)` | Do the four palaces together hold **all** the given stars |
| `have_one_of(stars)` | Do they hold **any one** of them |
| `not_have(stars)` | Do they hold **none** of them |
| `have_mutagen(m)` | Does any of the four carry the given mutagen |
| `not_have_mutagen(m)` | Do none of the four carry it |
```go
sp := chart.SurroundedPalaces(iztro.PalaceSoul)
sp.Have(iztro.StarTianfuMaj) // Tianfu in the surrounded set
sp.HaveOneOf(iztro.StarQingyangMin, iztro.StarTuoluoMin) // Qingyang or Tuoluo in sight
sp.NotHaveMutagen(iztro.MutagenJi) // no Hua Ji in sight
```
The astrolabe also offers three shortcuts that skip fetching the surrounded set first:
```python
chart.is_surrounded(PalaceName.SOUL, [MajorStar.TIANFU])
chart.is_surrounded_one_of(PalaceName.SOUL, [MinorStar.QINGYANG, MinorStar.TUOLUO])
chart.not_surrounded(PalaceName.SOUL, [MinorStar.HUOXING])
```
`have` requires **every** star in the list to be present; `have_one_of` requires only one. A
question like "is a malefic in sight?" almost always wants `have_one_of`.
### A worked example [#a-worked-example]
Testing whether the Soul palace is "flanked by auspicious stars and clear of malefics":
```python
from x_iztro.enums import PalaceName, MinorStar
chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US")
sp = chart.surrounded_palaces(PalaceName.SOUL)
lucky = sp.have_one_of([MinorStar.ZUOFU, MinorStar.YOUBI,
MinorStar.WENCHANG, MinorStar.WENQU])
clean = sp.not_have([MinorStar.QINGYANG, MinorStar.TUOLUO,
MinorStar.HUOXING, MinorStar.LINGXING])
print(lucky, clean, lucky and clean)
```
```text
True False False
```
This chart has Wenchang and Wenqu in the surrounded set, but it also sees some of Qingyang, Tuoluo,
Huoxing and Lingxing, so the pattern does not hold.
Star classes and keys are on [Stars](/en/docs/guide/concepts/stars).
# Horoscopes (/en/docs/guide/concepts/horoscope)
What each of the six time scopes computes, why the palace name on a cell changes, how the childhood scope is derived, and where flowing stars and yearly twelve gods come from.
*For: everyone. Code is at the end of the page*
The natal chart never changes. A **horoscope** is the dynamic information you get by laying time on
top of it. Give a target date and x-iztro returns all six scopes at once.
## The same cell, a different palace name [#the-same-cell-a-different-palace-name]
This is the part of horoscopes people trip over most, so it goes first:
Reading a horoscope means treating the palace the horoscope landed on as "the Soul palace for this
step", with the other eleven re-laid-out around it. So **a cell that is the Wealth palace on the
natal chart may be the Soul palace within some decadal**.
Every horoscope scope hands back its own re-laid-out list of twelve palace names, ordered by slot on
the chart. The natal palace names are untouched; the two sets coexist — when reading results, be
clear which one you are holding.
## The six scopes [#the-six-scopes]
| Scope | Period | Derived from |
| ----------- | --------------- | --------------------------------------------------------------------------------------------------- |
| Decadal | Ten years | The Five Elements class starting nominal age, plus a direction from gender and year-branch polarity |
| Age fortune | One year | Origin from the year branch's trine group, direction from gender |
| Yearly | One year | The lunar year the target date falls in |
| Monthly | One month | The lunar month the target date falls in |
| Daily | One day | The target date |
| Hourly | One double-hour | The target hour index |
Decadal and age fortune both "advance by age", but under completely different rules, and each runs
its own course; yearly through hourly "advance by calendar". The two tracks run in parallel and
together make up one query's result.
**Nominal age** (虚岁) is East Asian age reckoning: you are 1 at birth and gain a year at the turn of
the year, not on your birthday.
## What each scope carries [#what-each-scope-carries]
Apart from age fortune and the yearly scope, every level has the same structure:
| Field | Meaning |
| ---------------------------------- | ------------------------------------------------------------------------------------ |
| `index` | Which slot on the chart this horoscope landed on (0–11; slot 0 is the Yin palace) |
| `name` | The scope name, translated into the chart language (`decadal`, `yearly`, …) |
| `heavenly_stem` / `earthly_branch` | This scope's stem and branch |
| `palace_names` | The twelve palace names re-laid-out with this scope's slot as the Soul palace |
| `mutagen` | The mutagen stars raised by this scope's stem, in the order Lu, Quan, Ke, Ji |
| `stars` | Flowing stars distributed across the twelve palaces; empty for scopes that have none |
Age fortune additionally carries the **nominal age** (`nominal_age`).
The yearly scope additionally carries the **yearly twelve gods**: the Sui-qian and Jiang-qian gods
re-placed from the yearly branch. These two groups are a different thing from the two the palaces
carry natally — the natal ones are placed from the birth-year branch, the yearly ones from the
target year's branch.
## The childhood scope [#the-childhood-scope]
Decadals only begin at the nominal age set by the Five Elements class (2 for water 2nd, 6 for fire
6th). The years before that belong to no decadal, and are derived as the **childhood scope**.
The childhood scope cycles through six palaces by nominal age, by the mnemonic "first Soul, second
Wealth, third Health, fourth Spouse, fifth Spirit, sixth Career":
| Nominal age | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | … |
| ---------------- | ---- | ------ | ------ | ------ | ------ | ------ | ---- | ------ | ------ |
| Childhood palace | soul | wealth | health | spouse | spirit | career | soul | wealth | cycles |
When the target date falls before the decadals begin, the decadal field returns the childhood scope
instead, with the scope name shown as `childhood`. The field structure is unchanged, so callers need
no special handling; check the scope name only when you need to tell them apart.
## Flowing stars [#flowing-stars]
The yearly, monthly and other scopes carry a batch of stars that exist only at that level, called
**flowing stars** (运昌 Yunchang, 运曲 Yunqu, 运魁 Yunkui, 运钺 Yunyue, 运鸾 Yunluan, 运喜 Yunxi,
运禄 Yunlu, 运羊 Yunyang, 运陀 Yuntuo, 运马 Yunma, plus 流昌 Liuchang, 流曲 Liuqu … at the yearly
level). They are stored grouped by palace.
A star's scope field says which level it belongs to: natal stars are `origin`, decadal flowing stars
are `decadal`, yearly ones are `yearly`.
## Boundaries change the results [#boundaries-change-the-results]
A horoscope's stems, branches and nominal ages are affected by two configuration switches:
* `horoscope_divide` decides whether the horoscope year turns over at lunar New Year or at the
Beginning of Spring (立春, the solar term around 4 February), and whether the monthly scope divides
on the first of the lunar month or on solar terms.
* `age_divide` decides whether nominal age increments at the turn of the lunar year or only after
the birthday.
Query near the start of a year or around a birthday and these two switches change the returned stems,
branches and nominal age directly. See [Config in depth](/en/docs/guide/guides/config).
## In code [#in-code]
A horoscope is raised from an already-charted astrolabe. Birth parameters, chart language and
configuration all come from the chart, so you supply only the target date and hour:
```python
h = chart.horoscope("2024-10-1", 0)
```
```rust
let h = astrolabe.horoscope("2024-10-1", 0)?;
```
```go
h, err := astrolabe.Horoscope("2024-10-1", 0)
```
Reading the six scopes:
```python
chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US")
h = chart.horoscope("2024-10-1", 0)
print(h.decadal.name, h.decadal.heavenly_stem + h.decadal.earthly_branch, h.decadal.mutagen)
print(h.age.name, "nominal age", h.age.nominal_age)
print(h.yearly.name, h.yearly.heavenly_stem + h.yearly.earthly_branch)
print("rearranged yearly palace names:", h.yearly.palace_names)
print("yearly Sui-qian gods:", h.yearly.yearly_dec_star.suiqian12)
```
```text
decadal gengchen ['sun', 'general', 'moon', 'fortunate']
age nominal age 25
yearly jiachen
rearranged yearly palace names: ['spouse', 'siblings', 'soul', 'parents', 'spirit', 'property', 'career', 'friends', 'surface', 'health', 'wealth', 'children']
yearly Sui-qian gods: ['sorrowing', 'illness', 'initial', 'unlucky', 'downcast', 'tied', 'official', 'consumer', 'wastrel', 'virtuous', 'sinister', 'blessed']
```
Walking the flowing stars:
```python
for palace_index, stars in enumerate(h.yearly.stars or []):
for s in stars:
print(palace_index, s.name, s.scope)
```
Age fortune and the yearly scope each carry one extra datum of their own (nominal age, the yearly
gods), so in Rust their shared fields live under `.base` — but both types implement `Deref`, so
`h.yearly.heavenly_stem` reads directly. The layer is flattened when serialized to Python and Go,
where you write `h.yearly.heavenly_stem` as well.
# Patterns (/en/docs/guide/concepts/patterns)
What a pattern is, the principles x-iztro judges by, how the natal and horoscope views differ, and the complete table of all 64 patterns.
*For: everyone. The full table of 64 patterns is at the end of the page*
## What a pattern is [#what-a-pattern-is]
When a particular set of stars lands on a chart in a particular arrangement, tradition gives that
combination a name — 紫府同宫 ("Emperor and Empress in One Palace"), 杀破狼 ("Marshal, Rebel and
Wolf"), 阳梁昌禄 ("Sun, Sage, Scholar and Money"). A named star arrangement like this is a
**pattern** (格局).
A pattern is not a separate algorithm. It is **pattern matching on a chart that has already been
cast**: are these stars in these palaces, are they bright enough, do they carry a transformation.
Once the chart is cast, its patterns are already determined.
Traditionally a pattern also comes with a verdict ("brings rank", "brings solitude", and so on).
x-iztro **only judges whether the arrangement holds; it never grades it**. Verdicts are
interpretation, schools disagree, and that is left to you or to your model.
## How x-iztro judges [#how-x-iztro-judges]
Sources disagree about patterns. The same name often has both a loose and a strict reading. The
principles this engine follows:
1. **Findings of fact only.** The output says "this arrangement holds, and here are the stars and
palaces that evidence it". No auspiciousness, no strength, no score.
2. **Every rule cites its source.** Each rule carries the classical quotation it comes from, the
reading adopted, and why that reading was chosen over the alternative.
3. **Multiple readings are reported as a `variant`, not hidden behind a switch.** When a pattern has
several forms, the engine records which one matched in the `variant` field and lets you decide
whether to accept it. 机巨同临 (Advisor and Advocator Together, `ji_ju_tong_lin`) in the You
palace, for instance, reports `variant` `"you"`,
because some sources hold that the You placement does not count — the engine reports it and
labels it.
4. **"Broken" is a flag, not a veto.** When a source's "spoiled by malefics" condition fires, the
pattern is still reported, with `broken` set true. Whether the arrangement holds is a fact;
whether it is good is interpretation.
5. **"Body-or-Soul" patterns record whichever palace matched.** Classical texts often say "in the
Soul or Body palace", meaning either qualifies. Such patterns are judged at both palaces;
`palace_index` records the one that actually matched, and if both match, two hits are reported.
6. **Empty palaces borrow from the opposite palace.** When a palace holds no major star, tradition
borrows the opposite palace's majors. The evidence still records the palace the star **actually
occupies**, not the one it was borrowed into.
7. **No golden data, so the evidence is home-grown.** iztro has no pattern API, so unlike chart
casting there is no field-for-field golden dataset here. In its place, four layers of tests:
positive and negative unit tests per rule (around 80 of them); a reproduction of all 32 example
charts from the source page on real charts; a bulk sanity and invariant sweep over the 1,560
tier-1 charts, including a per-pattern count of how many charts it hits; and output snapshots of
4 charts across 6 languages — Rust writes the baseline, Python and Go read the same files back,
and all three sides must agree byte for byte.
## What one hit looks like [#what-one-hit-looks-like]
A hit is a `PatternHit`. The fields have the same names in all three programming languages (casing
follows each language's convention; the one exception is Rust, whose struct calls `palace_index`
simply `palace` — the serialized DTO key is `palaceIndex` everywhere):
| Field | Meaning |
| --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `key` | Language-independent pattern key, e.g. `zi_fu_tong_gong`. Predicate logic uses this |
| `name` | Pattern name, translated to the chart's language |
| `scope` | The view it was judged in: `origin` for natal, otherwise the horoscope level (`decadal`, `yearly`, …) |
| `palace_index` | Slot of the palace where the pattern formed (0-11, Yin palace is 0) |
| `palace_name` / `palace_name_key` | That palace's name and key **in this view** |
| `variant` | Which reading matched; absent for single-reading patterns |
| `broken` | Whether the "spoiled" condition fired. The hit is reported either way; this is only a flag |
| `stars` | The stars evidencing the pattern with their palaces, each carrying `key`, `name`, `palace_index`, and brightness and mutagen where they exist |
Listing every pattern on a chart:
```rust
let en = Language::EnUS;
let chart = by_solar("1985-5-3", 9, Gender::Male, true, en, Config::default())?;
for hit in chart.patterns() {
println!("{} | {}",
translate_pattern(hit.key, en),
translate_palace(chart.palaces[hit.palace].name, en));
}
```
```python
from x_iztro import Astro
chart = Astro().by_solar("1985-5-3", 9, "male", language="en-US")
for hit in chart.patterns():
print(hit.name, "|", hit.palace_name)
```
```go
chart, _ := iztro.BySolar("1985-5-3", 9, iztro.GenderMale, true, iztro.LanguageEnUS, nil)
hits, _ := chart.Patterns(nil)
for _, h := range hits {
fmt.Println(h.Name, "|", h.PalaceName)
}
```
**Output**
```text
General and Wolf Together | surface
Empress and Minister Facing the Palace | soul
Marshal, Rebel and Wolf | surface
Money and Horse Galloping Together | soul
Officer and Helper Flanking Life | soul
Literary Nobility and Brilliance | surface
Literary Stars Facing Life | soul
Literary Stars in Hidden Support | soul
Literary Stars in Hidden Support | soul
```
This chart's Body palace sits on the Surface (Travel) palace, so the three "Body-or-Soul" patterns
here (General and Wolf Together, Marshal-Rebel-Wolf, Literary Nobility and Brilliance) record the
Surface palace rather than the Soul palace.
`variant` and `broken` come from the same set of hits:
```python
for hit in chart.patterns():
if hit.variant or hit.broken:
print(f"{hit.name}: variant={hit.variant} broken={hit.broken}")
```
```text
Empress and Minister Facing the Palace: variant=soul_empty broken=False
Money and Horse Galloping Together: variant=surround broken=False
Literary Stars Facing Life: variant=None broken=True
Literary Stars in Hidden Support: variant=opposite broken=False
Literary Stars in Hidden Support: variant=surround broken=False
```
`soul_empty` records that this chart's Soul palace really is empty (the classical "no major star in
the Soul palace" branch, which x-iztro notes rather than requires); `broken` on Literary Stars
Facing Life records that malefics or a Ji transformation appear in the surrounded palaces; the
`surround` on the galloping pattern and the `opposite` / `surround` on Hidden Support are each one
reading of that pattern — see the full table at the end of the page.
## Natal and horoscope run the same rules [#natal-and-horoscope-run-the-same-rules]
Internally the engine abstracts "the natal twelve palaces" and "the composed twelve palaces of one
horoscope level" into a single view, and the rules only ever see that view. A horoscope view swaps
three things and then runs every rule again:
* **The Soul palace becomes that level's Soul palace.** Whichever palace the decadal has moved to is
the Soul palace of that level, and the twelve palace names are re-derived from it.
* **That level's flowing stars are merged in.** Flowing stars (the decadal/annual Lucun 运禄/流禄,
the decadal/annual Wenchang 运昌/流昌 and the rest)
count **as their natal counterparts** during judgement: seeing a flowing Lucun on a horoscope
chart is, to the rules, seeing Lucun.
* **Mutagens become that level's mutagens.** The natal view reads birth-year transformations; the
decadal view reads the transformations flown by the decadal stem, and so on.
This is why the sources' "if the natal chart has the arrangement and the decadal then arrives at it,
its benefit is enjoyed" falls out for free — it is just the natal rules re-run in the decadal view.
A horoscope view **has no Body palace**, so "Body-or-Soul" patterns are judged only at that level's
Soul palace there.
```rust
let h = chart.horoscope("2025-6-1", 0)?;
for hit in h.patterns(Scope::Decadal) {
println!("{} {:?}", translate_pattern(hit.key, en), hit.variant);
}
```
```python
h = chart.horoscope("2025-6-1", 0)
for hit in h.patterns(Scope.DECADAL):
print(hit.name, hit.scope, hit.variant)
```
```go
h, _ := chart.Horoscope("2025-6-1", 0)
hits, _ := h.Patterns(iztro.ScopeDecadal, nil)
for _, x := range hits {
fmt.Printf("%s %s %q\n", x.Name, x.Scope, x.Variant)
}
```
For the female chart of 2000-8-16, hour index 2, in the decadal view (Python version):
```text
Marshal, Rebel and Wolf decadal None
Meeting of Wind and Cloud decadal None
Meeting of Wind and Cloud decadal yearly
```
The natal view of that same chart holds only "Empress and Minister Facing the Palace" — the
Marshal-Rebel-Wolf pattern holds at this level only because the decadal moved the Soul palace.
Asking a horoscope object for the `origin` level gives exactly what calling `patterns()` on the
astrolabe gives.
### Two patterns that exist only in horoscope views [#two-patterns-that-exist-only-in-horoscope-views]
Two of the 64 are **transit patterns**: judged only in a horoscope view, never reported on the natal
chart.
| Pattern | When judged | Notes |
| ---------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| 禄衰马困 `lu_shuai_ma_kun` | Any horoscope level | Judged at whichever level the view is: decadal view judges the decadal, yearly view the year |
| 风云际会 `feng_yun_ji_hui` | Decadal view only | It compares "two limits both meeting fortune and horse", which spans levels, so it is reported once, from the decadal view |
风云际会's `variant` records both the pair of limits and how strictly they "meet": no `variant` or
`same_palace` takes decadal + minor limit, `yearly` or `yearly_same_palace` takes decadal + annual
(a reading some schools prefer); the `same_palace` forms are the strict reading — both limits' Soul
palaces hold Lucun, Tianma or the Lu transformation in-palace — while the plain forms accept the
surrounded set. Each pair reports one hit, two at most; on the chart above both pairs hold.
## Which table decides Sun and Moon brightness [#which-table-decides-sun-and-moon-brightness]
日月并明 (Sun and Moon Both Bright), 日月反背 (Sun and Moon Both Dim) and 丹墀桂墀 (Cinnabar and
Cassia Steps) turn on whether the Sun and the Moon are bright, and there are two
traditions for deciding that, which do not always agree:
| Reading | Basis | Key |
| -------------------------- | ------------------------------------------------------------------------------------ | ------------ |
| Brightness table (default) | The chart's own brightness: Miao and Wang are bright, Xian and Bu are dim | `table` |
| Traditional position | Sun bright in Yin–Wu and dim in You–Chou; Moon bright in You–Chou and dim in Mao–Wei | `positional` |
x-iztro's brightness table matches iztro **value for value**, which is a hard line for the whole
library. By that table the Moon in the You palace is Bu, i.e. not bright — and the Moon in both
example charts on the source page's 日月并明 entry sits in exactly that palace. So under the default
reading those two examples do **not** form the pattern.
Most traditional brightness tables record the Moon in You as Wang, which is what the page's examples
rely on. To reproduce the traditional judgement, switch the reading to `positional`: the conclusion
then matches the page's examples, without touching the brightness table and without affecting chart
casting's parity with iztro.
```python
from x_iztro import Astro, PatternConfig, BrightnessSource
chart = Astro().by_solar("1985-1-5", 11, "female", language="en-US")
print([h.name for h in chart.patterns()])
print([h.name for h in chart.patterns(
PatternConfig(brightness_source=BrightnessSource.POSITIONAL))])
```
```text
['Money and Horse Galloping Together', 'Officer and Helper Flanking Life', 'Sitting on and Facing Nobility']
['Sun and Moon Both Bright', 'Money and Horse Galloping Together', 'Officer and Helper Flanking Life', 'Sitting on and Facing Nobility']
```
## The three switches [#the-three-switches]
`PatternConfig` has exactly three fields. Anything that is merely a second *reading* of a pattern
goes through `variant` instead; only data readings that change the **finding of fact itself** live
here.
| Field | Default | Effect |
| ------------------- | ------- | -------------------------------------------------------------------------- |
| `brightness_source` | `table` | Basis for Sun and Moon brightness, see above |
| `borrow` | `true` | Whether an empty palace borrows the opposite palace's majors |
| `flow_stars` | `true` | Whether flowing stars count as their natal counterparts in horoscope views |
Turn `borrow` off and an empty palace stays empty, so patterns that relied on borrowing stop being
reported. Turn `flow_stars` off and horoscope views recognise natal auxiliaries only. Neither
affects chart casting.
## All 64 patterns [#all-64-patterns]
In the order of the source page's entries. In the Class column, **transit** means the pattern is
judged only in horoscope views; everything else is judged in both the natal and the horoscope views.
The condition column gives the gist of the formal condition. Each rule's full reading, its classical
quotation, and the reasoning behind choosing between competing readings live in the implementation's
doc comments (`src/pattern/rules/`).
| # | Name | key | Condition | variant | broken | Class |
| -- | ------------------------------------------------ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------- |
| 1 | Sovereign and Ministers Assembly (君臣庆会) | `jun_chen_qing_hui` | Ziwei or Tianfu assembling with the attending stars in one of four forms (see variant) | `zi_po_zuo_you_jia` Ziwei and Pojun in the Soul palace, Zuofu and Youbi flanking; `zi_xiang_chang_qu_axis` Ziwei and Tianxiang in the Soul palace, Wenchang and Wenqu on the Soul-Surface axis; `tian_fu_ji_yin_tong_liang_jia` Tianfu in the Soul palace with all four of Tianji, Taiyin, Tiantong and Tianliang spread over the two flanking palaces (an empty flank borrows the opposite palace's majors); `zi_zuo_you_tong_gong` Ziwei with Zuofu and Youbi all in the Soul palace | Yes (fourth form only: flagged when malefics or Ji appear in the surrounded palaces; the first three require their absence to form at all) | general |
| 2 | Emperor and Empress in One Palace (紫府同宫) | `zi_fu_tong_gong` | Ziwei and Tianfu together in the Soul palace (only possible at Yin or Shen) | — | — | general |
| 3 | Golden Carriage Escort (金舆扶驾) | `jin_yu_fu_jia` | Tianfu in the Soul palace at Chou or Wei, flanked by Taiyang and Taiyin | — | — | general |
| 4 | Emperor and Empress Flanking Life (紫府夹命) | `zi_fu_jia_ming` | Tianji and Taiyin together in the Soul palace, flanked by Ziwei and Tianfu (only possible at Yin or Shen) | — | — | general |
| 5 | Emperor Facing Southern Light (极向离明) | `ji_xiang_li_ming` | Ziwei in the Soul palace at Wu, with no malefic or Ji in the surrounded palaces | — | — | general |
| 6 | Emperor at Mao or You (极居卯酉) | `ji_ju_mao_you` | Ziwei and Tanlang together in the Soul palace at Mao or You | — | — | general |
| 7 | Advisor, Moon, Fortunate and Sage (机月同梁) | `ji_yue_tong_liang` | Soul palace at Yin or Shen holding Tiantong with Tianliang, or Tianji with Taiyin (an empty palace borrows the opposite palace's majors) | `surround` the loose reading: all four of the stars present across the Soul palace's surrounded palaces (borrowing included) | — | general |
| 8 | Benevolent Shelter of the Court (善荫朝纲) | `shan_yin_chao_gang` | Tianji and Tianliang together in the Soul or Body palace (only possible at Chen or Xu) | — | — | general |
| 9 | Advisor and Advocator Together (机巨同临) | `ji_ju_tong_lin` | Tianji and Jumen together in the Soul palace (only possible at Mao or You) | `you` Soul palace at You (some sources exclude the You placement) | — | general |
| 10 | Advisor and Advocator at Mao (机巨居卯) | `ji_ju_ju_mao` | Tianji and Jumen together in the Soul palace at Mao | — | — | general |
| 11 | Sun and Moon in One Palace (日月同宫) | `ri_yue_tong_gong` | Taiyang and Taiyin together in the Soul palace (only possible at Chou or Wei) | — | — | general |
| 12 | Advocator and Sun in One Palace (巨日同宫) | `ju_ri_tong_gong` | Taiyang and Jumen together in the Soul palace (Yin and Shen both count) | — | — | general |
| 13 | Sun Shining on Thunder Gate (日照雷门) | `ri_zhao_lei_men` | Taiyang and Tianliang together at Mao, that palace being the Soul or the Career palace | `career` in the Career palace (the classical "in the Career palace likewise"; no variant means the Soul palace) | — | general |
| 14 | Sun and Moon Both Bright (日月并明) | `ri_yue_bing_ming` | Taiyang and Taiyin both bright within the Soul palace's surrounded palaces | — | — | general |
| 15 | Sun and Moon Both Dim (日月反背) | `ri_yue_fan_bei` | Taiyang and Taiyin both dim within the Soul palace's surrounded palaces | — | — | general |
| 16 | Sun and Moon Lighting the Wall (日月照璧) | `ri_yue_zhao_bi` | Taiyang and Taiyin together in the Property palace | — | — | general |
| 17 | Golden Radiance (金灿光辉) | `jin_can_guang_hui` | Taiyang alone in the Soul palace at Wu | — | Yes (malefics or Ji in the surrounded palaces) | general |
| 18 | Sun and Moon Hiding Their Light (日月藏辉) | `ri_yue_cang_hui` | Sun and Moon both dim, and Jumen also seen in the surrounded palaces | — | — | general |
| 19 | Cinnabar and Cassia Steps (丹墀桂墀) | `dan_chi_gui_chi` | Sun and Moon both bright, and the Soul palace itself holds the bright Taiyang or the bright Taiyin | — | — | general |
| 20 | Sun and Moon Flanking Life (日月夹命) | `ri_yue_jia_ming` | Taiyang and Taiyin in the palaces either side of the Soul palace, which holds no void star and does hold an auspicious star | — | — | general |
| 21 | Sun and Moon Flanking Wealth (日月夹财) | `ri_yue_jia_cai` | As above with the Wealth palace in place of the Soul palace | — | — | general |
| 22 | Bright Moon at Heaven's Gate (月朗天门) | `yue_lang_tian_men` | Taiyin in the Soul palace at Hai | — | — | general |
| 23 | Moon Rising over the Sea (月生沧海) | `yue_sheng_cang_hai` | Tiantong and Taiyin together at Zi, in either the Soul or the Property palace | `soul` in the Soul palace (the page's alternative name 水澄桂萼, "clear water, cassia blossom"); `property` in the Property palace (the classical wording) | — | general |
| 24 | Pearl Emerging from the Sea (明珠出海) | `ming_zhu_chu_hai` | Empty Soul palace at Wei, with Tiantong and Jumen in the opposite palace at Chou | — | — | general |
| 25 | General and Wolf Together (武贪同行) | `wu_tan_tong_xing` | Wuqu and Tanlang together in the Soul or Body palace (only possible at Chou or Wei) | — | — | general |
| 26 | Bell, Scholar, Tuoluo and General (铃昌陀武) | `ling_chang_tuo_wu` | Lingxing, Wenchang, Tuoluo and Wuqu all present across the Soul palace's surrounded palaces | — | — | general |
| 27 | Punishment and Prisoner Flanking the Seal (刑囚夹印) | `xing_qiu_jia_yin` | Lianzhen and Tianxiang together with a punishment star (Tianxing or Qingyang) in the Soul or Body palace | — | — | general |
| 28 | Born at the Wrong Time (生不逢时) | `sheng_bu_feng_shi` | A void star in the Soul palace sharing it with Lianzhen | `pojun` the star sharing it is Pojun (a form other sources add) | — | general |
| 29 | Heroic Star Facing the Origin (雄宿朝元) | `xiong_su_chao_yuan` | Lianzhen alone in the Soul palace at Yin or Shen | — | Yes (Huoxing, Lingxing, Qingyang, Tuoluo, Dikong or Dijie in the surrounded palaces) | general |
| 30 | Empress and Minister Facing the Palace (府相朝垣) | `fu_xiang_chao_yuan` | Tianfu in the Career palace and Tianxiang in the Wealth palace, both facing the Soul palace | `soul_empty` the Soul palace really is empty | — | general |
| 31 | Fire and Wolf (火贪) | `huo_tan` | Tanlang in the Soul palace with Huoxing in the same palace | `surround` Huoxing only in the surrounded palaces | — | general |
| 32 | Bell and Wolf (铃贪) | `ling_tan` | Tanlang in the Soul palace with Lingxing in the same palace | `surround` Lingxing only in the surrounded palaces | — | general |
| 33 | Jade Hidden in Stone (石中隐玉) | `shi_zhong_yin_yu` | Jumen in the Soul or Body palace, that palace being at Zi or Wu | — | — | general |
| 34 | Sage and Horse Drifting (梁马飘荡) | `liang_ma_piao_dang` | Tianliang and Tianma together in the Soul or Body palace | — | — | general |
| 35 | Sun, Sage, Scholar and Money (阳梁昌禄) | `yang_liang_chang_lu` | Taiyang, Tianliang, Wenchang and Lucun all present across the Soul palace's surrounded palaces | — | — | general |
| 36 | Marshal, Rebel and Wolf (杀破狼) | `sha_po_lang` | Any of Qisha, Pojun or Tanlang in the Soul or Body palace (the three are always in trine) | — | — | general |
| 37 | Marshal Facing the Dipper (七杀朝斗) | `qi_sha_chao_dou` | Qisha in the Soul palace, that palace being at Zi, Wu, Yin or Shen | `yang_dou` Soul palace at Yin or Zi; `chao_dou` Soul palace at Wu or Shen | — | general |
| 38 | Money Waning, Horse Trapped (禄衰马困) | `lu_shuai_ma_kun` | Within the horoscope Soul palace's surrounded palaces, Lucun shares a palace with a void or wasting star while Tianma shares one with a malefic or Ji | `qisha` Qisha also present in the limit's surrounded palaces (the classical strict reading holds too) | — | **transit** |
| 39 | Heroic Star Enthroned (英星入庙) | `ying_xing_ru_miao` | Pojun in the Soul palace at Zi or Wu | — | — | general |
| 40 | Waters Flowing East (众水朝东) | `zhong_shui_chao_dong` | Pojun and Wenqu together in the Soul palace at Yin or Mao | — | — | general |
| 41 | Three Wonders Assembly (三奇加会) | `san_qi_jia_hui` | Lu, Quan and Ke transformations all present across the Soul palace's surrounded palaces | `ke_soul_lu_wealth_quan_career` Ke in the Soul palace, Lu in Wealth, Quan in Career | — | general |
| 42 | Money and Horse Galloping Together (禄马交驰) | `lu_ma_jiao_chi` | Lucun and Tianma in the same palace (any palace; a chart may produce several hits) | `surround` both present across the Soul palace's surrounded palaces without sharing one (recorded at the Soul palace) | — | general |
| 43 | Mandarin Ducks of Fortune (禄合鸳鸯) | `lu_he_yuan_yang` | Lucun and the Lu transformation paired at the Soul palace: same palace, or one in the Soul and one in the Surface palace | `opposite` on the Soul-Surface axis (no variant means same palace) | — | general |
| 44 | Open and Hidden Fortune (明禄暗禄) | `ming_lu_an_lu` | Lucun (or the Lu transformation) in the Soul palace, with the other in its hidden-harmony palace | — | — | general |
| 45 | Money and Horse Bearing the Seal (禄马佩印) | `lu_ma_pei_yin` | Lucun, Tianma and Tianxiang all in one palace (any palace qualifies and is recorded) | — | Yes (a void star in that palace) | general |
| 46 | Double Canopy (两重华盖) | `liang_chong_hua_gai` | Lucun and the Lu transformation both in the Soul palace, together with a void-family star | `kong_yao` the loose reading taking Tiankong, Jiekong or Xunkong (no variant means the classical Dikong / Dijie) | — | general |
| 47 | Meeting of Wind and Cloud (风云际会) | `feng_yun_ji_hui` | The decadal and one other limit each meet Lucun, Tianma or the Lu transformation in their surrounded palaces | `yearly` / `yearly_same_palace` decadal + annual (no variant / `same_palace` means decadal + minor limit); the `same_palace` forms are the strict reading with the stars in-palace at both limits' Soul palaces | — | **transit** (decadal view only) |
| 48 | Qingyang and Tuoluo Flanking Life (羊陀夹命) | `yang_tuo_jia_ming` | Tuoluo and Qingyang in the palaces either side of the Soul palace; the flanked palace necessarily holds Lucun, which is recorded with the evidence | — | — | general |
| 49 | Arrow at the Horse's Head (马头带箭) | `ma_tou_dai_jian` | Soul palace at Wu with Qingyang in it, and Tiantong with Taiyin in the Soul palace (borrowed from opposite if empty) | `tanlang_lu` the side form: Tanlang with the Lu transformation sharing Wu with Qingyang | — | general |
| 50 | Officer and Helper in One Palace (左右同宫) | `zuo_you_tong_gong` | Zuofu and Youbi together in the Soul or Body palace (trine only does not count) | — | — | general |
| 51 | Officer and Helper Flanking Life (左右夹命) | `zuo_you_jia_ming` | Zuofu and Youbi in the palaces either side of the Soul palace | — | — | general |
| 52 | Officer and Helper Attending the Emperor (辅弼拱主) | `fu_bi_gong_zhu` | Ziwei in the Soul palace, attended or flanked by Zuofu and Youbi | `surround` both in the surrounded palaces; `jia` both flanking | — | general |
| 53 | Kui and Yue Flanking Life (魁钺夹命) | `kui_yue_jia_ming` | Tiankui and Tianyue in the palaces either side of the Soul palace (same palace or trine does not count) | — | — | general |
| 54 | Sitting on and Facing Nobility (坐贵向贵) | `zuo_gui_xiang_gui` | Tiankui and Tianyue on the Soul and Surface palaces respectively | — | — | general |
| 55 | Void and Robbery Flanking Life (劫空夹命) | `jie_kong_jia_ming` | Dijie and Dikong in the palaces either side of the Soul palace | — | — | general |
| 56 | Fortune Meeting Two Killers (禄逢两杀) | `lu_feng_liang_sha` | Lucun sharing the Soul palace with a void star, plus Dikong or Dijie in the surrounded palaces | — | — | general |
| 57 | Literary Nobility and Brilliance (文贵文华) | `wen_gui_wen_hua` | Wenchang and Wenqu together in the Soul palace, the Body palace, or any of the Soul palace's surrounded palaces | — | — | general |
| 58 | Literary Stars Facing Life (文星朝命) | `wen_xing_chao_ming` | Wenchang and Wenqu both present in the Soul palace's surrounded palaces (same palace included) | — | Yes (malefics or Ji in the surrounded palaces) | general |
| 59 | Scholar and Artist Flanking Life (昌曲夹命) | `chang_qu_jia_ming` | Wenchang and Wenqu in the palaces either side of the Soul palace | — | Yes (malefics or Ji in the surrounded palaces) | general |
| 60 | Literary Stars in Hidden Support (文星暗拱) | `wen_xing_an_gong` | Wenchang and Wenqu supporting the Soul palace by flanking it, facing it from the Surface palace, or meeting it in the surrounded set (three readings judged independently) | `jia` the two flanking the Soul palace; `opposite` both in the Surface palace facing it; `surround` both present across the Soul palace's surrounded palaces | — | general |
| 61 | Power and Fortune at Birth (权禄生逢) | `quan_lu_sheng_feng` | The Quan-bearing and Lu-bearing stars together in the Soul palace, both at Miao or Wang | — | — | general |
| 62 | Fame Open, Fortune Hidden (科明暗禄) | `ke_ming_an_lu` | The Ke transformation in the Soul palace, with Lucun or the Lu transformation in its hidden-harmony palace | `hua_lu` the hidden-harmony palace holds the Lu transformation (a reading of some schools; no variant means Lucun) | — | general |
| 63 | Fame, Power and Fortune Flanking (科权禄夹) | `ke_quan_lu_jia` | Two of the Lu, Quan and Ke transformations in the palaces either side of the Soul palace | — | — | general |
| 64 | Top Graduate Appointed (甲第登庸) | `jia_di_deng_yong` | The Ke transformation in the Soul palace, with Quan facing it from the Surface or a trine palace | `complete` the Lu transformation or Lucun is also met | — | general |
The source page teaches 火贪 and 铃贪 as a single entry; x-iztro splits them into two independent
keys. Hence 63 entries but 64 pattern keys.
### Easily misread points [#easily-misread-points]
Flanking (夹) means two stars in the palaces **immediately either side** of the target (slots -1 and
+1), which never overlaps the surrounded palaces. 昌曲夹命 (Scholar and Artist Flanking Life) and
文星朝命 (Literary Stars Facing Life) each take one of those: flanking goes to the former, being met
in the surrounded set (same palace included) to the latter. 文星暗拱 (Literary Stars in Hidden
Support) instead reports all three readings — flanking (`jia`), facing from the Surface palace
(`opposite`) and the surrounded set (`surround`) — because the source page's author notes the
pattern's "name and reading do not quite agree"; the choice is left to the caller, so on one chart
it can hit alongside either of the other two.
The hidden-harmony palace pairs branches as Zi-Chou, Yin-Hai, Mao-Xu, Chen-You, Si-Shen, Wu-Wei.
明禄暗禄 and 科明暗禄 use that, not the opposite palace.
"Void star" in these conditions means the four adjective stars Xunkong, Kongwang, Jielu and Jiekong;
"Kong-Jie" means the two minor stars Dikong and Dijie. Double Canopy (两重华盖) and Fortune Meeting
Two Killers (禄逢两杀) take the latter, Born at the Wrong Time (生不逢时) and Money and Horse
Bearing the Seal (禄马佩印) the former.
It is "Lucun and Tianma in the same palace", reported for whichever palace qualifies, with that
palace in `palace_index`; one chart may produce more than one hit. Tianma only ever falls at Yin,
Shen, Si or Hai, so the palace is always one of those four.
## API reference [#api-reference]
* [Rust — patterns](/en/docs/rust/patterns)
* [Python — patterns](/en/docs/python/patterns)
* [Go — Patterns](/en/docs/go/patterns)
## Pattern readings live in a knowledge pack [#pattern-readings-live-in-a-knowledge-pack]
This page and the three API references cover **judgement**: which stars in which arrangement make a
pattern form. What a formed pattern then *means* is interpretation, and interpretation is a school's
opinion, so it lives in a [knowledge pack](/en/docs/guide/guides/knowledge-pack) — the bundled
default pack carries classical quotations, a prose description of the conditions and a reading for
each of the 64 patterns.
A hit's `key` is exactly the key used in the pack's `patterns` section:
```python
pack = KnowledgePack.builtin()
for hit in chart.patterns():
print(hit.name, "|", pack.pattern(hit.key).quotes[0])
```
Disagree with a reading? Write an overlay pack replacing those entries; the judgement is unaffected.
## Sources and credit [#sources-and-credit]
The pattern entries, example charts and classical quotations come from the 格局 (Patterns) page of
[iztro-docs](https://github.com/SylarLong/iztro-docs) (MIT License, by Sylar Long); the quotations
themselves are from 《紫微斗数全书》 (the Complete Book of Ziwei Doushu). The judgement engine, the
pattern names in six languages, and
the choices made between competing readings are x-iztro's own work — iztro itself has no
corresponding API.
# How charting works (/en/docs/guide/concepts/how-it-works)
Nine steps from birth data to a complete natal chart — what each step does and what it works from.
*For: everyone. To drive the steps through the API, see [The API behind the nine charting steps](/en/docs/guide/guides/step-api)*
Charting is not mysticism; it is a chain of determinate derivations. Given the same birth data and
the same school-of-thought choices, anyone should arrive at exactly the same chart. This page breaks
that chain into nine steps.
Everyday charting does not require walking these steps yourself — the charting entry point does all
of it. Read this page to know where the results come from, and why a particular field looks the way
it does.
## The whole flow [#the-whole-flow]
**Convert Gregorian to lunar**
— obtain the lunar year, month and day, plus the four pillars
**Fix the month index**
— resolve leap-month attribution
**Locate the Soul and Body palaces**
— from the month index and the hour
**Determine the Five Elements class**
— from the Soul palace's stem and branch
**Place Ziwei and Tianfu**
— from the Five Elements class and the lunar day
**Place the fourteen major stars**
— unfolded from the positions of Ziwei and Tianfu
**Place minor and adjective stars**
— each derived from the year stem, year branch, month, day or hour
**Place the four groups of twelve gods**
— Changsheng, Boshi, Sui-qian, Jiang-qian
**Derive decadals and age fortune**
— from the Five Elements class, gender and year branch
***
## Step by step [#step-by-step]
### 1. Convert Gregorian to lunar [#1-convert-gregorian-to-lunar]
Zi Wei Dou Shu is built on the lunar calendar, but the input is usually Gregorian. This step also
produces the stem-branch pillars for the year, month, day and hour.
When the year's stem and branch turn over is configurable: someone born between lunar New Year and
the Beginning of Spring (立春, the solar term around 4 February) gets a different year pillar under
each setting. Year-derived adjective stars take their year branch from a *separate* switch, and the
two switches can be set independently — so year-derived adjective stars and the major stars may rest
on different year branches. This is iztro's actual behaviour, reproduced verbatim by x-iztro.
### 2. Fix the month index [#2-fix-the-month-index]
Days after the 15th of a leap month count towards the following month (this can be switched off),
and the late Zi hour takes no part in the correction.
### 3. Locate the Soul and Body palaces [#3-locate-the-soul-and-body-palaces]
Start at the Yin palace as the first lunar month, count forward to the month of birth, then count
backward from there to the hour of birth — where you land is the **Soul palace**. The **Body palace**
uses the same starting point but counts the hour forward. The Soul palace's heavenly stem is derived
from the year stem by the Five Tigers rule.
### 4. Determine the Five Elements class [#4-determine-the-five-elements-class]
Looked up from the **Soul palace's stem and branch**, with five possible values: water 2nd, wood
3rd, metal 4th, earth 5th, fire 6th.
The class number (2/3/4/5/6) gets used twice further on: as the divisor when placing Ziwei, and as
the starting nominal age when deriving the decadals.
The earth plate takes the class from the Body palace's stem and branch instead, and the human plate
from the Spirit palace's. Change the stem and branch the class is taken from and steps 5, 6, 8 and 9
are all recomputed — that is what the charting-perspective configuration does.
### 5. Place Ziwei and Tianfu [#5-place-ziwei-and-tianfu]
Divide the lunar day by the class number and apply the Ziwei placement rule to fix which palace
Ziwei falls in. Tianfu mirrors Ziwei about the Yin–Shen axis: the Tianfu slot = 12 − the Ziwei slot
(modulo 12).
### 6. Place the fourteen major stars [#6-place-the-fourteen-major-stars]
The six stars of the Ziwei series unfold **counter-clockwise** from Ziwei's slot at fixed offsets,
and the eight stars of the Tianfu series unfold **clockwise** from Tianfu's slot. The offsets are
not contiguous — there are gaps:
| Ziwei series (backward, from Ziwei's slot) | Slots back |
| ------------------------------------------ | ---------- |
| Ziwei | 0 |
| Tianji | 1 |
| Taiyang | 3 |
| Wuqu | 4 |
| Tiantong | 5 |
| Lianzhen | 8 |
| Tianfu series (forward, from Tianfu's slot) | Slots forward |
| ------------------------------------------- | ------------- |
| Tianfu | 0 |
| Taiyin | 1 |
| Tanlang | 2 |
| Jumen | 3 |
| Tianxiang | 4 |
| Tianliang | 5 |
| Qisha | 6 |
| Pojun | 10 |
With both series laid out, all fourteen major star positions are fixed. The birth-year stem's
mutagen marks are stamped on in this step too.
### 7. Place minor and adjective stars [#7-place-minor-and-adjective-stars]
Each group is placed from something different:
| Group | Derived from | Example |
| ------------------------------- | ------------------------- | -------------------------------------------------------------------------- |
| Lucun, Qingyang, Tuoluo | Year stem | "jia's lu goes to the Yin palace", with Qingyang before and Tuoluo after |
| Tianma | Year branch | Only ever lands on the four horse branches (yin, shen, si, hai) |
| Tiankui, Tianyue | Year stem | |
| Zuofu, Youbi | Lunar month | "from Chen, count the months forward for Zuofu" |
| Wenchang, Wenqu | Hour branch | "from Xu, count the hours backward for Wenchang" |
| Huoxing, Lingxing | Year branch + hour branch | |
| Dikong, Dijie | Hour branch | Start at the Hai palace for the Zi hour, Dikong backward and Dijie forward |
| Santai, Bazuo, Enguang, Tiangui | Lunar day | Counted from the positions of the minor stars |
| Year-derived adjective stars | Year stem or year branch | The largest group |
All four use the same lunar day count, but start from different places and run in different
directions: Santai counts **forward** from Zuofu's slot, Bazuo **backward** from Youbi's slot,
Enguang forward from Wenchang's slot and Tiangui forward from Wenqu's slot (with Enguang and Tiangui
each then stepping back one). This is iztro's actual algorithm, reproduced verbatim by x-iztro.
### 8. Place the four groups of twelve gods [#8-place-the-four-groups-of-twelve-gods]
Each group fills all twelve palaces with twelve markers, exactly one per palace:
| Group | Origin | Direction |
| -------------------------- | -------------------------------------------------------------- | ------------------------------- |
| The twelve Changsheng gods | Fixed by the Five Elements class (water 2nd starts at Shen, …) | Gender and year-branch polarity |
| The twelve Boshi gods | The palace holding Lucun | As above |
| The twelve Sui-qian gods | The palace of the year branch | Always forward |
| The twelve Jiang-qian gods | Fixed by the year branch's trine group | Always forward |
**Sui-qian** (岁前, "ahead of the year") is a cycle of twelve annual markers counted from the year
branch itself. **Jiang-qian** (将前, "ahead of the general") is a second cycle of twelve counted from
the General star, whose position comes from the year branch's trine group. Both are per-palace
annual markers; they are unrelated to the fourteen major stars.
### 9. Derive decadals and age fortune [#9-derive-decadals-and-age-fortune]
Decadals start at the Soul palace, ten years per palace; the starting nominal age is the class
number (water 2nd starts at nominal age 2, wood 3rd at 3, …). Direction is decided by the polarity
of gender against the polarity of the year branch.
Age fortune runs on a separate scheme: it starts from the palace fixed by the year branch's trine
group and advances one palace per nominal year, with direction depending on gender alone (male
forward, female backward).
**Nominal age** (虚岁) is East Asian age reckoning: you are 1 at birth and gain a year at the turn of
the year, not on your birthday.
***
## Horoscopes are a separate line [#horoscopes-are-a-separate-line]
The nine steps above produce the **natal chart**, computed once and fixed thereafter. A horoscope
projects the natal chart onto a moment in time: the stems and branches for that level are computed
from the target date, then the twelve palace names are re-laid-out and that level's mutagens and
flowing stars are derived.
The natal chart does not change; what changes is "which palace you are standing in right now". See
[Horoscopes](/en/docs/guide/concepts/horoscope).
## Driving the steps through the API [#driving-the-steps-through-the-api]
Each of the nine steps has a corresponding public function in x-iztro, so you can take just the
result of one intermediate step. The mapping table, and which steps configuration changes, are on
[The API behind the nine charting steps](/en/docs/guide/guides/step-api).
# Overview (/en/docs/guide/guides)
Guides for AI readings, configuration and schools, cross-language predicates, multilingual output, error handling and extension.
*For: everyone. Each card says who it is for*
Everything you actually run into once it is installed lives in this chapter.
## The three easiest traps to fall into [#the-three-easiest-traps-to-fall-into]
1. **Predicating on star names.** Switch the chart language and every branch silently fails. Use the
[language-independent keys](/en/docs/guide/guides/keys).
2. **Forgetting the late Zi hour.** Born between 23:00 and 24:00 means hour index `12`, not `0`, and
the two produce different charts. See
[Config](/en/docs/guide/guides/config#late-zi-hour-attribution-day_divide).
3. **Treating a palace slot as a position in the name order.** Slot 0 of the twelve-palace array is
the Yin palace, not the Soul palace. See
[The twelve palaces](/en/docs/guide/concepts/palaces).
# Using it without writing code (/en/docs/guide/guides/for-non-developers)
What x-iztro does, where it is typically used, what to hand your engineers, and how accurate it is.
*For: Zi Wei enthusiasts · product and decision makers. No code required anywhere on this page*
## What this is [#what-this-is]
x-iztro is a **charting engine**: give it a date of birth, an hour of birth and a gender, and it
computes a complete Zi Wei Dou Shu chart — and can turn that chart into text a language model can
read.
It is not an app and not a website. It is code for programs to use, and an engineer has to build it
into your own product.
## What it can do [#what-it-can-do]
* **Compute a complete natal chart**: where the twelve palaces sit and their stems and branches,
which stars fall into each palace, star brightness and mutagens, the four groups of twelve gods,
the decadals and the age fortune track.
* **Compute the horoscope for any moment**: six scopes — decadal, age fortune, yearly, monthly,
daily, hourly.
* **Turn a chart into AI-readable text in one call**: no need to describe the chart yourself; paste
the generated text into a language model and start asking.
* **Six chart languages**: Simplified Chinese, Traditional Chinese, English, Japanese, Korean,
Vietnamese.
* **Switchable schools**: the year boundary, late Zi hour attribution, the Zhongzhou school and the
heaven/earth/human plates are all configuration.
What it does **not** do is interpret. It can tell you which star landed in which palace; "how will
this person's career go" is not the library's job — that step goes to an AI or to a person.
## Where it is typically used [#where-it-is-typically-used]
### An AI reading bot [#an-ai-reading-bot]
The most common shape. A user gives their birth details in conversation, the backend calls x-iztro
to chart, and the generated chart text is sent to a language model along with the user's question;
the model produces the reading.
The library gets the chart right, the model does the talking — two separate jobs, each reliable on
its own terms.
### The backend of a Zi Wei app [#the-backend-of-a-zi-wei-app]
The app's frontend draws the chart and handles interaction; the charting computation sits on the
server. One body of computation can serve iOS, Android, web and mini-programs at the same time.
### Bulk data analysis [#bulk-data-analysis]
Questions like "across these hundred thousand birth records, what fraction have Ziwei in the Soul
palace?" or "how does a given pattern correlate with a given field". x-iztro charts in milliseconds,
so hundreds of thousands of records finish on a single machine.
## Do we need an engineer? [#do-we-need-an-engineer]
Yes. x-iztro is a code library, not software you can open.
But the work is small: install it, write three lines of calls, wire the result into a flow you
already have. A competent backend engineer usually has a working prototype inside half a day.
## What to tell your engineers [#what-to-tell-your-engineers]
Passing along the following is enough:
| Item | Detail |
| -------------------- | ------------------------------------------------------------------------------------------------- |
| Repository | [github.com/x-haose/x-iztro](https://github.com/x-haose/x-iztro) |
| Programming language | Rust, Python or Go — pick one, the results are identical |
| Installation | Python: `pip install x-iztro`; Go: `go get`; Rust: `cargo add` |
| Requirements | Python 3.10 or later / Go 1.22 or later / Rust edition 2024 |
| Inputs | Three things: date of birth, hour index (0–12), gender |
| For AI use | Call `chart.to_text()` to get the chart text and feed it straight to a model |
| Docs | [Getting started](/en/docs/guide/getting-started), [Semantic text](/en/docs/guide/guides/to-text) |
It is not the clock hour; it is one of thirteen values, 0 through 12. 0 is the early Zi hour
(00:00–01:00), 1 is Chou, 2 is Yin, … 11 is Hai, and 12 is the late Zi hour (23:00–24:00). The full
table is on [The inputs you need](/en/docs/guide/getting-started#hour-index).
**Born after 23:00 means 12, not 0** — the two values produce different charts.
## How accurate is it? [#how-accurate-is-it]
"Accurate" has a very specific definition here: **identical field for field to the JavaScript
[iztro](https://github.com/SylarLong/iztro)**.
iztro is one of the most complete and longest-maintained open-source Zi Wei Dou Shu charting
implementations, and a fair number of frontend projects use it. x-iztro treats it as the reference
and checks against it field by field across 716,314 test cases — any divergence is treated
as a bug and fixed.
The **boundary** of that claim matters too: agreeing with iztro is not the same as being "the one
correct answer in the art". Zi Wei has many schools, and different schools genuinely place stars and
assign mutagens differently. What x-iztro guarantees is "given the same set of school choices, it
computes exactly what the reference implementation computes" — not "this set of school choices is
the right one".
See [Accuracy](/en/docs/guide/about/accuracy).
## What MIT means [#what-mit-means]
x-iztro is open source under the MIT licence. For a user, that means:
* **Commercial use is fine**, with no fee and no need to notify the author.
* **Closed-source use is fine**: build it into your commercial product and the product itself need
not be open sourced.
* **Modification is fine.**
* The single obligation is to **keep the copyright notice** (usually on the product's "open source
licences" page).
* The author **provides no warranty**: if something goes wrong in use, that is on you.
Among open-source licences, MIT is the least restrictive category, and legal teams rarely object.
# Semantic text (to_text) (/en/docs/guide/guides/to-text)
Project a chart, a horoscope or a palace into natural-language text — for a language model, or for a person to read.
*For: everyone. This is the library's most direct use*
A chart has three projections in x-iztro: `to_json` / the DTO is the structured form for machines,
the translated fields are the display form for UIs, and **to\_text is the natural-language form for
models and people** — a complete written description of the chart's facts. Feeding it to a language
model is its most common use, but it is not itself a prompt and contains no instructions. Assembling
that description by hand is tedious and easy to get incomplete, so every readable object carries its
own to\_text.
| Entry | Contents |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Chart `to_text` | The natal chart: basic info, each palace's stem and branch, decadal, age-fortune nominal ages, four groups of twelve gods, three star groups, plus pattern hits |
| Horoscope `to_text` | The horoscope: decadal, age fortune, yearly, monthly, daily and hourly scopes, each with its mutagens, flowing stars and patterns |
| Palace `to_text` | A single palace, identical to that palace's section in the natal text |
| Surrounded palaces `to_text` | The target palace, its opposite, and the wealth and career positions read together |
| `patterns_to_text` | The pattern-hit list on its own, from the natal or any horoscope perspective |
Everything generates in the **chart language**: a Chinese chart yields Chinese text, an English
chart English text.
## Usage [#usage]
```python
chart = astro.by_solar("2000-8-16", 2, "female", language="en-US")
h = chart.horoscope("2025-1-1", 0)
text = f"{chart.to_text()}\n{h.to_text()}" # str(chart) / str(h) are equivalent
```
```rust
let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?;
let h = chart.horoscope("2025-1-1", 0)?;
let text = format!("{}\n{}", chart.to_text(), h.to_text());
```
```go
chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil)
h, _ := chart.Horoscope("2025-1-1", 0)
natal, _ := chart.ToText()
fortune, _ := h.ToText()
```
The finer-grained entries:
```python
chart.palace("soul").to_text() # one palace
chart.surrounded_palaces("soul").to_text() # surrounded palaces
chart.patterns_to_text() # natal patterns
h.patterns_to_text("yearly") # patterns from the yearly perspective
```
```go
chart.PalaceToText(iztro.PalaceTarget{Key: iztro.PalaceSoul})
chart.SurroundedPalacesToText(iztro.PalaceTarget{Key: iztro.PalaceSoul})
chart.PatternsToText(nil)
h.PatternsToText(iztro.ScopeYearly, nil)
```
On the Rust side these are `PalaceRef::to_text()`, `SurroundedPalaces::to_text(lang)` and the free
functions of the `text` module (`astrolabe_to_text` / `horoscope_to_text` / `palace_to_text` /
`surrounded_palaces_to_text` / `patterns_to_text`); the convenience methods emit in the chart
language, the free functions take an explicit language. All three languages emit identical text.
## What the natal text looks like [#what-the-natal-text-looks-like]
Below is the complete opening, three palaces and the closing patterns section of the chart for
2000-8-16, Yin hour, female, charted with `language="en-US"`:
```text
=== Basic Info ===
Gender: female
Solar Date: 2000-8-16
Lunar Date: 二〇〇〇年七月十七
Chinese Date: geng chen - jia shen - bing woo - geng yin
Time: Tiger hour (03:00~05:00)
Zodiac Sign: leo
Zodiac Animal: dragon
Soul Palace Branch: woo
Body Palace Branch: xu
Soul Star: rebel
Body Star: scholar
Five Elements Class: wood 3rd
Birth-Year Mutagen: sunA, generalB, moonC, fortunateD
=== Palaces ===
--- wealth ---
Stem-Branch: wuyin
Decadal: 43-52
Age Fortune Years: 9, 21, 33, 45, 57, 69, 81, 93, 105, 117
Twelve Gods: dissipated, gossip, sorrowing, varied
Major Stars: general([+1])[B], minister([+3])
Minor Stars: horse
Adjective Stars: considery, senior, ageless, psychic, gourmet, gloomy, upset
--- spouse [Original Palace] ---
Stem-Branch: gengchen
Decadal: 23-32
Age Fortune Years: 7, 19, 31, 43, 55, 67, 79, 91, 103, 115
Twelve Gods: dead, general, initial, religious
Major Stars: marshal([+3])
Minor Stars: helper, impulsive([-3])
Adjective Stars: awarded, religious
--- career [Body Palace] ---
Stem-Branch: bingxu
Decadal: 83-92
Age Fortune Years: 1, 13, 25, 37, 49, 61, 73, 85, 97, 109
Twelve Gods: infancy, ambush, wastrel, hapless
Major Stars: judge([0]), empress([+3])
Minor Stars: officer
Adjective Stars: gifted, frail
(the other nine palaces are elided)
=== Patterns ===
- Empress and Minister Facing the Palace(soul): empress([+3]), minister([+3])
```
Palaces come out in slot order on the chart, not in palace-name order. The closing `Patterns`
section lists every hit of the [pattern engine](/en/docs/guide/concepts/patterns) on the natal
chart, one per line: pattern name, the palace it lands in, and the stars that form it.
`Lunar Date` is the only field that is not localized: the lunar date is written with Chinese
numerals whatever the chart language. `二〇二四年腊月初二` below is the 2nd day of the 12th lunar
month, 2024. `Chinese Date` is the four pillars romanized in pinyin.
## What the horoscope text looks like [#what-the-horoscope-text-looks-like]
Target date 2025-1-1, early Zi hour:
```text
=== Horoscope ===
Target Date: 2025-1-1 / 二〇二四年腊月初二
--- Decadal Fortune ---
Decadal Fortune Soul Palace: Natal spouse (gengchen)
Decadal Fortune Mutagen: sunA, generalB, moonC, fortunateD
Decadal Fortune Patterns: Marshal, Rebel and Wolf(soul), Meeting of Wind and Cloud(soul)
spouse (wealth):
Major Stars: general([+1])[B], minister([+3])
Minor Stars: horse
Scope Stars: horse(D)
siblings (children):
Major Stars: sun([+3])[A], sage([+3])
Scope Stars: artist(D)
...
Age Fortune Soul Palace: Natal career (Nominal Age 25)
Age Fortune Palace Names: career, friends, surface, health, wealth, children, spouse, siblings, soul, parents, spirit, property
Age Fortune Mutagen: fortunateA, advisorB, scholarC, judgeD
Major Stars: judge([0]), empress([+3])
Minor Stars: officer
Adjective Stars: gifted, frail
--- Yearly ---
Yearly Soul Palace: Natal spouse (jiachen)
Yearly Mutagen: judgeA, rebelB, generalC, sunD
Yearly Patterns: Marshal, Rebel and Wolf(soul), Money and Horse Galloping Together(spouse), Scholar and Artist Flanking Life(soul) [Broken], ...
spouse (wealth):
Major Stars: general([+1])[B], minister([+3])
Minor Stars: horse
Scope Stars: money(Y), horse(Y)
Twelve Gods: sorrowing, varied
...
Monthly Soul Palace: Natal friends (dingchou)
Monthly Palace Names: property, career, friends, surface, health, wealth, children, spouse, siblings, soul, parents, spirit
Monthly Mutagen: moonA, fortunateB, advisorC, advocatorD
Monthly Scope Stars: attractive(M)(property), artist(M)(surface), tangled(M)(surface), money(M)(health), ...
Monthly Patterns: Advisor, Moon, Fortunate and Sage(soul), Sun Shining on Thunder Gate(career), ...
Daily Soul Palace: Natal surface (gengwoo)
Daily Palace Names: spirit, property, career, friends, surface, health, wealth, children, spouse, siblings, soul, parents
Daily Mutagen: sunA, generalB, moonC, fortunateD
Daily Scope Stars: artist(d)(property), cheerful(d)(property), aide(d)(health), ...
Daily Patterns: Fire and Wolf(soul), Bell and Wolf(soul), Marshal, Rebel and Wolf(soul), ...
Hourly Soul Palace: Natal surface (bingzi)
Hourly Palace Names: spirit, property, career, friends, surface, health, wealth, children, spouse, siblings, soul, parents
Hourly Mutagen: fortunateA, advisorB, scholarC, judgeD
Hourly Scope Stars: horse(H)(spirit), attractive(H)(property), tangled(H)(career), ...
Hourly Patterns: Fire and Wolf(soul), Bell and Wolf(soul), Marshal, Rebel and Wolf(soul), ...
```
The decadal, yearly and finer scopes each carry a patterns line from their own perspective, with
palace names written as re-laid out at that scope; age fortune has no pattern perspective and
carries only its re-laid-out palace names and mutagens. When the subject
has not yet entered the decadals, the decadal section's heading and labels all read `Childhood
Fortune` instead of `Decadal Fortune` — childhood and decadal are different reading semantics.
## Format conventions [#format-conventions]
| Notation | Meaning |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `judge([0])` | The parentheses hold [brightness](/en/docs/guide/concepts/stars#brightness) on a −3…+3 scale |
| `sun([+3])[A]` | The square brackets hold the [mutagen](/en/docs/guide/concepts/mutagen); `A`/`B`/`C`/`D` are Lu/Quan/Ke/Ji |
| `--- career [Body Palace] ---` | The bracket marks this palace as also being the Body palace |
| `--- spouse [Original Palace] ---` | The bracket marks this palace as the [Original palace](/en/docs/guide/concepts/palaces#the-original-palace) |
| `- Empress and Minister Facing the Palace(soul): …` | A pattern line: pattern name, landing palace (immediately after the name), forming stars |
| `Scholar and Artist Flanking Life(soul) [Broken]` | `[Broken]` marks a pattern that forms but is broken |
| `Decadal Fortune Soul Palace: Natal spouse (gengchen)` | Which natal palace this level's Soul palace landed on; the parentheses hold that level's stem and branch |
| `Age Fortune Soul Palace: Natal career (Nominal Age 25)` | Where age fortune landed, with the [nominal age](/en/docs/guide/concepts#four-concepts-to-get-straight-first) (虚岁, the East Asian reckoning that starts at 1 on the day of birth) in parentheses |
| `spouse (wealth):` | Inside a horoscope section, **the name in front is this level's re-laid-out palace name and the parentheses hold the natal palace name** |
| `Twelve Gods: dissipated, gossip, sorrowing, varied` | In a natal palace this line is always one god from each of the four groups, in the order Changsheng-12, Boshi-12, Sui-qian-12, Jiang-qian-12; the per-palace `Twelve Gods` line in the yearly section holds only the Sui-qian and Jiang-qian pair |
| `Age Fortune Palace Names: …`, `Monthly Palace Names: …` | That level's twelve re-laid-out palace names, in **natal slot order** (starting from the Yin palace) |
| `Scope Stars: horse(D)` | Flowing stars, with a scope suffix: `(D)` decadal, `(Y)` yearly, `(M)` monthly, `(d)` daily, `(H)` hourly |
| `attractive(M)(property)` | On monthly and finer scopes, the second parenthesis holds the re-laid-out palace the flowing star lands in |
`spouse (wealth):` says "within this decadal this cell is called the Spouse palace, and on the natal
chart it is the Wealth palace". A horoscope reading goes by the name in front; the one in
parentheses is there so you can map back to the natal chart. See
[Horoscopes](/en/docs/guide/concepts/horoscope#the-same-cell-a-different-palace-name).
`(D)` and `(Y)` after a flowing star mark the scope. But `considery(Y)` is the literal en-US name of
the star Nianjie (年解), suffix included — iztro's vocabulary bakes it in to separate 年解 from 解神
(`jieshen`, `considery`). So `considery(Y)` appears among the **Adjective Stars** of the natal chart,
where no scope suffix is implied. Verify by key, never by parsing the name.
Date fields echo the input verbatim, without zero padding: pass `"2000-8-16"` and you get
`Solar Date: 2000-8-16`.
## Length [#length]
An English natal text runs about 3,570 characters and the horoscope section about 5,980, so the two
together stay under 9,600 characters. A Chinese chart is shorter — star names are two characters
rather than a word — at about 1,810 and 2,490, around 4,300 combined. Any mainstream model's context
window holds either comfortably; trimming is normally unnecessary.
## Wiring it to a model [#wiring-it-to-a-model]
The generated text is pure description and contains no instructions. In practice, put your analysis
request in front of it:
```python
system = ("You are a Zi Wei Dou Shu analyst. Answer from the given chart only; "
"do not invent information that is not on it.")
user = f"""{chart.to_text()}
{chart.horoscope("2025-1-1", 0).to_text()}
Analyse this person's career prospects for 2025."""
```
For a fuller integration (tool calls, and not letting the model chart for itself) see
[Letting an AI read the chart](/en/docs/guide/guides/llm).
Prefer a Chinese chart even when your product is in English. The English vocabulary is iztro's
interpretive word list — Ziwei is `emperor`, Qisha is `marshal` — brightness degrades to marks such
as `[+3]`, mutagens become `A`/`B`/`C`/`D`, and a couple of entries are not English words at all
(`considery`, `disastery`). None of that matches the rendering conventional in English-language Zi
Wei writing, so a model may not recognise it.
Mainstream models handle Chinese Zi Wei terminology well, and feeding them the Chinese text gets
better results. When you genuinely need English, attach a
[star-name table](/en/docs/guide/concepts/stars#star-name-table) alongside it.
## When you need finer control [#when-you-need-finer-control]
to\_text covers the general case. To customise the text structure — describing only a few palaces, or
emitting JSON instead of text — walk the chart data and assemble it yourself; every field is public.
See the [Data model](/en/docs/guide/data-model).
# Letting an AI read the chart (/en/docs/guide/guides/llm)
Charting to the library, reading to the model — how to wire x-iztro into an AI application, plus a few traps already hit.
*For: developers · product and decision makers*
This is the single most important point. Language models cannot compute stems, branches and star
placements reliably — they produce results that **look plausible and are wrong**, wrong in no
discernible pattern, and you cannot tell from the output.
Charting is deterministic computation; give it to the library. The model only interprets. That
division of labour is the whole premise of putting Zi Wei into an AI application.
## The minimal integration [#the-minimal-integration]
Turn the chart into text, put your analysis request in front of it, and send them together:
```python
from x_iztro import Astro
astro = Astro()
chart = astro.by_solar("2000-8-16", 2, "female")
system = ("You are a Zi Wei Dou Shu analyst. Answer from the given chart only; "
"do not invent information that is not on it.")
user = f"""{chart.to_text()}
{chart.horoscope("2025-1-1", 0).to_text()}
Analyse this person's career prospects for 2025."""
```
For what the generated text looks like and how to read its format, see
[Semantic text](/en/docs/guide/guides/to-text).
## Exposing it as a tool call [#exposing-it-as-a-tool-call]
Letting the model decide when to chart is more flexible than hard-coding the flow in the
application: the model handles understanding and interpretation, x-iztro gets the numbers right. A
minimal tool definition:
```python
{
"name": "cast_chart",
"description": "Zi Wei Dou Shu charting. Given a Gregorian birthday, hour index and gender, "
"returns a structured description of the complete natal chart.",
"input_schema": {
"type": "object",
"properties": {
"solar_date": {"type": "string", "description": "Gregorian birthday, format YYYY-M-D"},
"time_index": {"type": "integer", "minimum": 0, "maximum": 12,
"description": "hour index; 0 = early Zi hour (00-01), "
"12 = late Zi hour (23-24)"},
"gender": {"type": "string", "enum": ["male", "female"]},
},
"required": ["solar_date", "time_index", "gender"],
},
}
```
The implementation just calls `chart.to_text()` and returns the text. Make the horoscope a
separate tool (one extra parameter, the target date) so the model can fetch it when it needs it.
A user saying "11 at night" means index `12`, not `0`, and the model will not work that out for
itself. Put the meaning of 0–12 in the parameter description, or have the tool take a birth time as
`HH:MM` and do the conversion yourself.
## Feed the model a Chinese chart [#feed-the-model-a-chinese-chart]
The chart itself is independent of the chart language, but the generated prompt follows it. The
default Chinese chart is the better choice, even for an English-language product.
Mainstream models handle Chinese Zi Wei terminology well. An English chart, by contrast, uses
iztro's interpretive word list (Ziwei is `emperor`, Qisha is `marshal`), degrades brightness to
marks such as `[+3]`, writes mutagens as `A`/`B`/`C`/`D`, and includes a couple of entries that are
not English words (`considery`, `disastery`). None of that matches the rendering conventional in
English-language Zi Wei writing, so a model may not recognise it.
When you need English output, the move is to **feed the model a Chinese chart and ask it to answer
in English**, rather than switching to an English chart.
## Predicate on keys [#predicate-on-keys]
If your application branches on the contents of a chart ("use the more cautious script when the Soul
palace holds Hua Ji"), predicate on the
[language-independent keys](/en/docs/guide/guides/keys) rather than matching text — otherwise
switching chart language makes every branch fail silently.
```python
soul = chart.palace("soulPalace")
if soul.has_mutagen("sihuaJi"):
prompt_style = "cautious"
```
## Don't treat the model's reading as a computed result [#dont-treat-the-models-reading-as-a-computed-result]
A model may quietly "fill in" information the chart does not carry — an extra star, a misstated
decadal range, two palace names swapped.
If a reading feeds back into your product (written to a database, pushed as a notification, driving
a decision), take every fact that can be read off the chart *from the chart*, not from the model's
prose. Treat model output as text and nothing more.
## Letting an AI read this documentation [#letting-an-ai-read-this-documentation]
This site also serves plain-text endpoints intended for model consumption (`llms.txt`, per-page
Markdown) — see
[Documentation endpoints for AI](/en/docs/guide/guides/llms-txt).
# Knowledge packs (/en/docs/guide/guides/knowledge-pack)
How reading texts and school-specific attributes are kept out of the core, what the bundled default pack contains, how to write an overlay, and how to read one from each language.
*For: anyone who wants written interpretation on top of a chart*
Casting a chart gives you facts: the Soul palace sits at Wu, Wuqu is in the Wealth palace carrying
the Quan transformation, this chart forms 府相朝垣. The next questions — "what does Wuqu mean",
"what is good about 府相朝垣" — are not facts. They are **opinions**, and different schools,
different books and different teachers answer them differently.
x-iztro keeps the two apart. The core only judges facts (charting, horoscopes, patterns); reading
texts and the school-specific star attributes live in a **knowledge pack**. A pack is a JSON file
whose protocol is "language-independent key → text and attributes". One default pack ships inside
the library so everything works out of the box; if you disagree with what it says, write an overlay
pack and change those entries.
## What belongs where [#what-belongs-where]
| | Core | Knowledge pack |
| --------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| Contents | Twelve palaces, star placement, brightness, transformations, horoscopes, pattern hits | Star readings, pattern readings, palace and transformation meanings, glossary, school-specific star attributes |
| Nature | Facts, checkable field by field against iztro | Opinions; another school means another pack |
| What being wrong looks like | The chart is miscast | You disagree with the reading |
| How to change it | You cannot (changing it means it is no longer this algorithm) | Swap the pack or write an overlay |
The seam between them is the **language-independent key**: `ziweiMaj` for a star,
`zi_fu_tong_gong` for a pattern, `soulPalace` for a palace, `sihuaLu` for a transformation. Every
field the core emits carries these keys (see [the key contract](/en/docs/guide/guides/keys)), so you
take a key straight to the pack — no matching on translated names, and the chart language never
enters into it.
## What the bundled default pack contains [#what-the-bundled-default-pack-contains]
| Section | Entries | Contents |
| ---------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `stars` | 162 | 14 major, 14 minor, 38 adjective, 46 decorative and 50 flowing stars — every `StarKey` has an entry. The major, minor, adjective and decorative ones carry the card attributes (yin-yang, five elements, dipper, chemistry, career, duty, aliases, element colour, energy colour) plus a body of text; the 14 major stars also carry readings for pairing with each other major star; the flowing-star entries (`category: "flow"`) are cross-references pointing at their natal minor-star counterparts, with the machine-readable table served by `flow_star_counterparts` (Go `FlowStarCounterparts`) |
| `patterns` | 64 | Classical quotations, a prose description of the conditions, and the reading |
| `palaces` | 12 | What each of the twelve palaces means |
| `mutagens` | 4 | What Lu, Quan, Ke and Ji each mean |
| `concepts` | 49 | Glossary and basic concepts (same palace, the palace in question, the Body palace, six-harmony branches, the surrounded set, flying-star transformations …) |
The content comes from the 学习 (Learn) pages of
[iztro-docs](https://github.com/SylarLong/iztro-docs) (MIT License, by Sylar Long), pinned to a
source commit; the pack's `source` section records the origin, commit, licence and author in full.
The text has been edited by x-iztro into third-person reference prose (declared in
`source.adapted`). Text fields are Markdown.
There is no bundled pack for the other five languages: Rust's `KnowledgePack::builtin` returns
`None`, Python and Go raise `invalid_argument`. For another language, write your own pack — or hand
the Chinese entries to an LLM along with the chart and let it translate as it interprets.
The default pack adds roughly 380 KB to the wasm binary embedded in the Go package. The Rust and
Python sides embed the same data.
## What a pack looks like [#what-a-pack-looks-like]
The complete field reference is
[`knowledge/SCHEMA.md`](https://github.com/x-haose/x-iztro/blob/main/knowledge/SCHEMA.md) in the
repository. Every entry and every field is optional — what is missing simply was not written:
```json
{
"schema": 1,
"id": "iztro-docs",
"version": "2026-08-19+ec2d58b",
"language": "zh-CN",
"extends": null,
"source": {
"name": "iztro-docs",
"url": "https://github.com/SylarLong/iztro-docs",
"commit": "ec2d58bb8b2a0d243d91212a1e3c87ab866858ee",
"license": "MIT",
"author": "Sylar Long",
"retrievedAt": "2026-08-19",
"adapted": "文本由 x-iztro 在 iztro-docs 原文基础上整理改写为第三人称释义口吻……"
},
"stars": {
"ziweiMaj": {
"name": "紫微",
"category": "major",
"group": null,
"attributes": {
"yinYang": "yin",
"fiveElements": "earth",
"stem": "ji",
"dipper": "中天星系",
"chemistry": "尊贵",
"career": "官禄主",
"duty": "众星枢纽,长五行,孕万物",
"aliases": ["帝王星", "老板星", "俸禄星"],
"elementColor": "黄色",
"energyColor": "紫光"
},
"intro": "紫微星号称 `帝王星`,并非指紫微坐命者能成帝王……",
"combinations": { "tianfuMaj": "紫微星和 `天府星` 都是帝星……" }
}
},
"patterns": {
"zi_fu_tong_gong": {
"name": "紫府同宫",
"quotes": ["紫府同宫终身福厚。"],
"conditions": "指紫微星和天府星同宫,这两颗星只会在寅宫和申宫同宫;其组合特质与紫微天府星曜组合一致。",
"intro": "“终身福厚”并非定数,但紫府同宫格的人一定无法接受平凡的人生……"
}
},
"palaces": { "soulPalace": { "name": "命宫", "intro": "命宫是决定星盘主人属性的宫位……" } },
"mutagens": { "sihuaLu": { "name": "化禄", "intro": "**五行**:土;**意象**:开心、忙碌、增加、包容、多\n\n化禄星简称 `禄`……" } },
"concepts": { "tong-gong": { "title": "遇、加、逢、同宫、同度", "intro": "指星曜在同一个宫位里面……" } }
}
```
Keys are always language-independent; the values above are Chinese because the bundled pack is
zh-CN.
## Reading a pack [#reading-a-pack]
```rust
use x_iztro::{KnowledgePack, Language, StarKey};
let pack = KnowledgePack::builtin(Language::ZhCN).expect("zh-CN has a builtin pack");
let ziwei = pack.star(StarKey::ZiweiMaj).unwrap();
println!("{:?} {:?}", ziwei.name, ziwei.attributes.aliases);
let head: String = pack.star_intro(StarKey::ZiweiMaj).unwrap().chars().take(12).collect();
println!("{head}");
```
```text
Some("紫微") Some(["帝王星", "老板星", "俸禄星"])
紫微星号称 `帝王星`,
```
```python
from x_iztro import KnowledgePack
from x_iztro.enums import MajorStar
pack = KnowledgePack.builtin()
ziwei = pack.star(MajorStar.ZIWEI)
print(ziwei.name, ziwei.attributes.aliases)
print(pack.star_intro(MajorStar.ZIWEI)[:12])
```
```text
紫微 ['帝王星', '老板星', '俸禄星']
紫微星号称 `帝王星`,
```
```go
pack, err := iztro.BuiltinKnowledgePack(iztro.LanguageZhCN)
if err != nil {
log.Fatal(err)
}
ziwei := pack.Star(iztro.StarZiweiMaj)
fmt.Println(ziwei.Name, ziwei.Attributes.Aliases)
fmt.Println(string([]rune(pack.StarIntro(iztro.StarZiweiMaj))[:12]))
```
```text
紫微 [帝王星 老板星 俸禄星]
紫微星号称 `帝王星`,
```
A key that is not in the pack comes back empty everywhere: `None` in Rust and Python, `nil` in Go
(and an empty string from `StarIntro`).
## Pairing it with pattern hits [#pairing-it-with-pattern-hits]
The `key` on a pattern hit is exactly the key used in the pack's `patterns` section:
```rust
let pack = KnowledgePack::builtin(Language::ZhCN).unwrap();
let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::ZhCN, Config::default())?;
for hit in chart.patterns() {
let entry = pack.pattern(hit.key).unwrap();
let quote = entry.quotes.as_ref().and_then(|q| q.first());
println!("{} | {:?}", translate_pattern(hit.key, Language::ZhCN), quote);
}
```
```text
府相朝垣 | Some("府相朝垣命必荣")
```
```python
pack = KnowledgePack.builtin()
chart = Astro().by_solar("2000-8-16", 2, "female")
for hit in chart.patterns():
entry = pack.pattern(hit.key)
print(hit.name, "|", entry.quotes[0])
```
```text
府相朝垣 | 府相朝垣命必荣
```
```go
pack, _ := iztro.BuiltinKnowledgePack(iztro.LanguageZhCN)
chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageZhCN, nil)
hits, _ := chart.Patterns(nil)
for _, hit := range hits {
entry := pack.Pattern(hit.Key)
fmt.Println(hit.Name, "|", entry.Quotes[0])
}
```
```text
府相朝垣 | 府相朝垣命必荣
```
Stars work the same way: take each star's `key` from the chart to `pack.star(key)`, a palace's
`palaceNameKey` to `pack.palace(key)`, and a transformation key to `pack.mutagen(key)`.
## Writing an overlay pack [#writing-an-overlay-pack]
An overlay is the same format, containing only the entries and fields you want to change, with
`extends` naming the pack it overlays. This one replaces Ziwei's reading and aliases and swaps out
the reading for 紫府同宫, leaving everything else untouched:
```json
{
"schema": 1,
"id": "my-school",
"version": "2026-08-19",
"language": "zh-CN",
"extends": "iztro-docs",
"stars": {
"ziweiMaj": {
"intro": "紫微在我这一派看来先看格局高低,再论性情。",
"attributes": { "aliases": ["帝座"] }
}
},
"patterns": {
"zi_fu_tong_gong": { "intro": "紫府同宫,我只把它当作起点高,不当作福厚。" }
}
}
```
Merging produces a new pack:
```rust
let base = KnowledgePack::builtin(Language::ZhCN).unwrap();
let overlay = KnowledgePack::from_json(&std::fs::read_to_string("my-school.json")?)?;
let pack = base.merged(&[&overlay]);
let ziwei = pack.star(StarKey::ZiweiMaj).unwrap();
println!("{:?} {:?} {:?}", ziwei.name, ziwei.attributes.aliases, ziwei.attributes.chemistry);
println!("{:?}", pack.pattern_intro(PatternKey::ZiFuTongGong));
```
```python
base = KnowledgePack.builtin()
overlay = KnowledgePack.from_json(open("my-school.json", encoding="utf-8").read())
pack = base.merged(overlay)
ziwei = pack.star(MajorStar.ZIWEI)
print(ziwei.name, ziwei.attributes.aliases, ziwei.attributes.chemistry)
print(pack.pattern_intro(PatternKey.ZI_FU_TONG_GONG))
```
```go
base, _ := iztro.BuiltinKnowledgePack(iztro.LanguageZhCN)
data, _ := os.ReadFile("my-school.json")
overlay, err := iztro.ParseKnowledgePack(data)
if err != nil {
log.Fatal(err)
}
pack, err := base.Merged(overlay)
if err != nil {
log.Fatal(err)
}
ziwei := pack.Star(iztro.StarZiweiMaj)
fmt.Println(ziwei.Name, ziwei.Attributes.Aliases, ziwei.Attributes.Chemistry)
fmt.Println(pack.PatternIntro(iztro.PatternZiFuTongGong))
```
All three produce the same result: Ziwei keeps its `name` (紫微) and `chemistry` (尊贵) while
`intro` and `aliases` come from the overlay; 紫府同宫 keeps its `quotes` and `conditions` and takes
the new `intro`.
Merging is implemented once, in the Rust core. Python's `merged` and Go's `Merged` both call into
it, so the merged result is byte-identical across the three languages instead of each side
re-implementing the rules.
## Merge rules [#merge-rules]
Starting from the base pack, each section (`stars`, `patterns`, `palaces`, `mutagens`, `concepts`)
is merged key by key:
* For an entry present in the overlay, its **non-null fields** replace the corresponding fields of
the same-keyed base entry; fields it does not mention are kept
* `attributes` and `combinations` merge the same way, field by field and sub-key by sub-key
* Array fields (`aliases`, `quotes`) are replaced wholesale, never merged element by element
* Keys the base does not have are added
* Writing a field explicitly as `null` does **not** delete the base content (absent and null mean
the same thing); to delete, replace the whole pack
* After merging, `id`, `version`, `language` and `source` come from the overlay when non-empty,
while `extends` stays the base's
A `schema` newer than this library supports is an error rather than a best-effort parse.
## Why the star attributes live here [#why-the-star-attributes-live-here]
A star's five-element attribution looks like a fact but is also an opinion. iztro's own `starsInfo`
table and the iztro-docs star cards already disagree with each other:
| Star | iztro `starsInfo` | iztro-docs card |
| ------------ | ----------------- | ------------------------------------------------------- |
| 贪狼 (Tanlang) | Water | 甲 (Jia) Wood — qi is Water |
| 巨门 (Jumen) | yin Earth | 癸 (Gui) Water and 己 (Ji) Earth (holding Metal and Wood) |
Two data sets by the same author disagreeing is the clearest sign that these attributes are a
school's reading, not a single answer. So x-iztro's core `StarInfo` stays value-for-value identical
to iztro's (code ported from it does not change behaviour), while the card attributes go into the
knowledge pack where they can be swapped.
## Later [#later]
The same protocol supports loading and distributing overlay packs, and handing a pack to an LLM
along with the chart. That belongs to the application layer, not to this library.
## API reference [#api-reference]
* [Rust — knowledge](/en/docs/rust/knowledge)
* [Python — knowledge](/en/docs/python/knowledge)
* [Go — KnowledgePack](/en/docs/go/knowledge)
## Sources and credit [#sources-and-credit]
Every text in the default pack comes from the 学习 (Learn) pages of
[iztro-docs](https://github.com/SylarLong/iztro-docs), MIT License, by Sylar Long. The pack
protocol, the editorial rewrite of the default pack and the three-language API are x-iztro's own work.
# Reverse lookup (/en/docs/guide/guides/reverse)
Recover candidate birth dates from four BaZi pillars or from chart features - what each entry point means, how pillars follow the Config boundaries, the 60-year cycle, and truncation semantics.
*For: people who remember the chart but not the birthday; people who need to turn a BaZi into a Zi Wei chart*
Charting goes "birth moment → chart". The reverse need comes up all the time:
* you hold an old chart or a set of BaZi pillars, but the birthday is lost;
* someone gives you their BaZi but not a solar birth date, and casting a Zi Wei chart needs the solar date and hour;
* all you remember is "soul palace in Wu, Wood 3rd class, Ziwei in the soul palace" and you want the day back.
x-iztro provides two reverse entry points. Both return **birth candidates**
(solar date + hour index) that you can feed straight back into charting:
| Entry point | Input | Meaning |
| --------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------- |
| `solar_dates_by_bazi` | the four BaZi pillars | every birth moment in range whose pillars are exactly these |
| `reverse_chart` | soul/body palace branches, five elements class, star placements, birth-year mutagens | every birth moment in range whose chart satisfies all the conditions |
Both are implemented as "pruned enumeration + full re-charting": cheap table
lookups discard impossible days wholesale, and each survivor is verified with
the very same code the forward charting uses. **Reverse results therefore have
zero divergence from forward charting** — every candidate really does satisfy
the conditions when charted, and the target birth moment is always among the
candidates.
## From BaZi pillars to birth dates [#from-bazi-pillars-to-birth-dates]
```rust
use x_iztro::*;
// 庚辰 甲申 丙午 庚寅
let cands = solar_dates_by_bazi(
(HeavenlyStem::Geng, EarthlyBranch::Chen),
(HeavenlyStem::Jia, EarthlyBranch::Shen),
(HeavenlyStem::Bing, EarthlyBranch::Wu),
(HeavenlyStem::Geng, EarthlyBranch::Yin),
(1900, 2100),
&Config::default(),
)?;
for c in &cands {
println!("{} {}", c.solar_date, c.time_index);
}
```
```python
from x_iztro import solar_dates_by_bazi
from x_iztro.enums import EarthlyBranch as B, HeavenlyStem as S
# 庚辰 甲申 丙午 庚寅
cands = solar_dates_by_bazi(
(S.GENG, B.CHEN), (S.JIA, B.SHEN), (S.BING, B.WU), (S.GENG, B.YIN),
year_range=(1900, 2100),
)
for c in cands:
print(c.solar_date, c.time_index)
```
```go
cands, err := iztro.SolarDatesByBazi(
iztro.Pillar{iztro.StemGeng, iztro.BranchChen}, // 庚辰
iztro.Pillar{iztro.StemJia, iztro.BranchShen}, // 甲申
iztro.Pillar{iztro.StemBing, iztro.BranchWu}, // 丙午
iztro.Pillar{iztro.StemGeng, iztro.BranchYin}, // 庚寅
1900, 2100, nil)
if err != nil {
log.Fatal(err)
}
for _, c := range cands {
fmt.Println(c.SolarDate, c.TimeIndex)
}
```
**Output** (identical in all three languages)
```text
1940-8-31 2
2000-8-16 2
2060-8-1 2
```
### Multiple solutions and the 60-year cycle [#multiple-solutions-and-the-60-year-cycle]
The sexagenary year cycle repeats every 60 years, so the same four pillars
recur roughly every 60 years apart — a set of pillars over a wide range is
**inherently multi-solution**. The example above has three hits in 1900–2100.
Narrow the year range to within one cycle (60 years) and usually a single
solution remains; with a wide range, common sense about the person's age picks
the right candidate.
### Two candidates around the Zi hour [#two-candidates-around-the-zi-hour]
The Zi hour straddles midnight and splits into the early Zi hour (index 0,
0:00–1:00 of the day) and the late Zi hour (index 12, 23:00–24:00), and under
the default `day_divide` reading the late Zi hour takes the **next** day's day
pillar. A set of pillars whose hour branch is Zi can therefore yield two
candidates on adjacent days: the early Zi hour of one day and the late Zi hour
of the day before. This is not an error — both candidates chart back to exactly
the same four pillars; the BaZi alone cannot tell them apart.
## Which reading of the pillars? It follows Config [#which-reading-of-the-pillars-it-follows-config]
The four pillars are not absolute: when the year changes (lunar new year or
the Beginning of Spring, 立春), when the month changes (the 1st or the solar
term), and which day the
late Zi hour belongs to all differ between schools. In x-iztro these
boundaries live on [`Config`](/en/docs/guide/guides/config): `year_divide`
governs the year pillar, `horoscope_divide` the month pillar, `day_divide` the
late-Zi-hour day pillar.
`solar_dates_by_bazi` interprets the pillars **under the config you pass** —
the same semantics as the `raw_dates.chinese_date` a charted astrolabe reports.
The same birth moment can carry different pillars under different readings.
Take 2001-2-1 in the Mao hour, which falls after the lunar new year (Jan 24)
but before the Beginning of Spring (立春, Feb 4):
| Reading | Pillars (year, month, day, hour) |
| --------------------------------------------------------------- | ------------------------------------------------- |
| Default (year at lunar new year, month at the 1st) | 辛巳 Xin-Si · 庚寅 Geng-Yin · 乙未 Yi-Wei · 己卯 Ji-Mao |
| `Exact` (year at the Beginning of Spring, month at solar terms) | 庚辰 Geng-Chen · 己丑 Ji-Chou · 乙未 Yi-Wei · 己卯 Ji-Mao |
The year pillar (辛巳 → 庚辰) and the month pillar (庚寅 → 己丑) both change between the two
readings; the day and hour pillars stay the same.
So before reversing a BaZi, find out which reading produced it and pass the
matching config. Chart with a config, reverse with the same config, and the
round trip always closes:
```rust
let cfg = Config {
year_divide: YearDivide::Exact,
horoscope_divide: HoroscopeDivide::Exact,
..Config::default()
};
let chart = by_solar("2001-2-1", 3, Gender::Female, true, Language::EnUS, cfg.clone())?;
let p = chart.raw_dates.chinese_date;
let cands = solar_dates_by_bazi(p.yearly, p.monthly, p.daily, p.hourly, (1980, 2020), &cfg)?;
assert!(cands.iter().any(|c| c.solar_date == "2001-2-1" && c.time_index == 3));
```
## From chart features to birth dates [#from-chart-features-to-birth-dates]
When you remember the chart but cannot produce a full BaZi, use
`reverse_chart`. Every condition is optional, but at least one must be given;
all given conditions must hold **simultaneously**:
| Condition | Meaning |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `soul_branch` / `body_branch` | earthly branch of the soul / body palace |
| `five_elements_class` | five elements class |
| `stars` | star placements (star + branch), any number of them |
| `mutagens` | which star carries each birth-year mutagen \[Lu, Quan, Ke, Ji]; give any subset |
| `year_range` | inclusive solar year range, default 1900–2100 |
| `fix_leap` | leap month correction, same meaning as the charting parameter; defaults to `true` when absent (a `*bool` on the Go side, `nil` meaning absent) |
| `limit` | candidate cap, 0 takes the default of 512 |
```rust
use x_iztro::*;
let r = reverse_chart(
&ReverseCriteria {
soul_branch: Some(EarthlyBranch::Wu),
five_elements_class: Some(FiveElementsClass::Wood3rd),
stars: vec![StarPosition { star: StarKey::ZiweiMaj, branch: EarthlyBranch::Wu }],
mutagens: [Some(StarKey::TaiyangMaj), None, None, None], // Taiyang carries Lu
year_range: (1998, 2002),
..Default::default()
},
&Config::default(),
)?;
println!("{} candidates, truncated = {}", r.candidates.len(), r.truncated);
```
```python
from x_iztro import ReverseCriteria, StarPosition, reverse_chart
from x_iztro.enums import EarthlyBranch, FiveElementsClass, MajorStar
r = reverse_chart(ReverseCriteria(
soul_branch=EarthlyBranch.WU,
five_elements_class=FiveElementsClass.WOOD_3,
stars=[StarPosition(star=MajorStar.ZIWEI, branch=EarthlyBranch.WU)],
mutagens=(MajorStar.TAIYANG, None, None, None), # Taiyang carries Lu
year_range=(1998, 2002),
))
print(len(r.candidates), "candidates, truncated =", r.truncated)
```
```go
r, err := iztro.ReverseChart(&iztro.ReverseCriteria{
SoulBranch: iztro.BranchWu,
FiveElementsClass: iztro.ClassWood3rd,
Stars: []iztro.StarPosition{{Star: iztro.StarZiweiMaj, Branch: iztro.BranchWu}},
Mutagens: [4]string{iztro.StarTaiyangMaj, "", "", ""}, // Taiyang carries Lu
YearRange: [2]int{1998, 2002},
}, nil)
if err != nil {
log.Fatal(err)
}
fmt.Println(len(r.Candidates), "candidates, truncated =", r.Truncated)
```
**Output**
```text
39 candidates, truncated = false
```
All 39 candidates fall in the 庚辰 year (2000-2-11 through 2001-1-11), the real
birth moment 2000-8-16 hour index 2 among them. Chart any of them and every
condition holds — soul palace in Wu, Wood 3rd class, Ziwei in the Wu palace,
Taiyang carrying Lu.
`reverse_chart` judgement also runs entirely under the config: the mutagen
table, the school and every boundary follow the config you pass, so charting a
candidate with the same config is guaranteed to satisfy the conditions.
Chart layout (star placement, brightness, mutagens) does not depend on gender —
gender only affects the direction the decadal horoscope advances. The target of
a reverse lookup is the birth moment, so the criteria carry no gender; chart the
recovered candidates with whichever gender applies.
Conditions can only be **natal chart** features: horoscope-scope flow stars
(运魁, 流昌 and the like) never appear on a natal chart, and passing one is an
error.
## Performance and truncation [#performance-and-truncation]
The cost is driven by how selective the conditions are: a soul palace branch,
the five elements class, major star placements and birth-year mutagens each
prune whole months or years of the search space — **the more specific the
conditions and the narrower the year range, the faster**. Order-of-magnitude
figures (Apple Silicon, release build, first call in a process): the 5-year feature lookup above
takes about 30 ms (including one-off table initialisation; repeat queries in the same process run
in about 1–2 ms); the same conditions over 1900–2100 hit the default candidate cap of 512 and
truncate after about 0.4 s (raising the cap, the full sweep of all 843 solutions takes about
0.7 s); a 200-year BaZi lookup takes about 0.1 s.
Loose conditions have very many solutions (a single soul palace branch matches
tens of thousands over the full range). When `limit` (default 512) is reached
the search **stops** and the result's `truncated` flag is set — later solutions
were never searched. This is truncation, not sampling. On `truncated = true`,
narrow `year_range` or add conditions and query again rather than raising
`limit` and brute-forcing.
## Error cases [#error-cases]
These return an `invalid_argument` error (`IztroError::InvalidArgument` in
Rust):
* a pillar whose stem and branch have mismatched polarity, such as 甲丑 — 甲 is
a yang stem and 丑 a yin branch, and no such pillar exists in the sexagenary
cycle;
* empty reverse criteria, or criteria containing a horoscope-scope flow star;
* a reversed year range, or one outside the supported span (solar 1583–9999).
Error classes and each language's error type are on
[Error handling](/en/docs/guide/guides/errors).
## API reference [#api-reference]
* Rust: [Reverse lookup](/en/docs/rust/reverse)
* Python: [Reverse lookup](/en/docs/python/reverse)
* Go: [Reverse lookup](/en/docs/go/reverse)
# Config in depth (/en/docs/guide/guides/config)
What each of the six switches changes, how to pass custom mutagen and brightness tables, and when you would actually notice a difference.
*For: developers · Zi Wei enthusiasts (the school differences in the first half need no code)*
Everything charting genuinely disagrees about is gathered into `Config`: six switches, plus two data
tables that can be replaced wholesale. The defaults match JS iztro exactly, so **passing no config
at all gives you the same chart iztro gives**.
| Switch | Accepts | Default | Governs |
| ------------------ | ---------------------------- | --------- | ----------------------------------------------------------------- |
| `year_divide` | `normal` / `exact` | `normal` | Which day the charting year's stem and branch turn over |
| `horoscope_divide` | `normal` / `exact` | `normal` | Which day horoscope stems/branches and the month pillar divide on |
| `age_divide` | `normal` / `birthday` | `normal` | When nominal age increments |
| `day_divide` | `forward` / `current` | `forward` | Whether the late Zi hour counts as today or tomorrow |
| `algorithm` | `default` / `zhongzhou` | `default` | Algorithm school |
| `astro_type` | `heaven` / `earth` / `human` | `heaven` | Charting perspective (heaven / earth / human plate) |
There are two override tables besides: `mutagens` (a custom mutagen table) and `brightness` (a
custom brightness table) — see
[Custom mutagen and brightness tables](#custom-mutagen-and-brightness-tables).
**Nominal age** (虚岁) is East Asian age reckoning: you are 1 at birth and gain a year at the turn of
the year rather than on your birthday.
## How to pass it [#how-to-pass-it]
```rust
use x_iztro::data::types::*;
let config = Config {
algorithm: Algorithm::Zhongzhou,
year_divide: YearDivide::Exact,
..Config::default()
};
by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, config)?;
```
```python
from x_iztro import ChartConfig
from x_iztro.enums import Algorithm, YearDivide
config = ChartConfig(
algorithm=Algorithm.ZHONGZHOU,
year_divide=YearDivide.EXACT,
)
astro.by_solar("2000-8-16", 2, "female", language="en-US", config=config)
```
```go
cfg := &iztro.Config{
Algorithm: "zhongzhou",
YearDivide: "exact",
}
iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, cfg)
```
Python and Go take only the keys you want to change and default the rest; Rust achieves the same
with `..Config::default()`.
***
## Year boundary: `year_divide` [#year-boundary-year_divide]
Decides which day **the year stem and branch used for charting** turn over on.
| Value | Turnover point |
| ------------------ | -------------------------------------------------------------- |
| `normal` (default) | The first day of the first lunar month (lunar New Year) |
| `exact` | The Beginning of Spring (立春, the solar term around 4 February) |
The year's stem and branch are the source of a whole chain: the natal mutagens, the soul and body
stars, the stems of the twelve palaces. So changing this switch can move a great deal of the chart.
**When you would notice**: for someone born between lunar New Year and the Beginning of Spring.
Those two dates are usually a few days to a couple of weeks apart, and a birthday inside that window
gets year stem-branch pairs one step apart under the two settings.
The Bazi (Four Pillars) system turns the year over at the Beginning of Spring without exception, so
choose `exact` when you need to line up with a Bazi chart. Standard Zi Wei practice turns the year
over at lunar New Year, and `normal` is also iztro's default. If in doubt, leave it alone — changing
it means no longer matching iztro's default output.
x-iztro reproduces a detail internal to iztro: not everything that depends on the year branch goes
through the same switch. The split is a fixed three:
1. **Follows `year_divide`'s year stem and branch**: the birth-year mutagens, the soul and body
stars, the stems of the twelve palaces, Lucun, Qingyang, Tuoluo, Tiankui, Tianyue, Tianma,
Hongluan, Tianxi, the twelve Changsheng gods and the twelve Boshi gods.
2. **Follows `horoscope_divide`'s year stem and branch**: every other year-derived adjective star,
plus the twelve Sui-qian gods and twelve Jiang-qian gods on the natal chart.
3. **Follows `horoscope_divide`'s month boundary**: the **month pillar** among the natal four
pillars.
The two switches can be set independently, so "major stars on one year branch and some adjective
stars on another" is a state that really occurs. It looks asymmetric, but it is iztro's actual
behaviour and has to be kept verbatim to hold zero divergence.
## Horoscope boundary: `horoscope_divide` [#horoscope-boundary-horoscope_divide]
Decides which day **horoscope stems and branches**, the **natal month pillar** and stem-branch month
numbering divide on.
| Value | Year boundary | Month boundary |
| ------------------ | ----------------------- | ---------------------------------------------------------------- |
| `normal` (default) | Lunar New Year | The first of the lunar month, month stem by the Five Tigers rule |
| `exact` | The Beginning of Spring | Solar terms |
**When you would notice**: when the query date lands early in the year (between lunar New Year and
the Beginning of Spring) or around any solar-term changeover, the yearly and monthly stem-branch
pairs shift by one step, which in turn changes the horoscope mutagens.
This switch also changes **the natal chart's month pillar**, not just horoscopes. Take 2000-8-5 in
the Yin hour: under `normal` the four pillars are `geng chen - jia shen - yi wei - wu yin`, and
under `exact` they are `geng chen - gui wei - yi wei - wu yin` — the month pillar moves from jia
shen (甲申) to gui wei (癸未).
## Nominal-age boundary: `age_divide` [#nominal-age-boundary-age_divide]
Decides when **nominal age** increments, which directly moves which palace age fortune lands on.
| Value | Increment point |
| ------------------ | --------------------------------------------- |
| `normal` (default) | A year is added at the turn of the lunar year |
| `birthday` | A year is added only after the lunar birthday |
**When you would notice**: when the query date falls between lunar New Year and the person's lunar
birthday. In that stretch the two settings differ by one nominal year, which puts age fortune on
adjacent palaces.
## Late Zi hour attribution: `day_divide` [#late-zi-hour-attribution-day_divide]
Decides which day the day pillar is taken from for someone born between 23:00 and 24:00 (hour index
`12`).
| Value | Behaviour |
| ------------------- | ------------------------------------------------------------------------------------------ |
| `forward` (default) | The late Zi hour belongs to the **following** day, and charting uses that day's day pillar |
| `current` | The late Zi hour belongs to the **current** day, charted as the early Zi hour of that day |
**When you would notice**: only on charts with hour index `12`; every other hour is unaffected.
`forward` pushes both the day pillar **and the lunar day used for placing Ziwei** to the following
day, while the lunar date string shown on the chart still reads the **day of birth**.
Take 2000-8-16 in the late Zi hour: the lunar date still displays `二〇〇〇年七月十七` (the 17th day of
the 7th lunar month), yet the four pillars come out as
`geng chen - jia shen - ding wei - geng zi` — the day pillar ding wei (丁未) already belongs to
17 August. Do not try to reason backwards from the lunar display string to the day pillar.
Whichever value you pick, the hour index field keeps the original input `12`; being attributed to
the following day does not turn it into `0` — so callers can always recover the true hour of birth.
## Algorithm school: `algorithm` [#algorithm-school-algorithm]
| Value | Notes |
| ------------------- | ---------------------------------------------------------------- |
| `default` (default) | The mainstream star-placement rules, matching JS iztro's default |
| `zhongzhou` | The Zhongzhou school |
Zhongzhou differs from the default school in four places, and **the mutagen table is not one of
them**:
| Change | `default` | `zhongzhou` |
| ------------------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------- |
| How the soul star is looked up | From the **Soul palace branch** | From the **birth-year branch** (so rearranging onto another Soul palace no longer moves it) |
| The twelve Sui-qian gods | Dahao `dahao` | Suipo `suipo` |
| Adjective stars | Jielu `jielu`, Kongwang `kongwang` | Jiekong `jiekong`, Jiesha `jieshaAdj`, Dahao `dahao`, Longde `longde` |
| Tianshang and Tianshi | Tianshang in Friends, Tianshi in Health | Swapped for the other gender polarity, the two stars trading positions |
Geng's Hua Ke is Taiyin under both schools. To adopt a different mutagen reading use the custom
mutagen table below; do not expect `algorithm` to do it.
```python
astro.by_solar("1990-11-5", 4, "male", language="en-US",
config=ChartConfig(algorithm=Algorithm.ZHONGZHOU))
```
## Charting perspective: `astro_type` [#charting-perspective-astro_type]
The Zhongzhou school reads one set of birth data as three charts, differing only in **which palace's
stem and branch the Five Elements class is taken from**:
| Perspective | Palace the class comes from | Soul palace of the new chart |
| ----------------------- | --------------------------- | ------------------------------------- |
| `heaven` (heaven plate) | Soul palace | Soul palace (i.e. the ordinary chart) |
| `earth` (earth plate) | Body palace | Body palace |
| `human` (human plate) | Spirit palace | Spirit palace |
Change the Five Elements class and the placement of Ziwei and Tianfu, the twelve palace names, the
Body palace branch, the twelve Changsheng gods, the decadals and the age fortune all move with it.
The minor stars, the adjective stars (Tianshang, Tianshi and Tiancai follow the Soul palace and are
re-placed), the twelve Boshi gods and the twelve Sui-qian and Jiang-qian gods carry over from the
heaven plate.
```python
earth = astro.by_solar("2000-8-16", 2, "female", language="en-US",
config=ChartConfig(astro_type=AstroType.EARTH))
```
```go
earth, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS,
&iztro.Config{AstroType: iztro.AstroEarth})
```
```rust
let earth = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS,
Config::default().with_astro_type(AstroType::Earth))?;
```
JS iztro puts `astroType` on the options object of `withOptions`, because its `config()` is a global
singleton that cannot hold a value that varies per chart. x-iztro's configuration is passed per call
in the first place, so it goes straight into `Config` and works from both charting entry points.
### Rearranging onto an arbitrary stem and branch [#rearranging-onto-an-arbitrary-stem-and-branch]
Beyond the heaven, earth and human plates, you can rearrange onto any stem and branch as the Soul
palace:
```python
body = chart.palace(PalaceName.BODY)
earth = chart.rearranged(body.heavenly_stem_key, body.earthly_branch_key)
```
```go
earth, _ := chart.Rearranged(body.HeavenlyStemKey, body.EarthlyBranchKey)
```
```rust
let earth = chart.rearranged(body.heavenly_stem, body.earthly_branch)?;
```
Rearranging onto the Body palace's stem and branch gives the same result as
`astro_type = earth`.
## Custom mutagen and brightness tables [#custom-mutagen-and-brightness-tables]
Mutagens and brightness are where school disagreement is most concentrated. `Config` lets you
**replace tables wholesale, by key**: supply the four mutagens for one heavenly stem and only that
stem changes, with every other stem still on the default table. Brightness works the same way.
### The mutagen table [#the-mutagen-table]
One heavenly stem takes four stars, always in the order **Lu, Quan, Ke, Ji**, and all four must be
given.
```rust
let config = Config::default().with_mutagens(
HeavenlyStem::Geng,
[StarKey::TaiyangMaj, StarKey::WuquMaj, StarKey::TiantongMaj, StarKey::TianfuMaj],
);
by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, config)?;
```
```python
cfg = ChartConfig(mutagens={
"gengHeavenly": ["taiyangMaj", "wuquMaj", "tiantongMaj", "tianfuMaj"],
})
chart = astro.by_solar("2000-8-16", 2, "female", language="en-US", config=cfg)
```
```go
cfg := &iztro.Config{Mutagens: map[string][]string{
"gengHeavenly": {"taiyangMaj", "wuquMaj", "tiantongMaj", "tianfuMaj"},
}}
```
With geng switched to "Tiantong takes Ke, Tianfu takes Ji", the mutagens on this geng-year chart
become:
```text
wealth general B
children sun A
career empress D
health fortunate C
```
A custom mutagen table also changes **every flying-star predicate** — which four stars a palace stem
flies out comes from the same table.
### The brightness table [#the-brightness-table]
One star takes twelve brightness values, ordered by slot on the chart (slot 0 is the Yin palace),
and all twelve must be given; use an empty value (an empty string in Python and Go) where a slot has
no brightness.
```python
cfg = ChartConfig(brightness={
"ziweiMaj": ["miao", "wang", "de", "li", "ping", "bu",
"xian", "miao", "wang", "de", "li", "ping"],
})
```
1. **Keys only, never translated names**: `"ziweiMaj"` works, `"emperor"` and `"紫微"` do not.
2. **Lengths are validated strictly**: a mutagen entry must have 4 items and a brightness entry 12;
one too many or too few is an error.
3. **They are not echoed in the output**: the override tables are charting *input*, not part of the
chart, so the `config` echoed on the astrolabe holds only the six switches and both tables read
back empty. If you need a record of them, keep your own copy of the config you passed.
## The config travels with the chart [#the-config-travels-with-the-chart]
The config used for charting is stored on the astrolabe and horoscopes read it from there, so
**a horoscope always uses the same config as the chart it came from** — you cannot end up with a
Zhongzhou natal chart and a default-school horoscope.
```python
chart = astro.by_solar("2000-8-16", 2, "female", language="en-US",
config=ChartConfig(age_divide="birthday"))
h = chart.horoscope("2024-10-1", 0) # inherits age_divide=birthday
```
```go
chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS,
&iztro.Config{AgeDivide: "birthday"})
h, _ := chart.Horoscope("2024-10-1", 0) // as above
```
## Test coverage [#test-coverage]
The non-default values of the four boundary switches have 9,696 dedicated golden cases (covering
combinations at both the charting and horoscope layers), and the Zhongzhou chart types have another
12,488, spanning the Beginning-of-Spring window day by day, the late Zi hour, and the days around a
birthday — every boundary that produces a divergence. The custom mutagen and brightness tables have
a dedicated set of tests of their own. See [Accuracy](/en/docs/guide/about/accuracy).
# The language-independent key contract (/en/docs/guide/guides/keys)
Why star names must not drive predicates, what the key fields are, and how each of the three programming languages uses them.
*For: developers*
## The problem [#the-problem]
The text in a chart follows the chart language. The same star is `emperor` on an English chart,
紫微 on a Simplified Chinese one and `자미` on a Korean one. If a predicate is written as:
```python
# what not to do
if any(s.name == "emperor" for s in soul.major_stars):
...
```
then this code is correct only when `language="en-US"`. Switch to any other chart language and it
fails silently — no error, it just returns `False` forever. That class of bug is very hard to spot.
## The solution [#the-solution]
For every field that gets translated, x-iztro also provides a **language-independent key**. Key
values are iztro's i18n key names: independent of the chart language, and never changing.
```json
{
"name": "emperor",
"key": "ziweiMaj",
"brightness": "[+3]",
"brightnessKey": "miao",
"mutagen": "A",
"mutagenKey": "sihuaLu"
}
```
`name` is for people, `key` is for code.
## Which fields have keys [#which-fields-have-keys]
| Data | Translated field | Key field | Example value |
| -------------------------------- | --------------------- | ---------------------- | ------------------- |
| Star | `name` | `key` | `ziweiMaj` |
| Brightness | `brightness` | `brightnessKey` | `miao` |
| Mutagen | `mutagen` | `mutagenKey` | `sihuaLu` |
| Palace name | `name` | `nameKey` | `soulPalace` |
| Heavenly stem | `heavenly_stem` | `heavenlyStemKey` | `jiaHeavenly` |
| Earthly branch | `earthly_branch` | `earthlyBranchKey` | `ziEarthly` |
| Five Elements class | `five_elements_class` | `fiveElementsClassKey` | `water2nd` |
| Soul / body star | `soul` / `body` | `soulKey` / `bodyKey` | `ziweiMaj` |
| Gender | `gender` | `genderKey` | `male` |
| The twelve Changsheng gods | `changsheng12` | `changsheng12Key` | `changsheng` |
| The twelve Boshi gods | `boshi12` | `boshi12Key` | `boshi` |
| The twelve Jiang-qian gods | `jiangqian12` | `jiangqian12Key` | `jiangxing` |
| The twelve Sui-qian gods | `suiqian12` | `suiqian12Key` | `suijian` |
| Mutagen stars of the palace stem | — | `mutagenStarKeys` | `["taiyangMaj", …]` |
## How each programming language uses them [#how-each-programming-language-uses-them]
### Python: enums [#python-enums]
Every enum in `x_iztro.enums` is a `StrEnum`, and **a member's value is the key**.
```python
from x_iztro.enums import MajorStar, Mutagen, PalaceName, Brightness
MajorStar.ZIWEI # "ziweiMaj"
Mutagen.LU # "sihuaLu"
PalaceName.SOUL # "soulPalace"
Brightness.MIAO # "miao"
```
The predicate methods accept enums:
```python
soul = chart.palace(PalaceName.SOUL)
soul.has([MajorStar.ZIWEI])
soul.has_mutagen(Mutagen.LU)
```
Because they are `StrEnum`s they are also strings, so they compare directly against key fields:
```python
star.key == MajorStar.ZIWEI # True
```
### Go: constants [#go-constants]
The constants in `keys.go` have the keys as their values:
```go
iztro.PalaceSoul // "soulPalace"
iztro.StarZiweiMaj // "ziweiMaj"
iztro.MutagenLu // "sihuaLu"
iztro.BrightnessMiao // "miao"
soul := chart.Palace(iztro.PalaceSoul)
soul.Has(iztro.StarZiweiMaj)
star.WithMutagen(iztro.MutagenQuan)
star.WithBrightness(iztro.BrightnessMiao)
```
### Rust: the enums themselves [#rust-the-enums-themselves]
The Rust side needs no key fields — the structs hold enums to begin with, and translation happens
only at display time.
```rust
if soul.has(&[StarKey::ZiweiMaj]) { }
```
When you do need the key string (for your own serialization, say), call `as_key()`:
```rust
Palace::Soul.as_key(); // "soulPalace"
Mutagen::Lu.as_key(); // "sihuaLu"
Brightness::Miao.as_key(); // "miao"
```
## How this is verified [#how-this-is-verified]
The same birthday is charted in all six chart languages and every key field must match one for one —
a line held by the binding contract test (`golden_contract`) and by the end-to-end golden tests on
the Go and Python sides.
So this code gives the same answer in every chart language:
```python
for lang in ["zh-CN", "zh-TW", "en-US", "ja-JP", "ko-KR", "vi-VN"]:
chart = astro.by_solar("2000-8-16", 2, "female", language=lang)
soul = chart.palace(PalaceName.SOUL)
assert soul.has_mutagen(Mutagen.LU) == expected
```
## When text is acceptable [#when-text-is-acceptable]
Display. Only display. Any comparison that feeds an `if` should use a key.
# Multilingual output (/en/docs/guide/guides/i18n)
Six chart languages, which fields get translated, what switching language does to the result, and two-way conversion between keys and names.
*For: developers*
## Supported chart languages [#supported-chart-languages]
"Chart language" means which language the human-readable text in the output is written in. It has
nothing to do with which **programming language** you call from.
| Value | Language | Rust enum |
| ------- | ---------------------------- | ---------------- |
| `zh-CN` | Simplified Chinese (default) | `Language::ZhCN` |
| `zh-TW` | Traditional Chinese | `Language::ZhTW` |
| `en-US` | English | `Language::EnUS` |
| `ja-JP` | Japanese | `Language::JaJP` |
| `ko-KR` | Korean | `Language::KoKR` |
| `vi-VN` | Vietnamese | `Language::ViVN` |
```python
chart = astro.by_solar("2000-8-16", 2, "female", language="en-US")
```
```go
chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageJaJP, nil)
```
```rust
by_solar("2000-8-16", 2, Gender::Female, true, Language::KoKR, Config::default())?;
```
## What gets translated [#what-gets-translated]
Everything meant for a person to read:
* Star names, palace names, mutagen names, brightness names
* Heavenly stems, earthly branches, the Five Elements class
* Hour names and their clock ranges, zodiac sign, zodiac animal, gender
* The Chinese rendering of the lunar date, and the stem-branch display string
* Horoscope scope names (decadal / yearly / …)
**Not translated**: every key field and every numeric field — a star's `key`, a palace's `nameKey`,
palace indexes, decadal ranges, [nominal ages](/en/docs/guide/concepts#four-concepts-to-get-straight-first) (虚岁). See
[The key contract](/en/docs/guide/guides/keys).
1. **English and Korean have no brightness translations**, so the output is a mark: `[+3]`
(miaowang), `[+2]` (wangxiang), `[+1]` (dedi), `[0]` (liyi), `[-1]` (pinghe), `[-2]` (budedi),
`[-3]` (luoxian). Traditional Chinese, Japanese and Vietnamese have real translations.
2. **English mutagens print as `A`/`B`/`C`/`D`**, in the order Lu, Quan, Ke, Ji.
3. **The non-Chinese vocabularies come from iztro's word list and are not guaranteed to be the
rendering conventional among practitioners in that language.** Some entries are simply wrong —
Korean renders the Original palace as `라인`, a transliteration of the English word "line". A few
English entries are not words at all (`considery`, `disastery`). Translations are for display;
predicate on the key fields.
## Switching chart language does not change the chart [#switching-chart-language-does-not-change-the-chart]
The chart language affects only the translation layer. Across all six languages, for one birthday:
* The positions of the twelve palaces and the order of the palace names are identical.
* The stars in each palace are identical.
* Mutagens, brightness, decadals, age fortune and horoscope stems and branches are identical.
All that changes is which characters those things are written in. So the two charts below are equal
field for field apart from the text:
```python
zh = astro.by_solar("2000-8-16", 2, "female", language="zh-CN")
en = astro.by_solar("2000-8-16", 2, "female", language="en-US")
assert zh.palace(PalaceName.SOUL).index == en.palace(PalaceName.SOUL).index
assert zh.soul_key == en.soul_key
```
Consistency across the six chart languages is covered by the variant golden tests, with zero
tolerated deviation.
## Converting between keys and names [#converting-between-keys-and-names]
When you hold only a key (or only a name in some language), use the two-way lookup functions rather
than charting again:
```rust
translate_key("ziweiMaj", Language::EnUS); // Some("emperor")
key_of("emperor"); // Some("ziweiMaj")
key_of("자미"); // Some("ziweiMaj")
```
When the category is already known, the strongly typed versions are more direct and drop the
`Option`:
```rust
use x_iztro::{translate_palace, translate_star};
translate_star(StarKey::ZiweiMaj, Language::ViVN); // Tử Vi
translate_palace(Palace::Soul, Language::KoKR); // 명궁
```
```python
i18n.key_of("emperor") # ziweiMaj
i18n.translate("ziweiMaj", "en-US") # emperor
i18n.key_of("자미") # ziweiMaj
```
```go
key, _ := iztro.KeyOf("emperor") // ziweiMaj
name, _ := iztro.Translate(iztro.StarZiweiMaj, iztro.LanguageEnUS) // emperor
key, _ = iztro.KeyOf("자미") // ziweiMaj
```
Both Go functions return `(string, error)`: an unknown key or a failed reverse lookup gives an empty
string and an `*iztro.Error` of category `invalid_argument`.
Coverage is 260 keys across twelve categories: stars, palaces (including the Body and Original
palaces), heavenly stems, earthly branches, brightness, mutagens, the Five Elements class, gender,
zodiac animal, hour, zodiac sign and horoscope scope. The complete list with per-entry notes is on
the i18n page for [Rust](/en/docs/rust/i18n), [Python](/en/docs/python/i18n) and
[Go](/en/docs/go/i18n).
`key_of("no such name")` returns `None` / an empty string; it does not echo the argument back.
There is also the matter of homographs: different keys translate to the same name in some languages
(`horse`, `dragon`, `유시` and others). Reverse lookup takes the first hit in a fixed scan order,
matching iztro's `kot` case for case. To pin down a category use `key_of_in` (Rust) /
`key_of(text, key_filter)` (Python) / `KeyOfIn` (Go), passing the shared suffix of the key names to
disambiguate: `"Maj"` searches only the fourteen major stars, `"Min"` only the minor stars,
`"Palace"` only palaces, `"Hour"` only hours.
## The three programming languages hold values differently [#the-three-programming-languages-hold-values-differently]
| | What the chart holds | Cost of switching chart language |
| ------ | -------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| Rust | Enums (`StarKey`, `Palace`, …), with only a few display fields as `String` | Call a translation function; one chart can emit several languages at once |
| Python | Both the translated and the key fields are already strings | Chart again |
| Go | As above | Chart again |
Charting itself is a matter of milliseconds, so charting more than once is not a problem. Keep
predicates on the key fields and switching chart language requires no code changes at all.
## There is no global language switch [#there-is-no-global-language-switch]
x-iztro keeps no "current language" global state: the language is passed as a parameter when
charting, and translation functions name their target language explicitly on every call.
A global language switch makes the same code produce different results depending on call order,
which is especially dangerous under concurrency. Explicit parameters make each call's result a
function of its arguments alone.
## What adding a language would touch [#what-adding-a-language-would-touch]
The vocabularies are not a resource file you can drop in; they are static tables compiled into the
library. Adding a language touches four places:
Add a variant to the
`Language`
enum in
`src/data/types.rs`
, and add its language code to
`as_code`
/
`from_code`
Add a vocabulary file under
`src/i18n/`
, implementing the same set of functions as the existing files (star names, palace names, stem and branch names, brightness, mutagens, …)
Add a dispatch arm to the
`match`
in every translation function in
`src/i18n/mod.rs`
— this is per function, not one place
Add an entry to
`lang_index`
and to the reverse-lookup scan order table in
`src/i18n/lookup.rs`
; the scan order decides which key a homographic name resolves to, and has to be checked against the golden data
The binding layer needs no changes: language codes are passed as strings, so a new enum variant is
immediately available in all three programming languages.
# Error handling (/en/docs/guide/guides/errors)
What is validated, at which layer, what the four error categories mean, and why the core refuses to panic.
*For: developers*
x-iztro validates external input as far in as it can, so all three programming languages sit behind
one line of defence. Each language only translates the error into its own conventional type; none of
them re-validates or reinterprets.
## Error categories [#error-categories]
Every error carries a machine-readable category, with the same values across languages — **branch on
it, do not parse the message**.
| Category | Meaning |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_date` | Malformed date, a date that does not exist, or one outside the supported range (Gregorian 1583–9999) |
| `invalid_time_index` | Hour index out of range (0–12 are legal) |
| `invalid_argument` | Any other invalid argument or config: unknown gender, chart language, star key, switch value, a wrong override-table length; or reverse-lookup input — mismatched pillar polarity, empty or flow-star criteria, a bad year range |
| `internal` | A defect or runtime failure inside the library. Not the caller's fault; please report it |
## The error type in each language [#the-error-type-in-each-language]
The `IztroError` enum, with `code()` giving the category:
```rust
match by_solar(date, ti, Gender::Female, true, Language::EnUS, Config::default()) {
Ok(chart) => { /* ... */ }
Err(e) => println!("{:20} {}", e.code(), e),
}
```
```text
invalid_date invalid solar date '2000-2-30': day is out of range for that month
invalid_date invalid solar date '1000-1-1': year must be within 1583-9999
invalid_time_index time_index must be 0-12, got 13
```
There are four variants: `InvalidDate`, `InvalidTimeIndex`, `InvalidArgument` and `Internal`. The enum is marked
`#[non_exhaustive]`, so leave a `_` arm when matching.
`IztroError`, a subclass of `ValueError`, so an existing `except ValueError` still catches it:
```python
from x_iztro import Astro, IztroError
try:
Astro().by_solar("2000-2-30", 2, "female")
except IztroError as e:
print(e.code, e)
```
```text
invalid_date invalid solar date '2000-2-30': day is out of range for that month
```
`*iztro.Error`, carrying `Code` and `Message`; four sentinel variables let `errors.Is` match by
category:
```go
_, err := iztro.BySolar("2000-13-1", 2, iztro.GenderMale, true, iztro.LanguageEnUS, nil)
if errors.Is(err, iztro.ErrInvalidDate) {
var e *iztro.Error
errors.As(err, &e)
fmt.Println(e.Code, e.Message)
}
```
```text
invalid_date invalid solar date '2000-13-1': month must be within 1-12
```
| Sentinel | Category |
| --------------------- | ---------------------- |
| `ErrInvalidDate` | `CodeInvalidDate` |
| `ErrInvalidTimeIndex` | `CodeInvalidTimeIndex` |
| `ErrInvalidArgument` | `CodeInvalidArgument` |
| `ErrInternal` | `CodeInternal` |
`Error()` prints with an `iztro: ` prefix; `Message` is the unprefixed text.
The C FFI and wasm exits render the same error as `{"error":"","code":""}`, generated
by serde so escaping is always complete.
## What is validated, with sample messages [#what-is-validated-with-sample-messages]
Messages start lower-case, introduce the detail after a colon, and carry the original input — so
batch processing can pinpoint which record went wrong.
| Input | Sample message |
| -------------------------------------- | ------------------------------------------------------------------------------------ |
| Gregorian date format | `invalid solar date 'not-a-date': year is not a number` |
| Gregorian date does not exist | `invalid solar date '2000-2-30': day is out of range for that month` |
| Gregorian year range | `invalid solar date '1000-1-1': year must be within 1583-9999` |
| Lunar month | `invalid lunar date '2000-13-1': month must be within 1-12` |
| Days in that lunar month | `invalid lunar date '2000-2-31': day is out of range for that lunar month` |
| Hour index | `time_index must be 0-12, got 13` |
| Gender | `invalid gender 'x': expected 'male' or 'female'` |
| Chart language | `invalid language 'fr-FR': expected one of zh-CN, zh-TW, en-US, ja-JP, ko-KR, vi-VN` |
| Custom mutagen table length | `invalid mutagens for 'gengHeavenly': expected 4 stars (lu, quan, ke, ji), got 3` |
| Override table given a translated name | `invalid mutagens for 'gengHeavenly': unknown star '太阳'` |
Dates and hour indexes are validated in the **core**, identically for all three programming
languages. Gender, chart language, configuration switches and star keys — the things passed as
strings — are validated in the **binding layer** as they are parsed; on the Rust side they are enums
to begin with, so there is no invalid value to reject.
## A miss is not an error [#a-miss-is-not-an-error]
Entry points that compute return errors; **query** methods return an empty value on a miss rather
than an error — "this chart does not have that star" is a normal result, not an exception.
| Situation | Returns |
| -------------------------------------------- | --------------------- |
| A star is not on this chart | `None` / `nil` |
| Palace index out of range | `None` / `nil` |
| The star has no brightness table | `None` / empty string |
| Reverse lookup of a name that does not exist | `None` / empty string |
`chart.palace("soulPalce")` raises nothing and simply returns empty; `has(["ziweiMj"])` returns
`False` forever. Use enums or constants in predicates (Python's `PalaceName.SOUL`, Go's
`iztro.PalaceSoul`) — then a misspelling is a compile-time or construction-time error, not a silent
runtime one. To validate a string that came from outside, feed it to the enum constructor:
`PalaceName("x")` raises `ValueError`.
## Why the core does not panic [#why-the-core-does-not-panic]
This is not a style preference; it is a hard constraint imposed by the wasm target.
On wasm a panic becomes a
**trap**
, aborting the call outright
`catch_unwind`
**does not work**
on wasm — the binding layer cannot catch it
Every trap
**permanently consumes**
stack space in the module instance, and once enough have accumulated even legitimate calls start failing
So the line of defence has to sit further in: all external input is validated before it reaches the
algorithm, and the entry points return a `Result`. The binding layer's `catch_unwind` is a backstop
for defects inside the library only; it carries no argument-validation duty.
The charting entry points do not panic on invalid **external input**. If you hit one anyway, it is a
defect inside the library; it is returned under the `internal` category and should be reported as a
bug — not something callers are expected to defend against.
## Writing a batch job [#writing-a-batch-job]
Skip the bad records, keep going with the good ones, rather than failing the whole batch:
```rust
let (charts, failed): (Vec<_>, Vec<_>) = rows
.iter()
.map(|r| by_solar(&r.date, r.ti, r.gender, true, Language::EnUS, Config::default()))
.partition(Result::is_ok);
```
```python
charts, failed = [], []
for row in rows:
try:
charts.append(Astro().by_solar(row["date"], row["ti"], row["gender"]))
except IztroError as e:
failed.append((row, e.code, str(e)))
```
```go
for _, row := range rows {
chart, err := iztro.BySolar(row.Date, row.TimeIndex, row.Gender, true, iztro.LanguageEnUS, nil)
if err != nil {
var e *iztro.Error
errors.As(err, &e)
failed = append(failed, failure{row, e.Code, e.Message})
continue
}
charts = append(charts, chart)
}
```
Per-API error behaviour is on the errors page for [Rust](/en/docs/rust/errors),
[Python](/en/docs/python/errors) and [Go](/en/docs/go/errors).
# Extending the astrolabe (/en/docs/guide/guides/plugins)
Hanging your own analysis rules off a chart — the extension point in each of the three programming languages.
*For: developers*
A chart is data; how it is interpreted is each school's own business. Zi Wei has many schools and
predicate rules vary from reader to reader, so cramming them all into the core is neither possible
nor desirable. What x-iztro does instead is let you attach your own rules to the astrolabe as
methods, called with the same syntax as the built-in ones.
## The extension point in each programming language [#the-extension-point-in-each-programming-language]
Each language uses the mechanism most natural to it; they are not forced into one shape:
| Programming language | Mechanism | Checked | Scope |
| -------------------------------- | ------------------------------ | ------------ | --------------------------------- |
| [Rust](/en/docs/rust/extend) | Extension trait | Compile time | Visible where the trait is `use`d |
| [Python](/en/docs/python/extend) | Attaching methods to the class | Run time | Process-wide |
| [Go](/en/docs/go/extend) | Struct embedding | Compile time | Only your own type |
All three do the same thing: you call `chart.my_method()` and have the astrolabe's full built-in
capability available. Exact syntax and runnable examples are on the respective pages.
## Shared conventions [#shared-conventions]
`star.key == "ziweiMaj"` holds on a chart in any language; `star.name == "emperor"` holds only on an
English one.
Inside an extension method, predicate on the `*_key` / `*Key` fields or the built-in predicate
methods, and reach for the translated name only when displaying. That way one rule gives the same
answer across all six chart languages. See
[The language-independent key contract](/en/docs/guide/guides/keys).
Keep `WealthAnalysis`, `CareerAnalysis` and `HealthAnalysis` as separate groups so callers pull in
what they need. One big bundle forces every call site to carry every method.
When the same rule has to work in all three programming languages, the current approach is to write
it three times and hold the line with a set of tests asserting the same values on the same chart.
Because the predicates rest on language-independent keys, three implementations with the same logic
necessarily produce the same results — the tests exist to prove the logic really is the same.
## An example [#an-example]
The same plugin in all three programming languages: take the Soul palace's major stars (borrowing
from the opposite palace when it is empty).
```rust
trait MyAnalysis {
fn major_star(&self) -> String;
}
impl MyAnalysis for Astrolabe {
fn major_star(&self) -> String {
let soul = self.palace(Palace::Soul).expect("the Soul palace always exists");
let source = if soul.is_empty() { soul.opposite_palace() } else { soul };
source.major_stars.iter()
.filter(|s| s.star_type == StarType::Major)
.map(|s| translate_star(s.key, self.language))
.collect::>().join(",")
}
}
chart.major_star() // emperor
```
```python
def my_analysis(cls: type[Astrolabe]) -> None:
def major_star(self) -> str:
soul = self.palace(PalaceName.SOUL)
source = soul.opposite_palace() if soul.is_empty() else soul
return ",".join(s.name for s in source.major_stars)
cls.major_star = major_star
load_plugin(my_analysis)
chart.major_star() # emperor
```
```go
type MyChart struct{ *iztro.Astrolabe }
func (c MyChart) MajorStar() string {
soul := c.Palace(iztro.PalaceSoul)
source := soul
if soul.IsEmpty() {
source = soul.OppositePalace()
}
names := []string{}
for _, s := range source.MajorStars {
if s.Type == iztro.StarTypeMajor {
names = append(names, s.Name)
}
}
return strings.Join(names, ",")
}
MyChart{chart}.MajorStar() // emperor
```
All three return `emperor` on an English chart of this birthday, and `紫微` on a Simplified Chinese
one — the same star, written differently.
# The API behind the nine charting steps (/en/docs/guide/guides/step-api)
Which public function corresponds to each of the nine charting steps, and which steps the configuration switches change.
*For: developers*
**Every one** of the [nine charting steps](/en/docs/guide/concepts/how-it-works) has a corresponding
public function in x-iztro. Everyday charting never needs them — call the charting entry point
instead. This page serves two needs: checking the derivation of one step, or reusing part of the
chain in a pipeline of your own.
The examples below use the chart for 2000-8-16, Yin hour, female — the same one as on
[How charting works](/en/docs/guide/concepts/how-it-works).
## Step-to-API mapping [#step-to-api-mapping]
### 2. Fix the month index [#2-fix-the-month-index]
```python
# 17th day of the 7th lunar month, not a leap month, Yin hour, leap correction on
utils.fix_lunar_month_index(7, 17, False, 2, True)
```
```text
6
```
[Rust](/en/docs/rust/util#fix_lunar_month_index--fix_lunar_day_index) ·
[Python](/en/docs/python/util#fix_lunar_month_index--fix_lunar_day_index) ·
[Go](/en/docs/go/util#fixlunarmonthindex--fixlunardayindex)
### 3. Locate the Soul and Body palaces [#3-locate-the-soul-and-body-palaces]
```python
utils.get_soul_and_body(6, 2, "gengHeavenly") # month index, hour index, year stem
```
```text
SoulAndBody(soul_index=4, body_index=8, heavenly_stem_of_soul='renHeavenly', earthly_branch_of_soul='wuEarthly')
```
[Rust](/en/docs/rust/util#get_soul_and_body) ·
[Python](/en/docs/python/util#get_soul_and_body) ·
[Go](/en/docs/go/util#getsoulandbody)
### 4. Determine the Five Elements class [#4-determine-the-five-elements-class]
```python
utils.get_five_elements_class("renHeavenly", "wuEarthly") # Soul palace stem, Soul palace branch
```
```text
wood3rd
```
[Rust](/en/docs/rust/util#get_five_elements_class) ·
[Python](/en/docs/python/util#get_five_elements_class) ·
[Go](/en/docs/go/util#getfiveelementsclass)
### 5. Place Ziwei and Tianfu [#5-place-ziwei-and-tianfu]
```python
star.get_start_index("2000-8-16", 2, "female")
```
```text
StartIndex(ziwei_index=4, tianfu_index=8)
```
[Rust](/en/docs/rust/star#get_start_index) ·
[Python](/en/docs/python/star#get_start_index) ·
[Go](/en/docs/go/star#getstartindex)
### 6 and 7. Place major stars, minor stars and adjective stars [#6-and-7-place-major-stars-minor-stars-and-adjective-stars]
Each of the three groups has an entry point returning that group's distribution across the twelve
palaces. There are also per-group slot-index functions (`get_lu_yang_tuo_ma_index`,
`get_chang_qu_index` and so on); the full list is on each language's star-placement page.
[Rust](/en/docs/rust/star) · [Python](/en/docs/python/star) · [Go](/en/docs/go/star)
### 8. Place the four groups of twelve gods [#8-place-the-four-groups-of-twelve-gods]
`get_changsheng12`, `get_boshi12` and `get_yearly12`, plus the two origin functions
`get_changsheng12_start_index` and `get_jiangqian12_start_index`.
### 9. Derive decadals and age fortune [#9-derive-decadals-and-age-fortune]
```python
r = utils.get_decadals_and_ages(4, "wood3rd", "female", "gengHeavenly", "chenEarthly")
print(r.decadals[0], r.ages[0])
```
```text
Decadal(range=(43, 52), heavenly_stem='戊', heavenly_stem_key='wuHeavenly', earthly_branch='寅', earthly_branch_key='yinEarthly') [9, 21, 33, 45, 57, 69, 81, 93, 105, 117]
```
This low-level function takes no chart language, so the display fields come out in the default
Simplified Chinese: `戊` is the stem wu and `寅` is the branch yin. The `*_key` fields alongside them
(`wuHeavenly`, `yinEarthly`) are language-independent — predicate on those. The `ages` list holds
nominal ages (虚岁, the reckoning that starts at 1 on the day of birth).
This function takes a Soul palace index and a Five Elements class directly, so you do not have to
assemble a full set of birth data first; its capability is a superset of iztro's counterpart. See
[Migrating from iztro: API mapping](/en/docs/guide/about/iztro-parity#decadals-and-age-fortune).
[Rust](/en/docs/rust/util#get_decadals_and_ages) ·
[Python](/en/docs/python/util#get_decadals_and_ages) ·
[Go](/en/docs/go/util#getdecadalsandages)
## Which steps the configuration changes [#which-steps-the-configuration-changes]
| Config | Steps affected |
| ----------------------- | ----------------------------------------------------------------------------------------- |
| `year_divide` | 1 (year stem and branch) → knock-on to 6, 7, 8, 9 |
| `horoscope_divide` | 1 (month pillar and the year branch used by year-derived adjective stars) → knock-on to 7 |
| `day_divide` | 1, 2 (late Zi hour attribution) → knock-on to 3, 5 |
| `age_divide` | 9 (when nominal age increments) |
| `algorithm` | 4 (Zhongzhou takes the soul star from the year branch), 7, 8 (placement of some stars) |
| `astro_type` | 4 onwards (the class comes from a different palace) → knock-on to 5, 6, 8, 9 |
| Custom mutagen table | 6 (mutagen marks) and every flying-star predicate |
| Custom brightness table | 6, 7 (star brightness) |
Per-item notes are on [Config in depth](/en/docs/guide/guides/config).
# Documentation endpoints for AI (/en/docs/guide/guides/llms-txt)
The llms.txt, llms-full.txt, per-page Markdown and Accept-header negotiation this site provides.
*For: developers · operations*
This page is about **making this documentation readable by an AI** — not about using the library.
To wire x-iztro into an AI application, see
[Letting an AI read the chart](/en/docs/guide/guides/llm).
## `/llms.txt` [#llmstxt]
A structural index of the site, listing the title, description and link of every page in that
language. Good for letting a model locate a page before fetching it.
## `/llms-full.txt` [#llms-fulltxt]
The full Markdown text of the whole documentation set in that language — one fetch gives a model
complete context.
## Both endpoints are per-language [#both-endpoints-are-per-language]
Index and full text exist once per language and are never mixed. The full text is meant to be
dropped into a context window wholesale, and mixing in a language you cannot use only crowds the
window.
| Endpoint | Contents |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `/en/llms.txt` · `/zh/llms.txt` | The structural index for that language, listing the other languages and the full-text endpoints at the end |
| `/en/llms-full.txt` · `/zh/llms-full.txt` | The full text for that language |
| `/llms.txt` · `/llms-full.txt` | The root paths that the llms.txt convention specifies; contents identical to the `/zh/` pair |
## Per-page Markdown [#per-page-markdown]
Append `.md` to any documentation page URL to get that page's Markdown source:
Both `.md` and `.mdx` work and return the same content. The "Copy Markdown" button under a page
title fetches exactly this endpoint, and the "Open" dropdown next to it can send the page straight
into ChatGPT or Claude.
## It also works without knowing the suffix convention [#it-also-works-without-knowing-the-suffix-convention]
When an AI agent requests any documentation page, declaring a preference for Markdown in the
`Accept` header returns the Markdown source instead of the full HTML page:
```bash
curl -H "Accept: text/markdown" /en/docs/rust/palace
```
For that one page, the HTML is around 400 KB and the Markdown around 14 KB.
All three routes return plain text with no navigation, styling or scripts — cheaper in tokens than
having a model scrape HTML, and more accurate.
## Suggested usage [#suggested-usage]
When asking an AI about x-iztro, hand it `/en/llms-full.txt` as context. The full text for a single
language is around 500 KB, comfortably inside the context window of any common model.
If you only care about one topic, fetching the relevant single-page `.md` is cheaper:
| Topic | Page |
| --------------------------- | --------------------------------------------- |
| Parameters and installation | `/en/docs/guide/getting-started.md` |
| Domain concepts | `/en/docs/guide/concepts.md` and its subpages |
| Boundaries and schools | `/en/docs/guide/guides/config.md` |
| Field dictionary | `/en/docs/guide/data-model.md` |
| to\_text format | `/en/docs/guide/guides/to-text.md` |
# Data model (/en/docs/guide/data-model)
The type and meaning of every field on Astrolabe, Palace, Star and Horoscope.
*For: developers*
This page is written against the serialized JSON field names (camelCase), which are the contract
shared by all three bindings. Naming translates as follows:
| Layer | Naming | Example |
| ----------------------- | ----------------------------- | ---------------- |
| JSON / binding contract | camelCase | `isBodyPalace` |
| Python | snake\_case | `is_body_palace` |
| Go | PascalCase | `IsBodyPalace` |
| Rust | snake\_case, values are enums | `is_body_palace` |
## Astrolabe [#astrolabe]
The return value of a charting entry point.
| Field | Type | Meaning |
| ------------------------------ | ---------------------- | ----------------------------------------------------------- |
| `gender` | string | Gender, translated text |
| `genderKey` | string | `"male"` / `"female"` |
| `solarDate` | string | Gregorian birthday, echoing the input |
| `lunarDate` | string | The lunar birthday written out (always Chinese numerals) |
| `chineseDate` | string | The four pillars as a display string |
| `rawDates` | [RawDates](#rawdates) | Structured lunar birthday and four pillars |
| `time` | string | Hour name, e.g. `Tiger hour` |
| `timeRange` | string | The hour's clock range, e.g. `03:00~05:00` |
| `sign` | string | Zodiac sign |
| `signKey` | string | Zodiac sign key, `aries` … `pisces` |
| `zodiac` | string | Zodiac animal, from the year branch |
| `zodiacKey` | string | Zodiac animal key, `rat` … `pig` |
| `earthlyBranchOfSoulPalace` | string | Soul palace branch |
| `earthlyBranchOfSoulPalaceKey` | string | Soul palace branch key |
| `earthlyBranchOfBodyPalace` | string | Body palace branch |
| `earthlyBranchOfBodyPalaceKey` | string | Body palace branch key |
| `soul` | string | Soul star |
| `soulKey` | string | Soul star key |
| `body` | string | Body star |
| `bodyKey` | string | Body star key |
| `fiveElementsClass` | string | Five Elements class |
| `fiveElementsClassKey` | string | Five Elements class key, e.g. `water2nd` |
| `palaces` | [Palace](#palace)\[12] | The twelve palaces; index 0 is the Yin palace |
| `timeIndex` | int | Hour index of birth, 0–12, keeping the original input value |
| `fixLeap` | bool | Whether leap-month correction is on |
| `language` | string | Chart language |
| `config` | [Config](#config) | The charting configuration |
## Palace [#palace]
| Field | Type | Meaning |
| ---------------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `index` | int | The palace's slot on the chart, 0–11; 0 is the Yin palace |
| `name` | string | Palace name |
| `nameKey` | string | Palace name key, e.g. `soulPalace` |
| `isBodyPalace` | bool | Is it the Body palace |
| `isOriginalPalace` | bool | Is it the Original palace |
| `heavenlyStem` | string | Palace stem |
| `heavenlyStemKey` | string | Palace stem key |
| `earthlyBranch` | string | Palace branch |
| `earthlyBranchKey` | string | Palace branch key |
| `majorStars` | [Star](#star)\[] | Major stars |
| `minorStars` | [Star](#star)\[] | Minor stars |
| `adjectiveStars` | [Star](#star)\[] | Adjective stars |
| `changsheng12` / `changsheng12Key` | string | The twelve Changsheng gods |
| `boshi12` / `boshi12Key` | string | The twelve Boshi gods |
| `jiangqian12` / `jiangqian12Key` | string | The twelve Jiang-qian gods |
| `suiqian12` / `suiqian12Key` | string | The twelve Sui-qian gods |
| `mutagenStarKeys` | string\[4] | Keys of the four stars this palace's stem mutates, in the order Lu, Quan, Ke, Ji; follows a [custom mutagen table](/en/docs/guide/guides/config#custom-mutagen-and-brightness-tables) |
| `decadal` | [Decadal](#decadal) | The decadal this palace governs |
| `ages` | int\[] | Nominal ages (虚岁) at which age fortune passes through this palace |
## Star [#star]
| Field | Type | Meaning |
| --------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `key` | string | Star key, e.g. `ziweiMaj` |
| `name` | string | Star name |
| `type` | string | `major` / `soft` / `tough` / `adjective` / `flower` / `helper` / `lucun` / `tianma` |
| `scope` | string | `origin` / `decadal` / `yearly` / `monthly` / `daily` / `hourly` |
| `brightness` | string | Brightness as display text. **Major and minor stars always have this key**, with an empty string where there is no brightness; on adjective and flowing stars the key is absent entirely |
| `brightnessKey` | string? | Brightness key. **Absent** when there is no brightness (not an empty string) |
| `mutagen` | string | Mutagen as display text. **The 18 mutable stars — the fourteen major stars plus Zuofu, Youbi, Wenchang and Wenqu — always have this key**, with an empty string where there is no mutagen; on every other star the key is absent entirely |
| `mutagenKey` | string? | Mutagen key. **Absent** when there is no mutagen |
The **translated fields** `brightness` and `mutagen` have their key present or absent according to
the star's category, and when present the value may be an empty string. The **key fields**
`brightnessKey` and `mutagenKey` simply omit the key when there is no value.
So "does this star have brightness?" must test whether `brightnessKey` is present, not whether the
`brightness` key exists — the latter is true for every major and minor star.
## Decadal [#decadal]
| Field | Type | Meaning |
| ------------------------------------ | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `range` | \[int, int] | Starting and ending [nominal age](/en/docs/guide/concepts#four-concepts-to-get-straight-first) (虚岁, the East Asian reckoning that starts at 1 on the day of birth), inclusive |
| `heavenlyStem` / `heavenlyStemKey` | string | Decadal heavenly stem |
| `earthlyBranch` / `earthlyBranchKey` | string | Decadal earthly branch |
## RawDates [#rawdates]
| Field | Type | Meaning |
| ------------------------- | ----------------- | ------------------------------------------------------------------------- |
| `lunarDate.lunarYear` | int | Lunar year |
| `lunarDate.lunarMonth` | int | Lunar month, 1–12 |
| `lunarDate.lunarDay` | int | Lunar day, 1–30 |
| `lunarDate.isLeap` | bool | Is it a leap month |
| `chineseDate.yearly` | \[string, string] | Year pillar, \[stem, branch] |
| `chineseDate.monthly` | \[string, string] | Month pillar |
| `chineseDate.daily` | \[string, string] | Day pillar |
| `chineseDate.hourly` | \[string, string] | Hour pillar |
| `chineseDate.yearlyKeys` | \[string, string] | The year pillar's [language-independent keys](/en/docs/guide/guides/keys) |
| `chineseDate.monthlyKeys` | \[string, string] | The month pillar's keys |
| `chineseDate.dailyKeys` | \[string, string] | The day pillar's keys |
| `chineseDate.hourlyKeys` | \[string, string] | The hour pillar's keys |
The stems and branches inside `rawDates.chineseDate` are the unlocalized originals — Chinese
characters under every chart language — so predicate on `*Keys`. Hand the `*Keys` to
`translate_chinese_date` to get a display string translated into any language, character for
character identical to the top-level `chineseDate` field.
## Config [#config]
| Field | Accepts | Default |
| ----------------- | ---------------------------- | --------- |
| `yearDivide` | `normal` / `exact` | `normal` |
| `horoscopeDivide` | `normal` / `exact` | `normal` |
| `ageDivide` | `normal` / `birthday` | `normal` |
| `dayDivide` | `forward` / `current` | `forward` |
| `algorithm` | `default` / `zhongzhou` | `default` |
| `astroType` | `heaven` / `earth` / `human` | `heaven` |
There are two further **input-only** keys, used to replace built-in data tables:
| Input key | Accepts |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| `mutagens` | `{stem key: [four star keys]}`, in the order Lu, Quan, Ke, Ji; exactly four required |
| `brightness` | `{star key: [twelve brightness keys]}`, first item the Yin palace; exactly twelve required, empty string where there is no brightness |
These two keys **are not echoed in the astrolabe's `config`**. They are charting input rather than
part of the chart, and adding them to the DTO would break the field contract with JS iztro. Keep
your own copy of the config if you need a record.
For what they mean, see [Config in depth](/en/docs/guide/guides/config).
## Horoscope [#horoscope]
| Field | Type | Meaning |
| ----------- | --------------------------------- | ------------------------------------------------------------- |
| `solarDate` | string | Target Gregorian date |
| `lunarDate` | string | Target lunar date |
| `decadal` | [HoroscopeScope](#horoscopescope) | The decadal, or the childhood scope before the decadals begin |
| `age` | HoroscopeScope | Age fortune, carrying `nominalAge` |
| `yearly` | HoroscopeScope | The yearly scope, carrying `yearlyDecStar` |
| `monthly` | HoroscopeScope | The monthly scope |
| `daily` | HoroscopeScope | The daily scope |
| `hourly` | HoroscopeScope | The hourly scope |
### HoroscopeScope [#horoscopescope]
| Field | Type | Meaning |
| ------------------------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `index` | int | This scope's slot on the chart, 0–11 |
| `name` | string | Scope name, translated text |
| `nameKey` | string | Scope key: `decadal` / `childhood` / `turn` (age fortune) / `yearly` / `monthly` / `daily` / `hourly` |
| `heavenlyStem` / `heavenlyStemKey` | string | This scope's heavenly stem |
| `earthlyBranch` / `earthlyBranchKey` | string | This scope's earthly branch |
| `palaceNames` | string\[12] | The palace names re-laid-out with this scope's slot as the Soul palace, in slot order |
| `palaceNameKeys` | string\[12] | The same as keys |
| `mutagen` | string\[4] | Mutagen star names, in the order Lu, Quan, Ke, Ji |
| `mutagenStarKeys` | string\[4] | The same as keys — star keys of the four mutated stars, synonymous with the palace field of the same name; the singular `mutagenKey` is the mutagen type (`sihuaLu` etc.) |
| `stars` | [Star](#star)\[]\[12]? | Flowing stars across the twelve palaces (the outer twelve entries are palaces, each inner list that palace's flowing stars); absent on scopes with no flowing stars |
| `nominalAge` | int? | Nominal age (虚岁); age fortune only |
| `yearlyDecStar` | [YearlyDecStar](#yearlydecstar)? | Yearly scope only |
When the subject has not yet entered the decadals, the `decadal` scope's `nameKey` is `childhood`
rather than `decadal` — childhood and decadal are different reading semantics. Test `nameKey` to
tell whether the scope is a childhood one; do not compare `name` translations.
### YearlyDecStar [#yearlydecstar]
| Field | Type | Meaning |
| --------------------------------- | ----------- | ------------------------------------------------------------------------------------ |
| `suiqian12` / `suiqian12Keys` | string\[12] | The twelve Sui-qian gods placed from the yearly branch; the index is the palace slot |
| `jiangqian12` / `jiangqian12Keys` | string\[12] | The twelve Jiang-qian gods placed from the yearly branch |
In Rust the common fields of `age` and `yearly` live under `.base` (`AgeItem { base, nominal_age }`)
and are flattened with `#[serde(flatten)]` on serialization, so the JSON — and the Python and Go
sides — see a flat structure.
## A complete JSON example [#a-complete-json-example]
Real output from `by_solar("2000-8-16", 2, female, language="en-US")` (top level, with the twelve
entries of `palaces` elided):
```json
{
"gender": "female",
"genderKey": "female",
"solarDate": "2000-8-16",
"lunarDate": "二〇〇〇年七月十七",
"chineseDate": "geng chen - jia shen - bing woo - geng yin",
"rawDates": {
"lunarDate": { "lunarYear": 2000, "lunarMonth": 7, "lunarDay": 17, "isLeap": false },
"chineseDate": {
"yearly": ["庚", "辰"], "monthly": ["甲", "申"],
"daily": ["丙", "午"], "hourly": ["庚", "寅"],
"yearlyKeys": ["gengHeavenly", "chenEarthly"],
"monthlyKeys": ["jiaHeavenly", "shenEarthly"],
"dailyKeys": ["bingHeavenly", "wuEarthly"],
"hourlyKeys": ["gengHeavenly", "yinEarthly"]
}
},
"time": "Tiger hour",
"timeRange": "03:00~05:00",
"sign": "leo", "signKey": "leo",
"zodiac": "dragon", "zodiacKey": "dragon",
"earthlyBranchOfSoulPalace": "woo", "earthlyBranchOfSoulPalaceKey": "wuEarthly",
"earthlyBranchOfBodyPalace": "xu", "earthlyBranchOfBodyPalaceKey": "xuEarthly",
"soul": "rebel", "soulKey": "pojunMaj",
"body": "scholar", "bodyKey": "wenchangMin",
"fiveElementsClass": "wood 3rd", "fiveElementsClassKey": "wood3rd",
"palaces": [ /* 12 entries */ ],
"timeIndex": 2,
"fixLeap": true,
"language": "en-US",
"config": {
"yearDivide": "normal", "horoscopeDivide": "normal",
"ageDivide": "normal", "dayDivide": "forward",
"algorithm": "default", "astroType": "heaven"
}
}
```
`lunarDate` is rendered with Chinese numerals whatever the language — `二〇〇〇年七月十七` is the 17th
day of the 7th lunar month, 2000. And the stems and branches under `rawDates.chineseDate` are
deliberately unlocalized originals; the localized form is the top-level `chineseDate`, romanized in
pinyin for English.
### One palace [#one-palace]
The Soul palace of the same chart (the `palaces` entry whose `index` is 4):
```json
{
"index": 4,
"name": "soul", "nameKey": "soulPalace",
"isBodyPalace": false, "isOriginalPalace": false,
"heavenlyStem": "ren", "heavenlyStemKey": "renHeavenly",
"earthlyBranch": "woo", "earthlyBranchKey": "wuEarthly",
"majorStars": [
{ "key": "ziweiMaj", "name": "emperor", "type": "major", "scope": "origin",
"brightness": "[+3]", "brightnessKey": "miao", "mutagen": "" }
],
"minorStars": [
{ "key": "wenquMin", "name": "artist", "type": "soft", "scope": "origin",
"brightness": "[-3]", "brightnessKey": "xian", "mutagen": "" }
],
"adjectiveStars": [
{ "key": "fengge", "name": "refined", "type": "adjective", "scope": "origin" },
{ "key": "tianfu", "name": "lucky", "type": "adjective", "scope": "origin" },
{ "key": "jielu", "name": "intercepted", "type": "adjective", "scope": "origin" },
{ "key": "feilian", "name": "instigated", "type": "adjective", "scope": "origin" },
{ "key": "nianjie", "name": "considery(Y)", "type": "helper", "scope": "origin" }
],
"changsheng12": "weak", "changsheng12Key": "shuai",
"boshi12": "dragon", "boshi12Key": "qinglong",
"jiangqian12": "disastery", "jiangqian12Key": "zhaisha",
"suiqian12": "downcast", "suiqian12Key": "sangmen",
"mutagenStarKeys": ["tianliangMaj", "ziweiMaj", "zuofuMin", "wuquMaj"],
"decadal": {
"range": [3, 12],
"heavenlyStem": "ren", "heavenlyStemKey": "renHeavenly",
"earthlyBranch": "woo", "earthlyBranchKey": "wuEarthly"
},
"ages": [5, 17, 29, 41, 53, 65, 77, 89, 101, 113]
}
```
Both Ziwei and Wenqu carry `mutagen: ""` — they are mutable stars that happen not to be mutated on
this chart, so the key is present with an empty value while `mutagenKey` is absent entirely. The
five adjective stars do not even have a `brightness` key.
`considery(Y)` and `disastery` above are the literal en-US renderings of 年解 (`nianjie`) and 灾煞
(`zhaisha`), inherited verbatim from iztro's vocabulary. `name` is display text; never parse it.
## What the `*Key` fields are [#what-the-key-fields-are]
Every field that gets translated has a companion field with a `Key` suffix, valued with iztro's i18n
key names and independent of the chart language:
```json
{ "name": "emperor", "key": "ziweiMaj", "brightness": "[+3]", "brightnessKey": "miao" }
```
The translated field is for people, the key field is for code. The contract is two rules:
1. **Every translated property `x` has a companion `xKey`**; array-valued ones use the plural
`Keys` (`palaceNameKeys`, `yearlyKeys`).
2. **An entity's own identity is simply `key`** — a star's key field is `key`, not `nameKey`.
The one naming fork is around mutagens: the singular `mutagenKey` is the mutagen type (`sihuaLu`
etc.), while the plural `mutagenStarKeys` holds the star keys of the four mutated stars. The
`semantic_contract` test enforces this contract — every translated field in the DTO must have its
key companion.
The `*Key` / `key` family, `genderKey`, `timeIndex`, `fixLeap`, `language` and `config` are
x-iztro's extensions over JS iztro; every other field matches iztro's `JSON.stringify` output key by
key and value by value, held by the binding contract test. Keep predicate logic on the key fields —
see [The key contract](/en/docs/guide/guides/keys).
## Exporting JSON [#exporting-json]
The Python side has export methods that emit exactly the contract above:
```python
chart.to_dict() # dict
chart.to_json(indent=2) # str
```
`Astrolabe`, `Palace` and `Star` hold back-references to each other (a palace holds the astrolabe it
belongs to), so `asdict()` recurses into them until `RecursionError`. Use `to_json()` for JSON.
# Overview (/en/docs/guide/about)
Accuracy guarantees, documentation endpoints for AI, and notes on the port and the architecture.
*For: everyone*
## Project information [#project-information]
| Item | Value |
| --------------- | ---------------------------------------------------------------- |
| iztro reference | v2.5.8 (version pinned) |
| Licence | MIT |
| Repository | [github.com/x-haose/x-iztro](https://github.com/x-haose/x-iztro) |
| crates.io | [x-iztro](https://crates.io/crates/x-iztro) |
| PyPI | [x-iztro](https://pypi.org/project/x-iztro/) |
The current version number is whatever is published on crates.io and PyPI.
# Accuracy (/en/docs/guide/about/accuracy)
How 716,314 golden test cases hold x-iztro to zero divergence from JS iztro, and where that claim stops.
*For: everyone. The "How the hash comparison works" section is for developers*
The most important property of a charting library is that **the results are right**. And "right" has
no authority to appeal to in Zi Wei Dou Shu — differences between implementations usually come from
school-of-thought choices, and it is hard to say who is wrong.
So x-iztro sets a very concrete target instead: **identical field for field to JS
[iztro](https://github.com/SylarLong/iztro) v2.5.8**. Treating that as the gold standard turns
differences from a matter of opinion into automatically detectable bugs.
## What iztro is, and why it makes a good gold standard [#what-iztro-is-and-why-it-makes-a-good-gold-standard]
iztro is an open-source Zi Wei Dou Shu charting library written in TypeScript. It is one of the most
complete and longest-maintained open-source implementations in the field, and a fair number of
frontend projects and mini-programs use it.
The reason for choosing it as the reference is not "it must be correct", but three engineering
properties:
1. **Complete**: natal chart, six horoscope levels, four groups of twelve gods, year-derived
adjective stars, the Zhongzhou school, six chart languages — nothing missing. You can only
compare against something that has the surface to compare.
2. **Deterministic**: the same input always yields the same output, with no randomness and no
external dependencies, so any difference is a difference in logic rather than noise.
3. **Pinnable**: pinning the version at v2.5.8 makes the reference stable. When iztro is upgraded,
regenerating the reference data turns the list of failing cases into the list of behavioural
changes between versions.
## What "accurate" does and does not mean here [#what-accurate-does-and-does-not-mean-here]
What x-iztro guarantees is: **given the same set of school choices, it computes exactly what a
mature implementation computes**.
It does **not** guarantee that those school choices are themselves "right". Whether geng's Hua Ke
goes to Taiyin or Tianfu, whether the year's stem and branch turn over at lunar New Year or at the
Beginning of Spring (立春, the solar term around 4 February), whether the late Zi hour belongs to
today or tomorrow — these have always been disputed. iztro picked one set, x-iztro follows it
verbatim, and turns the disputed points into
[configuration switches](/en/docs/guide/guides/config) so you can decide for yourself.
If your school differs from the default, change the config or supply a custom mutagen table; do not
expect the default output to match your lineage.
The reference data is generated on the JS side, so the cases are concentrated in the year range the
JS implementation generates reliably, with a further sampled layer every ten years across the
boundary eras (1583–1983 and 2044–2100).
x-iztro itself supports Gregorian years 1583–9999. Years outside the sampled range still chart, but
they have **not been compared case by case against golden data** — verify for yourself when using
extreme years.
## The coverage matrix [#the-coverage-matrix]
All reference data is generated by the pinned version of JS iztro, 716,314 cases in total:
| Layer | Cases | Coverage | Data format |
| --------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| Tier 1 | 1,560 | 60 years × 13 hours × both genders, **every field compared one by one** (including display fields, the Original palace and structured dates) | Full JSON |
| Tier 2 | 37,440 | 60 years × the 1st and 15th of each month × 13 hours × both genders | Compact JSON |
| Tier 3 | 586,430 | **every day** of 60 years × 13 hours × both genders × fix\_leap (leap months twice) | SHA-256 CSV |
| Boundary eras | 46,228 | Sampled every 10 years across 1583–1983 and 2044–2100, filling the blind spot left by Tier 1/2/3 covering only 1984–2043 | SHA-256 CSV |
| Horoscope | 5,760 | 360 charts × 16 target dates, all six horoscope levels, all fields | Compact JSON |
| Variants | 14,268 | by\_lunar day by day through leap months, the Zhongzhou school, all six chart languages | CSV / JSON |
| Config | 9,696 | Non-default values of the four boundary switches, at both the charting and horoscope layers | CSV / JSON |
| Zhongzhou chart types | 12,488 | Heaven / earth / human plate | SHA-256 CSV |
| 1602 window | 2,444 | 1602-2-20 through 4-25 day by day × 13 hours × both genders (leap-month dates twice for fix\_leap), locking the leap-month correction layer | SHA-256 CSV |
| **Total** | **716,314** | | |
Not counted in the table above:
* **1,559 translation reverse-lookup cases**: compared one by one against the actual values of
iztro's `kot`, holding the disambiguation order for homographic names.
* **13 binding contract cases**: the DTO compared key by key and value by value against iztro's
`JSON.stringify` output.
* **209 Python end-to-end cases** (including custom mutagen and brightness tables, full hour
coverage, patterns and knowledge packs).
* **Go end-to-end tests**: golden comparison, star placement, concurrency correctness, override
tables, and a barrage of invalid input.
* **C FFI boundary safety tests**: any invalid input must return an error JSON rather than crash.
* **Prompt snapshot tests**: natal and horoscope prompts compared byte for byte in both Chinese and
English.
## What each layer guards against [#what-each-layer-guards-against]
**Tier 1** catches field-level divergence. It compares every field including display strings and the
Original palace flag, so a translation or format that differs from iztro shows up immediately.
**Tier 2 and Tier 3** catch boundary dates. Errors in Zi Wei often appear only on particular dates —
leap months, month ends, the start of a year, solar-term changeovers. Tier 3 covers every single day
of 60 years, without exception.
**Boundary eras** catch the far ends of the year range. Tier 1/2/3 concentrate on 1984–2043; this
layer samples every ten years out to 1583 and 2100, guarding against the calendar algorithm quietly
drifting at the extremes.
**Horoscope** catches the horoscope layers. The 16 target dates are chosen exactly where trouble
lives: one per each of the 12 yearly branches, the childhood scope, advanced age, a leap month, and
the late Zi hour.
**Variants** catch schools and chart languages. The Zhongzhou school and all six chart languages get
a full comparison each, so switching algorithm school or language introduces no drift.
**Config** and **Zhongzhou chart types** catch boundaries and plates. The Beginning-of-Spring window
day by day, the late Zi hour, the days around a birthday — every switch is verified day by day
inside the window where it makes a difference.
**The 1602 window** catches defects in the lunar-calendar dependency itself. The Rust-side lunar
library's month table contradicts itself in 1602 (a 31-day second month); x-iztro corrects it at its
single lunar-table entry point against values cross-confirmed by three independent sources —
lunar-typescript, the Shou-Xing almanac (sxtwl) and the Korea Astronomy and Space Science
Institute's tables — and this layer locks the corrected window day by day. A separate full-domain
scan of every date from 1583 to 9999 (\~6.1 million charts, `#[ignore]`d) found no second window of
the same kind.
## How the hash comparison works [#how-the-hash-comparison-works]
For developers
Tier 3 has 586,430 cases; storing full JSON would run to tens of gigabytes. So these layers compare
**the SHA-256 of a canonical string**:
`tests/golden/canonical.mjs` on the JS side and `tests/common/mod.rs` on the Rust side implement the
same serialization rules and are **byte-for-byte isomorphic**. Each side flattens its chart into the
same canonical string and the hashes are compared — what is stored is the first 32 hex characters of
a SHA-256 rather than tens of kilobytes of JSON.
When a hash mismatches, the `--inspect` family of generator flags replays that case's JS output and
diffs it against the Rust canonical string, pinpointing the offending field directly.
## Running the tests [#running-the-tests]
```bash
# The regular layers: unit + Tier 1/2 + horoscope + variants + config + contract + the 1602 window, about a minute
cargo test
# All of Tier 3: 586,430 cases, about 70 seconds
cargo test --release --test golden_tier3 -- --ignored
# Binding end-to-end
cd python && pytest tests/ # run maturin develop first
cd go/iztro && go test ./...
```
## Regenerating the reference data [#regenerating-the-reference-data]
Requires a Node.js environment:
```bash
cd tests/golden
npm ci
npm run gen:all # every layer; per-generator scripts are the gen:* entries in package.json
```
The tier 3 and boundary-era generators skip files that already exist, so interrupted runs resume;
tier 3 also takes `node generate_tier3.mjs --range ` for parallel sharding (the full run
is about 30 minutes).
## Tracking a new iztro release [#tracking-a-new-iztro-release]
The procedure is fixed:
1. Bump the pinned iztro version in `tests/golden/package.json`.
2. Regenerate all the reference data.
3. Run `cargo test`.
The list of failing cases is the list of behavioural differences between the two versions — no need
to read a changelog, the tests tell you directly which fields moved.
That procedure covers **numeric** divergence. Tests say nothing when iztro adds or removes an API;
that part is checked by hand against
[Migrating from iztro: API mapping](/en/docs/guide/about/iztro-parity).
## What zero tolerance means [#what-zero-tolerance-means]
Any divergent case is treated as a bug; "the difference is tiny" and "that field doesn't matter" are
not accepted as reasons. Wherever iztro has a feature or a data table, x-iztro must produce the same
result — and only on top of that does anything else get discussed (language-independent keys, prompt
generation, giving the Config switches proper semantics).
# Architecture (/en/docs/guide/about/architecture)
The layering of the core and the three bindings, the trade-offs behind each, and the no-panic design constraint.
*For: developers*
x-iztro is one Rust core plus three bindings. The algorithm is implemented once, and Python, Go and
C callers all receive the same computed result.
## Layering [#layering]
```
┌──────────────────────────────┐
│ Rust core library │
│ astro/ charting, horoscopes,│
│ palace derivation │
│ star/ star placement │
│ data/ enums, constants, │
│ data tables │
│ i18n/ six vocabularies and │
│ two-way lookup │
└──────────────┬───────────────┘
│
bridge.rs (marshalling and dispatch)
dto.rs (serialization contract)
│
┌────────────────────┼────────────────────┐
│ │ │
python.rs wasm.rs ffi.rs
PyO3 extension wasm32-wasip1 C ABI
│ │ │
Python package Go package (wazero) C / C++ / other
```
The two shared layers have distinct jobs:
| Layer | Responsibility |
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `bridge.rs` | Argument parsing, dispatch by name, result marshalling. Python and Go go through the same function, leaving no room for behaviour to fork. It is a crate-internal module and not part of the public API surface |
| `dto.rs` | The serialization contract: camelCase keys, values translated into the chart language, plus the `*Key` identifiers and the charting context |
That keeps the binding files thin — all that is left in them is the language-specific part: wasm's
memory protocol, PyO3's exception types.
## Trade-offs in the three bindings [#trade-offs-in-the-three-bindings]
### Python: a native PyO3 extension [#python-a-native-pyo3-extension]
On the Rust side, pythonize converts directly between Python objects and Rust structs; on the Python
side, dataclasses wrap the result into a typed API.
* Compiled as an abi3 wheel (`abi3-py310`), so one wheel covers Python 3.10 and later.
* Zero runtime dependencies, pure stdlib (dataclasses + StrEnum).
* No JSON serialization round trip, so the overhead is the lowest of the three.
### Go: embedded WebAssembly [#go-embedded-webassembly]
Compiled to `wasm32-wasip1` and executed by wazero, a runtime written in pure Go.
The reason for choosing that over cgo is **keeping Go's cross-compilation**: cgo makes
`GOOS`/`GOARCH` cross-compilation extremely awkward and demands a C toolchain on the user's machine.
With the wasm approach `go get` is all it takes, and static linking and container builds are
unaffected.
A single wasm instance cannot be used concurrently, so the package maintains an **instance pool**
(capped at `GOMAXPROCS`): each call takes a free instance and returns it afterwards, and goroutines
are not serialized against each other.
The wasm module is compiled only once, and the compiled artifact is cached on disk under
`os.UserCacheDir()`, so only the very first run on a machine costs \~200ms; after that the first call
in each process costs \~30ms. `iztro.Warmup(ctx)` moves that cold start to service startup, and
`iztro.Close(ctx)` releases all instance memory.
wazero's compiler backend covers amd64 and arm64 only; other architectures fall back to the
interpreter — slower, same results.
Each call additionally costs one JSON encode/decode and a wasm memory copy; on the hot path a single
chart is on the order of 0.5ms.
### C FFI [#c-ffi]
A standard C ABI taking C strings and returning JSON strings. Errors come back as
`{"error":"..."}`, generated by serde so escaping is always complete. A `catch_unwind` sits around
the outside as a backstop.
## The core does not panic [#the-core-does-not-panic]
Date format and existence, the Gregorian year range and the hour index are validated in the
**core**, and the entry points return a `Result`. Gender, chart language, configuration switches and
keys — the things passed as strings — are validated in the binding layer as they are parsed (on the
Rust side they are enums to begin with). Neither place panics. The binding layer's `catch_unwind` is
a backstop for defects inside the library only; it carries no argument-validation duty.
On wasm a panic becomes a trap, and `catch_unwind` does not work there — it cannot catch it. Worse,
every trap permanently consumes stack space in the module instance, and once enough have
accumulated even legitimate calls start failing. Validation therefore has to live further in, with
all three programming languages behind one line of defence.
Each language's error type is on the errors page for [Rust](/en/docs/rust/errors),
[Python](/en/docs/python/errors) and [Go](/en/docs/go/errors).
## How consistency is enforced [#how-consistency-is-enforced]
Consistent behaviour across the three programming languages does not rest on discipline; it rests on
three structural constraints:
**One algorithm**
— all computation happens in the Rust core, and the bindings contain no Zi Wei logic at all
**One marshaller**
— Python and Go call the same
`bridge::query`
, so argument parsing and result shape cannot fork
**Paired assertions**
— every outward capability has a parity test on both the Python and Go sides, asserting the same values on the same chart
Predicates rest on language-independent keys throughout, so one analysis rule written in any of the
three programming languages produces the same result. The key conventions are on
[The language-independent key contract](/en/docs/guide/guides/keys).
## Versions [#versions]
| Item | Version |
| ------------------ | ----------------------- |
| iztro reference | v2.5.8 (version pinned) |
| Rust edition | 2024 |
| Python requirement | 3.10 or later |
| Go requirement | 1.22 or later |
For the current version number see [crates.io](https://crates.io/crates/x-iztro) and
[PyPI](https://pypi.org/project/x-iztro/).
## Licence [#licence]
MIT.
# Migrating from iztro: API mapping (/en/docs/guide/about/iztro-parity)
Where each public iztro API lands in the three x-iztro bindings, the few that changed shape and why, and the ones not provided.
*For: developers, especially anyone migrating from JS iztro*
x-iztro is a port of [iztro](https://github.com/SylarLong/iztro) v2.5.8. Every public iztro API has
an equivalent in all three bindings — Rust, Python and Go — with identical capability and a form
that suits each language.
This page is for people coming from iztro: when a name does not line up, look it up here. For how to
use each API, see that language's API reference.
## Names that map directly [#names-that-map-directly]
| iztro | Rust | Python | Go |
| ------------------------- | ------------------------------ | -------------------------- | -------------------------- |
| `astro.bySolar` | `by_solar` | `astro.by_solar` | `BySolar` |
| `astro.byLunar` | `by_lunar` | `astro.by_lunar` | `ByLunar` |
| `chart.horoscope` | `chart.horoscope` | `chart.horoscope` | `Horoscope` |
| `chart.palace` | `chart.palace` | `chart.palace` | `Palace` / `PalaceByIndex` |
| `chart.surroundedPalaces` | `chart.surrounded_palaces` | `chart.surrounded_palaces` | `SurroundedPalaces` |
| `palace.fliesTo` | `flies_to` | `flies_to` | `FliesTo` |
| `util.fixIndex` | `utils::fix_index` | `utils.fix_index` | `FixIndex` |
| `star.getMajorStar` | `star::query::get_major_stars` | `star.get_major_star` | `GetMajorStar` |
| `i18n.t` | `translate_key` | `i18n.translate` | `Translate` |
| `i18n.kot` | `key_of` | `i18n.key_of` | `KeyOf` |
The rest follow the same pattern: JS camelCase becomes snake\_case in Rust and Python, and
PascalCase in Go.
## The ones that changed shape [#the-ones-that-changed-shape]
These few are not transcribed, because transcribing them would have carried JS's limitations across
too.
### Charting perspective (heaven / earth / human plate) [#charting-perspective-heaven--earth--human-plate]
iztro puts `astroType` on the options object of `astro.withOptions`, because its `astro.config()` is
a global singleton that cannot hold a value varying per chart.
x-iztro's configuration is passed per call in the first place, so `astroType` goes straight into
`Config` and works from both charting entry points, with no extra entry point to remember:
```python
from x_iztro import Astro, ChartConfig
chart = Astro().by_solar("2000-8-16", 2, "female",
config=ChartConfig(astro_type="earth"))
```
Charting from an arbitrary stem and branch corresponds to `rearrangeAstrolable`, and is an astrolabe
method `rearranged(stem, branch)` in all three bindings.
### No global config and no global language [#no-global-config-and-no-global-language]
iztro's `astro.config()` and `i18n.setLanguage()` mutate module-level singletons, which is why
`astro.getConfig()` also has to exist to read the value back.
x-iztro has no global state: both the config and the language are passed on every call and held by
the caller. So `getConfig` and `setLanguage` are not provided — to read the value back, read your
own copy.
### Decadals and age fortune [#decadals-and-age-fortune]
`getHoroscope(param)` in `astro/palace` takes an `AstrolabeParam`. x-iztro's
`get_decadals_and_ages` takes a Soul palace index and a Five Elements class directly, so you do not
have to assemble a full set of birth data first; its capability is a superset of iztro's.
### The leap-month arguments of the lunar entry point [#the-leap-month-arguments-of-the-lunar-entry-point]
`byLunar(lunarDateStr, timeIndex, gender, isLeapMonth?, fixLeap?, language?)` describes the leap
month with two adjacent booleans: swap them and nothing complains while the chart silently shifts by a
month — and `fixLeap` only means anything when the input is a leap month in the first place. x-iztro
folds the pair into one three-way value: Rust `LeapMonth::{NotLeap, Leap, LeapFixed}`, Go
`NotLeapMonth / LeapMonthKeep / LeapMonthFixed`; Python keeps the two booleans but makes them
keyword-only (`is_leap_month=`, `fix_leap=`). The JSON wire protocol of the bindings still carries the
`isLeapMonth`/`fixLeap` keys, as in iztro. The solar entry point's `fixLeap` is a single boolean with
nothing to swap against, so it stays as it is.
Likewise, Go makes `gender` and `language` the named string types `Gender` / `Language`
(`GenderFemale`, `LanguageZhCN`): literals still work, but a stray string variable in the wrong
position is rejected at compile time.
### Plugins [#plugins]
iztro's `loadPlugin` / `use(plugin)` attaches functions to the astrolabe object at run time — a
product of JS having no other extension mechanism. All three bindings implement the same capability
using the answer their own language gives, at compile time or load time, without sacrificing type
checking:
| | Approach |
| ------ | ----------------------------------------------------------------------------------------------------------------------- |
| Rust | Extension trait |
| Python | `load_plugin` / `load_plugins` from `x_iztro.plugin`, attaching methods to the `Astrolabe` class |
| Go | Embedding `*Astrolabe` (Go does not allow adding methods to another package's type; embedding is the language's answer) |
For the syntax, see [Extending the astrolabe](/en/docs/guide/guides/plugins).
### Disambiguating a reverse lookup [#disambiguating-a-reverse-lookup]
The second parameter of `kot(value, k)` is a separate entry point in each binding: `key_of_in`
(Rust), `key_of(text, key_filter)` (Python), `KeyOfIn` (Go). The values match iztro case for case,
including which key homographic names such as `horse`, `dragon` and `유시` (Korean for the You hour) resolve to.
iztro's `kot` echoes the argument back on a miss; x-iztro returns `None` (Rust and Python) or an
empty string (Go). If you were relying on "treat a miss as the original value and carry on", that
has to change when migrating.
## The ones not provided [#the-ones-not-provided]
| iztro | Why not |
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `astro.astrolabeBySolarDate` / `astrolabeByLunarDate` | Aliases deprecated since iztro v2.0.5, with the same parameters and behaviour as `bySolar` / `byLunar` |
| `star.initStars` | In JS it is a factory returning 12 empty arrays; all three type systems already give a fixed-length array of 12 |
| `util.fixEarthlyBranchIndex` | Synonymous with `earthlyBranchIndexToPalaceIndex` |
| `palace.setAstrolabe` / `star.setPalace` / `star.setAstrolabe` | Wiring references between objects is internal behaviour, done automatically after parsing in all three bindings |
| The `astro/analyzer` module | Its 11 functions are free-function versions of palace and surrounded-palace methods (`hasStars` is `palace.has`); the capability is already covered by the object methods |
| The `calendar` module | Dead code in iztro v2.5.8: every live code path goes through the `lunar-lite` dependency instead, and the module is not in the package's root exports |
| The i18next instance in `i18n`'s default export | Third-party library instances are not passed through; translation and reverse lookup are covered by `translate` / `key_of` |
| `Astrolabe.copyright` | iztro's own copyright notice string |
## Behavioural points worth noting [#behavioural-points-worth-noting]
### Custom mutagen and brightness tables take keys only [#custom-mutagen-and-brightness-tables-take-keys-only]
The `mutagens` / `brightness` override tables on `Config` **accept language-independent keys only**:
`"ziweiMaj"` works, `"emperor"` and `"紫微"` do not — taking translated names would tie a config to
one chart language.
### Override table lengths are validated strictly [#override-table-lengths-are-validated-strictly]
A mutagen entry must have all 4 items (Lu, Quan, Ke, Ji) and a brightness entry all 12 (the first is
the Yin palace). One too many or too few raises `invalid_argument` outright; nothing is padded and
nothing is truncated. See
[Config in depth](/en/docs/guide/guides/config#custom-mutagen-and-brightness-tables).
### Override tables are not echoed in the output config [#override-tables-are-not-echoed-in-the-output-config]
The `config` echoed on the astrolabe holds only the six switches. The override tables are charting
input, and putting them into the DTO would break the field contract with iztro, so they read back
empty — keep your own copy if you need a record.
## What x-iztro adds [#what-x-iztro-adds]
### Pattern judgement (no iztro equivalent) [#pattern-judgement-no-iztro-equivalent]
iztro has no pattern API at all. On top of chart casting, x-iztro adds a pattern engine: 64 patterns,
one rule set shared by natal charts and horoscope views, each hit carrying the palace it formed in,
the reading that matched (`variant`), a "spoiled" flag (`broken`), and the evidencing stars. The
entries, example charts and classical quotations come from the 格局 (Patterns) page of iztro-docs (MIT License,
by Sylar Long); the judgement implementation, the pattern names in six languages, and the choices
between competing readings are x-iztro's own work.
The entry points on each side: `Astrolabe::patterns` / `HoroscopeRef::patterns` in Rust,
`Astrolabe.patterns` / `Horoscope.patterns` in Python, `Astrolabe.Patterns` / `Horoscope.Patterns`
in Go, plus the language-independent pattern keys (`PatternKey` in Rust and Python, the `PatternXxx`
constants in Go). The concepts and the full table of 64 are on
[Patterns](/en/docs/guide/concepts/patterns).
Since iztro has nothing to compare against, this area has no golden data; its correctness is held by
four layers of tests: a unit test per rule, a reproduction of the source page's 32 example charts on
real charts, a bulk invariant sweep over the 1,560 tier-1 charts, and output snapshots that all
three bindings read back from the same files.
### Knowledge packs (no iztro equivalent) [#knowledge-packs-no-iztro-equivalent]
iztro ships facts only; the reading texts for stars and patterns live on its documentation site, not
in the library. x-iztro turns interpretation into data behind a protocol: a knowledge pack is JSON
mapping "language-independent key → text and attributes", and one default pack ships inside the
library (107 stars, 64 patterns, 12 palaces, 4 transformations, 49 glossary entries, taken from the
学习 (Learn) pages of iztro-docs, MIT License, by Sylar Long). Disagree with it and you write an overlay
pack that merges field by field.
The entry points: Rust's `KnowledgePack::builtin` / `merged`, Python's `KnowledgePack.builtin` /
`merged`, Go's `BuiltinKnowledgePack` / `Merged`, with the merge implemented once in the Rust core.
A star's yin-yang, five elements, dipper and chemistry are **attributes** that live in the pack
rather than the core tables — the core `StarInfo` stays value-for-value identical to iztro's, while
those attributes are a school's reading. See
[Knowledge packs](/en/docs/guide/guides/knowledge-pack).
### Reverse lookup (no iztro equivalent) [#reverse-lookup-no-iztro-equivalent]
iztro goes one way only: birth moment → chart. x-iztro adds the reverse
direction: `solar_dates_by_bazi` recovers solar birth dates from four BaZi
pillars — interpreted under the boundary readings of the `Config` you pass, the
same semantics as `raw_dates.chinese_date` — and `reverse_chart` recovers birth
candidates from chart features (soul/body palace branches, five elements class,
star placements, birth-year mutagens). Both are pruned enumeration followed by
full re-charting, so results have zero divergence from forward charting. A set
of pillars recurs roughly every 60 years, so multiple solutions are inherent;
chart any candidate to reproduce the target.
The entry points: `solar_dates_by_bazi` / `reverse_chart` in Rust and Python,
`SolarDatesByBazi` / `ReverseChart` in Go (each with a Context variant). See
[Reverse lookup](/en/docs/guide/guides/reverse).
### Everything else [#everything-else]
* **Language-independent keys**: every field on the astrolabe carries a `*key` / `*Key` alongside
its translated name, valued with iztro's i18n keys. Predicate logic is therefore unaffected by the
chart language and never has to reverse-look-up a translation. See
[The key contract](/en/docs/guide/guides/keys).
* **The semantic text projection (to\_text)**: project an astrolabe, a horoscope, a palace or the
surrounded palaces into natural-language text, for a language model or a person. See
[Semantic text](/en/docs/guide/guides/to-text).
* **Up-front validation at the entry points**: invalid dates, out-of-range hours and the like return
an error rather than panicking, with a machine-readable category code. See
[Error handling](/en/docs/guide/guides/errors).
* **Custom mutagen and brightness tables**: replace the built-in data wholesale, by key. See
[Config in depth](/en/docs/guide/guides/config#custom-mutagen-and-brightness-tables).
* **`all_keys`**: fetch all 260 translatable keys at once.
## Numeric consistency [#numeric-consistency]
Beyond API parity, chart output has **zero field-level divergence** from iztro, held by 716,314
golden cases. The coverage matrix and how it is verified are on
[Accuracy](/en/docs/guide/about/accuracy).
# Overview (/en/docs/rust)
The crate layout, the type system, and how to read this reference.
The x-iztro Rust crate is the core of the whole project; both the Python and Go bindings call into it.
This section is the complete Rust API reference — every public function, type and method has its own
entry.
## Install [#install]
```toml title="Cargo.toml"
[dependencies]
x-iztro = "0.3"
```
The crate has no default features and works as-is. The `python` feature is only for building the PyO3
extension and is not needed by ordinary dependents.
## Your first chart [#your-first-chart]
```rust
use x_iztro::*;
fn main() -> Result<(), IztroError> {
let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?;
println!("{} {}", chart.solar_date, chart.lunar_date);
// 2000-8-16 二〇〇〇年七月十七
let soul = chart.palace(Palace::Soul).unwrap();
println!("{}", soul.data().major_stars.iter().map(|s| s.name.as_str()).collect::>().join(" "));
// emperor
Ok(())
}
```
`lunar_date` is a lunar date written in Chinese numerals in every output language — `二〇〇〇年七月十七`
is "the 17th day of the 7th lunar month of 2000".
## Crate layout [#crate-layout]
| Module | Contents | Page in this reference |
| ----------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `x_iztro::astro` | Charting, horoscopes, palace derivation, lightweight queries | [Charting entries](/en/docs/rust/astro), [Horoscope object](/en/docs/rust/horoscope), [Lightweight queries](/en/docs/rust/query) |
| `x_iztro::models` | `Astrolabe`, `PalaceData`, `Star`, `HoroscopeData` and the three view types | The four pages from [Astrolabe object](/en/docs/rust/astrolabe) onward |
| `x_iztro::star` | Star placement: low-level building blocks and entry points taking birth data | [Star placement](/en/docs/rust/star) |
| `x_iztro::data` | Enums, constants, star and stem/branch tables | [Data tables](/en/docs/rust/data) |
| `x_iztro::utils` | Index arithmetic, brightness and mutagen lookups and other utilities | [Utilities](/en/docs/rust/util) |
| `x_iztro::i18n` | Six vocabularies, `translate_*` and two-way lookup | [Translation](/en/docs/rust/i18n) |
| `x_iztro::error` | `IztroError`, `BridgeError` | [Error handling](/en/docs/rust/errors) |
| `x_iztro::text` | `astrolabe_to_text`, `horoscope_to_text`, `palace_to_text`, `surrounded_palaces_to_text`, `patterns_to_text` | [Charting entries](/en/docs/rust/astro#astrolabe_to_text--horoscope_to_text) |
| `x_iztro::dto` | The serialization DTOs shared across the language bindings | [Data model](/en/docs/guide/data-model) |
| `x_iztro::ffi` | The C ABI exports, for Go and C callers | Rust callers do not need it |
The marshalling and dispatch shared by the bindings (formerly `x_iztro::bridge`) is now crate-internal
and no longer part of the public API surface.
`use x_iztro::*;` brings in every entry function, data structure and enum plus the twelve
`translate_*` functions — every name in this reference written without a module prefix is among them.
The ones written with a prefix (`utils::fix_index`, `star::query::get_major_stars`,
`data::stars::get_star_info`, `astro::palace::get_decadals_and_ages`) are low-level building blocks
called by path.
## Two API layers [#two-api-layers]
The same job often exists at two levels in the crate; which one you want depends on what you already
have.
`by_solar` · `star::query::*` · `astro::query::*`
`star::location::*` · `star::decorative::*` · `astro::palace::*`
The derivation from birth data to the star-placement intermediates (effective hour, lunar year, month
and day, the two year pillars, the month index, the Soul and body palaces, the five elements class)
lives in `astro::context`. The birth-data layer calls it once and feeds the result to the low-level
building blocks. The two layers therefore always agree, and assembling a placement pipeline of your
own does not mean re-deriving everything from the date.
## View types [#view-types]
Rust's data structures do not hold the astrolabe themselves, so `PalaceData` cannot answer "which
palace is opposite me?" on its own. The crate binds data to astrolabe at the query entry points via
three view types:
| View | Returned by | Derefs to | Extra capability |
| ------------------ | ---------------------- | ---------------- | ------------------------------------------------------------------ |
| `PalaceRef<'a>` | `chart.palace(...)` | `&PalaceData` | Opposite palace, surrounded palaces, flying stars, mutagen palaces |
| `StarRef<'a>` | `chart.star(...)` | `&Star` | Its palace, that palace's opposite, the surrounded palaces |
| `HoroscopeRef<'a>` | `chart.horoscope(...)` | `&HoroscopeData` | Horoscope palace lookups without passing the astrolabe again |
```rust
let soul = chart.palace(Palace::Soul).unwrap();
soul.data().name; // reach the underlying fields through data()
soul.opposite_palace(); // view-only: the opposite palace
soul.flies_to(Palace::Wealth, &[Mutagen::Lu]);
```
All three views implement `Deref`, so `soul.name` and `soul.data().name` are equivalent.
## How to read an entry [#how-to-read-an-entry]
Every API entry is organized into the same eight sections:
**Purpose**
— one sentence on what it does
**Zi Wei meaning**
— the concept it corresponds to in Zi Wei Dou Shu (omitted for purely engineering functions)
**Signature**
— lifted verbatim from the source
**Parameters**
— name, type, whether required, default, description
**Return value**
— type and structure
**Example**
— a snippet you can run as-is
**Output**
— the real result of running that example
**Edge cases and pitfalls**
— empty values, out-of-range input, configuration effects, interactions with other APIs
Examples all use the same chart — **a female born 16 August 2000 in the Tiger hour**
(`("2000-8-16", 2, Gender::Female)`) — so they can be compared across pages. The full data for that
chart is on [the data model](/en/docs/guide/data-model).
# Charting entries (/en/docs/rust/astro)
by_solar, by_lunar, rearranged and the JSON convenience variants.
Charting is where everything starts: give a birth date, hour and gender, get an `Astrolabe`.
This page is the full reference for the four charting entry points.
The entry points that take external input (`by_solar`, `by_lunar`, the two JSON variants and
`get_horoscope`) all return a `Result`: date format and existence, the solar year range and the hour
index are validated up front in the core, and invalid input returns an `IztroError` rather than
panicking. `rearranged` also returns a `Result` (guarding against invalid `raw_dates` from
deserialized charts); functions whose parameters are all enums with no invalid values
(`astrolabe_to_text` and friends) return their result directly. Error types are on
[Error handling](/en/docs/rust/errors).
***
## by\_solar [#by_solar]
**Purpose** Chart a natal chart from a solar date.
**Zi Wei meaning** Zi Wei Dou Shu computes on the lunar calendar, but most people only remember their
solar birthday. This function converts solar to lunar first (including the year, month, day and hour
pillars) and places the stars from there.
When the year turns over is governed by `year_divide` — for someone born between lunar New Year and
the Beginning of Spring, the two settings give different year pillars, which in turn affects the
mutagens, the soul and body stars, and every year-based star.
**Signature**
```rust
pub fn by_solar(
solar_date: &str,
time_index: u8,
gender: Gender,
fix_leap: bool,
language: Language,
config: Config,
) -> Result
```
**Parameters**
| Parameter | Type | Required | Default | Description |
| ------------ | ---------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `solar_date` | `&str` | Yes | — | Solar date in `YYYY-M-D`; month and day need no zero padding. Years 1583–9999 |
| `time_index` | `u8` | Yes | — | Hour index 0–12. 0 is the early Zi hour (00:00–01:00), 12 the late Zi hour (23:00–24:00) |
| `gender` | `Gender` | Yes | — | `Gender::Male` or `Gender::Female`. Sets the direction of the decadal scope and of the Changsheng and Boshi gods |
| `fix_leap` | `bool` | Yes | — | Whether to correct for lunar leap months. When `true`, days after the fifteenth of a leap month count as the next month (except in the late Zi hour, see below) |
| `language` | `Language` | Yes | — | Output language; affects every translated field in the DTO. The `*_key` fields are unaffected |
| `config` | `Config` | Yes | — | Charting configuration: six switches plus custom tables. Use `Config::default()` for the defaults |
**Return value** `Astrolabe` — a complete chart with the twelve palaces, the four pillars, the soul
and body stars and the five elements class. The field list is on
[the data model](/en/docs/guide/data-model).
**Example**
```rust
use x_iztro::*;
let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?;
println!("{} | {} | {}", chart.solar_date, chart.lunar_date, chart.chinese_date);
println!("{} {} {}", chart.sign, chart.zodiac,
translate_five_elements_class(chart.five_elements_class, Language::EnUS));
println!("soul {} body {}",
translate_star(chart.soul, Language::EnUS),
translate_star(chart.body, Language::EnUS));
```
`five_elements_class`, `soul` and `body` are strongly typed enums, not strings — compare them directly
in predicates, and run them through `i18n::translate_*` when you need display text in the current
language.
**Output**
```text
2000-8-16 | 二〇〇〇年七月十七 | geng chen - jia shen - bing woo - geng yin
leo dragon wood 3rd
soul rebel body scholar
```
`lunar_date` stays in Chinese numerals in every language: `二〇〇〇年七月十七` is "the 17th day of the
7th lunar month of 2000". The star names are iztro's en-US vocabulary — `rebel` is Pojun, `scholar`
is Wenchang.
**Edge cases and pitfalls**
The Zi hour straddles midnight, splitting into the early Zi hour (00:00–01:00, belonging to the
current day) and the late Zi hour (23:00–24:00, belonging to the next). Their day pillars differ and
Ziwei's starting palace can be a day apart, so they must be distinguished — hence 13 indices.
The `day_divide` setting can reassign the late Zi hour to the current day; see
[Config in depth](/en/docs/guide/guides/config).
Four conditions must hold together for the month to advance: that lunar month really is a leap month,
`fix_leap` is `true`, the lunar day is greater than 15, and the hour index is not 12 (the late Zi
hour). Miss any one and the month index is that of the month itself.
Only for someone born in a lunar leap month after the fifteenth do `true` and `false` give different
month indices, which in turn affects Zuofu, Youbi and every month-based star.
The Gregorian reform of 1582 left a hole of dates that never existed, and the underlying calendar
library panics on them. The crate therefore limits solar support to 1583–9999 and returns
`IztroError::InvalidDate` outside that range.
Every predicate on the chart (`has`, `flies_to`, `with_mutagen` and so on) rests on
language-independent keys, so charting in a different language changes no predicate result — only
display fields such as `name`.
***
## by\_lunar [#by_lunar]
**Purpose** Chart a natal chart from a lunar date.
**Zi Wei meaning** The lunar date is Zi Wei Dou Shu's native input, and this skips the solar
conversion. Anyone who knows their lunar birthday can use it directly; the result is identical to
calling `by_solar` with the corresponding solar date.
**Signature**
```rust
pub fn by_lunar(
lunar_date: &str,
time_index: u8,
gender: Gender,
leap: LeapMonth,
language: Language,
config: Config,
) -> Result
```
**Parameters**
Identical to `by_solar` apart from the following two; `by_solar`'s `fix_leap` is folded into `leap` here.
| Parameter | Type | Required | Default | Description |
| ------------ | ----------- | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `lunar_date` | `&str` | Yes | — | Lunar date in `YYYY-M-D`; write the month as a positive number (leap months are flagged by the next parameter) |
| `leap` | `LeapMonth` | Yes | — | `NotLeap` — not a leap month; `Leap` — leap month, charted as itself; `LeapFixed` — leap month, and days after the 15th are treated as the next month (iztro `fixLeap`). Flagging a leap month that does not exist in that year falls back to the ordinary month |
**Return value** Same as `by_solar`.
**Example**
```rust
use x_iztro::*;
let a = by_lunar("2000-7-17", 2, Gender::Female, LeapMonth::NotLeap, Language::EnUS, Config::default())?;
let b = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?;
assert_eq!(a.solar_date, b.solar_date);
println!("{}", a.solar_date);
```
**Output**
```text
2000-8-16
```
**Edge cases and pitfalls**
Flag `leap` as a leap month when that month is not one and the chart is cast for the ordinary month
without an error (as in iztro). If you need strict validation, confirm the leap month exists for that
year and month before calling. `LeapMonth::from_flags(is_leap_month, fix_leap)` converts from the
iztro-style pair of booleans.
***
## rearranged [#rearranged]
**Purpose** Re-anchor the chart on a given stem and branch as the Soul palace and return a new chart;
the original is untouched.
**Zi Wei meaning** The Zhongzhou school reads one set of birth data as three charts: the heaven chart
anchors the five elements class on the Soul palace's pillar, the earth chart on the body palace's, the
human chart on the Spirit palace's. Change the anchoring pillar and the class changes, and with it the
placement of Ziwei and Tianfu, the twelve palace names, the Changsheng gods and the decadal and age
scopes are all recomputed.
This method opens that capability up to **any** stem and branch, not just those three.
**Signature**
```rust
pub fn rearranged(
&self,
from_stem: HeavenlyStem,
from_branch: EarthlyBranch,
) -> Result
```
**Parameters**
| Parameter | Type | Required | Default | Description |
| ------------- | --------------- | -------- | ------- | ----------------------------- |
| `from_stem` | `HeavenlyStem` | Yes | — | Stem of the new Soul palace |
| `from_branch` | `EarthlyBranch` | Yes | — | Branch of the new Soul palace |
**Return value** `Result`. A chart produced by a charting entry point always
rearranges successfully; `IztroError::Internal` is returned only when `raw_dates` was deserialized
or hand-built with a lunar month that does not exist in the month table.
Recomputed: the Soul and body palaces, the five elements class,
the fourteen major stars, the twelve palace names, the Changsheng gods, the decadal and age scopes,
plus Tianshang, Tianshi and Tiancai, which follow the Soul palace. Carried over from the original
chart: minor stars, the remaining adjective stars, the Boshi gods and the Sui-qian and Jiang-qian
gods.
On the rearranged chart, `patterns()` / `patterns_with()`, horoscope queries and the to\_text
projection
all compute from **the rearranged layout** — the five elements class, soul palace and decadal
ranges follow the new starting stem-branch; the birth data (dates and four pillars) stays unchanged.
**Example**
```rust
use x_iztro::*;
let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?;
// anchor on the original chart's body palace pillar — equivalent to the earth chart
let body = chart.palaces.iter().find(|p| p.is_body_palace).unwrap();
let earth = chart.rearranged(body.heavenly_stem, body.earthly_branch)?;
println!("heaven {} → earth {}",
translate_five_elements_class(chart.five_elements_class, Language::EnUS),
translate_five_elements_class(earth.five_elements_class, Language::EnUS));
```
**Output**
```text
heaven wood 3rd → earth earth 5th
```
**Edge cases and pitfalls**
For the heaven, earth and human charts just chart with
`Config::default().with_astro_type(AstroType::Earth)`; both charting entry points support it.
`rearranged` exists for anchoring on an arbitrary stem and branch.
Following: the Soul palace branch, the body palace branch, the five elements class, the soul star.
The soul star is looked up by the Soul palace branch, so moving the Soul palace updates it.
Not following: the body star. It is looked up by the **birth-year branch**, independent of where the
Soul palace sits, and re-anchoring does not change the year of birth.
With `algorithm` set to the Zhongzhou school the soul star is also taken from the year branch, in
which case it too stays put under re-anchoring.
`rearranged` returns a new chart and takes `&self` read-only. One original chart can be re-anchored
into several perspectives in a row without interference.
***
## by\_solar\_json / by\_lunar\_json [#by_solar_json--by_lunar_json]
**Purpose** Chart and return the DTO as a JSON string directly, sparing the caller the serialization.
**Signature**
```rust
pub fn by_solar_json(
solar_date: &str,
time_index: u8,
gender: Gender,
fix_leap: bool,
language: Language,
config: Config,
) -> Result
pub fn by_lunar_json(
lunar_date: &str,
time_index: u8,
gender: Gender,
leap: LeapMonth,
language: Language,
config: Config,
) -> Result
```
**Parameters** Exactly the same as the corresponding charting functions.
**Return value** `String` — the JSON serialization of the [DTO](/en/docs/guide/data-model), with
camelCase keys, values translated per `language`, plus the language-independent `*Key` fields.
**Example**
```rust
use x_iztro::*;
let json = by_solar_json("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?;
let v: serde_json::Value = serde_json::from_str(&json)?;
println!("{} {}", v["solarDate"], v["palaces"][0]["nameKey"]);
```
**Output**
```text
"2000-8-16" "wealthPalace"
```
**Edge cases and pitfalls**
These two are just shortcuts for `by_solar(...)?.to_dto()` plus serialization.
For further analysis on the Rust side use `by_solar` to get an `Astrolabe`, which gives you all the
query methods; reach for the JSON variants only when handing the result to another process or a
frontend.
***
## get\_horoscope [#get_horoscope]
**Purpose** Compute the horoscope for a target date, starting from a natal chart.
**Zi Wei meaning** A horoscope layers six scopes — decadal, age, yearly, monthly, daily and hourly —
onto the natal chart, each with its own starting palace, pillar and scope stars.
**Signature**
```rust
pub fn get_horoscope(
astrolabe: &Astrolabe,
solar_date: &str,
time_index: u8,
language: Language,
) -> Result
```
**Parameters**
| Parameter | Type | Required | Default | Description |
| ------------ | ------------ | -------- | ------- | ------------------------------------------------ |
| `astrolabe` | `&Astrolabe` | Yes | — | The natal chart |
| `solar_date` | `&str` | Yes | — | Target solar date in `YYYY-M-D`, years 1583–9999 |
| `time_index` | `u8` | Yes | — | Target hour index 0–12 |
| `language` | `Language` | Yes | — | Output language |
**Return value** `Result`. Details on
[the horoscope object](/en/docs/rust/horoscope).
**Example**
```rust
use x_iztro::*;
let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?;
let h = get_horoscope(&chart, "2025-1-1", 0, Language::EnUS)?;
println!("decadal palace index {}, yearly pillar {:?}{:?}",
h.decadal.index, h.yearly.heavenly_stem, h.yearly.earthly_branch);
```
**Output**
```text
decadal palace index 2, yearly pillar JiaChen
```
**Edge cases and pitfalls**
When you want to keep querying off the horoscope (fetching a scope's palaces, testing scope stars),
use the astrolabe method `chart.horoscope(...)` to get a `HoroscopeRef` — it holds the natal chart
too, so queries need not be handed the chart again. The free function here returns only the data.
***
## astrolabe\_to\_text / horoscope\_to\_text [#astrolabe_to_text--horoscope_to_text]
**Purpose** Project a chart or a horoscope into semantic text — the chart's facts in
natural-language form, for a language model or a person. Alongside `serde_json` (machine structure)
and the translated fields (display), it is the third projection of the same object.
**Signature** (the `x_iztro::text` module, which also holds `palace_to_text` /
`surrounded_palaces_to_text` / `patterns_to_text`)
```rust
pub fn astrolabe_to_text(astrolabe: &Astrolabe, lang: Language) -> String
pub fn horoscope_to_text(
astrolabe: &Astrolabe,
horoscope: &HoroscopeData,
lang: Language,
) -> String
```
Convenience methods that emit in the chart language: `Astrolabe::to_text()`,
`HoroscopeRef::to_text()`, `PalaceRef::to_text()`; `SurroundedPalaces::to_text(lang)` takes an
explicit language. The free functions' `lang` may differ from the charting language: star names,
hours, zodiac signs, stems and branches and flowing stars are all re-translated by key into the
target language, byte-identical to a chart cast in that language.
**Parameters**
| Parameter | Type | Required | Default | Description |
| ----------- | ---------------- | -------- | ------- | ---------------------------------------------------------------------------- |
| `astrolabe` | `&Astrolabe` | Yes | — | The natal chart |
| `horoscope` | `&HoroscopeData` | Yes | — | The result of `get_horoscope` |
| `lang` | `Language` | Yes | — | Output language, which switches both the section headings and the star names |
**Return value** `String`, sectioned plain text; the natal text closes with a patterns section, and
each horoscope scope carries a patterns line and flowing-star lines from its own perspective.
**Example**
```rust
use x_iztro::*;
let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?;
print!("{}", chart.to_text());
```
**Output**
```text
=== Basic Info ===
Gender: female
Solar Date: 2000-8-16
Lunar Date: 二〇〇〇年七月十七
Chinese Date: geng chen - jia shen - bing woo - geng yin
Time: Tiger hour (03:00~05:00)
Zodiac Sign: leo
Zodiac Animal: dragon
Soul Palace Branch: woo
Body Palace Branch: xu
Soul Star: rebel
Body Star: scholar
Five Elements Class: wood 3rd
Birth-Year Mutagen: sunA, generalB, moonC, fortunateD
=== Palaces ===
--- wealth ---
Stem-Branch: wuyin
Decadal: 43-52
Age Fortune Years: 9, 21, 33, 45, 57, 69, 81, 93, 105, 117
Twelve Gods: dissipated, gossip, sorrowing, varied
Major Stars: general([+1])[B], minister([+3])
Minor Stars: horse
Adjective Stars: considery, senior, ageless, psychic, gourmet, gloomy, upset
(the other eleven palaces follow the same format and are omitted here)
=== Patterns ===
- Empress and Minister Facing the Palace(soul): empress([+3]), minister([+3])
```
The complete output with a field-by-field walkthrough is on
[Semantic text](/en/docs/guide/guides/to-text).
This is an addition of x-iztro's beyond iztro, available in all three languages.
Wiring it to a model is covered on [Docs for AI](/en/docs/guide/guides/llm).
# Astrolabe object (/en/docs/rust/astrolabe)
The fields of Astrolabe, its lookup methods, and the surrounded-palace predicates.
`Astrolabe` is what charting produces and the entry point for every query. It holds all the data of
the twelve palaces along with chart-level information such as the four pillars, the soul and body
stars and the five elements class.
```rust
let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?;
```
The examples on this page all chart with `Language::EnUS`, so the display values in the output are
iztro's en-US vocabulary — `emperor` for Ziwei, `soul` for the Soul palace, and so on. Charting in
another language changes those strings only; the `*_key` fields and every predicate stay the same.
## Fields [#fields]
| Field | Type | Description |
| -------------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `gender` | `Gender` | Gender |
| `solar_date` | `String` | Solar date, as passed in |
| `lunar_date` | `String` | The lunar date written in Chinese numerals, e.g. `二〇〇〇年七月十七` — "the 17th day of the 7th lunar month of 2000" |
| `chinese_date` | `String` | Display string of the four pillars, e.g. `geng chen - jia shen - bing woo - geng yin` |
| `time` | `String` | Hour name, e.g. `Tiger hour` |
| `time_range` | `String` | The clock range of that hour, e.g. `03:00~05:00` |
| `sign` | `String` | Zodiac sign, from the solar date |
| `zodiac` | `String` | Chinese zodiac animal, from the year branch |
Display fields follow `language`. For predicates use the key fields in the next group.
| Field | Type | Description |
| ------------------------------- | ------------------- | -------------------------------------------------------------------------------------------- |
| `sign_key` | `String` | Zodiac sign key, `aries` … `pisces` |
| `zodiac_key` | `String` | Zodiac animal key, `rat` … `pig` |
| `earthly_branch_of_soul_palace` | `EarthlyBranch` | Branch of the Soul palace |
| `earthly_branch_of_body_palace` | `EarthlyBranch` | Branch of the body palace |
| `soul` | `StarKey` | Soul star |
| `body` | `StarKey` | Body star |
| `five_elements_class` | `FiveElementsClass` | Five elements class, which sets the starting age of the decadal scope and where Ziwei begins |
These are strongly typed enums, independent of language, and can be compared directly.
| Field | Type | Description |
| ----------- | ------------------ | --------------------------------------------------------------------------------------- |
| `palaces` | `[PalaceData; 12]` | The twelve palaces as a fixed-size array; index 0 is the Yin palace, 11 the Chou palace |
| `raw_dates` | `RawDates` | The structured lunar birth date and the four pillars as enums |
Indices into `palaces` are **palace indices**, not the palace-name order: `palaces[0]` is always the
Yin palace, and the Soul palace can be in any of the cells. Fetch it with
`chart.palace(Palace::Soul)`.
`raw_dates` is the data form of the two display strings `lunar_date` and `chinese_date`. Use it for
date arithmetic or stem-and-branch lookups instead of parsing the Chinese strings:
```rust
pub struct RawDates {
pub lunar_date: RawLunarDate,
pub chinese_date: RawChineseDate,
}
pub struct RawLunarDate {
pub lunar_year: i64, // lunar year
pub lunar_month: u32, // lunar month 1–12; whether it is a leap month is in is_leap
pub lunar_day: u32, // lunar day 1–30
pub is_leap: bool, // whether it is a leap month
}
pub struct RawChineseDate {
pub yearly: (HeavenlyStem, EarthlyBranch), // year pillar
pub monthly: (HeavenlyStem, EarthlyBranch), // month pillar
pub daily: (HeavenlyStem, EarthlyBranch), // day pillar
pub hourly: (HeavenlyStem, EarthlyBranch), // hour pillar
}
```
All three types are re-exported at the crate root, so `use x_iztro::*;` is enough.
| Field | Type | Description |
| ------------ | ---------- | -------------------------------------------------------------------------------------------------------- |
| `time_index` | `u8` | Birth hour index, kept as passed in even when `day_divide` reassigns the late Zi hour to the current day |
| `fix_leap` | `bool` | Whether leap-month correction was applied when charting |
| `language` | `Language` | Output language |
| `config` | `Config` | Charting configuration |
Horoscopes and prompts restart their computation from these four, so the charting parameters need not
be supplied again.
***
## palace [#palace]
**Purpose** Fetch a palace by index, by name, or as the body or original palace.
**Zi Wei meaning** The twelve palaces are the skeleton of a chart. Once the Soul palace is fixed the
other eleven follow counterclockwise in a fixed order.
The "body palace" is whichever of the twelve also carries that flag, marking where acquired effort
concentrates; the "original palace" is the one whose stem matches the birth-year stem, marking where
matters originate.
**Signature**
```rust
pub fn palace(&self, target: impl Into) -> Option>
```
**Parameters**
| Parameter | Type | Required | Default | Description |
| --------- | ------------------------- | -------- | ------- | ----------------------------------- |
| `target` | `impl Into` | Yes | — | Four spellings, see the table below |
All four variants of `PalaceTarget` have `From` implementations, so you write the value directly:
| Spelling | Example | Meaning |
| --------------- | -------------------------------------- | ------------------------------------------------- |
| Index | `chart.palace(0)` | Palace index 0–11, where 0 is the Yin palace |
| Name | `chart.palace(Palace::Soul)` | One of the twelve palace names |
| Body palace | `chart.palace(PalaceTarget::Body)` | Whichever palace carries the body-palace flag |
| Original palace | `chart.palace(PalaceTarget::Original)` | The palace whose stem matches the birth-year stem |
**Return value** `Option>`. An out-of-range index returns `None`; the name, body-palace
and original-palace spellings resolve on any chart and are never `None`.
**Example**
```rust
let en = Language::EnUS;
let soul = chart.palace(Palace::Soul).unwrap();
println!("{} {} {}", translate_palace(soul.name, en),
translate_heavenly_stem(soul.heavenly_stem, en),
translate_earthly_branch(soul.earthly_branch, en));
let body = chart.palace(PalaceTarget::Body).unwrap();
println!("body palace falls in {}", translate_palace(body.name, en));
let original = chart.palace(PalaceTarget::Original).unwrap();
println!("original palace is {}", translate_palace(original.name, en));
println!("the Yin palace is {}", translate_palace(chart.palace(0).unwrap().name, en));
```
`PalaceData::name` has the type `Palace`, an enum rather than a string, so it cannot be printed with
`{}` directly — the enum is a language-independent key, and display goes through `translate_palace`
to get text in the current language. The same holds for `heavenly_stem`, `earthly_branch`,
`five_elements_class` and the other enum fields.
**Output**
```text
soul ren woo
body palace falls in career
original palace is spouse
the Yin palace is wealth
```
**Edge cases and pitfalls**
The original palace requires the palace stem to equal the birth-year stem, and the palace not to be Zi
or Chou. Palace stems run forward from the Yin palace under the Five Tigers rule, and the ten palaces
from Yin to You walk the ten stems exactly once; Zi and Chou are the eleventh and twelfth cells and
repeat the stems of Yin and Mao — and it is precisely that repetition that excludes them.
So any birth-year stem hits exactly once between Yin and You: on every chart the original palace
exists, and it is unique.
Each of the twelve palace names appears exactly once on a chart, so a lookup by name is necessarily
unique. The body palace is a **flag**, not a name — it is also one of the twelve palaces (the Career
palace in the example above). The same goes for the original palace, which lands on the Spouse palace
here.
***
## star [#star]
**Purpose** Find a star by key and get a view that can trace back to its palace.
**Signature**
```rust
pub fn star(&self, key: StarKey) -> Option>
```
**Parameters**
| Parameter | Type | Required | Default | Description |
| --------- | --------- | -------- | ------- | ---------------------------------- |
| `key` | `StarKey` | Yes | — | Star key, e.g. `StarKey::ZiweiMaj` |
**Return value** `Option>`. `None` when the star is not on this chart.
**Example**
```rust
let en = Language::EnUS;
let ziwei = chart.star(StarKey::ZiweiMaj).unwrap();
println!("{} sits in {}", ziwei.name, translate_palace(ziwei.palace().name, en));
println!("its opposite palace is {}", translate_palace(ziwei.opposite_palace().name, en));
println!("brightness {:?} mutagen {:?}", ziwei.brightness, ziwei.mutagen);
```
`Star::name` is a `String` (already translated into the charting language) and prints directly;
`PalaceData::name` is an enum and goes through `translate_palace`.
**Output**
```text
emperor sits in soul
its opposite palace is surface
brightness Some(Miao) mutagen None
```
**Edge cases and pitfalls**
The search covers only the three groups of major, minor and adjective stars. The Changsheng, Boshi,
Sui-qian and Jiang-qian gods are one-per-palace marks rather than star lists — read them from fields
like `palace.changsheng12`.
***
## surrounded\_palaces [#surrounded_palaces]
**Purpose** Fetch the surrounded palaces of a target palace.
**Zi Wei meaning** The surrounded set is the most commonly used reading scope in Zi Wei Dou Shu: the
palace itself, its opposite (index +6), the career position (+4) and the wealth position (+8). The
four are read together rather than the palace alone, because the stars of the opposite and trine
palaces bear on the palace's affairs just as much.
**Signature**
```rust
pub fn surrounded_palaces(&self, target: impl Into) -> Option>
```
**Parameters** Same as `palace`; all four spellings are supported.
**Return value** `Option>`, holding the four `&PalaceData` values `target` /
`opposite` / `wealth` / `career` (not `PalaceRef`s: their fields read directly, but they carry none of
the methods that need the astrolabe for context, such as the opposite palace or flying stars).
Its predicates are on [Surrounded palaces](/en/docs/rust/surpalaces).
**Example**
```rust
let en = Language::EnUS;
let sp = chart.surrounded_palaces(Palace::Soul).unwrap();
println!("{} / {} / {} / {}",
translate_palace(sp.target.name, en), translate_palace(sp.opposite.name, en),
translate_palace(sp.wealth.name, en), translate_palace(sp.career.name, en));
println!("Ziwei in the surrounded set: {}", sp.have(&[StarKey::ZiweiMaj]));
```
**Output**
```text
soul / surface / wealth / career
Ziwei in the surrounded set: true
```
***
## is\_surrounded / is\_surrounded\_one\_of / not\_surrounded [#is_surrounded--is_surrounded_one_of--not_surrounded]
**Purpose** Test the surrounded palaces of a palace straight from the astrolabe, skipping the step of
fetching the set first.
**Signature**
```rust
pub fn is_surrounded(&self, target: impl Into, stars: &[StarKey]) -> bool
pub fn is_surrounded_one_of(&self, target: impl Into, stars: &[StarKey]) -> bool
pub fn not_surrounded(&self, target: impl Into, stars: &[StarKey]) -> bool
```
**Parameters**
| Parameter | Type | Required | Default | Description |
| --------- | ------------------------- | -------- | ------- | ----------------------------------- |
| `target` | `impl Into` | Yes | — | Located the same way as in `palace` |
| `stars` | `&[StarKey]` | Yes | — | A list of star keys |
**Return value**
| Method | Meaning |
| ---------------------- | ---------------------------------------------------------- |
| `is_surrounded` | **Every** star in the list is in the surrounded set |
| `is_surrounded_one_of` | **At least one** star in the list is in the surrounded set |
| `not_surrounded` | **None** of the stars in the list is in the surrounded set |
**Example**
```rust
use x_iztro::StarKey::*;
println!("{}", chart.is_surrounded(Palace::Soul, &[ZiweiMaj, TianxiangMaj]));
println!("{}", chart.is_surrounded_one_of(Palace::Soul, &[QishaMaj, PojunMaj]));
println!("{}", chart.not_surrounded(Palace::Soul, &[HuoxingMin]));
```
**Output**
```text
true
false
true
```
The Soul palace holds only Ziwei, while Tianxiang sits in the Wealth palace, one of the trine — hence
`true` on the first line. Neither Qisha nor Pojun is in any of the four, hence `false` on the second.
**Edge cases and pitfalls**
With an empty `stars` slice, `is_surrounded` and `not_surrounded` return `true` ("all elements
satisfy" and "no element fails" both hold vacuously) while `is_surrounded_one_of` returns `false`.
Make sure the list is non-empty before calling.
***
## horoscope / horoscope\_now [#horoscope--horoscope_now]
**Purpose** Compute the horoscope for a target date, starting from this chart.
**Signature**
```rust
pub fn horoscope(&self, target_date: &str, target_time_index: u8) -> Result, IztroError>
pub fn horoscope_now(&self) -> Result, IztroError>
```
**Parameters**
| Parameter | Type | Required | Default | Description |
| ------------------- | ------ | -------- | ------- | ---------------------------------------------------- |
| `target_date` | `&str` | Yes | — | Target solar date in `YYYY-M-D` |
| `target_time_index` | `u8` | Yes | — | Target hour index 0–12, which fixes the hourly scope |
`horoscope_now` takes the current date and hour from the local clock and has no parameters.
**Return value** `HoroscopeRef<'_>` — a horoscope view holding this chart, so palace lookups across
the six scopes need not be passed the astrolabe again.
Details on [the horoscope object](/en/docs/rust/horoscope).
**Example**
```rust
let en = Language::EnUS;
let h = chart.horoscope("2025-6-1", 0)?;
println!("decadal {} {}",
translate_heavenly_stem(h.decadal.heavenly_stem, en),
translate_earthly_branch(h.decadal.earthly_branch, en));
println!("yearly {} {}",
translate_heavenly_stem(h.yearly.heavenly_stem, en),
translate_earthly_branch(h.yearly.earthly_branch, en));
```
`decadal` / `monthly` / `daily` / `hourly` are `HoroscopeItem`s whose pillars read directly, while
`yearly` and `age` each carry one extra datum of their own (the shared fields sit under `base`),
but both implement `Deref`, so `h.yearly.heavenly_stem` reads directly as well.
**Output**
```text
decadal geng chen
yearly yi si
```
***
## to\_text [#to_text]
**Purpose** The chart's semantic text: a complete description for language models and people.
**Signature**
```rust
pub fn to_text(&self) -> String
```
Emits in the charting language; for an explicit language use the free function
`text::astrolabe_to_text(astrolabe, lang)` — `lang` may differ from the charting language, with
every field re-translated by key into the target language. For single palaces and surrounded palaces see
`PalaceRef::to_text()` / `SurroundedPalaces::to_text(lang)`. The full format is on
[Semantic text](/en/docs/guide/guides/to-text).
**Example**
```rust
println!("{}", chart.to_text().chars().take(77).collect::());
```
**Output**
```text
=== Basic Info ===
Gender: female
Solar Date: 2000-8-16
Lunar Date: 二〇〇〇年七月十七
```
***
## to\_dto [#to_dto]
**Purpose** Convert the chart into the serialization structure that matches the JS iztro field
contract.
**Signature**
```rust
pub fn to_dto(&self) -> AstrolabeDto
```
**Return value** `x_iztro::dto::AstrolabeDto` — camelCase keys with values translated into the
**charting language**, plus the language-independent `*Key` fields and the charting context
(`genderKey` / `timeIndex` / `fixLeap` / `language` / `config`).
The field list is on [the data model](/en/docs/guide/data-model).
**Example**
```rust
let dto = chart.to_dto();
let json = serde_json::to_string(&dto)?;
let v: serde_json::Value = serde_json::from_str(&json)?;
println!("{} {}", v["solarDate"], v["palaces"][4]["nameKey"]);
println!("{}", v["config"]["yearDivide"]);
```
**Output**
```text
"2000-8-16" "soulPalace"
"normal"
```
**Edge cases and pitfalls**
The DTO is for the language bindings and for frontends. For analysis on the Rust side use `Astrolabe`
itself — it has every query method, while the DTO is only data. To get a JSON string in one step, use
[`by_solar_json`](/en/docs/rust/astro#by_solar_json--by_lunar_json).
The `overrides` of `Config` (the custom mutagen and brightness tables) do not go into the DTO: they
are charting input rather than result, and echoing them back would break the field contract with JS
iztro.
# Palace object (/en/docs/rust/palace)
The fields of PalaceData plus every star predicate, empty-palace check and flying-star method.
Palaces are where most Zi Wei analysis happens. The data itself is a `PalaceData`, while
`chart.palace(...)` returns a `PalaceRef` — the same data plus a reference back to the astrolabe.
| | `PalaceData` | `PalaceRef<'a>` |
| ------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| Where it comes from | `chart.palaces[i]`, `sp.target` and other fields | `chart.palace(...)`, `star.palace()`, every query entry other than `sp` |
| Fields | All | All readable through `Deref`; `data()` reaches the underlying value |
| Predicates | `has` / `is_empty` / the `flies_to` family (the target palace must be a `&PalaceData`) | The same methods, with the target palace written as an index or a palace name |
| View-only | — | `opposite_palace` / `surrounded_palaces` / `mutaged_places` / `astrolabe` |
The entries on this page give the signatures in their `PalaceRef` form; the same methods on
`PalaceData` differ only in the target-palace parameter of the flying-star family (`&PalaceData`
rather than `impl Into`).
```rust
let soul = chart.palace(Palace::Soul).unwrap();
soul.name; // reached directly through Deref
soul.opposite_palace(); // view-only
```
The examples on this page all chart with `Language::EnUS`, so the display values in the output are
iztro's en-US vocabulary — `emperor` for Ziwei, `soul` for the Soul palace, and so on. Enum fields
such as `name` are themselves language-independent; the translation to text happens only at display
time through `translate_*`.
## Fields [#fields]
| Field | Type | Description |
| -------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------- |
| `index` | `usize` | Palace index 0–11, where 0 is the Yin palace |
| `name` | `Palace` | Palace name |
| `is_body_palace` | `bool` | Whether this is the body palace |
| `is_original_palace` | `bool` | Whether this is the original palace (stem equal to the year stem, and not the Zi or Chou palace) |
| `heavenly_stem` | `HeavenlyStem` | Palace stem, which determines the mutagens this palace flies out |
| `earthly_branch` | `EarthlyBranch` | Palace branch, fixed by the index: 0 is yin, 11 is chou |
| `major_stars` | `Vec` | Whichever of the fourteen major stars fall here, in placement order |
| `minor_stars` | `Vec` | Whichever of the fourteen minor stars fall here |
| `adjective_stars` | `Vec` | Adjective stars |
| `changsheng12` | `StarKey` | The Changsheng god of this palace, exactly one per palace |
| `boshi12` | `StarKey` | The Boshi god |
| `jiangqian12` | `StarKey` | The Jiang-qian god |
| `suiqian12` | `StarKey` | The Sui-qian god |
| `decadal` | `Decadal` | The decade: age range plus stem and branch |
| `ages` | `Vec` | Nominal ages at which the age scope passes through this palace |
| `overrides` | `Option>` | The custom mutagen and brightness tables in effect when charting; `None` when nothing was customized |
Major, minor and adjective stars are **lists** — a palace can hold zero or many.
The Changsheng, Boshi, Jiang-qian and Sui-qian gods are marks of which each palace has **exactly
one**, filling one full cycle across the twelve palaces, so they are single-valued fields rather than
lists.
What `overrides` carries are the custom tables from the charting configuration — the flying-star
methods look up mutagens by palace stem, and a custom table may have rewritten the mutagens of some
stem, so the palace has to carry it around.
It takes no part in serialization: neither the DTO nor the JSON output has this item.
***
## has / not\_have / has\_one\_of [#has--not_have--has_one_of]
**Purpose** Test which stars sit in this palace.
**Zi Wei meaning** Where stars fall is the basic information on a chart. "The Soul palace holds Ziwei
and Tianxiang" is `has(&[ZiweiMaj, TianxiangMaj])`. The search covers all three groups of major, minor
and adjective stars.
**Signature**
```rust
pub fn has(&self, stars: &[StarKey]) -> bool
pub fn not_have(&self, stars: &[StarKey]) -> bool
pub fn has_one_of(&self, stars: &[StarKey]) -> bool
```
**Parameters**
| Parameter | Type | Required | Default | Description |
| --------- | ------------ | -------- | ------- | ------------------- |
| `stars` | `&[StarKey]` | Yes | — | A list of star keys |
**Return value**
| Method | Meaning |
| ------------ | ----------------------------------------------- |
| `has` | Every star in the list is in this palace |
| `not_have` | No star in the list is in this palace |
| `has_one_of` | At least one star in the list is in this palace |
**Example**
```rust
use x_iztro::StarKey::*;
let soul = chart.palace(Palace::Soul).unwrap();
println!("{}", soul.has(&[ZiweiMaj, TianxiangMaj]));
println!("{}", soul.has_one_of(&[QishaMaj, ZiweiMaj]));
println!("{}", soul.not_have(&[HuoxingMin, LingxingMin]));
```
**Output**
```text
false
true
true
```
On this chart the Soul palace holds only Ziwei, with Tianxiang in the Wealth palace, so `has` — which
demands both — is `false`.
**Edge cases and pitfalls**
With an empty list, `has` and `not_have` return `true` while `has_one_of` returns `false`.
***
## has\_mutagen / not\_have\_mutagen [#has_mutagen--not_have_mutagen]
**Purpose** Test whether this palace carries a given mutagen.
**Zi Wei meaning** Natal mutagens are determined by the **birth-year stem** and marked on the
corresponding stars. A palace "having lu" means some star sitting in it was given lu by the birth-year
stem.
Note this differs from flying stars — flying looks at the palace stem, while this looks at the mark
already on the star.
**Signature**
```rust
pub fn has_mutagen(&self, mutagen: Mutagen) -> bool
pub fn not_have_mutagen(&self, mutagen: Mutagen) -> bool
```
**Parameters**
| Parameter | Type | Required | Default | Description |
| --------- | --------- | -------- | ------- | ---------------------------------- |
| `mutagen` | `Mutagen` | Yes | — | One of `Lu` / `Quan` / `Ke` / `Ji` |
**Return value** `bool`. Only `major_stars` and `minor_stars` are scanned — **not** adjective stars.
**Example**
```rust
let children = chart.palace(Palace::Children).unwrap();
println!("Children palace has lu: {}", children.has_mutagen(Mutagen::Lu));
println!("Children palace lacks ji: {}", children.not_have_mutagen(Mutagen::Ji));
```
**Output**
```text
Children palace has lu: true
Children palace lacks ji: true
```
**Edge cases and pitfalls**
`has_mutagen` looks only at the mutagen marks on major and minor stars; an adjective star carrying a
mark does not count (this reproduces iztro's behaviour). To include adjective stars, walk the
`mutagen` field of `adjective_stars` yourself.
Natal mutagens only ever land on the fourteen major stars and a few minor stars, so on real charts the
two readings usually agree.
***
## is\_empty / is\_empty\_excluding [#is_empty--is_empty_excluding]
**Purpose** Test whether this palace is empty.
**Zi Wei meaning** An "empty palace" holds none of the fourteen major stars. Empty palaces are read
by borrowing the major stars of the opposite palace, and the test is a very common branch in Zi Wei
analysis. Minor and adjective stars do not by default prevent a palace from counting as empty.
**Signature**
```rust
pub fn is_empty(&self) -> bool
pub fn is_empty_excluding(&self, exclude_stars: &[StarKey]) -> bool
```
**Parameters**
| Parameter | Type | Required | Default | Description |
| --------------- | ------------ | -------- | ------- | -------------------------------------------------------------------------------------------------------------- |
| `exclude_stars` | `&[StarKey]` | Yes | — | Stars that additionally count: with no major star but one of these present, the palace is **not** empty either |
**Return value** `bool`. The order of decision is: major stars first — any present and it is not
empty; then `exclude_stars` — a hit and it is not empty; only if neither holds is the palace empty.
**Example**
```rust
let parents = chart.palace(Palace::Parents).unwrap();
println!("Parents palace empty: {}", parents.is_empty());
let friends = chart.palace(Palace::Friends).unwrap();
println!("Friends palace empty: {}", friends.is_empty());
// the Parents palace has no major star but does hold Tuoluo — counting Tuoluo makes it non-empty
println!("Parents palace counting Tuoluo: {}", parents.is_empty_excluding(&[StarKey::TuoluoMin]));
```
**Output**
```text
Parents palace empty: true
Friends palace empty: false
Parents palace counting Tuoluo: false
```
On this chart only the Parents and Property palaces lack major stars. The Friends palace holds Taiyin
and so is not empty.
**Edge cases and pitfalls**
`exclude_stars` does not mean "ignore these stars in the test"; it means "these stars count too".
It has no effect at all when the palace already holds a major star — a major star settles the question
before the list is consulted.
`is_empty` checks `major_stars` only. A palace packed with minor and adjective stars but no major star
is still empty. To have certain minor stars count as "filling" the palace, pass them to
`is_empty_excluding`.
***
## flies\_to / flies\_one\_of\_to / not\_fly\_to [#flies_to--flies_one_of_to--not_fly_to]
**Purpose** Test whether the mutagens flown by this palace's stem land in a target palace.
**Zi Wei meaning** The core technique of the flying-star school. Every palace has its own stem, and
the stem determines through the mutagen table which four stars take lu, quan, ke and ji. If a
transformed star happens to sit in the target palace, that is "this palace flies X into the target
palace". "The Soul palace flies lu into Wealth" says that the smooth going of the Soul palace's
affairs lands on wealth.
**Signature**
```rust
pub fn flies_to(&self, target: impl Into, mutagens: &[Mutagen]) -> bool
pub fn flies_one_of_to(&self, target: impl Into, mutagens: &[Mutagen]) -> bool
pub fn not_fly_to(&self, target: impl Into, mutagens: &[Mutagen]) -> bool
```
**Parameters**
| Parameter | Type | Required | Default | Description |
| ---------- | ------------------------- | -------- | ------- | --------------------------------------------------------------- |
| `target` | `impl Into` | Yes | — | The target palace: index / name / body palace / original palace |
| `mutagens` | `&[Mutagen]` | Yes | — | The mutagens to check |
**Return value**
| Method | Meaning |
| ----------------- | ------------------------------------------------------ |
| `flies_to` | **All** the listed mutagens fly into the target palace |
| `flies_one_of_to` | **At least one** of them flies into the target palace |
| `not_fly_to` | **None** of them flies into the target palace |
**Example**
```rust
let soul = chart.palace(Palace::Soul).unwrap();
println!("Soul flies lu into Wealth: {}", soul.flies_to(Palace::Wealth, &[Mutagen::Lu]));
println!("Soul flies lu or ji into Surface: {}", soul.flies_one_of_to(Palace::Surface, &[Mutagen::Lu, Mutagen::Ji]));
println!("Soul does not fly quan into Children: {}", soul.not_fly_to(Palace::Children, &[Mutagen::Quan]));
```
**Output**
```text
Soul flies lu into Wealth: false
Soul flies lu or ji into Surface: false
Soul does not fly quan into Children: true
```
**Edge cases and pitfalls**
With an empty `mutagens` slice, `flies_to` returns `false` while `flies_one_of_to` and `not_fly_to`
return `true`.
That runs against the intuition that a universal statement holds vacuously over the empty set, but it
reproduces iztro's behaviour: `flies_to` first works out the stars to look for and returns false the
moment there are none. Passing an empty list is usually a caller oversight — confirm the list is
non-empty.
When the target resolves to no palace, all three methods on a `PalaceRef` return `false`, including
the semantically negative `not_fly_to` — failing to locate a palace is not the same as "nothing flew
in". Indices are taken modulo 12 first, so values like `12` or `-1` are not location failures.
Once `Config::with_mutagens` replaces the table for a heavenly stem, the stars flown by palaces
carrying that stem change with it. The flying-star methods read the table that was in effect during
charting, not the built-in default.
Writing the palace itself as the target means "self-mutagen" semantically. The `self_mutaged` family
is more direct there.
***
## self\_mutaged / self\_mutaged\_one\_of / not\_self\_mutaged [#self_mutaged--self_mutaged_one_of--not_self_mutaged]
**Purpose** Test whether this palace self-mutates.
**Zi Wei meaning** A self-mutagen is when a star transformed by the palace's own stem happens to sit
in that palace. It reads as "releasing its own energy back into itself", unlike the directed action of
flying into another palace.
**Signature**
```rust
pub fn self_mutaged(&self, mutagens: &[Mutagen]) -> bool
pub fn self_mutaged_one_of(&self, mutagens: &[Mutagen]) -> bool
pub fn not_self_mutaged(&self, mutagens: &[Mutagen]) -> bool
```
**Parameters**
| Parameter | Type | Required | Default | Description |
| ---------- | ------------ | -------- | ------- | ------------------------------------------------------ |
| `mutagens` | `&[Mutagen]` | Yes | — | The mutagens to check; an empty slice means "all four" |
**Return value**
| Method | Meaning |
| --------------------- | ------------------------------------------------------------------- |
| `self_mutaged` | All the listed mutagens are self-mutated |
| `self_mutaged_one_of` | At least one of them is self-mutated; an empty list checks all four |
| `not_self_mutaged` | None of them is self-mutated; an empty list checks all four |
**Example**
```rust
let career = chart.palace(Palace::Career).unwrap();
println!("Career self-mutates lu: {}", career.self_mutaged(&[Mutagen::Lu]));
println!("Career self-mutates ji: {}", career.self_mutaged(&[Mutagen::Ji]));
println!("Career has any self-mutagen: {}", career.self_mutaged_one_of(&[]));
println!("Career has no self-mutagen: {}", career.not_self_mutaged(&[]));
```
**Output**
```text
Career self-mutates lu: false
Career self-mutates ji: true
Career has any self-mutagen: true
Career has no self-mutagen: false
```
The Career palace's stem is bing, bing sends ji to Lianzhen, and Lianzhen sits right in the Career
palace — hence a self-mutated ji.
**Edge cases and pitfalls**
`self_mutaged_one_of` and `not_self_mutaged` read an empty list as "all four mutagens", not as the
empty set. `self_mutaged` makes no such fallback: an empty list degenerates into "does this palace
contain the empty set", which is always `true` — the exact opposite of the empty-list `false` of
`flies_to`. Do not carry the intuition from one family over to the other.
***
## mutaged\_places / mutagen\_stars [#mutaged_places--mutagen_stars]
**Purpose** Get which palaces the four stars transformed by this palace's stem land in, or get those
four stars themselves.
**Zi Wei meaning** The panoramic version of flying-star analysis: instead of asking "does it fly to
that palace?", collect all four landing places for lu, quan, ke and ji at once.
**Signature**
```rust
pub fn mutaged_places(&self) -> Vec