# 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>> pub fn mutagen_stars(&self, mutagens: &[Mutagen]) -> Vec ``` **Parameters** | Parameter | Type | Required | Default | Description | | ---------- | ------------ | -------- | ------- | ------------------------------------------------------------------- | | `mutagens` | `&[Mutagen]` | Yes | — | Which mutagen slots to take; repeating one repeats it in the output | **Return value** `mutaged_places` returns a `Vec` of length 4 in the order **lu, quan, ke, ji**, with `None` in a slot whose transformed star is not on this chart. `mutagen_stars` returns a `Vec` in the order the mutagens were passed. **Example** ```rust let soul = chart.palace(Palace::Soul).unwrap(); for (m, place) in ["lu", "quan", "ke", "ji"].iter().zip(soul.mutaged_places()) { match place { Some(p) => println!("{m} → {}", translate_palace(p.name, Language::EnUS)), None => println!("{m} → not on this chart"), } } println!("{:?}", soul.mutagen_stars(&[Mutagen::Lu, Mutagen::Ji])); ``` **Output** ```text lu → children quan → soul ke → career ji → wealth [TianliangMaj, WuquMaj] ``` The Soul palace's stem is ren, and ren sends lu to Tianliang, quan to Ziwei, ke to Zuofu and ji to Wuqu; those four stars sit in the Children, Soul, Career and Wealth palaces respectively. `mutagen_stars` gives "which stars this palace's stem transforms", unrelated to a star's own `mutagen` field (the natal mutagens), which is determined by the birth-year stem. `PalaceRef::mutaged_places` takes no parameters and always returns a result of length 4 in the slots lu, quan, ke, ji. The method of the same name on `PalaceData` takes the twelve-palace slice (`p.mutaged_places(&chart.palaces)`) and returns `Vec>` palace indices rather than palace views. *** ## opposite\_palace / surrounded\_palaces [#opposite_palace--surrounded_palaces] **Purpose** Get this palace's opposite palace and its surrounded set. **Zi Wei meaning** The opposite palace sits at index +6, and the two are always read facing each other. The surrounded set adds +4 (the career position) and +8 (the wealth position) to that. **Signature** ```rust pub fn opposite_palace(&self) -> PalaceRef<'a> pub fn surrounded_palaces(&self) -> SurroundedPalaces<'a> ``` **Return value** `opposite_palace` always exists and does not return an `Option`. For `surrounded_palaces` see [Surrounded palaces](/en/docs/rust/surpalaces). **Example** ```rust let en = Language::EnUS; let soul = chart.palace(Palace::Soul).unwrap(); println!("the opposite of {} is {}", translate_palace(soul.name, en), translate_palace(soul.opposite_palace().name, en)); println!("malefics in the surrounded set: {}", soul.surrounded_palaces().have_one_of(&[StarKey::HuoxingMin, StarKey::LingxingMin])); ``` **Output** ```text the opposite of soul is surface malefics in the surrounded set: true ``` *** ## astrolabe [#astrolabe] **Purpose** Get back from a palace to the astrolabe it belongs to. **Signature** ```rust pub fn astrolabe(&self) -> &'a Astrolabe ``` **Return value** `&Astrolabe`. A view always holds its astrolabe, so this does not return an `Option`. **Example** ```rust let soul = chart.palace(Palace::Soul).unwrap(); println!("{}", translate_five_elements_class(soul.astrolabe().five_elements_class, Language::EnUS)); ``` **Output** ```text wood 3rd ``` *** ## to\_text [#to_text] **Purpose** The palace's semantic text, identical to that palace's section in the natal text. **Signature** ```rust pub fn to_text(&self) -> String ``` Defined on `PalaceRef` and emitting in the chart's charting language; for an explicit language use the free function `text::palace_to_text(palace, lang)`. **Example** ```rust let soul = chart.palace(Palace::Soul).unwrap(); print!("{}", soul.to_text()); ``` **Output** ```text --- soul --- Stem-Branch: renwoo Decadal: 3-12 Age Fortune Years: 5, 17, 29, 41, 53, 65, 77, 89, 101, 113 Twelve Gods: weak, dragon, downcast, disastery Major Stars: emperor([+3]) Minor Stars: artist([-3]) Adjective Stars: refined, lucky, intercepted, instigated, considery(Y) ``` The full format is on [Semantic text](/en/docs/guide/guides/to-text). # Star object (/en/docs/rust/star-object) The fields of Star, its brightness and mutagen predicates, and the back-references StarRef adds. A `Star` is one star sitting in a palace, carrying its type, brightness and mutagen mark. `chart.star(...)` returns a `StarRef`, which adds the ability to trace back to the palace it sits in. ```rust let ziwei = chart.star(StarKey::ZiweiMaj).unwrap(); let _name = &ziwei.name; // reached directly through Deref let _palace = ziwei.palace(); // view-only: back to its palace ``` 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. `Star` itself is not `Copy`, so fields reached through `Deref` are borrowed (`&ziwei.name`) or cloned; they cannot be moved out. ## Fields [#fields] | Field | Type | Description | | ------------ | -------------------- | ------------------------------------------------------------------------------------ | | `key` | `StarKey` | Star key, independent of language; use it in predicates | | `name` | `String` | Star name, translated into the charting language | | `star_type` | `StarType` | Star type, see below | | `scope` | `Scope` | Which layer it acts on: `Origin` for natal stars, the matching scope for scope stars | | `brightness` | `Option` | Brightness; `None` for stars with no brightness table | | `mutagen` | `Option` | Natal mutagen; `None` for stars the birth-year stem did not transform | ### The eight values of StarType [#the-eight-values-of-startype] | Value | Meaning | Typical members | | ----------- | ------------------------ | -------------------------------------------------- | | `Major` | The fourteen major stars | Ziwei, Tianfu, Qisha, Pojun | | `Soft` | Auspicious stars | Zuofu, Youbi, Wenchang, Wenqu, Tiankui, Tianyue | | `Tough` | Malefic stars | Qingyang, Tuoluo, Huoxing, Lingxing, Dikong, Dijie | | `Adjective` | Adjective stars | Santai, Bazuo, Tianxing, Tianyao | | `Flower` | Peach-blossom stars | Hongluan, Tianxi, Xianchi | | `Helper` | Jieshen | Jieshen | | `Lucun` | Lucun | Lucun | | `Tianma` | Tianma | Tianma | Lucun and Tianma each get a category of their own, because in the traditional division they are neither purely auspicious nor purely malefic and predicates routinely single them out. Only twenty stars have a brightness table — the fourteen major stars plus Wenchang, Wenqu, Huoxing, Lingxing, Qingyang and Tuoluo. Brightness is simply not a concept for the rest, whose `brightness` is `None`. *** ## with\_brightness [#with_brightness] **Purpose** Test whether this star is at one of the given brightness levels. **Zi Wei meaning** Brightness (miao, wang, de, li, ping, bu, xian) describes how strong a star is in its palace. Each star has a fixed value in each of the twelve palaces; at miao or wang its power comes out in full, at xian it is constrained. **Signature** ```rust pub fn with_brightness(&self, brightness: &[Brightness]) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------ | --------------- | -------- | ------- | ---------------------------------------------- | | `brightness` | `&[Brightness]` | Yes | — | A list of brightness levels; any match is true | **Return value** `bool`. Always `false` for a star with no brightness. **Example** ```rust let ziwei = chart.star(StarKey::ZiweiMaj).unwrap(); println!("{}", ziwei.with_brightness(&[Brightness::Miao])); println!("{}", ziwei.with_brightness(&[Brightness::Wang, Brightness::De])); ``` **Output** ```text true false ``` **Edge cases and pitfalls** The semantics are "any match", not "all match" — a star has exactly one brightness, so demanding all of them would be permanently false for a list longer than one. *** ## with\_mutagen [#with_mutagen] **Purpose** Test whether this star carries a given natal mutagen. **Zi Wei meaning** Natal mutagens are fixed by the birth-year stem: a given year always sends lu, quan, ke and ji to four particular stars. The mark travels with the star, whichever palace it lands in. **Signature** ```rust pub fn with_mutagen(&self, mutagens: &[Mutagen]) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ---------- | ------------ | -------- | ------- | ------------------------------------- | | `mutagens` | `&[Mutagen]` | Yes | — | A list of mutagens; any match is true | **Return value** `bool`. Always `false` for a star the birth-year stem did not transform. **Example** ```rust let ziwei = chart.star(StarKey::ZiweiMaj).unwrap(); let taiyang = chart.star(StarKey::TaiyangMaj).unwrap(); println!("Ziwei takes lu: {}", ziwei.with_mutagen(&[Mutagen::Lu])); println!("Taiyang takes lu: {}", taiyang.with_mutagen(&[Mutagen::Lu])); ``` **Output** ```text Ziwei takes lu: false Taiyang takes lu: true ``` This chart's birth-year stem is geng, and geng sends lu to Taiyang, so the mark lands on Taiyang rather than Ziwei. **Edge cases and pitfalls** `with_mutagen` looks at the mark the **birth-year stem** placed on this star; only four stars on a chart carry one. Mutagens flown by palace stems do not show up here — for those use the palace's [`flies_to`](/en/docs/rust/palace#flies_to--flies_one_of_to--not_fly_to) family. *** ## palace / opposite\_palace / surrounded\_palaces [#palace--opposite_palace--surrounded_palaces] **Purpose** Trace from a star back to its palace, that palace's opposite, and its surrounded set. **Signature** ```rust pub fn palace(&self) -> PalaceRef<'a> pub fn opposite_palace(&self) -> PalaceRef<'a> pub fn surrounded_palaces(&self) -> SurroundedPalaces<'a> ``` **Return value** All three necessarily exist and return no `Option` — a `StarRef` can only be produced by a chart query, so it inherently knows where it belongs. **Example** ```rust let en = Language::EnUS; let ziwei = chart.star(StarKey::ZiweiMaj).unwrap(); println!("{}", translate_palace(ziwei.palace().name, en)); println!("{}", translate_palace(ziwei.opposite_palace().name, en)); println!("Tianxiang in the same palace or the trine: {}", ziwei.surrounded_palaces().have(&[StarKey::TianxiangMaj])); ``` **Output** ```text soul surface Tianxiang in the same palace or the trine: true ``` **Edge cases and pitfalls** A star appears exactly once on a chart, so `chart.star(key)` has a unique result. Horoscope scope stars are not in the natal chart's star lists; to reach them use the horoscope object's [`palace`](/en/docs/rust/horoscope#palace) with a scope argument. # Surrounded palaces (/en/docs/rust/surpalaces) The four palaces of SurroundedPalaces and its five predicates. The surrounded set is the most commonly used reading scope in Zi Wei Dou Shu. A matter cannot be read from its own palace alone: the stars of the opposite palace and the two trine palaces bear on it just as much, and only all four together give the full picture. The examples on this page all chart with `Language::EnUS`, so the display values in the output are iztro's en-US vocabulary. ## The four palaces [#the-four-palaces] | Field | Offset | Traditional name | Meaning | | ---------- | ------ | ----------------- | --------------------------------------------- | | `target` | +0 | The palace itself | The matter itself | | `opposite` | +6 | Opposite palace | The facing side; the most immediate influence | | `career` | +4 | Career position | One of the trine | | `wealth` | +8 | Wealth position | One of the trine | All four fields have the type `&'a PalaceData` (not `PalaceRef`): their fields read directly, and the methods of the [palace object](/en/docs/rust/palace) that need no astrolabe context (`has`, `is_empty`, `has_mutagen`, `mutagen_stars`) are all callable on them. For methods that do trace back to the astrolabe, such as `opposite_palace` or `surrounded_palaces`, take `sp.target.index` and go through `chart.palace(...)` to get a `PalaceRef`. `SurroundedPalaces` declares `wealth` before `career`, but the offsets are `career = +4` and `wealth = +8`. Anchored on the Soul palace, +4 lands on the Career palace and +8 on the Wealth palace — that is how the names and the offsets line up. `wealth` and `career` mean "the trine positions relative to this palace", not the two fixed palace names among the twelve. Anchored on the Soul palace they happen to land on the Wealth and Career palaces; anchored elsewhere they are other palaces. ## Three ways to get one [#three-ways-to-get-one] ```rust // from the astrolabe let sp = chart.surrounded_palaces(Palace::Soul).unwrap(); // from a palace let sp = chart.palace(Palace::Soul).unwrap().surrounded_palaces(); // from a star (the surrounded set of the palace it sits in) let sp = chart.star(StarKey::ZiweiMaj).unwrap().surrounded_palaces(); ``` All three give the same result; pick whichever matches what you already have. *** ## have / not\_have / have\_one\_of [#have--not_have--have_one_of] **Purpose** Test whether the four palaces together hold the given stars. **Zi Wei meaning** A phrase like "Ziwei is in the surrounded set" asks exactly whether a star appears anywhere among these four palaces, without asking which one. **Signature** ```rust pub fn have(&self, stars: &[StarKey]) -> bool pub fn not_have(&self, stars: &[StarKey]) -> bool pub fn have_one_of(&self, stars: &[StarKey]) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ------------ | -------- | ------- | ------------------- | | `stars` | `&[StarKey]` | Yes | — | A list of star keys | **Return value** | Method | Meaning | | ------------- | ------------------------------------------------------------------------------------ | | `have` | Every star in the list appears among the four palaces (not necessarily the same one) | | `not_have` | No star in the list appears | | `have_one_of` | At least one star in the list appears | **Example** ```rust use x_iztro::StarKey::*; let sp = chart.surrounded_palaces(Palace::Soul).unwrap(); println!("{}", sp.have(&[ZiweiMaj, TianxiangMaj])); println!("{}", sp.have_one_of(&[QishaMaj, PojunMaj])); println!("{}", sp.not_have(&[HuoxingMin])); ``` **Output** ```text true false true ``` Ziwei is in the Soul palace and Tianxiang in the Wealth palace — different palaces, but both within the four, so `have` is `true`. **Edge cases and pitfalls** `have(&[A, B])` means "A and B both appear among these four palaces", not that they sit together. For same-palace tests use the palace's [`has`](/en/docs/rust/palace#has--not_have--has_one_of). `have` and `not_have` return `true` for an empty list; `have_one_of` returns `false`. *** ## have\_mutagen / not\_have\_mutagen [#have_mutagen--not_have_mutagen] **Purpose** Test whether the four palaces carry a given natal mutagen. **Zi Wei meaning** "Ji is in the surrounded set" means one of these palaces holds a star the birth-year stem sent ji to — a common condition when locating a source of pressure. **Signature** ```rust pub fn have_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`. **Example** ```rust let sp = chart.surrounded_palaces(Palace::Soul).unwrap(); println!("lu in the surrounded set: {}", sp.have_mutagen(Mutagen::Lu)); println!("ji in the surrounded set: {}", sp.have_mutagen(Mutagen::Ji)); println!("no ke in the surrounded set: {}", sp.not_have_mutagen(Mutagen::Ke)); ``` **Output** ```text lu in the surrounded set: false ji in the surrounded set: false no ke in the surrounded set: true ``` This chart's natal mutagens land in four palaces: Taiyang with lu in Children, Wuqu with quan in Wealth, Taiyin with ke in Friends, Tiantong with ji in Health. The Soul palace's surrounded set is Soul, Surface, Wealth and Career — only the quan is among them, so asking for lu and for ji both give `false`, while asking for quan would give `true`. **Edge cases and pitfalls** This looks at the **natal mutagen** marks on stars, unrelated to mutagens flown by palace stems. For those, use the palace's flying-star methods. *** ## to\_text [#to_text] **Purpose** The surrounded palaces' semantic text: one section each for the target palace, its opposite, and the wealth and career positions. **Signature** ```rust pub fn to_text(&self, lang: Language) -> String ``` `SurroundedPalaces` holds four palace references and records no language, so pass one explicitly — usually `chart.language` to match the natal chart. The equivalent free function is `text::surrounded_palaces_to_text(sp, lang)`. **Example** ```rust let sp = chart.surrounded_palaces(Palace::Soul).unwrap(); println!("{}", sp.to_text(chart.language).lines().next().unwrap()); ``` **Output** ```text Target Palace: soul (renwoo) ``` The full format is on [Semantic text](/en/docs/guide/guides/to-text). # Horoscope object (/en/docs/rust/horoscope) The data structures of the six scopes, plus palace lookups that need not be handed the astrolabe again. A horoscope projects the natal chart onto a point in time. The same chart shows a different palace layout in different years — which is exactly what "the decadal scope has moved to that palace" means. ```rust let h = chart.horoscope("2025-6-1", 0)?; ``` `HoroscopeRef` holds the natal chart that produced it, so none of the query methods need the astrolabe passed in again. The examples on this page all start from a `Language::EnUS` natal chart, so the display values in the output are iztro's en-US vocabulary. ## HoroscopeData [#horoscopedata] `HoroscopeRef` derefs to `HoroscopeData`, which has eight fields: two date strings and the six scopes. | Field | Type | Description | | ------------ | --------------- | --------------------------------------------------------- | | `solar_date` | `String` | Target solar date, as passed in | | `lunar_date` | `String` | The target date's lunar form, written in Chinese numerals | | `decadal` | `HoroscopeItem` | The decadal scope | | `age` | `AgeItem` | The age scope | | `yearly` | `YearlyItem` | The yearly scope | | `monthly` | `HoroscopeItem` | The monthly scope | | `daily` | `HoroscopeItem` | The daily scope | | `hourly` | `HoroscopeItem` | The hourly scope | `solar_date` is the **target** date, not the birth date; the birth date lives on the natal chart, as `h.astrolabe().solar_date`. ## The six scopes [#the-six-scopes] | Field | Type | Span | Description | | --------- | --------------- | --------------- | --------------------------------------------------------------------- | | `decadal` | `HoroscopeItem` | Ten years | The decadal scope; the childhood scope for the years before it begins | | `age` | `AgeItem` | One year | The age scope, moving one palace per nominal year | | `yearly` | `YearlyItem` | One year | The yearly scope, its palace fixed by the year's pillar | | `monthly` | `HoroscopeItem` | One month | The monthly scope | | `daily` | `HoroscopeItem` | One day | The daily scope | | `hourly` | `HoroscopeItem` | One double-hour | The hourly scope | Both advance once per year, but they start differently: the age scope starts from the birth-year branch and steps forward with the nominal age, while the yearly scope simply asks which palace that year's pillar falls in. The two lines are independent, and Zi Wei practice usually reads them together. ### HoroscopeItem [#horoscopeitem] | Field | Type | Description | | ---------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `index` | `usize` | Which palace this scope lands on (a palace index) | | `name` | `String` | Display name of the scope, translated into the output language | | `name_key` | `HoroscopeName` | The scope's language-independent key; before the decadals begin the decadal scope carries `Childhood`, a different reading semantics from `Decadal` — predicate on this, never on the translation | | `heavenly_stem` | `HeavenlyStem` | Stem of the scope, which determines the mutagens it flies | | `earthly_branch` | `EarthlyBranch` | Branch of the scope | | `palace_names` | `Vec` | The twelve palace names re-derived with this scope's palace as the Soul palace, indexed by palace index | | `mutagen` | `Vec` | The stars this scope's stem transforms, in the order lu, quan, ke, ji | | `stars` | `Option>>` | The scope stars of this layer; `None` for layers that have none | `age` and `yearly` are not `HoroscopeItem`s themselves but wrappers carrying one extra datum each: ```rust pub struct AgeItem { pub base: HoroscopeItem, pub nominal_age: u32, // the nominal age for that date } pub struct YearlyItem { pub base: HoroscopeItem, pub yearly_dec_star: YearlyDecStar, } pub struct YearlyDecStar { pub jiangqian12: Vec, // the yearly Jiang-qian gods, indexed by palace index pub suiqian12: Vec, // the yearly Sui-qian gods, indexed by palace index } ``` `AgeItem` and `YearlyItem` both implement `Deref`, so the shared fields read directly: `h.yearly.heavenly_stem`, `h.age.index`; take `.base` when you need the whole `HoroscopeItem`. All four types are re-exported at the crate root. **Example** ```rust let h = chart.horoscope("2025-6-1", 0)?; for item in [&h.decadal, &h.monthly, &h.daily, &h.hourly] { println!("{} lands on palace {} with pillar {} {}", item.name, item.index, translate_heavenly_stem(item.heavenly_stem, Language::EnUS), translate_earthly_branch(item.earthly_branch, Language::EnUS)); } println!("age scope nominal age {}", h.age.nominal_age); ``` **Output** ```text decadal lands on palace 2 with pillar geng chen monthly lands on palace 3 with pillar ren woo daily lands on palace 8 with pillar xin chou hourly lands on palace 8 with pillar wu zi age scope nominal age 26 ``` *** ## age\_palace [#age_palace] **Purpose** Get the palace the age scope occupies this year. **Zi Wei meaning** The age scope is a line advancing year by year; whichever palace it lands on becomes the focus for that year. **Signature** ```rust pub fn age_palace(&self) -> PalaceRef<'a> ``` **Return value** `PalaceRef` — a palace on the natal chart, always present. **Example** ```rust let h = chart.horoscope("2025-6-1", 0)?; println!("{}", translate_palace(h.age_palace().name, Language::EnUS)); ``` **Output** ```text property ``` *** ## palace [#palace] **Purpose** Get one of the twelve palaces as re-derived under a given horoscope scope. **Zi Wei meaning** Once the decadal scope reaches a palace, the twelve palaces are re-anchored with that palace as the "decadal Soul palace". "The decadal Spouse palace" refers to that re-anchored naming, and it is usually not the same palace as the natal Spouse palace. **Signature** ```rust pub fn palace(&self, name: Palace, scope: Scope) -> Option> ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | -------- | -------- | ------- | -------------------------------------- | | `name` | `Palace` | Yes | — | The palace name to fetch | | `scope` | `Scope` | Yes | — | Which scope's twelve palaces to search | **Return value** `Option>` — a palace on the natal chart (the same cell carries different names under different scopes). With `Origin` as the scope, these are the natal twelve palaces. **Example** ```rust let en = Language::EnUS; let h = chart.horoscope("2025-6-1", 0)?; println!("the decadal Soul palace is the natal {}", translate_palace(h.palace(Palace::Soul, Scope::Decadal).unwrap().name, en)); println!("the natal Soul palace is {}", translate_palace(h.palace(Palace::Soul, Scope::Origin).unwrap().name, en)); ``` **Output** ```text the decadal Soul palace is the natal spouse the natal Soul palace is soul ``` **Edge cases and pitfalls** On the palace object returned by `palace(Soul, Decadal)`, `name` is still the **natal palace name** (Spouse in the example), because it is that cell on the natal chart. To see what the cell is called at the decadal layer, read `h.decadal.palace_names[index]`. *** ## surround\_palaces [#surround_palaces] **Purpose** Get the surrounded palaces of a palace under a given horoscope scope. **Signature** ```rust pub fn surround_palaces(&self, name: Palace, scope: Scope) -> Option> ``` **Parameters** Same as `palace`. **Return value** `Option>`; its predicates are on [Surrounded palaces](/en/docs/rust/surpalaces). **Example** ```rust let h = chart.horoscope("2025-6-1", 0)?; let sp = h.surround_palaces(Palace::Wealth, Scope::Yearly).unwrap(); println!("the surrounded set of the yearly Wealth palace is anchored on the natal {}", translate_palace(sp.target.name, Language::EnUS)); ``` **Output** ```text the surrounded set of the yearly Wealth palace is anchored on the natal health ``` *** ## has\_horoscope\_stars / has\_one\_of\_horoscope\_stars / not\_have\_horoscope\_stars [#has_horoscope_stars--has_one_of_horoscope_stars--not_have_horoscope_stars] **Purpose** Test whether a palace under a given scope holds the given scope stars. **Zi Wei meaning** Scope stars are a group produced by each horoscope layer: Tiankui, Tianyue, Wenchang, Wenqu, Lucun, Qingyang, Tuoluo, Tianma, Hongluan and Tianxi. They carry different names in different layers — Yunkui and Yunyue at the decadal layer, Liukui and Liuyue at the yearly layer — with the same meaning applied to their own time span. **Signature** ```rust pub fn has_horoscope_stars(&self, name: Palace, scope: Scope, stars: &[StarKey]) -> bool pub fn has_one_of_horoscope_stars(&self, name: Palace, scope: Scope, stars: &[StarKey]) -> bool pub fn not_have_horoscope_stars(&self, name: Palace, scope: Scope, stars: &[StarKey]) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ------------ | -------- | ------- | -------------------------------------------------- | | `name` | `Palace` | Yes | — | The palace name under that scope | | `scope` | `Scope` | Yes | — | The horoscope scope | | `stars` | `&[StarKey]` | Yes | — | Scope star keys, which must use that layer's names | **Return value** | Method | Meaning | | ---------------------------- | ----------------------- | | `has_horoscope_stars` | All of them are present | | `has_one_of_horoscope_stars` | At least one is present | | `not_have_horoscope_stars` | None is present | **Example** ```rust use x_iztro::StarKey::*; let h = chart.horoscope("2025-6-1", 0)?; println!("{}", h.has_horoscope_stars(Palace::Soul, Scope::Decadal, &[Yunlu])); println!("{}", h.has_one_of_horoscope_stars(Palace::Soul, Scope::Decadal, &[Yunlu, Yunyang])); println!("{}", h.not_have_horoscope_stars(Palace::Soul, Scope::Decadal, &[Yuntuo])); ``` **Output** ```text false false true ``` **Edge cases and pitfalls** The three methods use `scope` + `name` to locate one cell on the natal chart, but the set of stars compared against is always the **union of the decadal and yearly scope stars**, regardless of `scope`. So with `scope` set to `Monthly` the question is "does this cell, named as a monthly palace, hold any decadal or yearly scope star?" — not the monthly scope's own stars. The scope stars of the monthly, daily and hourly layers take no part in the comparison here. For a layer's scope-star distribution, use a field like `h.monthly.stars`, or [`get_horoscope_stars`](/en/docs/rust/star#get_horoscope_stars). The decadal scope stars are `Yunlu`, `Yunyang` and so on, the yearly ones `Liulu`, `Liuyang` and so on; the two groups have different key names. Since the compared set is always the union of those two groups, both `Yunlu` and `Liulu` can be found under any `scope` — only their palaces differ. The per-layer key table is on [Star placement](/en/docs/rust/star#get_horoscope_stars). `Origin` walks the natal twelve palaces, so a palace is still located; there simply are no scope stars on the natal chart, and what is compared remains whichever decadal and yearly scope stars fall on that cell. *** ## has\_horoscope\_mutagen [#has_horoscope_mutagen] **Purpose** Test whether a palace under a given scope carries a mutagen flown by that scope's stem. **Zi Wei meaning** Every horoscope layer has a stem of its own, and it transforms four stars just as the birth-year stem does. A question like "does the decadal lu land in the decadal Wealth palace?" is asking about this. **Signature** ```rust pub fn has_horoscope_mutagen(&self, name: Palace, scope: Scope, mutagen: Mutagen) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | --------- | -------- | ------- | -------------------------------- | | `name` | `Palace` | Yes | — | The palace name under that scope | | `scope` | `Scope` | Yes | — | The horoscope scope | | `mutagen` | `Mutagen` | Yes | — | One of the four mutagens | **Return value** `bool`. It checks whether the star transformed by that layer's stem sits among the target palace's major or minor stars (adjective stars are not considered). **Example** ```rust let h = chart.horoscope("2025-6-1", 0)?; println!("{}", h.has_horoscope_mutagen(Palace::Soul, Scope::Decadal, Mutagen::Lu)); // the four stars this layer transforms can be read directly println!("{:?}", h.decadal.mutagen.iter() .map(|s| translate_star(*s, Language::EnUS)).collect::>()); ``` **Output** ```text false ["sun", "general", "moon", "fortunate"] ``` The decadal stem is geng, and geng sends lu to Taiyang (`sun`), quan to Wuqu (`general`), ke to Taiyin (`moon`) and ji to Tiantong (`fortunate`). **Edge cases and pitfalls** There is no such thing as a "layer stem" at the natal layer — the natal mutagens are already marked on the stars' own `mutagen` fields. `has_horoscope_mutagen(name, Scope::Origin, m)` therefore returns `false` outright, which does not mean the natal chart lacks that mutagen. For natal mutagens use the palace's [`has_mutagen`](/en/docs/rust/palace#has_mutagen--not_have_mutagen). *** ## astrolabe / data / into\_data [#astrolabe--data--into_data] **Purpose** Get back to the natal chart, or take out the horoscope's plain data. **Signature** ```rust pub fn astrolabe(&self) -> &'a Astrolabe pub fn data(&self) -> &HoroscopeData pub fn into_data(self) -> HoroscopeData ``` **Return value** | Method | Use | | ----------- | ------------------------------------------------------------------------------------------ | | `astrolabe` | Back to the natal chart that produced this horoscope | | `data` | Borrow the underlying data; the view implements `Deref`, so `h.decadal` usually suffices | | `into_data` | Take the data and drop the borrow of the astrolabe, for cases needing a `'static` lifetime | **Example** ```rust let h = chart.horoscope("2025-6-1", 0)?; println!("{}", h.astrolabe().solar_date); let data: HoroscopeData = h.into_data(); // no longer borrows chart println!("{}", data.solar_date); ``` **Output** ```text 2000-8-16 2025-6-1 ``` *** ## to\_text [#to_text] **Purpose** The horoscope's semantic text: a complete description for language models and people. **Signature** ```rust pub fn to_text(&self) -> String ``` Defined on `HoroscopeRef` and emitting in the chart's charting language; for an explicit language use the free function `text::horoscope_to_text(astrolabe, horoscope, lang)`. **Return value** `String` — sectioned plain text; each scope carries a patterns line and flowing-star lines from its own perspective. The full format is on [Semantic text](/en/docs/guide/guides/to-text). **Example** ```rust let h = chart.horoscope("2025-1-1", 0)?; println!("{}", h.to_text().chars().take(39).collect::()); ``` **Output** ```text === Horoscope === Target Date: 2025-1-1 ``` *** ## to\_dto [#to_dto] **Purpose** Convert the horoscope data into the serialization structure that matches the JS iztro field contract. **Signature** ```rust pub fn to_dto(&self, lang: Language) -> HoroscopeDto ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ---------- | -------- | ------- | ---------------------------------------- | | `lang` | `Language` | Yes | — | Which language the translated fields use | It is defined on `HoroscopeData` (not on `HoroscopeRef`). The horoscope data does not itself record a language, so it has to be given explicitly here — usually `chart.language`, to stay consistent with the natal chart. **Return value** `x_iztro::dto::HoroscopeDto`, camelCase keys plus the `*Key` identifiers. **Example** ```rust let h = chart.horoscope("2025-6-1", 0)?; let json = serde_json::to_string(&h.to_dto(chart.language))?; let v: serde_json::Value = serde_json::from_str(&json)?; println!("{} {}", v["solarDate"], v["decadal"]["heavenlyStem"]); println!("{}", v["age"]["nominalAge"]); ``` **Output** ```text "2025-6-1" "geng" 26 ``` # Patterns (/en/docs/rust/patterns) Pattern hits on natal and horoscope charts, the reading switches, pattern keys, and the serialisation DTO. A pattern (格局) is the recognition of a named star arrangement on a chart. The same 64 rules are judged on natal charts and on horoscope views. For what patterns are and each rule's condition and source, see the [concept page](/en/docs/guide/concepts/patterns). ```rust let chart = by_solar("1985-5-3", 9, Gender::Male, true, Language::EnUS, Config::default())?; let hits = chart.patterns(); ``` The examples on this page all start from a `Language::EnUS` natal chart, so the display values in the output are the English translations. ## Types [#types] All of these are re-exported from the crate root: `PatternHit`, `StarAt`, `PatternConfig`, `BrightnessSource`, `PatternKey`, plus the constant `ALL_PATTERNS` and the function `patterns_at`. ### PatternHit [#patternhit] One hit. | Field | Type | Meaning | | --------- | ---------------------- | ------------------------------------------------------------------------------------------------------ | | `key` | `PatternKey` | The pattern | | `scope` | `Scope` | The view it was judged in: `Scope::Origin` for natal, otherwise that level | | `palace` | `usize` | Slot of the palace where the pattern formed (0-11, Yin palace is 0) | | `variant` | `Option<&'static str>` | Which reading matched; `None` for single-reading patterns | | `broken` | `bool` | Whether the "spoiled by malefics" condition fired. The hit is reported either way; this is only a flag | | `stars` | `Vec` | The stars evidencing the pattern, with their palaces | `PatternHit` implements `Clone`, `PartialEq`, `Eq`, `Serialize` and `Deserialize`. ### StarAt [#starat] One evidencing star. | Field | Type | Meaning | | ------------ | -------------------- | --------------------------------------------------------------------------------- | | `star` | `StarKey` | The star | | `palace` | `usize` | The slot the star **actually occupies** (when borrowed, not the borrowing palace) | | `brightness` | `Option` | Brightness; `None` for stars with no brightness table | | `mutagen` | `Option` | The mutagen in this view: birth-year for natal, that level's for horoscope views | ### PatternConfig [#patternconfig] The reading switches. Anything that is merely a second *form* of the same pattern goes through `PatternHit::variant`; only data readings that change the **finding of fact itself** live here, which is why there are just three fields. ```rust pub struct PatternConfig { pub brightness_source: BrightnessSource, // default Table pub borrow: bool, // default true pub flow_stars: bool, // default true } ``` | Field | Default | Effect | | ------------------- | ------------------------- | -------------------------------------------------------------------------- | | `brightness_source` | `BrightnessSource::Table` | Basis for Sun and Moon brightness | | `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 | `BrightnessSource` has two variants: `Table` follows the chart's brightness table (Miao and Wang bright, Xian and Bu dim — matching iztro value for value), `Positional` follows the traditional placement (Sun bright Yin–Wu, dim You–Chou; Moon bright You–Chou, dim Mao–Wei). The trade-off is explained on the [concept page](/en/docs/guide/concepts/patterns#which-table-decides-sun-and-moon-brightness). `PatternConfig` implements `Default`; change one field with struct update syntax: ```rust let cfg = PatternConfig { brightness_source: BrightnessSource::Positional, ..Default::default() }; ``` ### PatternKey [#patternkey] The language-independent key of each of the 64 patterns. `Copy` + `Hash`, so it works directly as a `HashMap` key. | Method | Signature | Meaning | | ------------------- | ---------------------------------------- | ------------------------------------------------------ | | `as_key` | `fn as_key(self) -> &'static str` | The snake\_case key, e.g. `"sha_po_lang"` | | `from_key` | `fn from_key(key: &str) -> Option` | Reverse lookup; unknown strings give `None` | | `is_horoscope_only` | `fn is_horoscope_only(self) -> bool` | Whether it is a transit pattern (horoscope views only) | The constant `ALL_PATTERNS: [PatternKey; 64]` lists every pattern in the source page's order. For names use `translate_pattern(key, lang)`, available in all six languages. **Example** ```rust println!("{}", ALL_PATTERNS.len()); println!("{}", PatternKey::ShaPoLang.as_key()); println!("{:?}", PatternKey::from_key("sha_po_lang")); println!("{:?}", PatternKey::from_key("nope")); println!("{}", PatternKey::FengYunJiHui.is_horoscope_only()); ``` **Output** ```text 64 sha_po_lang Some(ShaPoLang) None true ``` *** ## patterns [#patterns] **Purpose** Every pattern hit on the natal chart, with the default reading. **In Zi Wei terms** Lists every named star arrangement that holds on this chart, together with the palace it formed in and the stars that evidence it. **Signature** ```rust impl Astrolabe { pub fn patterns(&self) -> Vec } ``` **Returns** `Vec` in the source page's entry order; an empty `Vec` when nothing holds. The two transit patterns (禄衰马困 `lu_shuai_ma_kun`, 风云际会 `feng_yun_ji_hui`) never appear on a natal chart. **Example** ```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!("{} {} broken={}", translate_pattern(hit.key, en), hit.palace, hit.broken); } ``` **Output** ```text General and Wolf Together 11 broken=false Empress and Minister Facing the Palace 5 broken=false Marshal, Rebel and Wolf 11 broken=false Money and Horse Galloping Together 5 broken=false Officer and Helper Flanking Life 5 broken=false Literary Nobility and Brilliance 11 broken=false Literary Stars Facing Life 5 broken=true Literary Stars in Hidden Support 5 broken=false Literary Stars in Hidden Support 5 broken=false ``` Reading one hit's evidence: ```rust let hit = chart.patterns().into_iter() .find(|h| h.key == PatternKey::FuXiangChaoYuan) .unwrap(); println!("{} variant={:?}", translate_pattern(hit.key, en), hit.variant); for s in &hit.stars { println!(" {} palace {} brightness {:?}", translate_star(s.star, en), s.palace, s.brightness); } ``` ```text Empress and Minister Facing the Palace variant=Some("soul_empty") empress palace 9 brightness Some(De) minister palace 1 brightness Some(Xian) ``` **Edges and traps** Most patterns form at the Soul palace, but "Body-or-Soul" patterns (武贪同行 `wu_tan_tong_xing`, 杀破狼 `sha_po_lang`, 石中隐玉 `shi_zhong_yin_yu` and others) are judged at both, and `palace` records whichever matched — if both match, two hits come back. 禄马交驰 `lu_ma_jiao_chi` goes further: it is reported for any palace that qualifies, so one chart may produce several hits. When an empty palace borrows the opposite palace's majors, `StarAt::palace` records the palace the star actually occupies (the opposite one), not the borrowing palace. For where the pattern formed, read `PatternHit::palace`. Judgement runs on an already-cast chart, so there is no external input left to validate and nothing can fail. Errors can only come from the charting entry points (`by_solar` / `by_lunar`). *** ## patterns\_with [#patterns_with] **Purpose** As `patterns`, with an explicit reading. **Signature** ```rust impl Astrolabe { pub fn patterns_with(&self, config: &PatternConfig) -> Vec } ``` **Parameters** | Parameter | Type | Required | Default | Meaning | | --------- | ---------------- | -------- | ------- | --------------------------------------------------------------- | | `config` | `&PatternConfig` | yes | — | The reading; `PatternConfig::default()` reproduces `patterns()` | **Returns** As `patterns`. **Example** The same chart under both Sun/Moon brightness readings: ```rust let chart = by_solar("1985-1-5", 11, Gender::Female, true, en, Config::default())?; let cfg = PatternConfig { brightness_source: BrightnessSource::Positional, ..Default::default() }; println!("{:?}", chart.patterns().iter() .map(|h| translate_pattern(h.key, en)).collect::>()); println!("{:?}", chart.patterns_with(&cfg).iter() .map(|h| translate_pattern(h.key, en)).collect::>()); ``` **Output** ```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"] ``` *** ## HoroscopeRef::patterns [#horoscoperefpatterns] **Purpose** Pattern hits in the view of one horoscope level. **In Zi Wei terms** Takes that level's palace as the Soul palace, merges in that level's flowing stars and mutagens, and runs every rule again. This is how "if the natal chart has the arrangement and the decadal then arrives at it, its benefit is enjoyed" is computed. **Signature** ```rust impl<'a> HoroscopeRef<'a> { pub fn patterns(&self, scope: Scope) -> Vec pub fn patterns_with(&self, scope: Scope, config: &PatternConfig) -> Vec } ``` **Parameters** | Parameter | Type | Required | Default | Meaning | | --------- | ---------------- | ----------------------- | ------- | -------------------------------- | | `scope` | `Scope` | yes | — | The level whose view to judge in | | `config` | `&PatternConfig` | yes for `patterns_with` | — | The reading | **Returns** `Vec`, each carrying the level passed in as its `scope`. Passing `Scope::Origin` gives exactly what `patterns()` on the astrolabe gives. **Example** ```rust let chart = by_solar("2000-8-16", 2, Gender::Female, true, en, Config::default())?; let h = chart.horoscope("2025-6-1", 0)?; for hit in h.patterns(Scope::Decadal) { println!("{} {:?} {:?}", translate_pattern(hit.key, en), hit.scope, hit.variant); } ``` **Output** ```text Marshal, Rebel and Wolf Decadal None Meeting of Wind and Cloud Decadal None Meeting of Wind and Cloud Decadal Some("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. **Edges and traps** The Body palace is a natal concept. In a horoscope view, "Body-or-Soul" patterns are judged only at that level's Soul palace. 禄衰马困 `lu_shuai_ma_kun` is judged at whichever level the current view is (decadal view judges the decadal, yearly view the year); when the limit's Soul-palace trine set also holds Qisha (the classical strict reading holds too), `variant` is `Some("qisha")`. 风云际会 `feng_yun_ji_hui` compares two limits across levels, so it is judged once, in the `Scope::Decadal` view. Its `variant` records both the pair of limits and how strictly they "meet" Lu and the Horse: a decadal * minor-limit hit is `None` (trine-set meeting) or `Some("same_palace")` (the strict reading — both limits' Soul palaces hold the stars in-palace); a decadal + annual hit is `Some("yearly")` or `Some("yearly_same_palace")`. Each pair reports one hit, two at most. Under the default reading a flowing Lucun reads as Lucun, a flowing Wenchang as Wenchang, and so on. To turn that off, use `patterns_with` with `flow_stars: false`. *** ## patterns\_at [#patterns_at] **Purpose** The free-function form of horoscope pattern judgement, taking a `HoroscopeData` rather than a `HoroscopeRef`. **Signature** ```rust pub fn patterns_at( astrolabe: &Astrolabe, horoscope: &HoroscopeData, scope: Scope, config: &PatternConfig, ) -> Vec ``` **Returns** As `HoroscopeRef::patterns_with`. Use it when all you hold is a `HoroscopeData` (deserialised from elsewhere, say); where a `HoroscopeRef` is at hand, the method form is shorter. *** ## patterns\_dto [#patterns_dto] **Purpose** The hits in serialisation form: camelCase keys, values translated into the chart's language, alongside the language-independent keys. All three bindings and the C FFI go through this layer. **Signature** ```rust impl Astrolabe { pub fn patterns_dto(&self, config: &PatternConfig) -> Vec } impl HoroscopeData { pub fn patterns_dto( &self, astrolabe: &Astrolabe, scope: Scope, config: &PatternConfig, ) -> Vec } ``` **Returns** `Vec`. Compared with `PatternHit` it adds four things: each hit carries `name` (the translation) and `palaceName` / `palaceNameKey` (the forming palace's name in this view), and each evidencing star carries `name` plus `brightnessKey` / `mutagenKey`. Optional keys with no value are omitted on serialisation. **Example** ```rust let chart = by_solar("2000-8-16", 2, Gender::Female, true, en, Config::default())?; let dto = chart.patterns_dto(&PatternConfig::default()); println!("{}", serde_json::to_string_pretty(&dto[0]).unwrap()); ``` **Output** ```json { "key": "fu_xiang_chao_yuan", "name": "Empress and Minister Facing the Palace", "scope": "origin", "palaceIndex": 4, "palaceName": "soul", "palaceNameKey": "soulPalace", "broken": false, "stars": [ { "key": "tianfuMaj", "name": "empress", "palaceIndex": 8, "brightness": "[+3]", "brightnessKey": "miao" }, { "key": "tianxiangMaj", "name": "minister", "palaceIndex": 0, "brightness": "[+3]", "brightnessKey": "miao" } ] } ``` The DTO field is called `palaceIndex` while the Rust struct field is `palace` — DTO key names follow the JS-side naming convention, the Rust side takes the shorter name. *** ## Semantic text [#semantic-text] The pattern-hit list projects to text via `text::patterns_to_text`, one hit per line: pattern name, landing palace, forming stars, with broken patterns marked `[Broken]`. ```rust pub fn patterns_to_text(hits: &[PatternHit], palace_names: &[Palace], lang: Language) -> String ``` `palace_names` is the twelve palace names in slot order under the judging perspective — pass each palace's `name` from `chart.palaces` for the natal view, or the scope's `palace_names` for a horoscope view. ```rust use x_iztro::text::patterns_to_text; let hits = chart.patterns(); let names: Vec = chart.palaces.iter().map(|p| p.name).collect(); print!("{}", patterns_to_text(&hits, &names, chart.language)); ``` **Output** ```text - Empress and Minister Facing the Palace(soul): empress([+3]), minister([+3]) ``` The chart's `to_text` already carries this section; the standalone call suits cases that want only the pattern summary. # Lightweight queries (/en/docs/rust/query) The Chinese zodiac animal, zodiac sign and Soul palace major stars, without charting the whole thing. Some questions do not need a whole chart. These five functions each run only as far as necessary and return; their results always agree with the corresponding fields of a full chart, because they go through the same core logic. The examples on this page all chart with `Language::EnUS`, so the display values in the output are iztro's en-US vocabulary. *** ## get\_zodiac\_by\_solar\_date [#get_zodiac_by_solar_date] **Purpose** Get the Chinese zodiac animal from a solar date. **Zi Wei meaning** The zodiac animal is determined by the **year branch**, and when the year branch turns over is governed by `year_divide`. For someone born between lunar New Year and the Beginning of Spring, the two settings give different animals — not a bug, a difference of school. **Signature** ```rust pub fn get_zodiac_by_solar_date( solar_date: &str, language: Language, config: Config, ) -> Result ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------ | ---------- | -------- | ------- | ------------------------------------- | | `solar_date` | `&str` | Yes | — | Solar date in `YYYY-M-D` | | `language` | `Language` | Yes | — | Output language | | `config` | `Config` | Yes | — | Only `year_divide` affects the result | **Return value** `String` — the animal name translated into the language. **Example** ```rust println!("{}", get_zodiac_by_solar_date("2000-8-16", Language::EnUS, Config::default())?); ``` **Output** ```text dragon ``` **Edge cases and pitfalls** By default the year turns over at lunar New Year. Switch to `YearDivide::Exact` and it turns over at the Beginning of Spring, so people born from late January to early February can get a different animal. *** ## get\_sign\_by\_solar\_date / get\_sign\_by\_lunar\_date [#get_sign_by_solar_date--get_sign_by_lunar_date] **Purpose** Get the zodiac sign. **Zi Wei meaning** The zodiac sign is a Western astrology concept determined solely by the solar date, unrelated to the Zi Wei algorithm. The lunar version converts to solar first, so both give the same result for the same day. **Signature** ```rust pub fn get_sign_by_solar_date(solar_date: &str, language: Language) -> Result pub fn get_sign_by_lunar_date( lunar_date: &str, is_leap_month: bool, language: Language, ) -> Result ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------------------- | ---------- | -------- | ------- | ------------------------------------------------------ | | `solar_date` / `lunar_date` | `&str` | Yes | — | The date in `YYYY-M-D` | | `is_leap_month` | `bool` | Yes | — | Lunar version only: whether that month is a leap month | | `language` | `Language` | Yes | — | Output language | There is no `config` parameter — zodiac signs are unaffected by any setting. **Return value** `String`. **Example** ```rust println!("{}", get_sign_by_solar_date("2000-8-16", Language::EnUS)?); println!("{}", get_sign_by_lunar_date("2000-7-17", false, Language::EnUS)?); ``` **Output** ```text leo leo ``` *** ## get\_major\_star\_by\_solar\_date / get\_major\_star\_by\_lunar\_date [#get_major_star_by_solar_date--get_major_star_by_lunar_date] **Purpose** Get just the Soul palace's major stars, without charting the whole thing. **Zi Wei meaning** The major stars of the Soul palace are the single most commonly asked item in Zi Wei Dou Shu. When the Soul palace is empty, convention borrows the major stars of the opposite palace, and this function already handles that step. **Signature** ```rust pub fn get_major_star_by_solar_date( solar_date: &str, time_index: u8, fix_leap: bool, language: Language, config: Config, ) -> Result pub fn get_major_star_by_lunar_date( lunar_date: &str, time_index: u8, leap: LeapMonth, language: Language, config: Config, ) -> Result ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------------------- | ----------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------ | | `solar_date` / `lunar_date` | `&str` | Yes | — | The date | | `time_index` | `u8` | Yes | — | Hour index 0–12; the Soul palace is fixed jointly by month and hour | | `fix_leap` | `bool` | Yes | — | Solar version only: whether a solar date falling after the 15th of a leap month is treated as the next month | | `leap` | `LeapMonth` | Yes | — | Lunar version only: `NotLeap` / `Leap` / `LeapFixed`, see [`by_lunar`](/en/docs/rust/astro#by_lunar) | | `language` | `Language` | Yes | — | Output language | | `config` | `Config` | Yes | — | Charting configuration | **Return value** `String` — several major stars separated by commas; the opposite palace's major stars when the Soul palace is empty. Rust names these two functions in the singular, matching iztro, even though the Soul palace can hold more than one major star. The plural `get_major_stars` on [Star placement](/en/docs/rust/star#get_major_stars--get_minor_stars--get_adjective_stars) is a different function: it returns the distribution across all twelve palaces. **Example** ```rust let cfg = Config::default(); println!("{}", get_major_star_by_solar_date("2000-8-16", 2, true, Language::EnUS, cfg.clone())?); println!("{}", get_major_star_by_solar_date("2000-8-16", 2, true, Language::ZhCN, cfg.clone())?); ``` **Output** ```text emperor 紫微 ``` **Edge cases and pitfalls** The Soul palace is located jointly from the lunar month and the birth hour, so `time_index` is required. Knowing only the date and not the hour, Zi Wei Dou Shu cannot fix a Soul palace. The return value is a translated string that changes with the language. For programmatic checks use `major_star_keys_of_soul_palace` below, or chart the whole thing and compare the `key` of the `major_stars`. *** ## major\_star\_keys\_of\_soul\_palace [#major_star_keys_of_soul_palace] **Purpose** The Soul palace's major stars as language-independent keys — the key form of the two functions above, for programmatic checks. **Signature** ```rust pub fn major_star_keys_of_soul_palace(astrolabe: &Astrolabe) -> Vec ``` Takes an already-cast chart (pair it with `by_solar` / `by_lunar`); the borrowing rule shares one implementation with the translated form: an empty Soul palace borrows its opposite's major stars, and an empty list comes back when the opposite has none either. Keys are independent of the chart language. **Example** ```rust let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?; println!("{:?}", major_star_keys_of_soul_palace(&chart)); ``` **Output** ```text ["ziweiMaj"] ``` # Utilities (/en/docs/rust/util) Index arithmetic, brightness and mutagen lookups, Soul and body palace derivation, and the four-pillar display string. These functions are the parts the charting algorithm is assembled from. They come in handy when you implement Zi Wei logic yourself or want to double-check a step of the derivation; everyday charting does not call them directly. Every enum in the parameters and return values is language-independent and interoperates directly with the fields on a chart. *** ## fix\_index [#fix_index] **Purpose** Constrain any integer to the cyclic range `0..max` (0 included, `max` excluded). **Zi Wei meaning** The twelve palaces form a ring: one step past the Chou palace (index 11) is back to the Yin palace (index 0). Every "count n forward, count n backward" derivation relies on this wrapping. **Signature** ```rust pub fn fix_index(index: i32, max: i32) -> usize ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ----- | -------- | ------- | ------------------------------------------ | | `index` | `i32` | Yes | — | The index to fix, possibly negative | | `max` | `i32` | Yes | — | Cycle length: 12 for palaces, 10 for stems | **Return value** `usize`, within `0..max` (`max` itself never appears). Passing 0 for `max` triggers a divide-by-zero panic; keeping it positive is the caller's job — on a chart it is always 12 or 10. **Example** ```rust println!("{} {}", utils::fix_index(-1, 12), utils::fix_index(13, 12)); ``` **Output** ```text 11 1 ``` **Edge cases and pitfalls** Negatives wrap by mathematical modulo (-1 → 11) rather than clamping to 0. *** ## earthly\_branch\_to\_palace\_index [#earthly_branch_to_palace_index] **Purpose** Convert an earthly branch to a palace index. **Zi Wei meaning** The twelve palaces start from the **Yin palace** while the natural order of the branches starts from **zi**, putting them two positions apart. This function handles that conversion: yin → 0, mao → 1, …, zi → 10, chou → 11. **Signature** ```rust pub fn earthly_branch_to_palace_index(branch: EarthlyBranch) -> usize ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | --------------- | -------- | ------- | ----------- | | `branch` | `EarthlyBranch` | Yes | — | The branch | **Return value** `usize`, 0–11. **Example** ```rust println!("yin={} zi={}", utils::earthly_branch_to_palace_index(EarthlyBranch::Yin), utils::earthly_branch_to_palace_index(EarthlyBranch::Zi)); ``` **Output** ```text yin=0 zi=10 ``` *** ## time\_to\_index [#time_to_index] **Purpose** Convert a clock hour to an hour index. **Zi Wei meaning** A day holds twelve double-hours of two hours each, but the Zi hour straddles midnight and splits into the early Zi hour (0) and the late Zi hour (12), giving 13 index values. **Signature** ```rust pub fn time_to_index(hour: u8) -> u8 ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ---- | -------- | ------- | -------------------- | | `hour` | `u8` | Yes | — | The clock hour, 0–23 | **Return value** `u8`, 0–12. **Example** ```rust println!("{} {} {}", utils::time_to_index(0), utils::time_to_index(4), utils::time_to_index(23)); ``` **Output** ```text 0 2 12 ``` Midnight is the early Zi hour, 4 o'clock the Tiger hour, 23 o'clock the late Zi hour. *** ## get\_age\_index [#get_age_index] **Purpose** Get the starting palace index of the age scope from the birth-year branch. **Zi Wei meaning** The age scope starts from a fixed palace and steps forward with the nominal age. The starting palace is set by the trine group of the birth-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. **Signature** ```rust pub fn get_age_index(branch: EarthlyBranch) -> usize ``` **Return value** `usize`, 0–11. **Example** ```rust println!("{}", utils::get_age_index(EarthlyBranch::Chen)); ``` **Output** ```text 8 ``` A chen year belongs to the shen/zi/chen group, so the age scope starts at the Xu palace, whose index is 8. *** ## get\_brightness [#get_brightness] **Purpose** Look up a star's brightness in a given palace. **Signature** ```rust pub fn get_brightness(star: StarKey, palace_index: i32, config: &Config) -> Option ``` **Parameters** | Parameter | Type | Required | Default | Description | | -------------- | --------- | -------- | ------- | ----------------------------------------------------- | | `star` | `StarKey` | Yes | — | Star key | | `palace_index` | `i32` | Yes | — | Palace index; out-of-range values are taken modulo 12 | | `config` | `&Config` | Yes | — | A custom brightness table changes the result | **Return value** `Option`. `None` for stars with no brightness table. **Example** ```rust let cfg = Config::default(); println!("{:?}", utils::get_brightness(StarKey::ZiweiMaj, 4, &cfg)); println!("{:?}", utils::get_brightness(StarKey::LucunMin, 0, &cfg)); ``` **Output** ```text Some(Miao) None ``` Ziwei is at miao in the Woo palace (index 4); Lucun has no brightness table. *** ## get\_mutagen / get\_mutagens\_by\_heavenly\_stem [#get_mutagen--get_mutagens_by_heavenly_stem] **Purpose** Look up the mutagens of a heavenly stem. **Zi Wei meaning** Each of the ten stems assigns four fixed stars to lu, quan, ke and ji. `get_mutagen` asks "what does this star take under this stem", while `get_mutagens_by_heavenly_stem` asks "which four stars does this stem transform". **Signature** ```rust pub fn get_mutagen(star: StarKey, stem: HeavenlyStem, config: &Config) -> Option pub fn get_mutagens_by_heavenly_stem(stem: HeavenlyStem, config: &Config) -> [StarKey; 4] ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | -------------- | -------- | ------- | ----------------------------------------- | | `star` | `StarKey` | Yes | — | Star key | | `stem` | `HeavenlyStem` | Yes | — | Heavenly stem | | `config` | `&Config` | Yes | — | A custom mutagen table changes the result | **Return value** `get_mutagen` returns `Option`, `None` when the star is not in that stem's mutagen table. `get_mutagens_by_heavenly_stem` returns a fixed array of four, in the order **lu, quan, ke, ji**. **Example** ```rust let cfg = Config::default(); println!("{:?}", utils::get_mutagen(StarKey::TaiyangMaj, HeavenlyStem::Geng, &cfg)); println!("{:?}", utils::get_mutagen(StarKey::ZiweiMaj, HeavenlyStem::Geng, &cfg)); println!("{:?}", utils::get_mutagens_by_heavenly_stem(HeavenlyStem::Geng, &cfg) .iter().map(|s| translate_star(*s, Language::EnUS)).collect::>()); ``` **Output** ```text Some(Lu) None ["sun", "general", "moon", "fortunate"] ``` *** ## get\_soul\_and\_body [#get_soul_and_body] **Purpose** Derive the Soul and body palaces from the lunar month index, the hour and the year stem. **Zi Wei meaning** The Soul palace is the origin of the whole chart: start at the Yin palace for the first month, count forward to the birth month, then count backward from there to the birth hour. The body palace uses the same starting point but counts the hour forward. The Soul palace's stem comes from the year stem via the Five Tigers rule. **Signature** ```rust pub fn get_soul_and_body(month_index: usize, time_index: u8, yearly_stem: HeavenlyStem) -> SoulAndBody ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------- | -------------- | -------- | ------- | ---------------------------------------------------------------------------------- | | `month_index` | `usize` | Yes | — | Lunar month index with the first month at 0; obtained from `fix_lunar_month_index` | | `time_index` | `u8` | Yes | — | Hour index 0–12 | | `yearly_stem` | `HeavenlyStem` | Yes | — | Birth-year stem | **Return value** `SoulAndBody`, holding `soul_index`, `body_index`, `heavenly_stem_of_soul` and `earthly_branch_of_soul`. **Example** ```rust let sb = get_soul_and_body(6, 2, HeavenlyStem::Geng); println!("soul index {} body index {} soul branch {}", sb.soul_index, sb.body_index, translate_earthly_branch(sb.earthly_branch_of_soul, Language::EnUS)); ``` **Output** ```text soul index 4 body index 8 soul branch woo ``` *** ## get\_five\_elements\_class [#get_five_elements_class] **Purpose** Derive the five elements class from the Soul palace's stem and branch. **Zi Wei meaning** The five elements class (water 2nd, wood 3rd, metal 4th, earth 5th, fire 6th) decides two major things: where Ziwei starts, and the age at which the decadal scope begins. **Signature** ```rust pub fn get_five_elements_class(stem: HeavenlyStem, branch: EarthlyBranch) -> FiveElementsClass ``` **Return value** `FiveElementsClass`. **Example** ```rust let c = get_five_elements_class(HeavenlyStem::Ren, EarthlyBranch::Wu); println!("{}", translate_five_elements_class(c, Language::EnUS)); ``` **Output** ```text wood 3rd ``` *** ## get\_palace\_names [#get_palace_names] **Purpose** Derive the twelve palace names from the Soul palace index. **Zi Wei meaning** Once the Soul palace is fixed, the other eleven run counterclockwise in a fixed order: Soul, Siblings, Spouse, Children, Wealth, Health, Surface, Friends, Career, Property, Spirit, Parents. **Signature** ```rust pub fn get_palace_names(soul_index: usize) -> [Palace; 12] ``` **Return value** A fixed array of twelve **indexed by palace index** — item `i` is the name of `chart.palaces[i]`. **Example** ```rust let names = get_palace_names(4); println!("{:?}", names.iter().take(4).map(|p| translate_palace(*p, Language::EnUS)).collect::>()); ``` **Output** ```text ["wealth", "children", "spouse", "siblings"] ``` The Soul palace is at index 4, so index 0 (the Yin palace) is Wealth. *** ## get\_decadals\_and\_ages [#get_decadals_and_ages] **Purpose** Derive the decadal and age scopes of the twelve palaces from the Soul palace index and the five elements class. **Zi Wei meaning** The decadal scope starts at the Soul palace, ten years per palace, beginning at the age given by the class number (water 2nd at 2, wood 3rd at 3, and so on), with direction from gender polarity and year-branch polarity; the age scope starts at the palace fixed by the trine group of the year branch and moves one palace per nominal year. **Signature** ```rust pub fn get_decadals_and_ages( soul_index: usize, five_elements_class: FiveElementsClass, gender: Gender, yearly_stem: HeavenlyStem, yearly_branch: EarthlyBranch, ) -> ([Decadal; 12], [Vec; 12]) ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------------- | ------------------- | -------- | ------- | ------------------------------------------------------------------- | | `soul_index` | `usize` | Yes | — | Palace index of the Soul palace | | `five_elements_class` | `FiveElementsClass` | Yes | — | The class, which sets the starting age and where Ziwei begins | | `gender` | `Gender` | Yes | — | Gender; with the year-branch polarity it sets the decadal direction | | `yearly_stem` | `HeavenlyStem` | Yes | — | Year stem | | `yearly_branch` | `EarthlyBranch` | Yes | — | Year branch, which sets the starting palace of the age scope | **Return value** `([Decadal; 12], [Vec; 12])` — two fixed arrays of twelve, both indexed by palace index. The fields of `Decadal`: | Field | Type | Description | | ---------------- | --------------- | -------------------------------------------------------- | | `range` | `(u32, u32)` | Start and end nominal ages of the decade, both inclusive | | `heavenly_stem` | `HeavenlyStem` | Stem of that decade | | `earthly_branch` | `EarthlyBranch` | Branch of that decade | The second item is each palace's list of age-scope nominal ages. **Example** ```rust let (decadals, ages) = astro::palace::get_decadals_and_ages( 4, FiveElementsClass::Wood3rd, Gender::Female, HeavenlyStem::Geng, EarthlyBranch::Chen, ); let d = &decadals[0]; println!("Yin palace ages {:?} pillar {} {}", d.range, translate_heavenly_stem(d.heavenly_stem, Language::EnUS), translate_earthly_branch(d.earthly_branch, Language::EnUS)); println!("{:?}", &ages[0][..3]); ``` **Output** ```text Yin palace ages (43, 52) pillar wu yin [9, 21, 33] ``` **Edge cases and pitfalls** On a fully charted astrolabe every palace already carries `decadal` and `ages` fields with the same contents. This function is for cases where you want the scopes without charting the whole thing. *** ## fix\_lunar\_month\_index / fix\_lunar\_day\_index [#fix_lunar_month_index--fix_lunar_day_index] **Purpose** Compute the corrected lunar month index and day index. **Zi Wei meaning** Where leap-month days belong and where the late Zi hour belongs are two long-disputed boundaries in Zi Wei Dou Shu; these two functions pin the rules down: days after the fifteenth of a leap month count as the next month (can be turned off, and never in the late Zi hour), and the day index of the late Zi hour belongs to the next day. **Signature** ```rust pub fn fix_lunar_month_index( lunar_month: u32, lunar_day: u32, is_leap: bool, time_index: u8, fix_leap: bool, ) -> usize pub fn fix_lunar_day_index(lunar_day: u32, time_index: u8) -> u32 ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------- | ------ | -------- | ------- | ---------------------------------------- | | `lunar_month` | `u32` | Yes | — | Lunar month 1–12 | | `lunar_day` | `u32` | Yes | — | Lunar day | | `is_leap` | `bool` | Yes | — | Whether that month is a leap month | | `time_index` | `u8` | Yes | — | Hour index | | `fix_leap` | `bool` | Yes | — | Whether leap-month correction is enabled | **Return value** The month index is 0-based (the first month is 0); the day index is not decremented in the late Zi hour. Four conditions must hold together for `fix_lunar_month_index` to advance the month: `is_leap` true, `fix_leap` true, `lunar_day` greater than 15, and `time_index` not 12. Miss any one and the month itself is used. **Example** ```rust println!("{}", astro::builder::fix_lunar_month_index(7, 17, false, 2, true)); println!("{} {}", astro::builder::fix_lunar_day_index(17, 2), astro::builder::fix_lunar_day_index(17, 12)); ``` **Output** ```text 6 16 17 ``` The seventh month is not a leap month, giving index 6; day seventeen decrements to 16 in the Tiger hour, but stays 17 in the late Zi hour because that belongs to the next day. *** ## translate\_chinese\_date [#translate_chinese_date] **Purpose** Assemble the four pillars into a display string. **Signature** ```rust pub fn translate_chinese_date( pillars: [(HeavenlyStem, EarthlyBranch); 4], lang: Language, ) -> String ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ------------------------------------ | -------- | ------- | ----------------------------------------------------- | | `pillars` | `[(HeavenlyStem, EarthlyBranch); 4]` | Yes | — | The four pillars, in the order year, month, day, hour | | `lang` | `Language` | Yes | — | Output language | **Return value** `String`. When every term is a single character, the parts of a pillar run together and the pillars are separated by spaces; when any term is multi-character, the parts within a pillar are separated by spaces and the pillars by `-`. **Example** ```rust let pillars = [ (HeavenlyStem::Geng, EarthlyBranch::Chen), (HeavenlyStem::Jia, EarthlyBranch::Shen), (HeavenlyStem::Bing, EarthlyBranch::Wu), (HeavenlyStem::Geng, EarthlyBranch::Yin), ]; println!("{}", utils::translate_chinese_date(pillars, Language::EnUS)); println!("{}", utils::translate_chinese_date(pillars, Language::ZhCN)); ``` **Output** ```text geng chen - jia shen - bing woo - geng yin 庚辰 甲申 丙午 庚寅 ``` A chart's `chinese_date` field is generated this way, and the four-pillar enums can be taken from `chart.raw_dates.chinese_date`. ## merge\_stars [#merge_stars] **Purpose** Merge several "twelve palaces of stars" groups into one, palace by palace. **Zi Wei meaning** Star placement happens in batches: major stars, minor stars and adjective stars each produce their own list of twelve palaces. Use this function to fuse them into one complete chart face. **Signature** ```rust pub fn merge_stars(groups: &[[Vec; 12]]) -> [Vec; 12] ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | -------------------- | -------- | ------- | -------------------------------- | | `groups` | `&[[Vec; 12]]` | Yes | — | Several twelve-palace star lists | **Return value** The merged twelve-palace list, with each palace's stars concatenated in the order the groups were passed. **Example** ```rust use x_iztro::{star::query::{self, StarParam}, utils, Config, Gender, Language}; let param = StarParam { solar_date: "2000-8-16", time_index: 2, gender: Gender::Female, fix_leap: true, from: None, language: Language::EnUS, config: &Config::default(), }; let major = query::get_major_stars(¶m)?; let minor = query::get_minor_stars(¶m)?; let merged = utils::merge_stars(&[major, minor]); println!("{:?}", merged[0].iter().map(|s| s.name.as_str()).collect::>()); ``` **Output** ```text ["general", "minister", "horse"] ``` The array length is guaranteed to be 12 by the type system, so the length-validation failures possible on the Python and Go sides cannot occur here. *** ## parse\_heavenly\_stem / parse\_earthly\_branch [#parse_heavenly_stem--parse_earthly_branch] **Purpose** Turn a single Chinese character back into a stem or branch enum. **Zi Wei meaning** External systems (Bazi charting software, old hand-entered records) commonly give stems and branches as single Chinese characters. These two functions are the way to take that input in. **Signature** ```rust pub fn parse_heavenly_stem(s: &str) -> Option pub fn parse_earthly_branch(s: &str) -> Option ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ------ | -------- | ------- | ------------------------------------------------- | | `s` | `&str` | Yes | — | A single Chinese character such as `"甲"` or `"子"` | **Return value** `Option<...>`. Only single Chinese characters are recognized — not romanizations, not translations in other languages — and no whitespace is trimmed; anything else returns `None`. **Example** ```rust use x_iztro::astro::builder::{parse_earthly_branch, parse_heavenly_stem}; println!("{:?} {:?}", parse_heavenly_stem("庚"), parse_earthly_branch("辰")); println!("{:?} {:?}", parse_heavenly_stem("geng"), parse_heavenly_stem("庚 ")); ``` **Output** ```text Some(Geng) Some(Chen) None None ``` **Edge cases and pitfalls** These two handle Chinese characters only. To accept a translation in any language, go through [`key_of`](/en/docs/rust/i18n#key_of), which returns an i18n key, and convert it with `HeavenlyStem::from_key` / `EarthlyBranch::from_key`. They live under `x_iztro::astro::builder` and are not re-exported at the crate root, so the full path is required. # Star placement (/en/docs/rust/star) Where a group of stars lands given birth data, plus the low-level building blocks of the charting pipeline. Use this layer when you do not want a whole chart and only need "which palace does Lucun land in?" or "how are the adjective stars distributed on this chart?". The module has two layers: | Layer | Takes | Purpose | | ----------------------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------- | | `star::query` | Birth data | The outward placement entry points, the subject of this page | | `star::location` / `decorative` / `major` / `minor` / `adjective` | Precomputed indices | Building blocks of the charting pipeline, reusable in a pipeline of your own | Every index is a **palace index**: 0 is the Yin palace, 11 the Chou palace. The examples on this page all chart with `Language::EnUS`, so the star names in the output are iztro's en-US vocabulary — `emperor` for Ziwei, `general` for Wuqu, and so on. The indices themselves are language-independent. ## StarParam [#starparam] Every entry point in `star::query` shares this one parameter struct. ```rust pub struct StarParam<'a> { pub solar_date: &'a str, pub time_index: u8, pub gender: Gender, pub fix_leap: bool, pub from: Option<(HeavenlyStem, EarthlyBranch)>, pub language: Language, pub config: &'a Config, } ``` | Field | Type | Description | | ------------ | --------------------------------------- | --------------------------------------------------------------------------- | | `solar_date` | `&str` | Solar date in `YYYY-M-D` | | `time_index` | `u8` | Hour index 0–12 | | `gender` | `Gender` | Gender, which sets the direction of the Changsheng and Boshi gods | | `fix_leap` | `bool` | Whether to correct for leap months | | `from` | `Option<(HeavenlyStem, EarthlyBranch)>` | The pillar anchoring the five elements class; `None` uses the Soul palace's | | `language` | `Language` | Output language for star names | | `config` | `&Config` | Charting configuration | ```rust use x_iztro::star::query::StarParam; let cfg = Config::default(); let param = StarParam { solar_date: "2000-8-16", time_index: 2, gender: Gender::Female, fix_leap: true, from: None, language: Language::EnUS, config: &cfg, }; ``` Once `from` is given, the class is derived from that pillar instead, which in turn moves Ziwei and Tianfu and the Changsheng gods. How the other star groups are placed is unaffected. Use it to obtain the placements of the Zhongzhou school's earth and human charts. *** ## get\_start\_index [#get_start_index] **Purpose** Find the starting palaces of Ziwei and Tianfu. **Zi Wei meaning** Ziwei is the anchor of the whole chart, located from the five elements class and the lunar day by the Ziwei placement rule; the other thirteen major stars then spread out from Ziwei and Tianfu. Tianfu's position mirrors Ziwei's. **Signature** ```rust pub fn get_start_index(param: &StarParam) -> Result ``` **Return value** `StartIndex { ziwei: usize, tianfu: usize }`. **Example** ```rust let s = star::query::get_start_index(¶m)?; println!("Ziwei {} Tianfu {}", s.ziwei, s.tianfu); ``` **Output** ```text Ziwei 4 Tianfu 8 ``` **Edge cases and pitfalls** A different pillar in `from` changes the result — which is exactly where the Zhongzhou school's three charts differ. *** ## Landing indices per group [#landing-indices-per-group] The following six entry points share a shape: they take a `&StarParam` and return a struct whose fields are all palace indices. | Function | Return type | Fields | Placement rule | | -------------------------- | ------------- | ---------------------- | ---------------------------------------------------------------------------------------------- | | `get_lu_yang_tuo_ma_index` | `LuYangTuoMa` | `lu` `yang` `tuo` `ma` | The year stem places Lucun, with Qingyang ahead and Tuoluo behind; Tianma from the year branch | | `get_kui_yue_index` | `KuiYue` | `kui` `yue` | Year stem | | `get_chang_qu_index` | `ChangQu` | `chang` `qu` | Hour branch | | `get_kong_jie_index` | `KongJie` | `kong` `jie` | Hour branch | | `get_timely_star_index` | `TimelyStars` | `taifu` `fenggao` | Hour branch | | `get_luan_xi_index` | `LuanXi` | `hongluan` `tianxi` | Year branch | **Example** ```rust use x_iztro::star::query as sq; let l = sq::get_lu_yang_tuo_ma_index(¶m)?; println!("Lucun {} Qingyang {} Tuoluo {} Tianma {}", l.lu, l.yang, l.tuo, l.ma); let c = sq::get_chang_qu_index(¶m)?; println!("Wenchang {} Wenqu {}", c.chang, c.qu); let lx = sq::get_luan_xi_index(¶m)?; println!("Hongluan {} Tianxi {}", lx.hongluan, lx.tianxi); ``` **Output** ```text Lucun 6 Qingyang 7 Tuoluo 5 Tianma 0 Wenchang 6 Wenqu 4 Hongluan 9 Tianxi 3 ``` Qingyang sits one palace ahead of Lucun and Tuoluo one behind — the direct expression of the mnemonic "Qingyang before Lucun, Tuoluo after". *** ## get\_daily\_star\_index / get\_monthly\_star\_index / get\_yearly\_star\_index [#get_daily_star_index--get_monthly_star_index--get_yearly_star_index] **Purpose** Get the landing palaces of the adjective stars placed by day, month and year. **Zi Wei meaning** Adjective stars are grouped by how they are placed: day-based stars count forward from a minor star's position, starting at day one, to the birth day; month-based stars are located from the lunar month; year-based stars are the largest group and start from the year stem or year branch. **Signature** ```rust pub fn get_daily_star_index(param: &StarParam) -> Result pub fn get_monthly_star_index(param: &StarParam) -> Result pub fn get_yearly_star_index(param: &StarParam) -> Result ``` **Return value** | Type | Fields | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `DailyStar` | `santai` `bazuo` `enguang` `tiangui` | | `MonthlyStar` | `jieshen` `tianyao` `tianxing` `yinsha` `tianyue` `tianwu` | | `YearlyStars` | 29 fields: `tiancai` `tianshou` `tianchu` `posui` `feilian` `longchi` `fengge` `tianku` `tianxu` `tianguan` `tianfu` `tiande` `yuede` `tiankong` `jielu` `kongwang` `xunkong` `jiekong` `tianshang` `tianshi` `huagai` `xianchi` `guchen` `guasu` `jiesha` `nianjie` `dahao` `hongluan` `tianxi` | **Example** ```rust let d = sq::get_daily_star_index(¶m)?; println!("Santai {} Bazuo {} Enguang {} Tiangui {}", d.santai, d.bazuo, d.enguang, d.tiangui); let m = sq::get_monthly_star_index(¶m)?; println!("Jieshen {} Tianyao {} Tianxing {}", m.jieshen, m.tianyao, m.tianxing); let y = sq::get_yearly_star_index(¶m)?; println!("Xianchi {} Huagai {} Tianshang {} Tianshi {}", y.xianchi, y.huagai, y.tianshang, y.tianshi); ``` **Output** ```text Santai 0 Bazuo 10 Enguang 9 Tiangui 7 Jieshen 0 Tianyao 5 Tianxing 1 Xianchi 7 Huagai 2 Tianshang 9 Tianshi 11 ``` **Edge cases and pitfalls** Year-based adjective stars belong to the yearly spirits, so their year branch comes from `horoscope_divide` rather than `year_divide`. When the two settings differ, year-based stars and the major and minor stars can rest on different year branches — a deliberate distinction of school. `YearlyStars` carries `hongluan` and `tianxi`, and `get_luan_xi_index` gives those same two on their own. The values agree; the only difference is that `get_luan_xi_index` need not compute the other twenty-seven. These three enter the chart only when `algorithm` is the Zhongzhou school, replacing the default placements of Jielu, Kongwang and Dahao; under the default school they are still computed, just not placed into palaces. *** ## get\_major\_stars / get\_minor\_stars / get\_adjective\_stars [#get_major_stars--get_minor_stars--get_adjective_stars] **Purpose** Get the complete distribution of major, minor and adjective stars across the twelve palaces. **Signature** ```rust pub fn get_major_stars(param: &StarParam) -> Result<[Vec; 12], IztroError> pub fn get_minor_stars(param: &StarParam) -> Result<[Vec; 12], IztroError> pub fn get_adjective_stars(param: &StarParam) -> Result<[Vec; 12], IztroError> ``` **Return value** A fixed array of twelve, indexed by palace index. Each item is that palace's star list, possibly empty. Rust names these three in the plural; the Python and Go bindings name their equivalents in the singular (`get_major_star`, `GetMajorStar`, …). Do not confuse them with [`get_major_star_by_solar_date`](/en/docs/rust/query#get_major_star_by_solar_date--get_major_star_by_lunar_date), which returns only the Soul palace's major stars as a string. **Example** ```rust let major = sq::get_major_stars(¶m)?; for (i, stars) in major.iter().take(5).enumerate() { println!("[{i}] {:?}", stars.iter().map(|s| s.name.as_str()).collect::>()); } ``` **Output** ```text [0] ["general", "minister"] [1] ["sun", "sage"] [2] ["marshal"] [3] ["advisor"] [4] ["emperor"] ``` **Edge cases and pitfalls** The returned `Star`s carry brightness and natal mutagen marks and are identical to those from a full chart — they go through the same code. If you want the whole chart, `by_solar` is simpler. *** ## get\_changsheng12 / get\_boshi12 / get\_yearly12 [#get_changsheng12--get_boshi12--get_yearly12] **Purpose** Get how the four groups of twelve gods are arranged across the twelve palaces. **Zi Wei meaning** Each group is twelve marks filling the twelve palaces, exactly one per palace: the Changsheng gods start from the five elements class with direction from gender and year-branch polarity; the Boshi gods start from Lucun with the same direction rule; the Sui-qian gods run forward from the year branch, and the Jiang-qian gods start from the trine group of the year branch. **Signature** ```rust pub fn get_changsheng12(param: &StarParam) -> Result<[StarKey; 12], IztroError> pub fn get_boshi12(param: &StarParam) -> Result<[StarKey; 12], IztroError> pub fn get_yearly12(param: &StarParam) -> Result<([StarKey; 12], [StarKey; 12]), IztroError> ``` **Return value** A fixed array of twelve, indexed by palace index. `get_yearly12` returns two groups at once, in the order `(Sui-qian gods, Jiang-qian gods)`. **Example** ```rust let cs = sq::get_changsheng12(¶m)?; println!("{:?}", cs.iter().take(4).map(|s| translate_star(*s, Language::EnUS)).collect::>()); let (suiqian, jiangqian) = sq::get_yearly12(¶m)?; println!("{:?}", suiqian.iter().take(4).map(|s| translate_star(*s, Language::EnUS)).collect::>()); println!("{:?}", jiangqian.iter().take(4).map(|s| translate_star(*s, Language::EnUS)).collect::>()); ``` **Output** ```text ["dissipated", "buried", "dead", "sick"] ["sorrowing", "illness", "initial", "unlucky"] ["varied", "listless", "religious", "robbed"] ``` *** ## get\_changsheng12\_start\_index / get\_jiangqian12\_start\_index [#get_changsheng12_start_index--get_jiangqian12_start_index] **Purpose** Get just the starting palace of two of the god groups, without laying out the whole cycle. **Zi Wei meaning** The Changsheng starting point is set by the five elements class: water 2nd starts at Shen, wood 3rd at Hai, metal 4th at Si, earth 5th at Shen, fire 6th at Yin. The Jiangxing starting point is set by the trine group of the year branch: yin/woo/xu years at Woo, shen/zi/chen years at Zi, si/you/chou years at You, hai/mao/wei years at Mao. **Signature** ```rust pub fn get_changsheng12_start_index(five_elements_class: FiveElementsClass) -> usize pub fn get_jiangqian12_start_index(yearly_branch: EarthlyBranch) -> usize ``` **Return value** `usize`, 0–11. Neither function needs birth data, and neither can fail. **Example** ```rust use x_iztro::star::decorative::{get_changsheng12_start_index, get_jiangqian12_start_index}; println!("{} {}", get_changsheng12_start_index(FiveElementsClass::Water2nd), get_changsheng12_start_index(FiveElementsClass::Fire6th)); println!("{} {}", get_jiangqian12_start_index(EarthlyBranch::Zi), get_jiangqian12_start_index(EarthlyBranch::Wu)); ``` **Output** ```text 6 0 10 4 ``` Water 2nd puts Changsheng in Shen (index 6), fire 6th in Yin (index 0). *** ## get\_horoscope\_stars [#get_horoscope_stars] **Purpose** Get the scope-star distribution of a horoscope layer. **Zi Wei meaning** Scope stars are the ten stars a horoscope produces: Tiankui, Tianyue, Wenchang, Wenqu, Lucun, Qingyang, Tuoluo, Tianma, Hongluan and Tianxi. Where they land is fixed by that layer's stem and branch, and their names change with the layer. The yearly layer carries one extra star, Nianjie. **Signature** ```rust pub fn get_horoscope_stars( stem: HeavenlyStem, branch: EarthlyBranch, scope: Scope, lang: Language, ) -> [Vec; 12] ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | --------------- | -------- | ------- | ----------------------------------------------- | | `stem` | `HeavenlyStem` | Yes | — | Stem of that layer | | `branch` | `EarthlyBranch` | Yes | — | Branch of that layer | | `scope` | `Scope` | Yes | — | The horoscope layer, which fixes the star names | | `lang` | `Language` | Yes | — | Output language | **Return value** A fixed array of twelve, indexed by palace index. It cannot fail — the parameters are enums with no invalid values. **Star names per layer** | Natal | Decadal | Yearly | Monthly | Daily | Hourly | | -------- | -------- | -------- | -------- | ------- | -------- | | Tiankui | Yunkui | Liukui | Yuekui | Rikui | Shikui | | Tianyue | Yunyue | Liuyue | Yueyue | Riyue | Shiyue | | Wenchang | Yunchang | Liuchang | Yuechang | Richang | Shichang | | Wenqu | Yunqu | Liuqu | Yuequ | Riqu | Shiqu | | Lucun | Yunlu | Liulu | Yuelu | Rilu | Shilu | | Qingyang | Yunyang | Liuyang | Yueyang | Riyang | Shiyang | | Tuoluo | Yuntuo | Liutuo | Yuetuo | Rituo | Shituo | | Tianma | Yunma | Liuma | Yuema | Rima | Shima | | Hongluan | Yunluan | Liuluan | Yueluan | Riluan | Shiluan | | Tianxi | Yunxi | Liuxi | Yuexi | Rixi | Shixi | Those are the `StarKey` names. In the en-US vocabulary a scope star displays as its base name plus a layer marker — `money(D)` for Yunlu at the decadal layer, `(Y)` `(M)` `(d)` `(H)` for the yearly, monthly, daily and hourly layers, and no marker at the natal layer. **Example** ```rust use x_iztro::astro::horoscope::get_horoscope_stars; let decadal = get_horoscope_stars(HeavenlyStem::Jia, EarthlyBranch::Zi, Scope::Decadal, Language::EnUS); println!("{:?}", decadal.iter().take(4) .map(|g| g.iter().map(|s| s.name.as_str()).collect::>()).collect::>()); let origin = get_horoscope_stars(HeavenlyStem::Jia, EarthlyBranch::Zi, Scope::Origin, Language::EnUS); println!("{:?}", origin.iter().take(2) .map(|g| g.iter().map(|s| s.name.as_str()).collect::>()).collect::>()); ``` **Output** ```text [["money(D)", "horse(D)"], ["driven(D)", "attractive(D)"], [], ["scholar(D)"]] [["money", "horse"], ["driven", "attractive"]] ``` **Edge cases and pitfalls** The result for `Scope::Yearly` additionally contains Nianjie, located from the yearly branch and placed ahead of the ten scope stars. No other layer has it. *** ## The low-level building blocks [#the-low-level-building-blocks] The functions under `star::location` and `star::decorative` take precomputed indices rather than birth data. The charting pipeline uses them internally, and they are reusable in a pipeline of your own. ### star::location [#starlocation] | Function | Takes | Returns | | ----------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------- | | `get_start_index` | `lunar_day, time_index, month_day_count, five_elements_value` | `StartIndex { ziwei, tianfu }` | | `get_lu_yang_tuo_ma_index` | `stem, branch` | `LuYangTuoMa { lu, yang, tuo, ma }` | | `get_kui_yue_index` | `stem` | `KuiYue { kui, yue }` | | `get_zuo_you_index` | `lunar_month` | `ZuoYou { zuo, you }` | | `get_chang_qu_index` | `time_index` | `ChangQu { chang, qu }` | | `get_chang_qu_index_by_stem` | `stem` | `ChangQu { chang, qu }` (for horoscope layers) | | `get_daily_star_index` | `lunar_day, time_index, zuo_index, you_index, chang_index, qu_index` | `DailyStar { santai, bazuo, enguang, tiangui }` | | `get_timely_star_index` | `time_index` | `TimelyStars { taifu, fenggao }` | | `get_kong_jie_index` | `time_index` | `KongJie { kong, jie }` | | `get_huo_ling_index` | `branch, time_index` | `HuoLing { huo, ling }` | | `get_luan_xi_index` | `branch` | `LuanXi { hongluan, tianxi }` | | `get_huagai_xianchi_index` | `branch` | `HuagaiXianchi { huagai, xianchi }` | | `get_gu_gua_index` | `branch` | `GuGua { guchen, guasu }` | | `get_jiesha_adj_index` | `branch` | `usize` | | `get_dahao_index` | `branch` | `usize` | | `get_nianjie_index` | `branch` | `usize` | | `get_tianshang_tianshi_index` | `gender, yearly_branch, soul_index, algorithm` | `(usize, usize)`, Tianshang then Tianshi | | `get_tiancai_index` | `yearly_branch, soul_index` | `usize` | | `get_monthly_star_index` | `month_index` | `MonthlyStar { jieshen, tianyao, tianxing, yinsha, tianyue, tianwu }` | | `get_yearly_star_index` | `soul_index, body_index, yearly_stem, yearly_branch, gender, algorithm` | `YearlyStars` (the 29 fields above) | Every field of these structs is a `usize` palace index (0 being the Yin palace); `get_tianshang_tianshi_index` returns a bare tuple rather than a named struct. ### star::decorative [#stardecorative] | Function | Takes | Returns | | ------------------------------ | ---------------------------------------- | ---------------------------------------------------------- | | `get_changsheng12_start_index` | `five_elements_class` | `usize` | | `get_jiangqian12_start_index` | `yearly_branch` | `usize` | | `get_changsheng12` | the class, gender, year branch and so on | `[StarKey; 12]` | | `get_boshi12` | `lu_index, gender, yearly_branch` | `[StarKey; 12]` | | `get_yearly12` | the year branch and so on | `([StarKey; 12], [StarKey; 12])`, Sui-qian then Jiang-qian | ### star::major / minor / adjective [#starmajor--minor--adjective] `get_major_stars`, `get_minor_stars` and `get_adjective_stars` — same names as the functions under `star::query`, different parameters: this layer takes precomputed indices, that one takes birth data. The two layers share several function names (two `get_start_index`, for instance), distinguished by module path: `star::query::get_start_index` takes a `&StarParam`, while `star::location::get_start_index` takes the lunar day, the hour, the number of days in that month and the five elements class number. Use qualified paths when both modules are in scope. The derivation from birth data to the intermediates these blocks need lives in `astro::context::derive`; in a pipeline of your own, call it first to get the context and feed that to the blocks, without re-deriving the year pillar and Soul palace yourself. # Data tables (/en/docs/rust/data) The enums and their methods, the charting configuration, star information, stem and branch information, and the ordering constants. `x_iztro::data` holds two kinds of thing: the **enums** that run through the whole library (the types of the chart fields and of nearly every function parameter), and the **input tables** of the charting algorithm. Both are independent of output language — they are inputs to the algorithm, not results. The common enums are re-exported at the crate root, so `use x_iztro::*;` is enough. *** ## Enums [#enums] Nineteen enums plus `StarKey`. All of them implement `Debug`, `Clone`, `Copy`, `PartialEq`, `Eq` and serde's `Serialize` / `Deserialize`, so they compare directly and go straight into collections. ### Language-independent keys: as\_key / from\_key [#language-independent-keys-as_key--from_key] Most of the enums carry a mutually inverse pair of methods converting between a variant and its iztro i18n key string. These keys are exactly the values of the `*Key` fields in the DTO, and the values of the Python enums and Go constants — write the same string on any of the three sides and the predicates agree. | Enum | Variants | To key | From key | Example keys | | ------------------- | -------- | ---------- | ---------------- | -------------------------------- | | `StarKey` | 162 | `as_key()` | `from_key(&str)` | `ziweiMaj`, `yunlu` | | `Palace` | 12 | `as_key()` | `from_key(&str)` | `soulPalace`, `wealthPalace` | | `HeavenlyStem` | 10 | `as_key()` | `from_key(&str)` | `jiaHeavenly` | | `EarthlyBranch` | 12 | `as_key()` | `from_key(&str)` | `ziEarthly` | | `Mutagen` | 4 | `as_key()` | `from_key(&str)` | `sihuaLu` | | `Brightness` | 7 | `as_key()` | `from_key(&str)` | `miao` | | `FiveElementsClass` | 5 | `as_key()` | `from_key(&str)` | `water2nd` | | `StarType` | 8 | `as_key()` | — | `major`, `lucun` | | `Scope` | 6 | `as_key()` | `from_key(&str)` | `origin`, `decadal` | | `YearDivide` | 2 | `as_key()` | `from_key(&str)` | `normal` / `exact` | | `HoroscopeDivide` | 2 | `as_key()` | `from_key(&str)` | `normal` / `exact` | | `AgeDivide` | 2 | `as_key()` | `from_key(&str)` | `normal` / `birthday` | | `DayDivide` | 2 | `as_key()` | `from_key(&str)` | `forward` / `current` | | `Algorithm` | 2 | `as_key()` | `from_key(&str)` | `default` / `zhongzhou` | | `AstroType` | 3 | `as_key()` | `from_key(&str)` | `heaven` / `earth` / `human` | | `LeapMonth` | 3 | `as_key()` | `from_key(&str)` | `notLeap` / `leap` / `leapFixed` | `from_key` always returns an `Option`, giving `None` for an unknown key — which is how the bindings reject invalid input. ```rust println!("{}", StarKey::ZiweiMaj.as_key()); println!("{:?}", StarKey::from_key("taiyinMaj")); println!("{:?}", StarKey::from_key("nosuch")); println!("{} {}", Palace::Soul.as_key(), AstroType::Earth.as_key()); println!("{:?}", DayDivide::from_key("current")); ``` **Output** ```text ziweiMaj Some(TaiyinMaj) None soulPalace earth Some(Current) ``` ### The other methods [#the-other-methods] | Enum | Method | Description | | ------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `HeavenlyStem` | `index() -> usize` / `from_index(usize)` | Stem ordinal, jia = 0 … gui = 9 | | `EarthlyBranch` | `index() -> usize` / `from_index(usize)` | Branch ordinal, zi = 0 … hai = 11. **Not a palace index**; convert with [`earthly_branch_to_palace_index`](/en/docs/rust/util#earthly_branch_to_palace_index) | | `Palace` | `index() -> usize` / `from_index(usize)` | The name's ordinal within `PALACES`: Soul = 0, Parents = 1, …, Siblings = 11. **Not a position on the chart**; `from_index` takes the value modulo 12 and returns no `Option` | | `FiveElementsClass` | `value() -> usize` | The class number: water 2nd 2, wood 3rd 3, metal 4th 4, earth 5th 5, fire 6th 6 | | `Gender` | `yin_yang() -> YinYang` | Male is yang, female yin; it sets the direction of the decadal scope and the Changsheng gods | | `Language` | `as_code() -> &'static str` / `from_code(&str)` | Language codes such as `zh-CN`; `from_code` is case-insensitive and treats hyphen and underscore alike (`zh_cn` is accepted too) | | `YinYang` | `as_str() -> &'static str` | `阳` / `阴`, not internationalized | | `FiveElements` | `as_str() -> &'static str` | `木` `金` `水` `火` `土`, not internationalized | ```rust println!("{} {}", HeavenlyStem::Gui.index(), EarthlyBranch::Hai.index()); println!("{:?}", Palace::from_index(4)); println!("{}", FiveElementsClass::Wood3rd.value()); println!("{:?} {}", Gender::Female.yin_yang(), Gender::Female.yin_yang().as_str()); println!("{} {:?}", Language::JaJP.as_code(), Language::from_code("ZH-cn")); ``` **Output** ```text 9 11 Career 3 Yin 阴 ja-JP Some(ZhCN) ``` ### The variant listings [#the-variant-listings] Only the ones not obvious at a glance are listed; the variant names of stems, branches, palaces and stars correspond one-to-one with their keys. | Enum | Variants | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `YinYang` | `Yang` `Yin` | | `FiveElements` | `Wood` `Metal` `Water` `Fire` `Earth` | | `FiveElementsClass` | `Water2nd` `Wood3rd` `Metal4th` `Earth5th` `Fire6th` | | `Mutagen` | `Lu` `Quan` `Ke` `Ji` | | `Brightness` | `Miao` `Wang` `De` `Li` `Ping` `Bu` `Xian` | | `StarType` | `Major` `Soft` `Tough` `Adjective` `Flower` `Helper` `Lucun` `Tianma` | | `Scope` | `Origin` `Decadal` `Yearly` `Monthly` `Daily` `Hourly` | | `HoroscopeName` | `Decadal` `Childhood` `Age` `Yearly` `Monthly` `Daily` `Hourly` | | `Gender` | `Male` `Female` | | `Language` | `ZhCN` `ZhTW` `EnUS` `JaJP` `KoKR` `ViVN` | | `LeapMonth` | `NotLeap` `Leap` `LeapFixed` — how `by_lunar` treats the leap month; also `from_flags(is_leap_month, fix_leap)`, `is_leap_month()`, `fix_leap()` to convert to and from the iztro-style pair of booleans | | `Palace` | In `index()` order: `Soul` `Parents` `Spirit` `Property` `Career` `Friends` `Surface` `Health` `Wealth` `Children` `Spouse` `Siblings` | | `PalaceTarget` | `Index(usize)` `Name(Palace)` `Body` `Original` | `IztroError`, `StarType`, `Scope`, `Algorithm` and `AstroType` are marked `#[non_exhaustive]`, so a `match` outside the crate must carry a catch-all arm. Adding variants later is therefore not a breaking change. `Scope` has one member fewer than `HoroscopeName`: `Childhood`. The childhood scope is not a query layer of its own, only a display name for the decadal scope — before the decadal scope begins, `h.decadal.name` shows the childhood name, while `Scope::Decadal` is used as usual. `TimeIndex` is a type alias for `u8`, purely a readability marker with no extra validation. *** ## Config [#config] The charting configuration: six switches plus two optional custom tables. `Config::default()` matches the JS iztro defaults. | Field | Type | Default | Description | | ------------------ | ----------------------------- | --------- | -------------------------------------------------------------------------------------------- | | `year_divide` | `YearDivide` | `Normal` | Whether the year pillar turns over at lunar New Year or at the Beginning of Spring | | `horoscope_divide` | `HoroscopeDivide` | `Normal` | Whether horoscope pillars and the month pillar follow the lunar first day or the solar terms | | `age_divide` | `AgeDivide` | `Normal` | Whether the nominal age advances with the lunar year or with the birthday | | `day_divide` | `DayDivide` | `Forward` | Whether the late Zi hour belongs to the next day or the current one | | `algorithm` | `Algorithm` | `Default` | The school: default or Zhongzhou | | `astro_type` | `AstroType` | `Heaven` | The charting perspective: heaven / earth / human chart | | `overrides` | `Option>` | `None` | Custom mutagen and brightness tables | The meaning of the six switches and the schools behind them are on [Config in depth](/en/docs/guide/guides/config). `overrides` is marked `#[serde(skip)]`: it is charting **input** rather than result, and putting it in the DTO would break the field contract with JS iztro. The `config` object in the JSON output therefore holds only the six switches, and the custom tables are not echoed back. ### Construction methods [#construction-methods] The fields are all `pub` and can be set directly; the chained form is less work: | Method | Description | | -------------------------------------------------------------- | -------------------------------------------------------------------------------- | | `with_astro_type(AstroType) -> Config` | Set the charting perspective | | `with_mutagens(HeavenlyStem, [StarKey; 4]) -> Config` | Override one stem's mutagen table, in the order lu, quan, ke, ji | | `with_brightness(StarKey, [Option; 12]) -> Config` | Override one star's twelve-palace brightness table, index 0 being the Yin palace | ### Lookup methods [#lookup-methods] | Method | Description | | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | `mutagens_of(HeavenlyStem) -> [StarKey; 4]` | The mutagen table **actually in effect** for that stem: the override if there is one, the default table otherwise | | `brightness_of(StarKey, usize) -> Option` | The brightness actually in effect for that star in that palace; out-of-range palace indices are taken modulo 12 | **Example** ```rust let cfg = Config::default() .with_astro_type(AstroType::Earth) .with_mutagens(HeavenlyStem::Geng, [ StarKey::TaiyangMaj, StarKey::WuquMaj, StarKey::TianfuMaj, StarKey::TiantongMaj, ]); println!("{:?}", cfg.astro_type); println!("{:?}", cfg.mutagens_of(HeavenlyStem::Geng) .iter().map(|s| translate_star(*s, Language::EnUS)).collect::>()); println!("{:?}", cfg.mutagens_of(HeavenlyStem::Jia) .iter().map(|s| translate_star(*s, Language::EnUS)).collect::>()); println!("{:?}", cfg.brightness_of(StarKey::ZiweiMaj, 4)); let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, cfg)?; println!("{}", translate_five_elements_class(chart.five_elements_class, Language::EnUS)); ``` **Output** ```text Earth ["sun", "general", "empress", "fortunate"] ["judge", "rebel", "general", "sun"] Some(Miao) earth 5th ``` The mutagens of the geng stem have been replaced with "Taiyang lu, Wuqu quan, Tianfu ke, Tiantong ji" (the default table has Taiyin taking ke); the jia stem was not overridden and still uses the default table. **Edge cases and pitfalls** `with_mutagens` replaces **all four slots** of one stem at once — a single slot cannot be changed on its own; `with_brightness` replaces **all twelve palaces** of one star at once. Stems and stars not mentioned keep the default tables. `Config` contains an `Option>` and implements only `Clone`. To reuse one configuration in a loop, write `cfg.clone()` — cloning an `Arc` bumps a reference count rather than copying the tables. The mutagen table affects: the natal mutagen marks, the palace-stem flying-star methods, the mutagens of every horoscope layer, and `get_mutagen` / `get_mutagens_by_heavenly_stem`. The brightness table affects: a star's `brightness` field, the `with_brightness` predicate, and `get_brightness`. Neither changes where any star lands. ### TableOverrides [#tableoverrides] The carrier of those two tables inside `Config`. There is usually no need to build one directly — the two `with_*` methods above are enough. Build one yourself when you need to load several entries at once: | Method | Description | | ------------------------------------------------------------- | ------------------------------------------------------------ | | `set_mutagens(HeavenlyStem, [StarKey; 4])` | Write one stem's mutagen table | | `set_brightness(StarKey, [Option; 12])` | Write one star's brightness table | | `mutagens_of(HeavenlyStem) -> Option<&[StarKey; 4]>` | Get the overridden mutagen table, `None` when not overridden | | `brightness_of(StarKey) -> Option<&[Option; 12]>` | Get the overridden brightness table | | `is_empty() -> bool` | Whether there is no override at all | Note how these differ from the methods of the same name on `Config`: `TableOverrides::mutagens_of` reports only **whether there is an override**, while `Config::mutagens_of` reports the table **actually in effect** (falling back to the default when there is none). *** ## flow\_star\_counterparts [#flow_star_counterparts] **Purpose** The full table mapping flowing stars to their natal minor-star counterparts (50 entries). **Signature** ```rust pub fn flow_star_counterparts() -> Vec<(StarKey, StarKey)> pub fn natal_counterpart_of_flow_star(key: StarKey) -> Option ``` Both are re-exported at the crate root (`use x_iztro::*` brings them in). Each entry of the full table is (flowing star, natal minor-star counterpart), e.g. `(StarKey::Liuchang, StarKey::WenchangMin)`; the single-lookup form returns `None` for a non-flowing star. Flowing stars have no knowledge-pack entries of their own — their readings are looked up via the natal counterpart, and this table is the official mapping. *** ## get\_star\_info [#get_star_info] **Purpose** Get a star's brightness table, five element and polarity. **Signature** ```rust pub fn get_star_info(key: StarKey) -> Option ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | --------- | -------- | ------- | ----------- | | `key` | `StarKey` | Yes | — | Star key | **Return value** `Option`. Only twenty stars have an entry; the rest return `None`. The fields of `StarInfo`: | Field | Type | Description | | --------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------- | | `brightness` | `[Option; 12]` | Brightness across the twelve palaces, index 0 being the Yin palace; `None` where the palace has no brightness | | `five_elements` | `Option` | Five element | | `yin_yang` | `Option` | Polarity | The twenty with entries are the **fourteen major stars** plus Wenchang, Wenqu, Huoxing, Lingxing, Qingyang and Tuoluo — exactly those listed in the `STARS_WITH_INFO` constant. **Example** ```rust let info = data::stars::get_star_info(StarKey::ZiweiMaj).unwrap(); println!("five element {:?} polarity {:?}", info.five_elements, info.yin_yang); println!("brightness in the Yin palace {:?}", info.brightness[0]); println!("Lucun has an entry: {}", data::stars::get_star_info(StarKey::LucunMin).is_some()); ``` **Output** ```text five element Some(Earth) polarity Some(Yin) brightness in the Yin palace Some(Wang) Lucun has an entry: false ``` **Edge cases and pitfalls** Some stars have no five element or polarity in the table: Taiyang and Qisha have neither, Tanlang, Tianxiang, Tianliang and Pojun have no polarity, and the six minor stars have neither. Reading `None` means the table has no such datum; it is not an intermediate state produced by the algorithm. *** ## get\_heavenly\_stem\_info [#get_heavenly_stem_info] **Purpose** Get a heavenly stem's polarity, five element, clashing stem and four mutagen stars. **Zi Wei meaning** The mutagen table of the stems is the root of the whole mutagen system: the birth-year stem determines the natal mutagens, a palace stem determines what that palace flies, and a scope stem determines that layer's mutagens. **Signature** ```rust pub fn get_heavenly_stem_info(stem: HeavenlyStem) -> HeavenlyStemInfo ``` **Return value** `HeavenlyStemInfo`, with fields: | Field | Type | Description | | --------------- | ---------------------- | ------------------------------------------------------------- | | `yin_yang` | `YinYang` | Polarity | | `five_elements` | `FiveElements` | Five element | | `crash` | `Option` | Clashing stem; `None` for wu and ji, which clash with nothing | | `mutagen` | `[StarKey; 4]` | The four mutagen stars, in the order lu, quan, ke, ji | **Example** ```rust let jia = data::heavenly_stems::get_heavenly_stem_info(HeavenlyStem::Jia); println!("{:?} {:?} clashes with {:?}", jia.yin_yang, jia.five_elements, jia.crash); println!("{:?}", jia.mutagen.iter().map(|s| translate_star(*s, Language::EnUS)).collect::>()); println!("wu clashes with {:?}", data::heavenly_stems::get_heavenly_stem_info(HeavenlyStem::Wu).crash); ``` **Output** ```text Yang Wood clashes with Some(Geng) ["judge", "rebel", "general", "sun"] wu clashes with None ``` *** ## get\_earthly\_branch\_info [#get_earthly_branch_info] **Purpose** Get an earthly branch's polarity, five element, clashing branch, soul and body stars and bodily correspondences. **Signature** ```rust pub fn get_earthly_branch_info(branch: EarthlyBranch) -> EarthlyBranchInfo ``` **Return value** `EarthlyBranchInfo`, with fields: | Field | Type | Description | | --------------- | --------------- | ------------------------------------------------------------------------------- | | `yin_yang` | `YinYang` | Polarity, which sets the direction of the decadal scope and the Changsheng gods | | `five_elements` | `FiveElements` | Five element | | `crash` | `EarthlyBranch` | Clashing branch | | `soul` | `StarKey` | Soul star (looked up by the Soul palace branch) | | `body` | `StarKey` | Body star (looked up by the birth-year branch) | | `inside` | `&'static str` | Corresponding internal organ | | `outside` | `&'static str` | Corresponding body part | | `health_tip` | `&'static str` | Health note | `inside`, `outside` and `health_tip` exist only in Chinese and take no part in internationalization. **Example** ```rust let zi = data::earthly_branches::get_earthly_branch_info(EarthlyBranch::Zi); println!("{:?} {:?} clashes with {:?}", zi.yin_yang, zi.five_elements, zi.crash); println!("soul {} body {}", translate_star(zi.soul, Language::EnUS), translate_star(zi.body, Language::EnUS)); println!("{} / {}", zi.inside, zi.outside); ``` **Output** ```text Yang Water clashes with Wu soul wolf body impulsive 胆 / 下体 ``` *** ## Ordering constants [#ordering-constants] | Constant | Type | Contents | | ------------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `HEAVENLY_STEMS` | `[HeavenlyStem; 10]` | Stem order: jia, yi, bing, ding, wu, ji, geng, xin, ren, gui | | `EARTHLY_BRANCHES` | `[EarthlyBranch; 12]` | Branch order: zi, chou, yin, mao, chen, si, woo, wei, shen, you, xu, hai | | `PALACES` | `[Palace; 12]` | The twelve palace names running **counterclockwise** from the Soul palace: Soul, Parents, Spirit, Property, Career, Friends, Surface, Health, Wealth, Children, Spouse, Siblings | | `LANGUAGES` | `[&str; 6]` | Supported language codes | | `ZODIAC` | `[&str; 12]` | Chinese zodiac keys, in branch order | | `SIGNS` | `[&str; 12]` | Zodiac sign keys, in ecliptic order | | `CHINESE_TIME` | `[&str; 13]` | Hour keys, from the early Zi hour to the late Zi hour | | `TIME_RANGES` | `[&str; 13]` | The clock range of each hour | | `TIGER_RULE` | `[HeavenlyStem; 10]` | Five Tigers rule: year stem to first-month stem | | `RAT_RULE` | `[HeavenlyStem; 10]` | Five Rats rule: day stem to Zi-hour stem | | `MUTAGEN` | `[Mutagen; 4]` | Mutagen order: lu, quan, ke, ji (under `data::stars`) | `TIGER_RULE` and `RAT_RULE` are indexed by stem ordinal: `TIGER_RULE[0]` is the first-month stem for a jia year. **Example** ```rust use x_iztro::data::constants::*; println!("{} {} {}", LANGUAGES[0], ZODIAC[0], CHINESE_TIME[12]); println!("{}", TIME_RANGES[2]); println!("first-month stem of a jia year {}", translate_heavenly_stem(TIGER_RULE[0], Language::EnUS)); println!("Zi-hour stem of a jia day {}", translate_heavenly_stem(RAT_RULE[0], Language::EnUS)); ``` **Output** ```text en-US rat lateRatHour 03:00~05:00 first-month stem of a jia year bing Zi-hour stem of a jia day jia ``` *** ## Star enum listings [#star-enum-listings] | Constant | Length | Contents | | ----------------- | ------ | ---------------------------------------------------- | | `ALL_STARS` | 162 | Every star key, in the order `StarKey` declares them | | `STARS_WITH_INFO` | 20 | The twenty stars that have a `StarInfo` entry | **Example** ```rust println!("{} {}", data::stars::ALL_STARS.len(), data::stars::STARS_WITH_INFO.len()); // list every star that has a brightness table for star in data::stars::STARS_WITH_INFO { print!("{} ", translate_star(star, Language::EnUS)); } ``` **Output** ```text 162 20 emperor advisor sun general fortunate judge empress moon wolf advocator minister sage marshal rebel scholar artist impulsive spark driven tangled ``` *** ## get\_brightness\_table [#get_brightness_table] **Purpose** Get a star's twelve-palace brightness table as written in the data. **Signature** ```rust pub fn get_brightness_table(key: StarKey) -> Option<[Option; 12]> ``` **Return value** A fixed array of twelve with index 0 being the Yin palace; `None` in any palace with no brightness. Stars with no brightness table return the outer `None`. **Example** ```rust let t = data::stars::get_brightness_table(StarKey::ZiweiMaj).unwrap(); println!("{:?}", &t[..4]); println!("{:?}", data::stars::get_brightness_table(StarKey::LucunMin).is_none()); ``` **Output** ```text [Some(Wang), Some(Wang), Some(De), Some(Wang)] true ``` **Edge cases and pitfalls** `get_brightness_table` gives the **built-in default table** and ignores any configuration; [`utils::get_brightness`](/en/docs/rust/util#get_brightness) takes a `&Config`, so a custom brightness table changes its result. To double-check "what brightness was actually used on this chart", use the latter. `get_star_info(key).brightness` carries the same values as this function, and throws in the five element and polarity besides. # Translation (/en/docs/rust/i18n) Two-way lookup between keys and translations, plus the per-category translation functions. Every field on a chart already carries both a translation and a `*_key`, so manual translation is usually unnecessary. These functions exist for the cases where you have only a key (or only a translation in some language) and need to convert. Six languages are supported: `zh-CN`, `zh-TW`, `en-US`, `ja-JP`, `ko-KR`, `vi-VN`. *** ## translate\_key [#translate_key] **Purpose** Translate any key into a given language. **Signature** ```rust pub fn translate_key(key: &str, lang: Language) -> Option<&'static str> ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ---------- | -------- | ------- | -------------------------- | | `key` | `&str` | Yes | — | A language-independent key | | `lang` | `Language` | Yes | — | Target language | Covering 260 keys across twelve categories: | Category | Count | Examples | | ------------------------------------------------- | ----- | ------------------------------------------------------------ | | Stars | 162 | `ziweiMaj`, `changsheng`, `yunlu` | | Palaces (including the body and original palaces) | 14 | `soulPalace`, `wealthPalace`, `bodyPalace`, `originalPalace` | | Heavenly stems | 10 | `jiaHeavenly` | | Earthly branches | 12 | `ziEarthly` | | Brightness | 7 | `miao`, `wang` | | Mutagens | 4 | `sihuaLu` | | Five elements class | 5 | `water2nd` | | Gender | 2 | `male`, `female` | | Chinese zodiac | 12 | `rat`, `ox` | | Hours | 13 | `earlyRatHour` | | Zodiac signs | 12 | `aries` | | Horoscope scopes | 7 | `decadal`, `turn` | **Return value** `Option<&'static str>`. An unknown key returns `None`. **Example** ```rust println!("{:?}", translate_key("ziweiMaj", Language::EnUS)); println!("{:?}", translate_key("soulPalace", Language::JaJP)); println!("{:?}", translate_key("nosuch", Language::ZhCN)); ``` **Output** ```text Some("emperor") Some("命宮") None ``` *** ## key\_of [#key_of] **Purpose** Reverse-look-up a key from a translation in any language. **Signature** ```rust pub fn key_of(text: &str) -> Option<&'static str> pub fn key_of_in(text: &str, key_filter: &str) -> Option<&'static str> ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------ | ------ | -------- | ------- | ---------------------------------------------------------------------------------- | | `text` | `&str` | Yes | — | A translation in any supported language | | `key_filter` | `&str` | Yes | — | A substring the key name must contain, for disambiguating homographic translations | **Return value** `Option<&'static str>`. `None` when nothing matches. **Example** ```rust println!("{:?}", key_of("紫微")); println!("{:?}", key_of("emperor")); println!("{:?}", key_of("자미")); println!("{:?}", key_of("no such name")); ``` **Output** ```text Some("ziweiMaj") Some("ziweiMaj") Some("ziweiMaj") None ``` Translations in all three languages resolve to the same key. **Edge cases and pitfalls** A few translations are identical across categories: in en-US `horse` is both the zodiac horse and the star Tianma, `dragon` is both the zodiac dragon and Qinglong; in ko-KR `사` is both the branch si and Si among the Changsheng gods. `key_of` scans language by language, and within each language key by key, taking the first hit — in exactly the same order as iztro's `kot` (guarded case by case by golden tests). To pin down a category, use `key_of_in`, which only compares keys containing the substring: ```rust println!("{:?}", key_of("horse")); // Some("horse") (the zodiac horse) println!("{:?}", key_of_in("horse", "Min")); // Some("tianmaMin") (Tianma) println!("{:?}", key_of("유시")); // Some("hourly") (the hourly scope) println!("{:?}", key_of_in("유시", "Hour")); // Some("roosterHour") (the You hour) println!("{:?}", key_of_in("horse", "Palace")); // None ``` Common substrings: `Maj` for the fourteen major stars, `Min` for minor stars, `Heavenly` / `Earthly` for stems and branches, `Palace` for palaces, `Hour` for hours. When the filter matches nothing the result is `None`; it does not fall back to the unfiltered result. `key_of` walks 260 keys × 6 languages. The cost of a single call is negligible, but do not put it in an inner loop over every palace and star — use the `*_key` fields that come with the data there. *** ## all\_keys [#all_keys] **Purpose** Get all 260 translatable keys. **Signature** ```rust pub fn all_keys() -> Vec<&'static str> ``` **Return value** `Vec<&'static str>`, in the order `key_of` scans them: horoscope scopes, Chinese zodiac, hours, zodiac signs, five elements classes, heavenly stems, earthly branches, brightness, mutagens, stars, palaces, gender — matching the merge order of iztro's per-language translation files. **Example** ```rust use x_iztro::i18n::lookup::{all_keys, translate_key}; let keys = all_keys(); println!("{} keys", keys.len()); println!("{:?}", &keys[..4]); println!("{:?}", translate_key(keys[0], Language::EnUS)); ``` **Output** ```text 260 keys ["decadal", "childhood", "yearly", "monthly"] Some("decadal") ``` To iterate the keys of one category, the matching constant in the `data` module (`ALL_STARS`, `PALACES`, `HEAVENLY_STEMS`, `MUTAGEN` and so on) is simpler. *** ## Per-category translation functions [#per-category-translation-functions] When the category of a key is known, the matching strongly typed function is more direct and drops the `Option`. | Function | Input | | ------------------------------- | ------------------------------------- | | `translate_star` | `StarKey` | | `translate_palace` | `Palace` | | `translate_heavenly_stem` | `HeavenlyStem` | | `translate_earthly_branch` | `EarthlyBranch` | | `translate_brightness` | `Brightness` | | `translate_mutagen` | `Mutagen` | | `translate_five_elements_class` | `FiveElementsClass` | | `translate_gender` | `Gender` | | `translate_zodiac` | `EarthlyBranch` | | `translate_time` | `u8` (hour index 0–12) | | `translate_sign` | `usize` (sign index 0–11, from Aries) | | `translate_horoscope_name` | `HoroscopeName` | All take the form `fn(value, Language) -> &'static str` and return static strings without allocating. All twelve are re-exported at the crate root, so `use x_iztro::*;` brings them in. **Example** ```rust use x_iztro::i18n::{translate_palace, translate_star}; println!("{}", translate_star(StarKey::ZiweiMaj, Language::ViVN)); println!("{}", translate_palace(Palace::Soul, Language::KoKR)); ``` **Output** ```text Tử Vi 명궁 ``` *** ## There is no global language switch [#there-is-no-global-language-switch] x-iztro keeps no global "current language" 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 with multiple threads. Passing it explicitly means a call's result depends only on its arguments. To emit several languages within one process, just chart several times or call the translation functions repeatedly; they do not interfere: ```rust let en = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?; let zh = by_solar("2000-8-16", 2, Gender::Female, true, Language::ZhCN, Config::default())?; println!("{} / {}", en.palace(Palace::Soul).unwrap().major_stars[0].name, zh.palace(Palace::Soul).unwrap().major_stars[0].name); ``` **Output** ```text emperor / 紫微 ``` # Knowledge packs (/en/docs/rust/knowledge) KnowledgePack and its entry types, the bundled default pack, JSON parsing and serialization, overlay merging. A knowledge pack is JSON mapping "language-independent key → reading text and school attributes". The core only judges facts; reading texts and the school-specific star attributes live here. For the concept, the format and how to write an overlay, see the [knowledge pack guide](/en/docs/guide/guides/knowledge-pack); the full field reference is [`knowledge/SCHEMA.md`](https://github.com/x-haose/x-iztro/blob/main/knowledge/SCHEMA.md) in the repository. ```rust use x_iztro::{KnowledgePack, Language, StarKey}; let pack = KnowledgePack::builtin(Language::ZhCN).expect("zh-CN has a builtin pack"); let intro = pack.star_intro(StarKey::ZiweiMaj); ``` `KnowledgePack` is re-exported at the crate root; the other types live in `x_iztro::knowledge`. ## Types [#types] ### KnowledgePack [#knowledgepack] All fields are public and directly readable and writable. The map fields are `BTreeMap`, so iteration order is stable and sorted by key. | Field | Type | Meaning | | ---------- | -------------------------------- | ------------------------------------------------------------------------ | | `schema` | `u32` | Format version, currently `SCHEMA_VERSION` (1) | | `id` | `String` | Pack identifier; `"iztro-docs"` for the default pack | | `version` | `String` | Pack version; for the default pack, retrieval date + short source commit | | `language` | `String` | Language code of the texts, e.g. `"zh-CN"` | | `extends` | `Option` | The pack this overlay overlays; `None` for a standalone pack | | `source` | `Source` | Origin and licence | | `stars` | `BTreeMap` | Star entries, keyed by `StarKey::as_key` | | `patterns` | `BTreeMap` | Pattern entries, keyed by `PatternKey::as_key` | | `palaces` | `BTreeMap` | Palace entries, keyed by `Palace::as_key` | | `mutagens` | `BTreeMap` | Transformation entries, keyed by `Mutagen::as_key` | | `concepts` | `BTreeMap` | Glossary entries, keyed by slug | Implements `Clone`, `Default`, `PartialEq`, `Eq`, `Debug`, `Serialize`, `Deserialize`. ### Source [#source] | Field | Type | Meaning | | -------------- | ---------------- | --------------------------------------------------------------- | | `name` | `Option` | Source name | | `url` | `Option` | Source URL | | `commit` | `Option` | Source revision (git commit) | | `license` | `Option` | Licence | | `author` | `Option` | Author | | `retrieved_at` | `Option` | Retrieval date | | `adapted` | `Option` | Adaptation note: how the text was edited relative to the source | ### StarEntry [#starentry] | Field | Type | Meaning | | -------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `Option` | Display name in this pack's language | | `category` | `Option` | `"major"` / `"minor"` / `"adjective"` / `"dec"` / `"flow"` (a flowing star, a cross-reference entry pointing at its natal minor-star counterpart) | | `group` | `Option` | Grouping: the adjective star's category, the decorative star's group | | `attributes` | `StarAttributes` | School attributes; all `None` when absent | | `intro` | `Option` | Reading (Markdown) | | `combinations` | `BTreeMap` | Reading for sharing a palace with another major star, keyed by that star | ### StarAttributes [#starattributes] Every field is `Option`, except `aliases` which is `Option>`: `yin_yang` (`yin` / `yang`), `five_elements` (`wood` / `fire` / `earth` / `metal` / `water`), `stem` (`jia`…`gui`), `five_elements_note`, `dipper`, `chemistry`, `career`, `duty`, `aliases`, `element_color`, `energy_color`. `five_elements` and `yin_yang` here are what the pack's source says, and may differ from the core [`StarInfo`](/en/docs/rust/data), which is value-for-value identical to iztro's. The reason is in the [guide](/en/docs/guide/guides/knowledge-pack#why-the-star-attributes-live-here). ### PatternEntry [#patternentry] | Field | Type | Meaning | | ------------ | --------------------- | ------------------------------------------------ | | `name` | `Option` | Display name | | `quotes` | `Option>` | Classical quotations | | `conditions` | `Option` | The source's prose description of the conditions | | `intro` | `Option` | Reading | ### TextEntry / ConceptEntry [#textentry--conceptentry] `TextEntry` (palaces, transformations) has `name` and `intro`; `ConceptEntry` (glossary) has `title` and `intro`. All are `Option`. ### SCHEMA\_VERSION [#schema_version] ```rust pub const SCHEMA_VERSION: u32 = 1; ``` The highest format version this library supports. Parsing a higher `schema` is an error. *** ## builtin [#builtin] **Purpose** Get the bundled default knowledge pack. **Signature** ```rust impl KnowledgePack { pub fn builtin(language: Language) -> Option<&'static KnowledgePack> } ``` **Parameters** | Parameter | Type | Meaning | | ---------- | ---------- | ------------- | | `language` | `Language` | Text language | **Returns** `Option<&'static KnowledgePack>` — `None` when there is no bundled pack for that language. Only `Language::ZhCN` has one today. The pack is parsed once on first use and cached, so later calls are a free static reference. **Example** ```rust let pack = KnowledgePack::builtin(Language::ZhCN).unwrap(); println!("{} {} {}", pack.id, pack.version, pack.stars.len()); println!("{:?}", pack.source.license); println!("{}", KnowledgePack::builtin(Language::EnUS).is_some()); ``` **Output** ```text iztro-docs 2026-08-19+ec2d58b 162 Some("MIT") false ``` *** ## builtin\_json [#builtin_json] **Purpose** Get the raw JSON of the bundled pack, unparsed. **Signature** ```rust impl KnowledgePack { pub fn builtin_json(language: Language) -> Option<&'static str> } ``` **Returns** `Option<&'static str>`, zh-CN only, same as `builtin`. Use it to hand the pack to another process or write it to disk without a parse-and-serialize round trip — that is exactly what the bindings do. *** ## from\_json [#from_json] **Purpose** Parse a pack from JSON text. **Signature** ```rust impl KnowledgePack { pub fn from_json(json: &str) -> Result } ``` **Returns** `Result`. The error is a human-readable string, for three cases: JSON that does not fit the format (`invalid knowledge pack: ...`), a missing or zero `schema` (a pack must declare its format version), and a `schema` higher than `SCHEMA_VERSION` (no best-effort downgrade). This returns `String` rather than [`IztroError`](/en/docs/rust/errors): a knowledge pack is data the caller brings along, not input to a charting entry point. The bindings wrap it into each language's `invalid_argument` error. **Example** ```rust let pack = KnowledgePack::from_json(r#"{ "schema": 1, "id": "mine", "version": "1", "language": "zh-CN", "stars": {"ziweiMaj": {"intro": "我的紫微"}} }"#)?; println!("{:?}", pack.star_intro(StarKey::ZiweiMaj)); println!("{:?}", KnowledgePack::from_json(r#"{"schema": 99}"#)); println!("{:?}", KnowledgePack::from_json(r#"{"id": "x"}"#)); ``` **Output** ```text Some("我的紫微") Err("knowledge pack schema 99 is newer than supported 1") Err("knowledge pack must declare \"schema\" (currently 1)") ``` **Edges and traps** Every entry and field is optional. A map field (`source`, `stars`, `combinations` …) written as `null` is the same as leaving it out — Go serializes a nil map as `null`, and this keeps the default serialization of all three languages mutually parseable. A key in `stars` that is not a star key, or an unknown pattern key in `patterns`, is kept verbatim. Validating keys is the generator's and the tests' job; the parser only checks the format. *** ## to\_json [#to_json] **Purpose** Serialize to JSON (compact, no indentation). **Signature** ```rust impl KnowledgePack { pub fn to_json(&self) -> String } ``` **Returns** `String`. Optional fields that are `None` and empty maps are omitted, so `from_json(&pack.to_json())` equals the original pack. For indented output use `serde_json::to_string_pretty(&pack)`. *** ## merged [#merged] **Purpose** Layer overlay packs onto this one and return a new pack. **Signature** ```rust impl KnowledgePack { pub fn merged(&self, overlays: &[&KnowledgePack]) -> KnowledgePack } ``` **Parameters** | Parameter | Type | Meaning | | ---------- | ------------------- | ----------------------------------------------- | | `overlays` | `&[&KnowledgePack]` | Overlays applied in slice order; later ones win | **Returns** A `KnowledgePack`; neither this pack nor the overlays change. The rules are in the [guide](/en/docs/guide/guides/knowledge-pack#merge-rules): section by section, key by key, an overlay's non-`None` fields replace the same-keyed entry's fields, `attributes` and `combinations` merge field by field, array fields are replaced wholesale. **Example** ```rust let base = KnowledgePack::builtin(Language::ZhCN).unwrap(); let overlay = KnowledgePack::from_json(r#"{ "schema": 1, "id": "my-school", "version": "1", "language": "zh-CN", "extends": "iztro-docs", "stars": {"ziweiMaj": {"intro": "我的紫微", "attributes": {"aliases": ["帝座"]}}}, "patterns": {"zi_fu_tong_gong": {"intro": "我的紫府同宫"}} }"#)?; 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)); println!("{:?}", pack.pattern(PatternKey::ZiFuTongGong).unwrap().quotes); println!("{} {:?}", pack.id, base.star_intro(StarKey::ZiweiMaj).map(|s| s.chars().take(5).collect::())); ``` **Output** ```text Some("紫微") Some(["帝座"]) Some("尊贵") Some("我的紫府同宫") Some(["紫府同宫终身福厚。"]) my-school Some("紫微星号称") ``` *** ## merge [#merge] **Purpose** Merge one overlay into this pack in place. **Signature** ```rust impl KnowledgePack { pub fn merge(&mut self, overlay: &KnowledgePack) } ``` `merged` is the non-mutating version (clone, then `merge` each overlay). Use `merge` when you already hold a mutable pack and are layering many overlays, to skip the clone. *** ## star / pattern / palace / mutagen [#star--pattern--palace--mutagen] **Purpose** Look up an entry by language-independent key. **Signature** ```rust impl KnowledgePack { pub fn star(&self, key: StarKey) -> Option<&StarEntry> pub fn pattern(&self, key: PatternKey) -> Option<&PatternEntry> pub fn palace(&self, palace: Palace) -> Option<&TextEntry> pub fn mutagen(&self, mutagen: Mutagen) -> Option<&TextEntry> } ``` **Returns** `None` when the pack has no such entry. All four look the key up in the corresponding `BTreeMap` by `as_key()`, exactly as `pack.stars.get(key.as_key())` would; the glossary has no dedicated method, use `pack.concepts.get(slug)`. **Example** ```rust let pack = KnowledgePack::builtin(Language::ZhCN).unwrap(); let ziwei = pack.star(StarKey::ZiweiMaj).unwrap(); println!("{:?} {:?} {:?}", ziwei.name, ziwei.category, ziwei.attributes.dipper); println!("{:?}", ziwei.combinations.keys().collect::>()); println!("{:?}", pack.palace(Palace::Soul).and_then(|e| e.name.clone())); println!("{:?}", pack.mutagen(Mutagen::Lu).and_then(|e| e.name.clone())); println!("{:?}", pack.concepts.get("tong-gong").and_then(|e| e.title.clone())); ``` **Output** ```text Some("紫微") Some("major") Some("中天星系") ["pojunMaj", "qishaMaj", "tanlangMaj", "tianfuMaj", "tianxiangMaj"] Some("命宫") Some("化禄") Some("遇、加、逢、同宫、同度") ``` *** ## star\_intro / pattern\_intro [#star_intro--pattern_intro] **Purpose** Get the reading directly, skipping one layer of `Option`. **Signature** ```rust impl KnowledgePack { pub fn star_intro(&self, key: StarKey) -> Option<&str> pub fn pattern_intro(&self, key: PatternKey) -> Option<&str> } ``` **Returns** `None` both when the entry is missing and when it exists without a reading. **Example** List the natal patterns with their readings: ```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 intro = pack.pattern_intro(hit.key).unwrap_or("(not written in this pack)"); let head: String = intro.chars().take(10).collect(); println!("{} {}", translate_pattern(hit.key, Language::ZhCN), head); } ``` **Output** ```text 府相朝垣 “食禄千锺”的断语使 ``` # Reverse lookup (/en/docs/rust/reverse) solar_dates_by_bazi and reverse_chart - the functions and types for recovering candidate birth dates from BaZi pillars or chart features. Recover candidate birth dates from four BaZi pillars or from chart features. Both entry points are "pruned enumeration + full re-charting", so results have zero divergence from forward charting. Concepts, how pillars follow the Config boundaries, and the multi-solution / truncation semantics are on the [reverse lookup guide](/en/docs/guide/guides/reverse). ```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(), )?; ``` Everything is defined in `x_iztro::astro::reverse` and re-exported at the crate root. ## Types [#types] ### BirthCandidate [#birthcandidate] One candidate birth moment, ready to hand to [`by_solar`](/en/docs/rust/astro). | Field | Type | Meaning | | ------------ | -------- | ------------------------------------------------------ | | `solar_date` | `String` | solar date, `YYYY-M-D` | | `time_index` | `u8` | hour index 0–12 (0 = early Zi hour, 12 = late Zi hour) | ### StarPosition [#starposition] A star and the branch of the palace it sits in: the atomic condition of a feature lookup. | Field | Type | Meaning | | -------- | --------------- | -------------------------------------------------------------------------- | | `star` | `StarKey` | the star (natal chart stars only; horoscope-scope flow stars are rejected) | | `branch` | `EarthlyBranch` | the branch of its palace | ### ReverseCriteria [#reversecriteria] The condition set of a feature lookup. Implements `Default`; the idiomatic construction gives the conditions and closes with `..Default::default()`. Every condition is optional, but at least one must be given. | Field | Type | Default | Meaning | | --------------------- | --------------------------- | -------------- | -------------------------------------------------------------- | | `soul_branch` | `Option` | `None` | soul palace branch | | `body_branch` | `Option` | `None` | body palace branch | | `five_elements_class` | `Option` | `None` | five elements class | | `stars` | `Vec` | empty | star placements, all of which must hold | | `mutagens` | `[Option; 4]` | all `None` | which star carries each birth-year mutagen \[Lu, Quan, Ke, Ji] | | `year_range` | `(i64, i64)` | `(1900, 2100)` | inclusive solar year range, within 1583–9999 | | `fix_leap` | `bool` | `true` | leap month correction, same meaning as the charting parameter | | `limit` | `usize` | `0` | candidate cap; `0` takes `DEFAULT_REVERSE_LIMIT` | ### ReverseResult [#reverseresult] | Field | Type | Meaning | | ------------ | --------------------- | ------------------------------------------------------------------------------------------ | | `candidates` | `Vec` | the birth candidates satisfying every condition | | `truncated` | `bool` | whether the search stopped early at the candidate cap; later solutions were never searched | ### DEFAULT\_REVERSE\_LIMIT [#default_reverse_limit] ```rust pub const DEFAULT_REVERSE_LIMIT: usize = 512; ``` The candidate cap used when `ReverseCriteria::limit` is 0. *** ## solar\_dates\_by\_bazi [#solar_dates_by_bazi] Recover solar birth dates from four BaZi pillars. ```rust pub fn solar_dates_by_bazi( yearly: (HeavenlyStem, EarthlyBranch), monthly: (HeavenlyStem, EarthlyBranch), daily: (HeavenlyStem, EarthlyBranch), hourly: (HeavenlyStem, EarthlyBranch), year_range: (i64, i64), config: &Config, ) -> Result, IztroError> ``` The pillars are interpreted under the boundary readings of `config` (`year_divide` for the year pillar, `horoscope_divide` for the month pillar, `day_divide` for the late Zi hour) — the same semantics as the `raw_dates.chinese_date` a charted astrolabe reports, so reversing any chart's pillars always includes that chart's birth moment. A set of pillars recurs roughly every 60 years within the range; an hour branch of Zi may yield two candidates on adjacent days because of the early/late Zi hour split. **Example** ```rust let a = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?; let p = a.raw_dates.chinese_date; let cands = solar_dates_by_bazi(p.yearly, p.monthly, p.daily, p.hourly, (1900, 2100), &Config::default())?; for c in &cands { println!("{} {}", c.solar_date, c.time_index); } ``` **Output** ```text 1940-8-31 2 2000-8-16 2 2060-8-1 2 ``` **Errors** A pillar with mismatched stem/branch polarity (such as 甲丑 Jia-Chou — a yang stem on a yin branch), or a year range that is reversed or outside 1583–9999, returns [`IztroError::InvalidArgument`](/en/docs/rust/errors#invalidargument). *** ## reverse\_chart [#reverse_chart] Recover candidate birth dates from chart features. ```rust pub fn reverse_chart( criteria: &ReverseCriteria, config: &Config, ) -> Result ``` Judgement runs entirely under `config`: the mutagen table, the school and every boundary follow it, so charting a candidate with the same `config` is guaranteed to satisfy every condition. Chart layout does not depend on gender (gender only affects the direction the decadal horoscope advances), so the criteria carry no gender. **Example** ```rust 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], year_range: (1998, 2002), ..Default::default() }, &Config::default(), )?; println!("{} {}", r.candidates.len(), r.truncated); ``` **Output** ```text 39 false ``` **Errors** Empty criteria, a horoscope-scope flow star in `stars`, or an invalid year range returns [`IztroError::InvalidArgument`](/en/docs/rust/errors#invalidargument). Reaching `limit` stops the search; later solutions never appear in the result. On `truncated = true`, narrow `year_range` or add conditions and query again. # Extending the astrolabe (/en/docs/rust/extend) Adding your own analysis methods to Astrolabe and PalaceRef with extension traits. Zi Wei analysis rules differ from practitioner to practitioner and no library can enumerate them. x-iztro's answer is to let you attach your own rules as methods on the astrolabe — the call syntax matches the built-in methods, and it all happens at compile time, so it is type-checked and costs nothing at runtime. ## The recipe [#the-recipe] Define a trait declaring the methods you want to add Implement it for `Astrolabe` (or `PalaceRef` , or `StarRef` ) `use` the trait where you call it, and the methods are available ```rust use x_iztro::i18n::translate_star; use x_iztro::*; /// Adds two custom analysis methods to the astrolabe. trait MyAnalysis { /// Major stars of the Soul palace (borrowing the opposite palace when empty), comma separated fn major_star(&self) -> String; /// The number of the five elements class fn five_elements_value(&self) -> usize; } 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(",") } fn five_elements_value(&self) -> usize { self.five_elements_class.value() } } ``` **Usage** ```rust let chart = by_solar("2000-8-16", 2, Gender::Female, true, Language::EnUS, Config::default())?; println!("{}", chart.major_star()); println!("{}", chart.five_elements_value()); // the extension method follows the charting language let zh = by_solar("2000-8-16", 2, Gender::Female, true, Language::ZhCN, Config::default())?; println!("{}", zh.major_star()); ``` **Output** ```text emperor 3 紫微 ``` *** ## Extending other types [#extending-other-types] The same recipe works for palaces and stars. Mind the lifetime parameter when implementing for a view type: ```rust trait PalaceAnalysis { /// Whether this palace is "afflicted": holds a malefic and carries ji fn is_afflicted(&self) -> bool; } impl PalaceAnalysis for PalaceRef<'_> { fn is_afflicted(&self) -> bool { use x_iztro::StarKey::*; self.has_one_of(&[QingyangMin, TuoluoMin, HuoxingMin, LingxingMin, DikongMin, DijieMin]) && self.has_mutagen(Mutagen::Ji) } } ``` ```rust for palace in &chart.palaces { let p = chart.palace(palace.index).unwrap(); if p.is_afflicted() { println!("{} is afflicted", translate_palace(p.name, Language::EnUS)); } } ``` **Output** ```text health is afflicted ``` *** ## How to organize this [#how-to-organize-this] Keep `WealthAnalysis`, `CareerAnalysis` and `HealthAnalysis` as separate traits and let callers `use` what they need. One large trait forces every call site to pull in every method. `s.key == StarKey::ZiweiMaj` holds under any output language; `s.name == "emperor"` holds only on an en-US chart. Translate at display time only. When one rule set has to work in Python and Go too, writing it as a Rust extension does not help — the current approach is to write it once per side; the other two are documented on their own "Extending the astrolabe" pages. Since the predicates rest on language-independent keys, asserting the same values across the three implementations is enough to guarantee agreement. *** ## Compared to runtime injection [#compared-to-runtime-injection] Some libraries let you attach functions to objects at runtime. Rust's extension traits do the same thing at compile time; the differences are: | | Extension trait | Runtime injection | | ------------------------- | ------------------------- | ------------------------ | | Whether the method exists | Known at compile time | Known only at runtime | | Type checking | Yes | No | | Call cost | Same as a built-in method | One extra dynamic lookup | | When errors appear | Compilation fails | Fails at runtime | | Scope | Visible only where `use`d | Global or per instance | The price is that extension methods must be written at compile time and cannot be decided by a config file or user input. When you need that flexibility, dispatch yourself through a `HashMap bool>`. # Error handling (/en/docs/rust/errors) The four variants of IztroError, the code() classifier, BridgeError, and how to diagnose. Every entry point taking external input returns a `Result`. Date format, date existence, year range, hour index and the reverse-lookup inputs are all validated up front in the core, so invalid input yields an error value rather than a panic. ```rust #[non_exhaustive] pub enum IztroError { /// Malformed date string, non-existent date, or outside the solar range 1583–9999 InvalidDate(String), /// Hour index outside 0–12 InvalidTimeIndex(u8), /// Invalid reverse-lookup input: mismatched pillar polarity, empty criteria, bad year range InvalidArgument(String), /// The calendar library did not return a pillar or sign value that should exist Internal(String), } impl IztroError { /// Machine-readable classification of the error pub fn code(&self) -> &'static str } ``` `IztroError` implements `Display` and `std::error::Error`, so it propagates with `?` and prints with `{}`. ## code() [#code] `Display` gives human-facing wording that may be adjusted between versions; branch on `code()` in code: | Variant | `code()` | Meaning | | ------------------ | -------------------- | ------------------------------------------------ | | `InvalidDate` | `invalid_date` | The date is invalid | | `InvalidTimeIndex` | `invalid_time_index` | The hour index is out of range | | `InvalidArgument` | `invalid_argument` | Invalid input to the reverse-lookup entry points | | `Internal` | `internal` | A defect inside the library | These four values are the same set as Python's `IztroError.code` and Go's `iztro.Error.Code`, so branching logic carries across languages verbatim. ```rust let e = by_solar("2000-2-30", 2, Gender::Female, true, Language::EnUS, Config::default()) .unwrap_err(); println!("{} / {}", e.code(), e); ``` **Output** ```text invalid_date / invalid solar date '2000-2-30': day is out of range for that month ``` `IztroError` is marked `#[non_exhaustive]`, so a `match` outside the crate cannot be exhaustive and must carry a catch-all arm. Adding an error class later is therefore not a breaking change, and code branching on `code()` is unaffected either way. *** ## InvalidDate [#invaliddate] **Triggers** | Situation | Example | Message | | ---------------------------------- | ------------- | ------------------------------------ | | The format is not `YYYY-M-D` | `"2000/8/16"` | `expected 'YYYY-M-D'` | | Year, month or day is not a number | `"abc-8-16"` | `year is not a number` | | Month out of range | `"2000-13-1"` | `month must be within 1-12` | | That month has no such day | `"2000-2-30"` | `day is out of range for that month` | | Year outside the supported range | `"1500-1-1"` | `year must be within 1583-9999` | Lunar-only (reachable from `by_lunar`, `get_sign_by_lunar_date` and `get_major_star_by_lunar_date`): | Situation | Example | Message | | --------------------------------- | ------------------------------ | ------------------------------------------ | | That lunar year has no such month | a month missing from the table | `month does not exist in that lunar year` | | That lunar month has no such day | `"2000-7-30"` (a short month) | `day is out of range for that lunar month` | Lunar messages are prefixed `invalid lunar date '': ` and solar ones `invalid solar date '': `, so the message alone shows which entry point was taken. **Example** ```rust let cfg = Config::default(); for date in ["2000-13-1", "2000-2-30", "1500-1-1"] { match by_solar(date, 2, Gender::Female, true, Language::EnUS, cfg.clone()) { Ok(_) => println!("{date}: ok"), Err(e) => println!("{e}"), } } ``` **Output** ```text invalid solar date '2000-13-1': month must be within 1-12 invalid solar date '2000-2-30': day is out of range for that month invalid solar date '1500-1-1': year must be within 1583-9999 ``` The message carries the original input, so batch jobs can pinpoint which record failed. **Edge cases and pitfalls** The Gregorian reform year of 1582 contains a stretch of dates that never existed. The underlying calendar library has no definition for them, so support starts from 1583, after the reform. The upper bound of 9999 is where the lunar data tables end. Both `"2000-8-16"` and `"2000-08-16"` are accepted. The separator must be `-`. `by_lunar` checks whether that month really exists in that lunar year and how many days it has (30 in a long month, 29 in a short one). Flagging `leap` as a leap month when that year and month have none is not an error — the ordinary month is used. *** ## InvalidTimeIndex [#invalidtimeindex] **Trigger** An hour index greater than 12. **Example** ```rust let err = by_solar("2000-8-16", 13, Gender::Female, true, Language::EnUS, Config::default()) .unwrap_err(); println!("{err}"); ``` **Output** ```text time_index must be 0-12, got 13 ``` **Edge cases and pitfalls** The Zi hour straddles midnight and splits into the early Zi hour (index 0) and the late Zi hour (index 12), so there are 13 legal values. To convert from a clock hour use [`time_to_index`](/en/docs/rust/util#time_to_index), which is guaranteed to land in the legal range. *** ## InvalidArgument [#invalidargument] **Triggers** Caller mistakes at the reverse-lookup entry points: | Situation | Example | Message | | ---------------------------------------------- | ---------------------------- | ----------------------------------------------------------------------------- | | Pillar stem/branch polarity mismatch | 甲丑 (yang stem, yin branch) | `invalid yearly pillar: stem and branch must have the same polarity` | | Empty reverse criteria | `ReverseCriteria::default()` | `reverse criteria must contain at least one condition` | | A horoscope-scope flow star among the criteria | 流禄 | `star 'liulu' is a horoscope-scope star and never appears on the natal chart` | | Invalid year range | `(2100, 1900)` | `invalid year range 2100-1900: expected 1583-9999 with start <= end` | **Example** ```rust let jia_zi = (HeavenlyStem::Jia, EarthlyBranch::Zi); let err = solar_dates_by_bazi( (HeavenlyStem::Jia, EarthlyBranch::Chou), // 甲丑: mismatched polarity jia_zi, jia_zi, jia_zi, (1900, 2100), &Config::default(), ) .unwrap_err(); println!("{} / {}", err.code(), err); ``` **Output** ```text invalid_argument / invalid yearly pillar: stem and branch must have the same polarity ``` The semantics of the reverse-lookup entry points are on [Reverse lookup](/en/docs/rust/reverse). *** ## Internal [#internal] **Trigger** The calendar library `lunar_rust` did not hand back a pillar or zodiac-sign value that should exist. This is a defect inside the library rather than a caller's mistake. **Why not panic** On the wasm target a panic is a trap, and every trap permanently consumes stack space in the module instance. Turning such cases into error values keeps invalid calls from accumulating damage in the wasm instance on the Go side. **Message shape** `internal error: `, with `code()` returning `internal`. This variant has never been triggered on any date covered by the golden tests. If you do hit it, please report it as a bug together with the full charting parameters. *** ## BridgeError [#bridgeerror] The uniform error shape the bindings (C FFI / wasm / PyO3) report outward. Rust callers generally do not need it — it is the origin of the error objects on the Python and Go sides. ```rust pub struct BridgeError { /// invalid_date / invalid_time_index / invalid_argument / internal pub code: &'static str, /// Human-facing description of the error pub message: String, } impl BridgeError { pub fn invalid_argument(message: impl Into) -> Self pub fn internal(message: impl Into) -> Self } impl From for BridgeError { /* code and message carry straight over */ } ``` Its four classes are the same set as `IztroError`'s. The bindings receive strings rather than enums, so the validity of a gender, language, palace name, mutagen or config JSON can only be checked at that layer; those cases land in the `invalid_argument` class as well. All three exits serialize it into the same JSON: ```json { "error": "invalid solar date '2000-2-30': day is out of range for that month", "code": "invalid_date" } ``` On the Python side that becomes an `IztroError` (subclassing `ValueError`, carrying `.code`); on the Go side an `*iztro.Error` (carrying `Code`, comparable with `errors.Is` against sentinels). ## C FFI [#c-ffi] In the C ABI exported by `x_iztro::ffi`, the single query entry point is `iztro_query`: ```c // include/x_iztro.h char *iztro_query(const char *query_json); void iztro_free_string(char *s); ``` It takes a JSON document whose `kind` selects the query (for example `{"kind":"getPalaceNames","soulIndex":0}`), returning `{"value": }` on success and the error JSON with its `code` shown above on failure. It gathers the lightweight queries, palace derivation, utilities, star placement, data tables, translation and prompt generation behind one symbol instead of exporting one C symbol per function. Keys are camelCase, and stars, stems, branches, palaces and other identifiers are passed and returned as iztro i18n keys. Every returned string is handed back by the caller through `iztro_free_string`; these functions never return `NULL`. The `ffi` module is marked `#[doc(hidden)]` and is not meant for Rust callers — in Rust, call the `by_solar` family directly. *** ## Handling errors [#handling-errors] **Propagate with `?`** ```rust fn analyze(date: &str) -> Result { let chart = by_solar(date, 2, Gender::Female, true, Language::EnUS, Config::default())?; Ok(chart.palace(Palace::Soul).unwrap().major_stars .iter().map(|s| s.name.clone()).collect::>().join(",")) } ``` **Match on the variant** ```rust fn describe(date: &str, ti: u8) -> String { match by_solar(date, ti, Gender::Female, true, Language::EnUS, Config::default()) { Ok(chart) => format!("charted: {}", chart.solar_date), Err(IztroError::InvalidDate(msg)) => format!("bad date: {msg}"), Err(IztroError::InvalidTimeIndex(t)) => format!("hour index {t} out of range"), Err(e) => format!("other error [{}]: {e}", e.code()), } } println!("{}", describe("2000-8-16", 2)); println!("{}", describe("2000-2-30", 2)); println!("{}", describe("2000-8-16", 13)); ``` **Output** ```text charted: 2000-8-16 bad date: invalid solar date '2000-2-30': day is out of range for that month hour index 13 out of range ``` `IztroError` is marked `#[non_exhaustive]`, so a `match` outside the crate must carry a `_` or `Err(e)` catch-all arm or it will not compile. That is what keeps future variants from being a breaking change. **Convert to your own error type** `IztroError` implements `std::error::Error`, so it is caught directly by `Box`, `anyhow::Error`, or `thiserror`'s `#[from]`. ```rust #[derive(Debug, thiserror::Error)] enum AppError { #[error("charting failed: {0}")] Chart(#[from] x_iztro::IztroError), } ``` *** ## About panics [#about-panics] The charting entry points do not panic on invalid **external input**: dates and hour indices become an `IztroError`, and even the internal case of the calendar library failing to produce a value goes through `IztroError::Internal` rather than a panic. The only remaining source of panics would be a logic defect inside the library (a failed assertion, say), and such a case should be reported as a bug. On wasm a panic is a trap, and every trap permanently consumes stack space in the module instance. Validation therefore lives in the core rather than in the bindings — one line of defense shared by all three languages. # Overview (/en/docs/python) The package layout, the type system, and how to read this reference. The Python package is a typed wrapper around the Rust core: computation happens in Rust, while the Python side provides a strongly typed API of dataclasses and StrEnums with zero external dependencies. This section is the complete Python API reference — every function, class and method has its own entry. ## Install [#install] ```bash pip install x-iztro ``` Requires Python 3.10 or newer. The distribution ships a precompiled native extension (an abi3-py310 wheel), so installing needs no Rust toolchain. `enum.StrEnum` only entered the standard library in 3.11. On 3.10 `x_iztro.enums` falls back automatically to the equivalent `class StrEnum(str, Enum)` implementation — the members are still both strings and completion targets, and the two versions behave identically. ## Your first chart [#your-first-chart] ```python from x_iztro import Astro chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US") # 2 = Tiger hour (03:00–05:00) print(chart.solar_date, chart.lunar_date) # 2000-8-16 二〇〇〇年七月十七 soul = chart.palace("soulPalace") print(" ".join(s.name for s in soul.major_stars)) # emperor ``` `lunar_date` is a Chinese-numeral lunar date string and stays Chinese under every language: `二〇〇〇年七月十七` is the 17th day of the 7th lunar month of 2000. ## Package layout [#package-layout] | Module | Contents | Page in this reference | | ---------------- | -------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | `x_iztro.Astro` | The main charting class | [Charting entries](/en/docs/python/astro) | | `x_iztro.models` | The `Astrolabe`, `Palace`, `Star`, `Horoscope`, `ChartConfig` and related dataclasses | The four pages from [Astrolabe object](/en/docs/python/astrolabe) onward | | `x_iztro.enums` | StrEnums for every language-independent key, plus the `GenderType`, `LanguageType`, `TimeIndexType` and other type aliases | [Data tables](/en/docs/python/data), further down this page | | `x_iztro.query` | Lightweight queries for the zodiac animal, sign and Soul palace major stars | [Lightweight queries](/en/docs/python/query) | | `x_iztro.utils` | Index arithmetic, brightness and mutagen lookups | [Utilities](/en/docs/python/util) | | `x_iztro.star` | Star placement from birth data | [Star placement](/en/docs/python/star) | | `x_iztro.data` | Star and stem/branch tables, ordering constants | [Data tables](/en/docs/python/data) | | `x_iztro.i18n` | Two-way lookup between keys and translations | [Translation](/en/docs/python/i18n) | | `x_iztro.plugin` | Attaching custom methods to the astrolabe class | [Extending the astrolabe](/en/docs/python/extend) | `models` is an aggregation layer: `Astrolabe` actually lives in `x_iztro.astrolabe`, `Palace` in `x_iztro.palace`, `Star` in `x_iztro.star_object`, `Horoscope` in `x_iztro.horoscope`, `ChartConfig` in `x_iztro.config` and `SurroundedPalaces` in `x_iztro.surpalaces`. Importing from the `x_iztro` top level or from `x_iztro.models` gets you the same objects; take whichever you prefer. ## Type aliases [#type-aliases] `x_iztro.enums` holds a handful of `Literal` aliases whose job is to make your editor flag a wrong argument immediately: | Alias | Definition | | ----------------- | --------------------------------------------------------------------------------------- | | `GenderType` | `Literal["male", "female"]` | | `LanguageType` | `Literal["zh-CN", "zh-TW", "en-US", "ja-JP", "ko-KR", "vi-VN"]` | | `TimeIndexType` | `Literal[0, 1, …, 12]` | | `StarTypeLiteral` | `Literal["major", "soft", "tough", "adjective", "flower", "helper", "lucun", "tianma"]` | | `ScopeLiteral` | `Literal["origin", "decadal", "yearly", "monthly", "daily", "hourly"]` | They are annotations only and validate nothing at runtime — the real value checking happens in the core, which raises `IztroError` on an illegal value. ## Enums are the keys [#enums-are-the-keys] Every enum in `x_iztro.enums` is a `StrEnum` whose **value is the language-independent key**, so it compares directly against the `*_key` fields on the data objects: ```python from x_iztro import MajorStar, PalaceName soul = chart.palace(PalaceName.SOUL) print(soul.major_stars[0].key == MajorStar.ZIWEI) # True ``` Being `StrEnum`s, string literals work just as well — `chart.palace("soulPalace")` and `chart.palace(PalaceName.SOUL)` are equivalent. The enums earn their keep through IDE completion and spell checking. `star.name` varies with the charting language (`紫微` on a Chinese chart, `emperor` on an English one); `star.key` is `ziweiMaj` under any language. Every predicate should rest on the `*_key` fields or on the built-in predicate methods. ## The data objects are immutable [#the-data-objects-are-immutable] Astrolabes, palaces and stars are all `frozen=True` dataclasses whose fields cannot be assigned after construction. When you need a variant, use a method that returns a new object, such as `chart.rearranged(...)`. ```python try: chart.solar_date = "2001-1-1" except Exception as e: print(type(e).__name__, e) ``` **Output** ```text FrozenInstanceError cannot assign to field 'solar_date' ``` Immutability lets a chart travel safely between analysis functions, go into a cache and be shared across threads, without worrying that a change in one place will affect another. ## 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** (`by_solar("2000-8-16", 2, "female")`) — so they can be compared across pages. The full data for that chart is on [the data model](/en/docs/guide/data-model). Every example on these English pages charts in `en-US`, so the display values in the output blocks are the English translations. Changing the language changes only those display strings; the `*_key` identifiers and the results of every predicate method stay the same. # Charting entries (/en/docs/python/astro) The charting methods of the Astro class, re-anchoring, and the semantic text projection. Charting is where everything starts: give a birth date, hour and gender, get an `Astrolabe`. ```python from x_iztro import Astro astro = Astro() ``` `Astro` holds no internal state, so instantiate it once and use it everywhere — or construct one per call. Every entry point raises `IztroError` on invalid input (it subclasses `ValueError`, so `except ValueError` catches it too). Date format and existence, the solar year range and the hour index are validated up front in the core; string values such as gender, language and palace name are validated in the binding layer. The exception carries a machine-readable classification in `.code` — see [Error handling](/en/docs/python/errors). *** ## ChartConfig [#chartconfig] The charting configuration: six switches plus two optional override tables. It is a frozen dataclass in which every field has a default, and the defaults match JS iztro — `ChartConfig()` is equivalent to passing no `config` at all. | Field | Type | Default | Values (matching enum) | | ------------------ | ------------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------- | | `year_divide` | `str` | `"normal"` | `normal` lunar New Year's Day / `exact` Beginning of Spring (`YearDivide`) | | `horoscope_divide` | `str` | `"normal"` | `normal` first of the month / `exact` solar term (`HoroscopeDivide`) | | `age_divide` | `str` | `"normal"` | `normal` increments at the turn of the year / `birthday` increments on the birthday (`AgeDivide`) | | `day_divide` | `str` | `"forward"` | `forward` the late Zi hour belongs to the next day / `current` to the current day (`DayDivide`) | | `algorithm` | `str` | `"default"` | `default` / `zhongzhou` (`Algorithm`) | | `astro_type` | `str` | `"heaven"` | `heaven` heaven chart / `earth` earth chart / `human` human chart (`AstroType`) | | `mutagens` | `dict[str, list[str]] \| None` | `None` | Stem key → four star keys (Lu, Quan, Ke, Ji) | | `brightness` | `dict[str, list[str]] \| None` | `None` | Star key → twelve brightness keys; index 0 is the Yin palace, an empty string means no brightness in that palace | For what the six switches mean and the schools behind them, see [Config in depth](/en/docs/guide/guides/config). **Methods** | Method | Description | | ------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `to_dict() -> dict` | Convert to the camelCase config object the binding layer accepts; either table is omitted from the result when it is `None` | **Example** ```python from x_iztro import Astro, ChartConfig, AstroType, HeavenlyStem, MajorStar cfg = ChartConfig( astro_type="earth", mutagens={HeavenlyStem.GENG: [MajorStar.TAIYANG, MajorStar.WUQU, MajorStar.TIANFU, MajorStar.TIANTONG]}, ) print(cfg.to_dict()) chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US", config=cfg) print(chart.five_elements_class, chart.config.astro_type) print(chart.palace("soulPalace").mutagen_star_keys) ``` **Output** ```text {'yearDivide': 'normal', 'horoscopeDivide': 'normal', 'ageDivide': 'normal', 'dayDivide': 'forward', 'algorithm': 'default', 'astroType': 'earth', 'mutagens': {'gengHeavenly': ['taiyangMaj', 'wuquMaj', 'tianfuMaj', 'tiantongMaj']}} earth 5th earth ['tiantongMaj', 'tianjiMaj', 'wenchangMin', 'lianzhenMaj'] ``` The earth chart's Soul palace lands on the original chart's body palace (Career, bing-xu), so the palace-stem mutagens are taken from the bing stem. **Edge cases and pitfalls** `mutagens` replaces **all four** positions of one stem at a time, and `brightness` replaces **all twelve** palaces of one star at a time. The lengths are strictly validated: a mutagen list must be exactly four entries and a brightness list exactly twelve, and one entry too many or too few raises `IztroError`. Stems and stars you do not list keep the default table. Both the keys and the values of the two tables must be language-independent keys (`"gengHeavenly"`, `"taiyangMaj"`). Passing `"庚"` or `"太阳"` raises an `invalid mutagens key` style error. The enum members are `StrEnum`s, so use them as keys directly and `to_dict` converts them to strings for you. `to_dict` applies `str()` only to the keys and values inside the two tables; the six switch fields come out as they went in. Writing `ChartConfig(astro_type=AstroType.EARTH)` charts perfectly well (a `StrEnum` is equivalent to its string), it is only that the entry in `to_dict()`'s result prints as ``. To serialize the configuration as clean JSON, pass the switches as string literals, or run `str()` over them yourself. `chart.config` is restored from the output DTO and carries only the six switches — the two override tables are charting **input** rather than result, so they never enter the DTO (matching the field contract of JS iztro). The chart does keep the originals you passed in internally, however, so follow-up computations such as `chart.rearranged(...)`, `chart.horoscope(...)` and the to\_text projection still use those two tables; they are not silently dropped. To record the configuration, keep your `ChartConfig` object on your own call site. *** ## 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 method 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** ```python def by_solar( self, solar_date: str, time_index: TimeIndexType, gender: GenderType, *, fix_leap: bool = True, language: LanguageType = "zh-CN", config: ChartConfig | None = None, ) -> Astrolabe ``` **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` | `int` | 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` | `str` | Yes | — | `"male"` or `"female"`. Sets the direction of the decadal scope and of the Changsheng and Boshi gods | | `fix_leap` | `bool` | No | `True` | Keyword-only. Whether to correct for lunar leap months. When true, days from the sixteenth of a leap month onward count as the next month (the late Zi hour excepted, see below) | | `language` | `str` | No | `"zh-CN"` | Output language; affects every translated field. The `*_key` fields are unaffected | | `config` | `ChartConfig \| None` | No | `None` | Charting configuration; `None` takes 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. **Example** ```python from x_iztro import Astro chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US") print(chart.solar_date, "|", chart.lunar_date, "|", chart.chinese_date) print(chart.sign, chart.zodiac, chart.five_elements_class) print("soul", chart.soul, "body", chart.body) ``` **Output** ```text 2000-8-16 | 二〇〇〇年七月十七 | geng chen - jia shen - bing woo - geng yin leo dragon wood 3rd soul rebel body scholar ``` **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. When unsure of the index, convert with `utils.time_to_index(hour)`. Carrying over into the next month requires four conditions at once: 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 of them and the month index is that of the current month. So only for someone born in the second half of a lunar leap month do `True` and `False` give different month indices, which in turn affects Zuofu, Youbi and every month-based star. 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** ```python def by_lunar( self, lunar_date: str, time_index: TimeIndexType, gender: GenderType, *, is_leap_month: bool = False, fix_leap: bool = True, language: LanguageType = "zh-CN", config: ChartConfig | None = None, ) -> Astrolabe ``` **Parameters** Identical to `by_solar` apart from the following. Everything after `gender` is keyword-only — `is_leap_month` and `fix_leap` sit next to each other, and swapping them positionally would raise no error while silently shifting the chart by a month. | 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) | | `is_leap_month` | `bool` | No | `False` | Keyword-only. Whether that lunar month is a leap month. Has no effect if that month in that year has no leap month to begin with | **Return value** Same as `by_solar`. **Example** ```python a = Astro().by_lunar("2000-7-17", 2, "female", language="en-US") b = Astro().by_solar("2000-8-16", 2, "female", language="en-US") print(a.solar_date, a.solar_date == b.solar_date) ``` **Output** ```text 2000-8-16 True ``` **Edge cases and pitfalls** Pass `True` when that month is not a leap month and the parameter is silently ignored rather than raising an error. If you need strict validation, confirm the leap month exists for that year and month before calling. *** ## get\_horoscope [#get_horoscope] **Purpose** Compute the horoscope for a target date, starting from a natal chart. **Signature** ```python def get_horoscope( self, astrolabe: Astrolabe, target_date: str | None = None, target_time_index: TimeIndexType | None = None, ) -> Horoscope ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------------- | ------------- | -------- | ------- | ------------------------------------------------ | | `astrolabe` | `Astrolabe` | Yes | — | The natal chart | | `target_date` | `str \| None` | No | `None` | Target solar date; today when omitted | | `target_time_index` | `int \| None` | No | `None` | Target hour index; the current hour when omitted | **Return value** `Horoscope`, holding the chart passed in. Details on [the horoscope object](/en/docs/python/horoscope). **Example** ```python chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US") h = Astro().get_horoscope(chart, "2025-6-1", 0) print(h.decadal.heavenly_stem + h.decadal.earthly_branch) print(h.yearly.heavenly_stem + h.yearly.earthly_branch) ``` **Output** ```text gengchen yisi ``` **Edge cases and pitfalls** `chart.horoscope("2025-6-1", 0)` is exactly equivalent and needs no `Astro` instance in hand. `Astro.get_horoscope` exists so that "every entry point on one class" also works as a usage style. *** ## 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. **Signature** ```python def rearranged(self, from_stem: str, from_branch: str) -> Astrolabe ``` This is a method on `Astrolabe`, not on the `Astro` class. **Parameters** | Parameter | Type | Required | Default | Description | | ------------- | ----- | -------- | ------- | ---------------------------------------------------------------------------- | | `from_stem` | `str` | Yes | — | Stem key of the new Soul palace, from the `HeavenlyStem` enum's value set | | `from_branch` | `str` | Yes | — | Branch key of the new Soul palace, from the `EarthlyBranch` enum's value set | **Return value** A new `Astrolabe`. 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, the soul star, 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, the Sui-qian and Jiang-qian gods, and the body star. On the rearranged chart, `patterns()`, 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** ```python chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US") # anchor on the original chart's body palace pillar — equivalent to the earth chart body = next(p for p in chart.palaces if p.is_body_palace) earth = chart.rearranged(body.heavenly_stem_key, body.earthly_branch_key) print("heaven", chart.five_elements_class, "→ earth", earth.five_elements_class) ``` **Output** ```text heaven wood 3rd → earth earth 5th ``` **Edge cases and pitfalls** For the heaven, earth and human charts just chart with `ChartConfig(astro_type="earth")`; both charting entry points support it. `rearranged` exists for anchoring on an arbitrary stem and branch. The body star 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. The soul star is looked up by the Soul palace branch and therefore does update. *** ## Semantic text (to\_text) [#semantic-text-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 `to_dict`/`to_json` (machine structure) and the translated fields (display), it is the third projection of the same object. **Signature** The text projection lives on the objects themselves, not on `Astro`: ```python chart.to_text() # natal chart; str(chart) is equivalent chart.horoscope("2025-1-1", 0).to_text() # horoscope; str(h) is equivalent chart.palace("soul").to_text() # one palace chart.surrounded_palaces("soul").to_text() # surrounded palaces chart.patterns_to_text() # natal patterns ``` **Return value** `str` — sectioned plain text in the chart's own charting language; 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** ```python astro = Astro() chart = astro.by_solar("2000-8-16", 2, "female", language="en-US") print(chart.to_text()[:77]) ``` **Output** ```text === Basic Info === Gender: female Solar Date: 2000-8-16 Lunar Date: 二〇〇〇年七月十七 ``` **Edge cases and pitfalls** The output language follows the chart's `language` and is not set separately. For English text, chart in English. # Astrolabe object (/en/docs/python/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 is a `frozen=True` dataclass holding 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. ```python chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US") ``` The examples on this page all chart in `en-US`, so the display values in the output are the English translations. Charting in another language changes those strings and nothing else — the `*_key` identifiers and the results of every predicate method stay the same. ## Fields [#fields] | Field | Type | Description | | ------------------------------- | ----- | ------------------------------------ | | `gender` | `str` | Translated gender | | `solar_date` | `str` | Solar date, as passed in | | `lunar_date` | `str` | The lunar date written in Chinese | | `chinese_date` | `str` | Display string of the four pillars | | `time` | `str` | Hour name | | `time_range` | `str` | The clock range of that hour | | `sign` | `str` | Zodiac sign | | `zodiac` | `str` | Chinese zodiac animal | | `soul` | `str` | Translated soul star | | `body` | `str` | Translated body star | | `five_elements_class` | `str` | Translated five elements class | | `earthly_branch_of_soul_palace` | `str` | Translated branch of the Soul palace | | `earthly_branch_of_body_palace` | `str` | Translated branch of the body palace | Display fields follow `language`. For predicates use the `*_key` fields in the next group. | Field | Type | Description | | ----------------------------------- | ----- | ----------------------------------- | | `gender_key` | `str` | `"male"` / `"female"` | | `sign_key` | `str` | Zodiac sign key, `aries` … `pisces` | | `zodiac_key` | `str` | Zodiac animal key, `rat` … `pig` | | `soul_key` | `str` | Soul star key | | `body_key` | `str` | Body star key | | `five_elements_class_key` | `str` | Five elements class key | | `earthly_branch_of_soul_palace_key` | `str` | Key of the Soul palace branch | | `earthly_branch_of_body_palace_key` | `str` | Key of the body palace branch | The values correspond one to one with the enums in `x_iztro.enums` and compare directly with `==`. | Field | Type | Description | | ----------- | -------------- | ----------------------------------------------------------------- | | `palaces` | `list[Palace]` | The twelve palaces; index 0 is the Yin palace, 11 the Chou palace | | `raw_dates` | `RawDates` | The structured lunar birth date and the four-pillar keys | 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("soulPalace")`. The twelve palaces are built from the underlying DTO — and their back-references filled in — only on the **first access** to `chart.palaces`. A call that reads chart-level fields such as the dates or the soul and body stars pays none of that conversion cost. Once built they are cached on the instance, so every later access yields the very same objects. `raw_dates` is the data form of the two display strings `lunar_date` and `chinese_date`. Use it for date arithmetic or for table lookups by pillar, instead of parsing the Chinese strings: | Type | Fields | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `RawDates` | `lunar_date: RawLunarDate`, `chinese_date: RawChineseDate` | | `RawLunarDate` | `lunar_year: int`, `lunar_month: int` (1–12), `lunar_day: int`, `is_leap: bool` | | `RawChineseDate` | The raw, unlocalized pillar characters `yearly` / `monthly` / `daily` / `hourly` (each a `tuple[str, str]`), plus the matching keys `yearly_keys` / `monthly_keys` / `daily_keys` / `hourly_keys` | `RawChineseDate` also carries a method `pillar_keys() -> list[tuple[str, str]]` that hands back the four pillar keys in year, month, day, hour order all at once — exactly the input shape of [`utils.translate_chinese_date`](/en/docs/python/util#translate_chinese_date). ```python rd = chart.raw_dates print(rd.lunar_date.lunar_year, rd.lunar_date.lunar_month, rd.lunar_date.lunar_day, rd.lunar_date.is_leap) print(rd.chinese_date.yearly, rd.chinese_date.yearly_keys) print(rd.chinese_date.pillar_keys()) ``` **Output** ```text 2000 7 17 False ('庚', '辰') ('gengHeavenly', 'chenEarthly') [('gengHeavenly', 'chenEarthly'), ('jiaHeavenly', 'shenEarthly'), ('bingHeavenly', 'wuEarthly'), ('gengHeavenly', 'yinEarthly')] ``` The pillar characters stay Chinese on an English chart: they are the raw stem and branch glyphs, not a translated field. For English pillar names run the keys through [`utils.translate_chinese_date`](/en/docs/python/util#translate_chinese_date). | Field | Type | Description | | ------------ | ------------- | ---------------------------------------------------------------- | | `time_index` | `int` | Birth hour index | | `fix_leap` | `bool` | Whether leap-month correction was applied when charting | | `language` | `str` | Output language | | `config` | `ChartConfig` | Charting configuration — the six switches, restored from the DTO | Horoscopes, re-anchoring and prompts restart their computation from these four, so the charting parameters need not be supplied again. `chart.config` is restored from the output DTO and carries only the six switches; the custom mutagen and brightness tables passed in at charting time are not in it. The chart does keep the caller's originals internally, however, so follow-up computations such as `rearranged`, `horoscope` and prompt generation still use those two tables — they are not silently dropped. *** ## 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 "palace of origin" is the one whose stem matches the birth-year stem, marking where matters originate. **Signature** ```python def palace(self, index_or_name: int | PalaceName | str) -> Palace | None ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------- | ------------ | -------- | ------- | ----------------------------------- | | `index_or_name` | `int \| str` | Yes | — | Four spellings, see the table below | | Spelling | Example | Meaning | | ------------------------------------ | -------------------------------- | ---------------------------------------------------------------------- | | Index | `chart.palace(0)` | Palace index 0–11, where 0 is the Yin palace | | Name key | `chart.palace("soulPalace")` | One of the twelve palace-name keys, i.e. the value set of `PalaceName` | | Palace name in the charting language | `chart.palace("soul")` | The palace-name text as translated for the chart's language | | Body palace | `chart.palace("bodyPalace")` | Whichever palace carries the body-palace flag | | Palace of origin | `chart.palace("originalPalace")` | The palace whose stem matches the birth-year stem | **Return value** `Palace | None`. An out-of-range index or a misspelled name returns `None`; the name, body-palace and origin-palace spellings all resolve on any chart as long as they are spelled correctly. **Example** ```python soul = chart.palace("soulPalace") print(soul.name, soul.heavenly_stem + soul.earthly_branch) print("body palace falls in", chart.palace("bodyPalace").name) print("palace of origin is", chart.palace("originalPalace").name) print("the Yin palace is", chart.palace(0).name) ``` **Output** ```text soul renwoo body palace falls in career palace of origin is spouse the Yin palace is wealth ``` **Edge cases and pitfalls** The palace of origin 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 through You walk the ten stems exactly once each; Zi and Chou repeat the stems of Yin and Mao — and it is precisely that repetition that gets them excluded. So the birth-year stem always hits somewhere between Yin and You, and hits exactly once: the palace of origin exists on every chart, and is unique. The body palace likewise always exists. `None` can therefore only come from an out-of-range index or a misspelled name. `chart.palace("soulPalce")` (one `a` short) raises nothing and simply returns `None` — `palace` compares palace by palace rather than looking a table up, and no match means no result. The next `.name` then becomes `AttributeError: 'NoneType' object has no attribute 'name'`, with the crash site some distance from the actual typo. To catch it where it is written, use the enum: `PalaceName.SOUL` has IDE completion. When the name comes from external input, run it through the constructor first, which raises `ValueError` on an illegal value: ```python from x_iztro import PalaceName print(PalaceName("soulPalace")) # a StrEnum: printing it prints its value try: PalaceName("soulPalce") except ValueError as e: print("ValueError:", e) ``` **Output** ```text soulPalace ValueError: 'soulPalce' is not a valid PalaceName ``` The same rule applies to `star()` (a misspelled star name returns `None`) and to `has()` (a misspelled star name returns `False`, because "that key is not in the set"). The enum listing is on [Data tables](/en/docs/python/data#enum-listings). *** ## star / star\_in\_palace [#star--star_in_palace] **Purpose** Find a star by key, or get it together with the palace it sits in. **Signature** ```python def star(self, star: str) -> Star | None def star_in_palace(self, star: str) -> tuple[Star, Palace] | None ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ----- | -------- | ------- | -------------------------------------------------------------------------------------------------------------- | | `star` | `str` | Yes | — | A star key (such as `"ziweiMaj"`), or the star name **in the chart's charting language** (such as `"emperor"`) | **Return value** `None` when the star is not on this chart. `star_in_palace` returns a `(star, palace)` tuple, sparing a further call to `star.palace()`. **Example** ```python ziwei = chart.star("ziweiMaj") print(ziwei.name, "sits in", ziwei.palace().name) print("its opposite palace is", ziwei.opposite_palace().name) print("brightness", ziwei.brightness, "mutagen", ziwei.mutagen) star, palace = chart.star_in_palace("ziweiMaj") print(star.key, palace.name_key) ``` **Output** ```text emperor sits in soul its opposite palace is surface brightness [+3] mutagen None ziweiMaj soulPalace ``` **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_key`. *** ## 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** ```python def surrounded_palaces(self, index_or_name: int | PalaceName | str) -> SurroundedPalaces | None ``` **Parameters** Same as `palace`; all four spellings are supported. **Return value** `SurroundedPalaces | None`, holding the four `Palace`s `target` / `opposite` / `wealth` / `career`. Returns `None` when the palace cannot be located (out-of-range index or misspelled name). Its predicates are on [Surrounded palaces](/en/docs/python/surpalaces). **Example** ```python sp = chart.surrounded_palaces("soulPalace") print(sp.target.name, sp.opposite.name, sp.wealth.name, sp.career.name) print("Ziwei in the surrounded set:", sp.have(["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 chart, skipping the step of fetching the set first. **Signature** ```python def is_surrounded(self, index_or_name: int | PalaceName | str, stars: list[str]) -> bool def is_surrounded_one_of(self, index_or_name: int | PalaceName | str, stars: list[str]) -> bool def not_surrounded(self, index_or_name: int | PalaceName | str, stars: list[str]) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------- | ------------ | -------- | ------- | ----------------------------------- | | `index_or_name` | `int \| str` | Yes | — | Located the same way as in `palace` | | `stars` | `list[str]` | 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** ```python print(chart.is_surrounded("soulPalace", ["ziweiMaj", "tianxiangMaj"])) print(chart.is_surrounded_one_of("soulPalace", ["qishaMaj", "pojunMaj"])) print(chart.not_surrounded("soulPalace", ["huoxingMin"])) ``` **Output** ```text True False True ``` The Soul palace holds only Ziwei, while Tianxiang sits in the Wealth palace, one of the trine — hence the first line is true. Neither Qisha nor Pojun is in any of the four, hence the second is false. **Edge cases and pitfalls** With an empty `stars` list, `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] **Purpose** Compute the horoscope for a target date, starting from this chart. **Signature** ```python def horoscope( self, target_date: str | None = None, target_time_index: int | None = None, ) -> Horoscope ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------------- | ------------- | -------- | ------- | ------------------------------------------------ | | `target_date` | `str \| None` | No | `None` | Target solar date; today when omitted | | `target_time_index` | `int \| None` | No | `None` | Target hour index; the current hour when omitted | **Return value** `Horoscope` — a horoscope object holding this chart, so palace lookups across the six scopes need not be passed the astrolabe again. Details on [the horoscope object](/en/docs/python/horoscope). **Example** ```python h = chart.horoscope("2025-6-1", 0) print("decadal", h.decadal.heavenly_stem + h.decadal.earthly_branch) print("yearly ", h.yearly.heavenly_stem + h.yearly.earthly_branch) # both parameters can be omitted for right now now = chart.horoscope() ``` **Output** ```text decadal gengchen yearly yisi ``` *** ## to\_text [#to_text] **Purpose** The chart's semantic text: a complete description for language models and people; `str(chart)` is equivalent. **Signature** ```python def to_text(self) -> str ``` **Return value** `str` — sectioned plain text in the charting language: basic info, the twelve palaces, and the pattern hits. The full format is on [Semantic text](/en/docs/guide/guides/to-text). **Example** ```python print(chart.to_text()[:77]) ``` **Output** ```text === Basic Info === Gender: female Solar Date: 2000-8-16 Lunar Date: 二〇〇〇年七月十七 ``` For single-palace and surrounded-palace text see `palace(...).to_text()` and `surrounded_palaces(...).to_text()`; for pattern text see `patterns_to_text` on [Patterns](/en/docs/python/patterns). *** ## to\_dict / to\_json [#to_dict--to_json] **Purpose** Export the chart as JSON matching the field contract of JS iztro. **Signature** ```python def to_dict(self) -> dict[str, Any] def to_json(self, **kwargs: Any) -> str ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ---- | -------- | ------- | ---------------------------------------------------------------------------- | | `kwargs` | — | No | — | `to_json` only: forwarded to `json.dumps`, e.g. `indent=2`, `sort_keys=True` | `to_json` defaults to `ensure_ascii=False`, so non-ASCII characters land in the output directly rather than as `\uXXXX`. **Return value** `to_dict` returns a **deep copy** of the underlying DTO — camelCase keys, values translated for the charting language, plus the `*Key` language-independent identifiers and the charting context. Mutating it does not affect the chart. `to_json` returns the same data as a JSON string, matching iztro's `JSON.stringify(astrolabe)` key for key and value for value. **Example** ```python d = chart.to_dict() print(d["solarDate"], d["palaces"][4]["nameKey"]) print(d["config"]["yearDivide"], d["genderKey"], d["timeIndex"]) import json print(json.dumps({k: d[k] for k in ("gender", "solarDate", "lunarDate")}, ensure_ascii=False)) print(len(chart.to_json()) > 10000, chart.to_json()[:1]) ``` **Output** ```text 2000-8-16 soulPalace normal female 2 {"gender": "female", "solarDate": "2000-8-16", "lunarDate": "二〇〇〇年七月十七"} True { ``` When the native extension converts the underlying DTO into a Python `dict` the keys come out sorted by name, so the top-level keys of `to_dict()` / `to_json()` run `body`, `bodyKey`, `chineseDate`, … rather than in iztro's declaration order. The names and values correspond one for one; only the arrangement differs. For a fixed order, pick out the keys you need yourself. **Edge cases and pitfalls** `Astrolabe`, `Palace` and `Star` are all dataclasses, but palaces and stars each hold a reference back to the chart (`_astrolabe` / `_palace`). `dataclasses.asdict(chart)` follows that back-reference into infinite recursion and ends in `RecursionError`. Always export through `to_dict()` / `to_json()` — they take the underlying DTO directly, so they neither recurse nor lose fields. `config` echoes only the six switches. The custom mutagen and brightness tables passed in at charting time are **input** rather than result and do not enter the DTO — matching the field contract of JS iztro. To record which tables were used, keep your `ChartConfig` on your own call site. `Horoscope` carries a pair of methods by the same names and the same shape; see [the horoscope object](/en/docs/python/horoscope#to_dict--to_json). # Palace object (/en/docs/python/palace) The fields of Palace plus every star predicate, empty-palace check and flying-star method. Palaces are where most Zi Wei analysis happens. `chart.palace(...)` returns a `Palace`, which both holds the palace's data and can trace back to its astrolabe, its opposite palace and its surrounded set. ```python soul = chart.palace("soulPalace") ``` The examples on this page all chart in `en-US`, so the display values in the output are the English translations. ## Fields [#fields] | Field | Type | Description | | --------------------------------------- | ------------ | --------------------------------------------------------------------------------------------------- | | `index` | `int` | Palace index 0–11, where 0 is the Yin palace | | `name` / `name_key` | `str` | Translated palace name / its key | | `is_body_palace` | `bool` | Whether this is the body palace | | `is_original_palace` | `bool` | Whether this is the palace of origin (stem equal to the year stem, and not the Zi or Chou palace) | | `heavenly_stem` / `heavenly_stem_key` | `str` | Palace stem, which determines the mutagens this palace flies out | | `earthly_branch` / `earthly_branch_key` | `str` | Palace branch, fixed by the index: 0 is yin, 11 is chou | | `major_stars` | `list[Star]` | Whichever of the fourteen major stars fall here, in placement order | | `minor_stars` | `list[Star]` | Whichever of the fourteen minor stars fall here | | `adjective_stars` | `list[Star]` | Adjective stars | | `changsheng12` / `changsheng12_key` | `str` | The Changsheng god of this palace, exactly one per palace | | `boshi12` / `boshi12_key` | `str` | The Boshi god | | `jiangqian12` / `jiangqian12_key` | `str` | The Jiang-qian god | | `suiqian12` / `suiqian12_key` | `str` | The Sui-qian god | | `decadal` | `Decadal` | The decadal: age range plus stem and branch | | `ages` | `list[int]` | Nominal ages at which the age scope passes through this palace | | `mutagen_star_keys` | `list[str]` | The keys of the four stars transformed by this palace's **own stem**, in the order lu, quan, ke, ji | 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. It is computed from **the mutagen table in effect at charting time** — a custom table (`ChartConfig(mutagens=...)`) shows up here, and the flying-star methods read exactly this field. The birth-year mutagen is instead a mark stamped on a star's own `mutagen_key` field; the two are not the same thing. *** ## 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** ```python def has(self, stars: list[str]) -> bool def not_have(self, stars: list[str]) -> bool def has_one_of(self, stars: list[str]) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ----------- | -------- | ------- | ------------------------------------------------------------------------------------- | | `stars` | `list[str]` | Yes | — | A list of star keys; star names **in the chart's charting language** are accepted too | **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** ```python from x_iztro import MajorStar, MinorStar soul = chart.palace("soulPalace") print(soul.has([MajorStar.ZIWEI, MajorStar.TIANXIANG])) print(soul.has_one_of([MajorStar.QISHA, MajorStar.ZIWEI])) print(soul.not_have([MinorStar.HUOXING, MinorStar.LINGXING])) ``` **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`. What gets compared is the set of "all keys and translated names of the stars in this palace", and no match means not present — `soul.has(["ziweiMj"])` returns `False` without raising, which looks exactly like "the Soul palace has no Ziwei". A star name from external input can be caught on the spot by running it through the enum constructor first: `MajorStar("ziweiMj")` raises `ValueError`. For names hard-coded in your source, use the enum members and let the IDE complete them. *** ## 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** ```python def has_mutagen(self, mutagen: Mutagen) -> bool def not_have_mutagen(self, mutagen: Mutagen) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ----- | -------- | ------- | ------------------------------------------------------- | | `mutagen` | `str` | Yes | — | `"sihuaLu"` / `"sihuaQuan"` / `"sihuaKe"` / `"sihuaJi"` | **Return value** `bool`. Only `major_stars` and `minor_stars` are scanned — **adjective stars are not considered**. **Example** ```python from x_iztro import Mutagen children = chart.palace("childrenPalace") print("Children palace has lu:", children.has_mutagen(Mutagen.LU)) print("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). Birth-year mutagens only ever land on the fourteen major stars and a few minor stars, so on a real chart the two readings usually agree anyway. *** ## is\_empty [#is_empty] **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** ```python def is_empty(self, exclude_stars: list[str] | None = None) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------- | ------------------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------- | | `exclude_stars` | `list[str] \| None` | No | `None` | 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** ```python parents = chart.palace("parentsPalace") print("Parents palace empty:", parents.is_empty()) print("Friends palace empty:", chart.palace("friendsPalace").is_empty()) # the Parents palace has no major star but does hold Tuoluo — counting Tuoluo makes it non-empty print("Parents palace counting Tuoluo:", parents.is_empty(["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. Without `exclude_stars` only `major_stars` is checked. A palace packed with minor and adjective stars but no major star is still empty. *** ## 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** ```python def flies_to(self, target: Palace | int | str, mutagens: Mutagen | list[Mutagen]) -> bool def flies_one_of_to(self, target: Palace | int | str, mutagens: Mutagen | list[Mutagen]) -> bool def not_fly_to(self, target: Palace | int | str, mutagens: Mutagen | list[Mutagen]) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ---------- | ---------------------- | -------- | ------- | ------------------------------------------------------------------------------------------------- | | `target` | `Palace \| int \| str` | Yes | — | The target palace: a palace object, an index, a palace name, `"bodyPalace"` or `"originalPalace"` | | `mutagens` | `str \| list[str]` | Yes | — | The mutagens to check, one or a list | **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** ```python from x_iztro import Mutagen, PalaceName soul = chart.palace("soulPalace") print("Soul flies lu into Wealth:", soul.flies_to(PalaceName.WEALTH, Mutagen.LU)) print("Soul flies lu or ji into Surface:", soul.flies_one_of_to(PalaceName.SURFACE, [Mutagen.LU, Mutagen.JI])) print("Soul does not fly quan into Children:", soul.not_fly_to(PalaceName.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` list, `flies_to` returns `False` while `flies_one_of_to` and `not_fly_to` return `True`. That runs against the intuition that a universal statement is vacuously true on the empty set, but it reproduces iztro's behaviour: `flies_to` first works out which stars to look for and, finding none at all, decides false straight away. Passing an empty list is usually a caller oversight. When `target` is an out-of-range index or a misspelled palace name, all three methods return `False`, including the semantically negative `not_fly_to` — a failed lookup does not amount to "nothing flew in". Once `ChartConfig(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** ```python def self_mutaged(self, mutagens: Mutagen | list[Mutagen]) -> bool def self_mutaged_one_of(self, mutagens: Mutagen | list[Mutagen] | None = None) -> bool def not_self_mutaged(self, mutagens: Mutagen | list[Mutagen] | None = None) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ---------- | -------------------------- | --------------------- | ------- | ----------------------------------------------------------------------------- | | `mutagens` | `str \| list[str] \| None` | Depends on the method | `None` | The mutagens to check; omitting it in the latter two methods 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; omitting the argument checks all four | | `not_self_mutaged` | None of them is self-mutated; omitting the argument checks all four | **Example** ```python career = chart.palace("careerPalace") print("Career self-mutates lu:", career.self_mutaged(Mutagen.LU)) print("Career self-mutates ji:", career.self_mutaged(Mutagen.JI)) print("Career has any self-mutagen:", career.self_mutaged_one_of()) print("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 a missing argument (or an empty list) as "all four mutagens". `self_mutaged` has no such fallback, so an empty list degenerates into "does this palace contain the empty set", which is always `True` — the exact opposite of `flies_to`'s empty-list behaviour. 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** ```python def mutaged_places(self, all_palaces: list[Palace] | None = None) -> list[Palace | None] def mutagen_stars(self, mutagens: Mutagen | list[Mutagen]) -> list[str] ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------- | ---------------------- | -------- | ------- | ------------------------------------------------------- | | `all_palaces` | `list[Palace] \| None` | No | `None` | Usually omitted; the palace already holds its astrolabe | | `mutagens` | `str \| list[str]` | Yes | — | Which mutagen slots to take | **Return value** `mutaged_places` returns a list of length 4 in the order **lu, quan, ke, ji**, with `None` in a slot whose transformed star is not on the chart. `mutagen_stars` returns a list of star keys in the order the mutagens were passed. **Example** ```python soul = chart.palace("soulPalace") for m, place in zip(["lu", "quan", "ke", "ji"], soul.mutaged_places()): print(f"{m} →", place.name if place else "not on this chart") print(soul.mutagen_stars([Mutagen.LU, Mutagen.JI])) ``` **Output** ```text lu → children quan → soul ke → career ji → wealth ['tianliangMaj', 'wuquMaj'] ``` The Soul palace's stem is ren, whose four mutagens are Tianliang to lu, Ziwei to quan, Zuofu to ke and Wuqu to ji; those four stars sit in the Children, Soul, Career and Wealth palaces respectively. Without `all_palaces` the search covers the twelve palaces of the astrolabe this palace belongs to. A palace constructed on its own has neither an astrolabe nor a supplied range, and then the method returns an **empty list** rather than four `None`s. `mutaged_places` always takes all four slots in lu, quan, ke, ji order regardless of arguments; to pick out only some of them, use `mutagen_stars`. *** ## opposite\_palace / surrounded\_palaces / astrolabe [#opposite_palace--surrounded_palaces--astrolabe] **Purpose** Trace from a palace to its opposite palace, its surrounded set and its astrolabe. **Signature** ```python def opposite_palace(self) -> Palace | None def surrounded_palaces(self) -> SurroundedPalaces | None def astrolabe(self) -> Astrolabe | None ``` **Return value** A palace constructed on its own, detached from a chart, returns `None`; a palace obtained from a chart query never does. **Example** ```python soul = chart.palace("soulPalace") print("the opposite of", soul.name, "is", soul.opposite_palace().name) print("malefics in the surrounded set:", soul.surrounded_palaces().have_one_of(["huoxingMin", "lingxingMin"])) print(soul.astrolabe().five_elements_class) ``` **Output** ```text the opposite of soul is surface malefics in the surrounded set: True wood 3rd ``` *** ## to\_text [#to_text] **Purpose** The palace's semantic text, identical to that palace's section in the natal text. **Signature** ```python def to_text(self) -> str ``` **Example** ```python print(chart.palace("soul").to_text()) ``` **Output** ```text --- soul --- Stem-Branch: renwoo Decadal: 3-12 Age Fortune Years: 5, 17, 29, 41, 53, 65, 77, 89, 101, 113 Twelve Gods: weak, dragon, downcast, disastery Major Stars: emperor([+3]) Minor Stars: artist([-3]) Adjective Stars: refined, lucky, intercepted, instigated, considery(Y) ``` A palace constructed detached from a chart has no charting context and raises `ValueError`. The full format is on [Semantic text](/en/docs/guide/guides/to-text). # Star object (/en/docs/python/star-object) The fields of Star, its brightness and mutagen predicates, and tracing back to its palace. A `Star` is one star sitting in a palace, carrying its type, brightness and mutagen mark, and able to trace back to the palace it sits in. ```python ziwei = chart.star("ziweiMaj") ``` The examples on this page all chart in `en-US`, so the display values in the output are the English translations. ## Fields [#fields] | Field | Type | Description | | ---------------- | ------------- | -------------------------------------------------------------------------------------- | | `key` | `str` | Star key, independent of language; use it in predicates | | `name` | `str` | Star name, translated into the charting language | | `type` | `str` | Star type, see below | | `scope` | `str` | Which layer it acts on: `"origin"` for natal stars, the matching scope for scope stars | | `brightness` | `str \| None` | Translated brightness; `None` for stars with no brightness table | | `brightness_key` | `str \| None` | Brightness key | | `mutagen` | `str \| None` | Translated natal mutagen; `None` for stars the birth-year stem did not transform | | `mutagen_key` | `str \| None` | Mutagen key | ### The eight star types [#the-eight-star-types] | Value | Meaning | Typical members | | ----------- | ------------------------ | -------------------------------------------------- | | `major` | The fourteen major stars | Ziwei, Tianfu, Qisha, Pojun | | `soft` | Auspicious stars | Zuofu, Youbi, Wenchang, Wenqu, Tiankui, Tianyue | | `tough` | Malefic stars | Qingyang, Tuoluo, Huoxing, Lingxing, Dikong, Dijie | | `adjective` | Adjective stars | Santai, Bazuo, Tianxing, Tianyao | | `flower` | Peach-blossom stars | Hongluan, Tianxi, Xianchi | | `helper` | Jieshen | Jieshen | | `lucun` | Lucun | Lucun | | `tianma` | Tianma | Tianma | Lucun and Tianma each get a category of their own, because in the traditional division they are neither purely auspicious nor purely malefic and predicates routinely single them out. Only twenty stars have a brightness table — the fourteen major stars plus Wenchang, Wenqu, Huoxing, Lingxing, Qingyang and Tuoluo. Brightness is simply not a concept for the rest, whose `brightness` is `None`. *** ## with\_brightness [#with_brightness] **Purpose** Test whether this star is at one of the given brightness levels. **Zi Wei meaning** Brightness (miao, wang, de, li, ping, bu, xian) describes how strong a star is in its palace. Each star has a fixed value in each of the twelve palaces; at miao or wang its power comes out in full, at xian it is constrained. **Signature** ```python def with_brightness(self, brightness: Brightness | list[Brightness]) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------ | ------------------ | -------- | ------- | ------------------------------------------------------------------ | | `brightness` | `str \| list[str]` | Yes | — | A brightness key or a list of them; with a list, any match is true | **Return value** `bool`. Always false for a star with no brightness. **Example** ```python from x_iztro import Brightness ziwei = chart.star("ziweiMaj") print(ziwei.with_brightness(Brightness.MIAO)) print(ziwei.with_brightness([Brightness.WANG, Brightness.DE])) ``` **Output** ```text True False ``` **Edge cases and pitfalls** A list means "any match", not "all match" — a star has exactly one brightness, so demanding all of them would be permanently false for a list longer than one. *** ## with\_mutagen [#with_mutagen] **Purpose** Test whether this star carries a given natal mutagen. **Zi Wei meaning** Natal mutagens are fixed by the birth-year stem: a given year always sends lu, quan, ke and ji to four particular stars. The mark travels with the star, whichever palace it lands in. **Signature** ```python def with_mutagen(self, mutagen: Mutagen | list[Mutagen]) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ------------------ | -------- | ------- | --------------------------------------------------------------- | | `mutagen` | `str \| list[str]` | Yes | — | A mutagen key or a list of them; with a list, any match is true | **Return value** `bool`. Always false for a star the birth-year stem did not transform. **Example** ```python from x_iztro import Mutagen print("Ziwei takes lu:", chart.star("ziweiMaj").with_mutagen(Mutagen.LU)) print("Taiyang takes lu:", chart.star("taiyangMaj").with_mutagen(Mutagen.LU)) ``` **Output** ```text Ziwei takes lu: False Taiyang takes lu: True ``` This chart's birth-year stem is geng, and geng sends lu to Taiyang, so the mark lands on Taiyang rather than Ziwei. **Edge cases and pitfalls** `with_mutagen` looks at the mark the **birth-year stem** placed on this star; only four stars on a chart carry one. Mutagens flown by palace stems do not show up here — for those use the palace's [`flies_to`](/en/docs/python/palace#flies_to--flies_one_of_to--not_fly_to) family. *** ## palace / opposite\_palace / surrounded\_palaces [#palace--opposite_palace--surrounded_palaces] **Purpose** Trace from a star back to its palace, that palace's opposite, and its surrounded set. **Signature** ```python def palace(self) -> Palace | None def opposite_palace(self) -> Palace | None def surrounded_palaces(self) -> SurroundedPalaces | None ``` **Return value** A star constructed on its own, detached from a chart, returns `None`; a star obtained from a chart query never does. **Example** ```python ziwei = chart.star("ziweiMaj") print(ziwei.palace().name) print(ziwei.opposite_palace().name) print("Tianxiang in the same palace or the trine:", ziwei.surrounded_palaces().have(["tianxiangMaj"])) ``` **Output** ```text soul surface Tianxiang in the same palace or the trine: True ``` **Edge cases and pitfalls** A star appears exactly once on a chart, so `chart.star(key)` has a unique result. Horoscope scope stars are not in the natal chart's star lists; to reach them use the horoscope object's [`palace`](/en/docs/python/horoscope#palace) with a scope argument. # Surrounded palaces (/en/docs/python/surpalaces) The four palaces of SurroundedPalaces and its five predicates. The surrounded set is the most commonly used reading scope in Zi Wei Dou Shu. A matter cannot be read from its own palace alone: the stars of the opposite palace and the two trine palaces bear on it just as much, and only all four together give the full picture. The examples on this page all chart in `en-US`, so the display values in the output are the English translations. ## The four palaces [#the-four-palaces] | Field | Offset | Traditional name | Meaning | | ---------- | ------ | ----------------- | --------------------------------------------- | | `target` | +0 | The palace itself | The matter itself | | `opposite` | +6 | Opposite palace | The facing side; the most immediate influence | | `career` | +4 | Career position | One of the trine | | `wealth` | +8 | Wealth position | One of the trine | All four fields are `Palace` objects, so every method of the [palace object](/en/docs/python/palace) is available on them. `wealth` and `career` mean "the trine positions relative to this palace", not the two fixed palace names among the twelve. Anchored on the Soul palace they happen to land on the Wealth and Career palaces (+8 and +4) — that is where the names come from; anchored elsewhere they are other palaces. ## Three ways to get one [#three-ways-to-get-one] ```python # from the chart sp = chart.surrounded_palaces("soulPalace") # from a palace sp = chart.palace("soulPalace").surrounded_palaces() # from a star (the surrounded set of the palace it sits in) sp = chart.star("ziweiMaj").surrounded_palaces() ``` All three give the same result; pick whichever matches what you already have. *** ## have / not\_have / have\_one\_of [#have--not_have--have_one_of] **Purpose** Test whether the four palaces together hold the given stars. **Zi Wei meaning** A phrase like "Ziwei is in the surrounded set" asks exactly whether a star appears anywhere among these four palaces, without asking which one. **Signature** ```python def have(self, stars: list[str]) -> bool def not_have(self, stars: list[str]) -> bool def have_one_of(self, stars: list[str]) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ----------- | -------- | ------- | ------------------------------------------------------------------------------------- | | `stars` | `list[str]` | Yes | — | A list of star keys; star names **in the chart's charting language** are accepted too | **Return value** | Method | Meaning | | ------------- | ------------------------------------------------------------------------------------ | | `have` | Every star in the list appears among the four palaces (not necessarily the same one) | | `not_have` | No star in the list appears | | `have_one_of` | At least one star in the list appears | **Example** ```python from x_iztro import MajorStar, MinorStar sp = chart.surrounded_palaces("soulPalace") print(sp.have([MajorStar.ZIWEI, MajorStar.TIANXIANG])) print(sp.have_one_of([MajorStar.QISHA, MajorStar.POJUN])) print(sp.not_have([MinorStar.HUOXING])) ``` **Output** ```text True False True ``` Ziwei is in the Soul palace and Tianxiang in the Wealth palace — different palaces, but both within the four, so `have` is true. **Edge cases and pitfalls** `have([A, B])` means "A and B both appear among these four palaces", not that they sit together. For same-palace tests use the palace's [`has`](/en/docs/python/palace#has--not_have--has_one_of). `have` and `not_have` return `True` for an empty list; `have_one_of` returns `False`. *** ## have\_mutagen / not\_have\_mutagen [#have_mutagen--not_have_mutagen] **Purpose** Test whether the four palaces carry a given natal mutagen. **Zi Wei meaning** "Ji is in the surrounded set" means one of these palaces holds a star the birth-year stem sent ji to — a common condition when locating a source of pressure. **Signature** ```python def have_mutagen(self, mutagen: Mutagen) -> bool def not_have_mutagen(self, mutagen: Mutagen) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ----- | -------- | ------- | ----------------------- | | `mutagen` | `str` | Yes | — | One of the mutagen keys | **Return value** `bool`. **Example** ```python from x_iztro import Mutagen sp = chart.surrounded_palaces("soulPalace") print("lu in the surrounded set:", sp.have_mutagen(Mutagen.LU)) print("ji in the surrounded set:", sp.have_mutagen(Mutagen.JI)) print("no ke in the surrounded set:", sp.not_have_mutagen(Mutagen.KE)) ``` **Output** ```text lu in the surrounded set: False ji in the surrounded set: False no ke in the surrounded set: True ``` This chart's natal mutagens land in four palaces: Taiyang with lu in Children, Wuqu with quan in Wealth, Taiyin with ke in Friends, Tiantong with ji in Health. The Soul palace's surrounded set is Soul, Surface, Wealth and Career — only the quan falls inside it, so both the lu and the ji test `False` while a quan test would be `True`. **Edge cases and pitfalls** This looks at the **natal mutagen** marks on stars, unrelated to mutagens flown by palace stems. For those, use the palace's flying-star methods. *** ## to\_text [#to_text] **Purpose** The surrounded palaces' semantic text: one section each for the target palace, its opposite, and the wealth and career positions. **Signature** ```python def to_text(self) -> str ``` **Example** ```python sp = chart.surrounded_palaces("soul") print(sp.to_text().split("\n")[0]) ``` **Output** ```text Target Palace: soul (renwoo) ``` When the target palace is detached from a chart there is no charting context and `ValueError` is raised. The full format is on [Semantic text](/en/docs/guide/guides/to-text). # Horoscope object (/en/docs/python/horoscope) The data structures of the six scopes, plus palace lookups that need not be handed the astrolabe again. A horoscope projects the natal chart onto a point in time. The same chart shows a different palace layout in different years — which is exactly what "the decadal scope has moved to that palace" means. ```python h = chart.horoscope("2025-6-1", 0) ``` `Horoscope` holds the natal chart that produced it, so none of the query methods need the astrolabe passed in again. The trailing `astrolabe=None` parameter on every query method is there for the case where you hold horoscope data but keep the chart elsewhere; day to day you never pass it. The examples on this page all start from an `en-US` natal chart, so the display values in the output are the English translations. ## Fields [#fields] | Field | Type | Description | | --------------------------------------------------- | --------- | ---------------------------------------------- | | `solar_date` | `str` | The **target** solar date, as passed in | | `lunar_date` | `str` | The target date as a Chinese lunar date string | | `decadal` `age` `yearly` `monthly` `daily` `hourly` | See below | The six horoscope scopes | `solar_date` is the target date, not the birth date; the birth date is on the natal chart, reachable as `h.astrolabe().solar_date`. ## The six scopes [#the-six-scopes] | Field | Type | Span | Description | | --------- | ----------------- | --------------- | --------------------------------------------------------------------- | | `decadal` | `HoroscopeItem` | Ten years | The decadal scope; the childhood scope for the years before it begins | | `age` | `AgeItem` | One year | The age scope, moving one palace per nominal year | | `yearly` | `HoroscopeYearly` | One year | The yearly scope, its palace fixed by the year's pillar | | `monthly` | `HoroscopeItem` | One month | The monthly scope | | `daily` | `HoroscopeItem` | One day | The daily scope | | `hourly` | `HoroscopeItem` | One double-hour | The hourly scope | Both advance once per year, but they start differently: the age scope starts from the birth-year branch and steps forward with the nominal age, while the yearly scope simply asks which palace that year's pillar falls in. The two lines are independent, and Zi Wei practice usually reads them together. ### HoroscopeItem [#horoscopeitem] | Field | Type | Description | | --------------------------------------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `index` | `int` | Which palace this scope lands on (a palace index) | | `name` | `str` | Display name of the scope, translated into the output language | | `name_key` | `str` | Scope key: `decadal` / `childhood` (before the decadals begin) / `turn` (age fortune) / `yearly` / `monthly` / `daily` / `hourly`. Predicate on this, never on the translation | | `heavenly_stem` / `heavenly_stem_key` | `str` | Stem of the scope, which determines the mutagens it flies | | `earthly_branch` / `earthly_branch_key` | `str` | Branch of the scope | | `palace_names` / `palace_name_keys` | `list[str]` | The twelve palace names re-derived with this scope's palace as the Soul palace, indexed by palace index | | `mutagen` / `mutagen_star_keys` | `list[str]` | The stars this scope's stem transforms, in the order lu, quan, ke, ji; `mutagen_star_keys` holds the mutated stars' star keys, synonymous with the palace field of the same name | | `stars` | `list[list[Star]] \| None` | The scope stars of this layer; `None` for layers that have none | `AgeItem` and `HoroscopeYearly` both inherit `HoroscopeItem` and each add one field: | Type | Extra field | Description | | ----------------- | -------------------------------- | --------------------------------------- | | `AgeItem` | `nominal_age: int` | The nominal age at that date | | `HoroscopeYearly` | `yearly_dec_star: YearlyDecStar` | The yearly Sui-qian and Jiang-qian gods | ```python class YearlyDecStar: jiangqian12: list[str] # translated yearly Jiang-qian gods, indexed by palace index jiangqian12_keys: list[str] # the matching keys suiqian12: list[str] # translated yearly Sui-qian gods suiqian12_keys: list[str] # the matching keys ``` Because this is inheritance rather than wrapping, the shared fields are reached directly: write `h.yearly.heavenly_stem`, with no `.base` layer as on the Rust side. ```python h = chart.horoscope("2025-6-1", 0) print(h.yearly.heavenly_stem, h.yearly.earthly_branch, h.age.nominal_age) print(h.yearly.yearly_dec_star.suiqian12[:3]) print(h.yearly.yearly_dec_star.jiangqian12_keys[:3]) ``` **Output** ```text yi si 26 ['blessed', 'sorrowing', 'illness'] ['jiesha', 'zhaisha', 'tiansha'] ``` **Example** ```python h = chart.horoscope("2025-6-1", 0) for item in (h.decadal, h.monthly, h.daily, h.hourly): print(f"{item.name} lands on palace {item.index} with pillar {item.heavenly_stem}{item.earthly_branch}") print("age scope nominal age", h.age.nominal_age) print("decadal mutagens", h.decadal.mutagen) ``` **Output** ```text decadal lands on palace 2 with pillar gengchen monthly lands on palace 3 with pillar renwoo daily lands on palace 8 with pillar xinchou hourly lands on palace 8 with pillar wuzi age scope nominal age 26 decadal mutagens ['sun', 'general', 'moon', 'fortunate'] ``` *** ## age\_palace [#age_palace] **Purpose** Get the palace the age scope occupies this year. **Zi Wei meaning** The age scope is a line advancing year by year; whichever palace it lands on becomes the focus for that year. **Signature** ```python def age_palace(self, astrolabe: Astrolabe | None = None) -> Palace | None ``` **Parameters** | Parameter | Type | Required | Default | Description | | ----------- | ------------------- | -------- | ------- | ------------------------------------------------------------ | | `astrolabe` | `Astrolabe \| None` | No | `None` | Usually omitted; the horoscope already holds the natal chart | **Return value** `Palace | None` — a palace on the natal chart. The horoscope already holds the natal chart, so in practice this is never `None`; only a hand-built horoscope object bound to no chart and given no `astrolabe` comes up empty. **Example** ```python h = chart.horoscope("2025-6-1", 0) print(h.age_palace().name) ``` **Output** ```text property ``` *** ## palace [#palace] **Purpose** Get one of the twelve palaces as re-derived under a given horoscope scope. **Zi Wei meaning** Once the decadal scope reaches a palace, the twelve palaces are re-anchored with that palace as the "decadal Soul palace". "The decadal Spouse palace" refers to that re-anchored naming, and it is usually not the same palace as the natal Spouse palace. **Signature** ```python def palace( self, name: PalaceName | str, scope: Scope | ScopeLiteral, astrolabe: Astrolabe | None = None, ) -> Palace | None ``` **Parameters** | Parameter | Type | Required | Default | Description | | ----------- | ------------------- | -------- | ------- | -------------------------------------- | | `name` | `str` | Yes | — | The palace-name key to fetch | | `scope` | `str` | Yes | — | Which scope's twelve palaces to search | | `astrolabe` | `Astrolabe \| None` | No | `None` | Usually omitted | **Return value** `Palace | None` — a palace on the natal chart (the same cell carries different names under different scopes). With `"origin"` as the scope, these are the natal twelve palaces. A misspelled palace name or scope key returns `None` rather than raising. **Example** ```python from x_iztro import PalaceName, Scope h = chart.horoscope("2025-6-1", 0) print("the decadal Soul palace is the natal", h.palace(PalaceName.SOUL, Scope.DECADAL).name) print("the natal Soul palace is", h.palace(PalaceName.SOUL, Scope.ORIGIN).name) ``` **Output** ```text the decadal Soul palace is the natal spouse the natal Soul palace is soul ``` **Edge cases and pitfalls** On the palace object returned by `palace("soulPalace", "decadal")`, `name` is still the **natal palace name** (Spouse in the example), because it is that cell on the natal chart. To see what the cell is called at the decadal layer, read `h.decadal.palace_names[index]`. *** ## surround\_palaces [#surround_palaces] **Purpose** Get the surrounded palaces of a palace under a given horoscope scope. **Signature** ```python def surround_palaces( self, name: PalaceName | str, scope: Scope | ScopeLiteral, astrolabe: Astrolabe | None = None, ) -> SurroundedPalaces | None ``` **Parameters** Same as `palace`. **Return value** `SurroundedPalaces | None`; its predicates are on [Surrounded palaces](/en/docs/python/surpalaces). **Example** ```python h = chart.horoscope("2025-6-1", 0) sp = h.surround_palaces(PalaceName.WEALTH, Scope.YEARLY) print("the surrounded set of the yearly Wealth palace is anchored on the natal", sp.target.name) ``` **Output** ```text the surrounded set of the yearly Wealth palace is anchored on the natal health ``` *** ## has\_horoscope\_stars / has\_one\_of\_horoscope\_stars / not\_have\_horoscope\_stars [#has_horoscope_stars--has_one_of_horoscope_stars--not_have_horoscope_stars] **Purpose** Test whether a palace under a given scope holds the given scope stars. **Zi Wei meaning** Scope stars are a group produced by each horoscope layer: Tiankui, Tianyue, Wenchang, Wenqu, Lucun, Qingyang, Tuoluo, Tianma, Hongluan and Tianxi. They carry different names in different layers — Yunkui and Yunyue at the decadal layer, Liukui and Liuyue at the yearly layer — with the same meaning applied to their own time span. **Signature** ```python def has_horoscope_stars(self, name, scope, stars: list[str], astrolabe=None) -> bool def has_one_of_horoscope_stars(self, name, scope, stars: list[str], astrolabe=None) -> bool def not_have_horoscope_stars(self, name, scope, stars: list[str], astrolabe=None) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ----------- | ------------------- | -------- | ------- | -------------------------------------------------- | | `name` | `str` | Yes | — | The palace-name key under that scope | | `scope` | `str` | Yes | — | The horoscope scope | | `stars` | `list[str]` | Yes | — | Scope star keys, which must use that layer's names | | `astrolabe` | `Astrolabe \| None` | No | `None` | Usually omitted | **Return value** | Method | Meaning | | ---------------------------- | ----------------------- | | `has_horoscope_stars` | All of them are present | | `has_one_of_horoscope_stars` | At least one is present | | `not_have_horoscope_stars` | None is present | **Example** ```python h = chart.horoscope("2025-6-1", 0) print(h.has_horoscope_stars(PalaceName.SOUL, Scope.DECADAL, ["yunlu"])) print(h.has_one_of_horoscope_stars(PalaceName.SOUL, Scope.DECADAL, ["yunlu", "yunyang"])) print(h.not_have_horoscope_stars(PalaceName.SOUL, Scope.DECADAL, ["yuntuo"])) ``` **Output** ```text False False True ``` **Edge cases and pitfalls** The three methods use `scope` plus `name` to locate one cell on the natal chart, but the set of stars compared against is always the **union of the decadal scope stars and the yearly scope stars**, independent of `scope`. So passing `"monthly"` as the scope asks "does this monthly palace cell hold any decadal or yearly scope star?", not anything about the monthly layer's own scope stars — the monthly, daily and hourly layers' stars take no part in this comparison. To read a layer's scope-star distribution, use a field such as `h.monthly.stars`, or [`star.get_horoscope_star`](/en/docs/python/star#get_horoscope_star). The decadal scope stars are named `yunlu`, `yunyang`, …, and the yearly ones `liulu`, `liuyang`, …; the two sets of keys differ. Because the comparison set is always the union of both groups, `yunlu` and `liulu` are both findable under any `scope`, they simply land in different palaces. The per-layer key table is on [Star placement](/en/docs/python/star#get_horoscope_star), and the enum form is `HoroscopeStar`. *** ## has\_horoscope\_mutagen [#has_horoscope_mutagen] **Purpose** Test whether a palace under a given scope carries a mutagen flown by that scope's stem. **Zi Wei meaning** Every horoscope layer has a stem of its own, and it transforms four stars just as the birth-year stem does. A question like "does the decadal lu land in the decadal Wealth palace?" is asking about this. **Signature** ```python def has_horoscope_mutagen(self, name, scope, mutagen: Mutagen, astrolabe=None) -> bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ----------- | ------------------- | -------- | ------- | ------------------------------------ | | `name` | `str` | Yes | — | The palace-name key under that scope | | `scope` | `str` | Yes | — | The horoscope scope | | `mutagen` | `str` | Yes | — | A mutagen key | | `astrolabe` | `Astrolabe \| None` | No | `None` | Usually omitted | **Return value** `bool`. **Example** ```python from x_iztro import Mutagen h = chart.horoscope("2025-6-1", 0) print(h.has_horoscope_mutagen(PalaceName.SOUL, Scope.DECADAL, Mutagen.LU)) print(h.decadal.mutagen) ``` **Output** ```text False ['sun', 'general', 'moon', 'fortunate'] ``` The decadal stem is geng, and geng sends lu to Taiyang, quan to Wuqu, ke to Taiyin and ji to Tiantong. **Edge cases and pitfalls** The natal layer has no "layer stem" — the birth-year mutagens are already stamped on the stars' own `mutagen_key`. `has_horoscope_mutagen(name, "origin", m)` therefore returns `False` outright, which does not mean the natal chart lacks that mutagen. For natal mutagens use the palace's [`has_mutagen`](/en/docs/python/palace#has_mutagen--not_have_mutagen). Only the **major and minor stars** of the target palace are checked; adjective stars are not. *** ## scope\_item / astrolabe [#scope_item--astrolabe] **Purpose** Get the `HoroscopeItem` for a scope key, or get back to the natal chart. **Signature** ```python def scope_item(self, scope: Scope | ScopeLiteral) -> HoroscopeItem | None def astrolabe(self) -> Astrolabe | None ``` **Return value** `scope_item` returns `None` for the scope `"origin"` — the natal chart is not a horoscope layer. **Example** ```python h = chart.horoscope("2025-6-1", 0) print(h.scope_item(Scope.DECADAL).name) print(h.scope_item(Scope.ORIGIN)) print(h.astrolabe().solar_date) ``` **Output** ```text decadal None 2000-8-16 ``` **Edge cases and pitfalls** `scope_item` is for writing generic logic parameterized by scope, which is tidier than a chain of `if scope == ...`. `HoroscopeItem` also carries `palace_index_by_name(name)`, which turns a palace name into a palace index within that layer's twelve palaces, returning `None` when there is no match: ```python h = chart.horoscope("2025-6-1", 0) item = h.scope_item(Scope.DECADAL) print(item.palace_index_by_name(PalaceName.SOUL)) print(item.palace_index_by_name(PalaceName.WEALTH)) print(item.palace_index_by_name("nosuch")) ``` **Output** ```text 2 10 None ``` *** ## to\_text [#to_text] **Purpose** The horoscope's semantic text: a complete description for language models and people; `str(h)` is equivalent. **Signature** ```python def to_text(self) -> str ``` **Return value** `str` — sectioned plain text in the chart's charting language; each scope carries a patterns line and flowing-star lines from its own perspective. The full format is on [Semantic text](/en/docs/guide/guides/to-text). **Example** ```python h = chart.horoscope("2025-1-1", 0) print(h.to_text()[:39]) ``` **Output** ```text === Horoscope === Target Date: 2025-1-1 ``` **Edge cases and pitfalls** A horoscope constructed detached from a chart (outside `horoscope()`) has no charting context and raises `ValueError`. For the pattern hits as text, see `patterns_to_text` on [Patterns](/en/docs/python/patterns). *** ## to\_dict / to\_json [#to_dict--to_json] **Purpose** Export the horoscope as JSON matching the field contract of JS iztro. **Signature** ```python def to_dict(self) -> dict[str, Any] def to_json(self, **kwargs: Any) -> str ``` Shape and usage are the same as [the astrolabe's methods of the same names](/en/docs/python/astrolabe#to_dict--to_json): `to_dict` hands back a deep copy of the underlying DTO and `to_json` a JSON string, defaulting to `ensure_ascii=False`. Do not reach for `dataclasses.asdict` here either — the horoscope holds a reference to the natal chart and would recurse forever. **Example** ```python h = chart.horoscope("2025-6-1", 0) d = h.to_dict() print(d["solarDate"], d["decadal"]["heavenlyStem"], d["age"]["nominalAge"]) print(sorted(d.keys())) ``` **Output** ```text 2025-6-1 geng 26 ['age', 'daily', 'decadal', 'hourly', 'lunarDate', 'monthly', 'solarDate', 'yearly'] ``` # Patterns (/en/docs/python/patterns) Pattern hits on natal and horoscope charts, the PatternConfig readings, the PatternKey enum, and the predicate methods. A pattern (格局) is the recognition of a named star arrangement on a chart. The same 64 rules are judged on natal charts and on horoscope views. For what patterns are and each rule's condition and source, see the [concept page](/en/docs/guide/concepts/patterns). ```python from x_iztro import Astro chart = Astro().by_solar("1985-5-3", 9, "male", language="en-US") hits = chart.patterns() ``` The examples on this page all start from an `en-US` natal chart, so the display values in the output are the English translations. ## Types [#types] `PatternHit`, `PatternStar` and `PatternConfig` are frozen dataclasses; `PatternKey` and `BrightnessSource` are `StrEnum`s. All of them are exported from the `x_iztro` top level. ### PatternHit [#patternhit] | Field | Type | Meaning | | ----------------- | ------------------- | ------------------------------------------------------------------------------------------------------ | | `key` | `str` | Language-independent pattern key; its value domain is `PatternKey` | | `name` | `str` | Pattern name, translated to the chart's language | | `scope` | `str` | The view it was judged in: `"origin"` for natal, otherwise that level | | `palace_index` | `int` | Slot of the palace where the pattern formed (0-11, Yin palace is 0) | | `palace_name` | `str` | That palace's name **in this view** | | `palace_name_key` | `str` | The palace key; its value domain is `PalaceName` | | `variant` | `str \| None` | Which reading matched; `None` for single-reading patterns | | `broken` | `bool` | Whether the "spoiled by malefics" condition fired. The hit is reported either way; this is only a flag | | `stars` | `list[PatternStar]` | The stars evidencing the pattern, with their palaces | Three methods: | Method | Meaning | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `is_(key)` | Whether this is the given pattern; takes a `PatternKey` or a string | | `in_palace(name)` | Whether the forming palace is the given palace in this view; takes a `PalaceName`, a palace key, or the name in the chart's language | | `to_dict()` | The raw DTO the binding returned (camelCase keys) | ### PatternStar [#patternstar] | Field | Type | Meaning | | ------------------------------- | ------------- | --------------------------------------------------------------------------------- | | `key` | `str` | Language-independent star key | | `name` | `str` | Star name, translated to the chart's language | | `palace_index` | `int` | The slot the star **actually occupies** (when borrowed, not the borrowing palace) | | `brightness` / `brightness_key` | `str \| None` | Brightness display text and key; `None` when the star has none | | `mutagen` / `mutagen_key` | `str \| None` | The mutagen in this view, and its key; `None` when there is none | Two methods, `has_brightness(b)` and `has_mutagen(m)`, both comparing keys and therefore independent of the chart's language. ### PatternConfig [#patternconfig] The reading switches. Anything that is merely a second *form* of the same pattern goes through `PatternHit.variant`; only data readings that change the **finding of fact itself** live here, which is why there are just three fields. ```python from x_iztro import PatternConfig, BrightnessSource PatternConfig( brightness_source=BrightnessSource.TABLE, # default borrow=True, # default flow_stars=True, # default ) ``` | Field | Default | Effect | | ------------------- | ------------------------ | -------------------------------------------------------------------------- | | `brightness_source` | `BrightnessSource.TABLE` | Basis for Sun and Moon brightness | | `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 | `BrightnessSource` has two members: `TABLE` follows the chart's brightness table (Miao and Wang bright, Xian and Bu dim — matching iztro value for value), `POSITIONAL` follows the traditional placement (Sun bright Yin–Wu, dim You–Chou; Moon bright You–Chou, dim Mao–Wei). The trade-off is explained on the [concept page](/en/docs/guide/concepts/patterns#which-table-decides-sun-and-moon-brightness). `PatternConfig` is a frozen dataclass; change one field with `dataclasses.replace`, or just build a new one — all three fields have defaults, so name only the one you are changing. ### PatternKey [#patternkey] The language-independent key of each of the 64 patterns, as a `StrEnum`, so it compares directly against `PatternHit.key`: ```python from x_iztro import PatternKey print(len(list(PatternKey))) print(PatternKey.SHA_PO_LANG) print(PatternKey("sha_po_lang").name) ``` ```text 64 sha_po_lang SHA_PO_LANG ``` *** ## patterns [#patterns] **Purpose** Every pattern hit on the natal chart. **In Zi Wei terms** Lists every named star arrangement that holds on this chart, together with the palace it formed in and the stars that evidence it. **Signature** ```python def patterns(self, config: PatternConfig | None = None) -> list[PatternHit] ``` **Parameters** | Parameter | Type | Required | Default | Meaning | | --------- | ----------------------- | -------- | ------- | --------------------------------- | | `config` | `PatternConfig \| None` | no | `None` | The reading; omit for the default | **Returns** `list[PatternHit]` in the source page's entry order; an empty list when nothing holds. The two transit patterns (禄衰马困 `lu_shuai_ma_kun`, 风云际会 `feng_yun_ji_hui`) never appear on a natal chart. **Example** ```python chart = Astro().by_solar("1985-5-3", 9, "male", language="en-US") for hit in chart.patterns(): print(f"{hit.name} {hit.palace_index} {hit.palace_name} broken={hit.broken}") ``` **Output** ```text General and Wolf Together 11 surface broken=False Empress and Minister Facing the Palace 5 soul broken=False Marshal, Rebel and Wolf 11 surface broken=False Money and Horse Galloping Together 5 soul broken=False Officer and Helper Flanking Life 5 soul broken=False Literary Nobility and Brilliance 11 surface broken=False Literary Stars Facing Life 5 soul broken=True Literary Stars in Hidden Support 5 soul broken=False Literary Stars in Hidden Support 5 soul broken=False ``` Taking one hit and reading its evidence: ```python from x_iztro import PatternKey, PalaceName hit = next(h for h in chart.patterns() if h.is_(PatternKey.FU_XIANG_CHAO_YUAN)) print(hit.name, hit.variant, hit.in_palace(PalaceName.SOUL)) for s in hit.stars: print(" ", s.name, s.palace_index, s.brightness, s.brightness_key) ``` ```text Empress and Minister Facing the Palace soul_empty True empress 9 [+1] de minister 1 [-3] xian ``` Judging under an explicit reading: ```python from x_iztro import 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'] ``` **Edges and traps** Most patterns form at the Soul palace, but "Body-or-Soul" patterns (武贪同行 `wu_tan_tong_xing`, 杀破狼 `sha_po_lang`, 石中隐玉 `shi_zhong_yin_yu` and others) are judged at both, and `palace_index` records whichever matched — if both match, two hits come back. 禄马交驰 `lu_ma_jiao_chi` goes further: it is reported for any palace that qualifies, so one chart may produce several hits. On the chart above the Body palace sits on the Surface palace, which is why three of the hits record it. When an empty palace borrows the opposite palace's majors, `PatternStar.palace_index` records the palace the star actually occupies (the opposite one), not the borrowing palace. For where the pattern formed, read `PatternHit.palace_index`. `hit.is_(PatternKey.SHA_PO_LANG)` gives the same answer whatever language the chart was rendered in; `hit.name == "杀破狼"` only holds on a Chinese chart. The same goes for stars and palaces: use `key` and `palace_name_key` rather than `name` and `palace_name`. *** ## Horoscope.patterns [#horoscopepatterns] **Purpose** Pattern hits in the view of one horoscope level. **In Zi Wei terms** Takes that level's palace as the Soul palace, merges in that level's flowing stars and mutagens, and runs every rule again. This is how "if the natal chart has the arrangement and the decadal then arrives at it, its benefit is enjoyed" is computed. **Signature** ```python def patterns( self, scope: Scope | ScopeLiteral, config: PatternConfig | None = None, astrolabe: Astrolabe | None = None, ) -> list[PatternHit] ``` **Parameters** | Parameter | Type | Required | Default | Meaning | | ----------- | ----------------------- | -------- | ------- | ------------------------------------------------------------ | | `scope` | `Scope \| str` | yes | — | The level whose view to judge in | | `config` | `PatternConfig \| None` | no | `None` | The reading | | `astrolabe` | `Astrolabe \| None` | no | `None` | The chart; omitted, it uses the one this horoscope came from | **Returns** `list[PatternHit]`, each carrying the level passed in as its `scope`. Passing `Scope.ORIGIN` gives exactly what `patterns()` on the astrolabe gives. **Raises** `ValueError` — when no `astrolabe` was passed and this horoscope did not come from a chart. A horoscope obtained through `chart.horoscope(...)` never hits this. **Example** ```python from x_iztro import Scope chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US") h = chart.horoscope("2025-6-1", 0) for hit in h.patterns(Scope.DECADAL): print(hit.name, hit.scope, hit.variant) print(h.patterns("origin") == chart.patterns()) ``` **Output** ```text Marshal, Rebel and Wolf decadal None Meeting of Wind and Cloud decadal None Meeting of Wind and Cloud decadal yearly True ``` 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. **Edges and traps** The Body palace is a natal concept. In a horoscope view, "Body-or-Soul" patterns are judged only at that level's Soul palace. 禄衰马困 `lu_shuai_ma_kun` is judged at whichever level the current view is (decadal view judges the decadal, yearly view the year); when the limit's Soul-palace trine set also holds Qisha (the classical strict reading holds too), `variant` is `"qisha"`. 风云际会 `feng_yun_ji_hui` compares two limits across levels, so it is judged once, in the `Scope.DECADAL` view. Its `variant` records both the pair of limits and how strictly they "meet" Lu and the Horse: a decadal + minor-limit hit is `None` (trine-set meeting) or `"same_palace"` (the strict reading — both limits' Soul palaces hold the stars in-palace); a decadal + annual hit is `"yearly"` or `"yearly_same_palace"`. Each pair reports one hit, two at most. Under the default reading a flowing Lucun reads as Lucun, a flowing Wenchang as Wenchang, and so on. To turn that off, pass `PatternConfig(flow_stars=False)`. `patterns()` does not compute incrementally on an existing chart object; it sends the charting context (birth date, hour, gender, language, config) back into the core and starts a fresh judgement. So it neither mutates the chart nor caches — hold onto the returned list yourself if you are calling it in a loop. *** ## to\_dict [#to_dict] **Purpose** The raw DTO the binding returned: camelCase keys, values translated into the chart's language, alongside the language-independent keys. Use it to store hits verbatim, ship them to a frontend, or feed them to a model. **Signature** ```python def to_dict(self) -> dict[str, Any] ``` **Example** ```python import json chart = Astro().by_solar("1985-5-3", 9, "male", language="en-US") hit = next(h for h in chart.patterns() if h.is_(PatternKey.FU_XIANG_CHAO_YUAN)) print(json.dumps(hit.to_dict(), ensure_ascii=False, indent=2)) ``` **Output** ```json { "broken": false, "key": "fu_xiang_chao_yuan", "name": "Empress and Minister Facing the Palace", "palaceIndex": 5, "palaceName": "soul", "palaceNameKey": "soulPalace", "scope": "origin", "stars": [ { "brightness": "[+1]", "brightnessKey": "de", "key": "tianfuMaj", "name": "empress", "palaceIndex": 9 }, { "brightness": "[-3]", "brightnessKey": "xian", "key": "tianxiangMaj", "name": "minister", "palaceIndex": 1 } ], "variant": "soul_empty" } ``` Optional keys with no value (`variant`, brightness, mutagen) are omitted from the DTO entirely; they never come back as `null`. *** ## patterns\_to\_text [#patterns_to_text] **Purpose** The pattern hits as semantic text, one per line: pattern name, landing palace, forming stars, with broken patterns marked `[Broken]`. **Signature** ```python def patterns_to_text(self, config: PatternConfig | None = None) -> str # Astrolabe def patterns_to_text(self, scope, config=None, astrolabe=None) -> str # Horoscope ``` The same judgment as `patterns` (including re-anchoring context and judging criteria); the horoscope version writes palace names as re-laid out at that scope. **Example** ```python print(chart.patterns_to_text(), end="") ``` **Output** ```text - Empress and Minister Facing the Palace(soul): empress([+3]), minister([+3]) ``` The chart's and the horoscope's `to_text` each already carry a patterns section; the standalone call suits cases that want only the pattern summary. # Lightweight queries (/en/docs/python/query) The Chinese zodiac animal, zodiac sign and Soul palace major stars, without charting the whole thing. Some questions do not need a whole chart. These five functions each run only as far as necessary and return; their results always agree with the corresponding fields of a full chart, because they go through the same core logic. The examples on this page all query in `en-US`, so the display values in the output are the English translations. ```python from x_iztro import query ``` *** ## get\_zodiac\_by\_solar\_date [#get_zodiac_by_solar_date] **Purpose** Get the Chinese zodiac animal from a solar date. **Zi Wei meaning** The zodiac animal is determined by the **year branch**, and when the year branch turns over is governed by `year_divide`. For someone born between lunar New Year and the Beginning of Spring, the two settings give different animals — not a defect, a difference of school. **Signature** ```python def get_zodiac_by_solar_date( solar_date: str, language: LanguageType = "zh-CN", config: ChartConfig | None = None, ) -> str ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------ | --------------------- | -------- | --------- | ------------------------------------- | | `solar_date` | `str` | Yes | — | Solar date in `YYYY-M-D` | | `language` | `str` | No | `"zh-CN"` | Output language | | `config` | `ChartConfig \| None` | No | `None` | Only `year_divide` affects the result | **Return value** `str` — the animal name translated into the language. **Example** ```python print(query.get_zodiac_by_solar_date("2000-8-16", language="en-US")) ``` **Output** ```text dragon ``` **Edge cases and pitfalls** By default the year turns over at lunar New Year. Switch to `ChartConfig(year_divide="exact")` and it turns over at the Beginning of Spring, so people born from late January to early February can get a different animal. *** ## get\_sign\_by\_solar\_date / get\_sign\_by\_lunar\_date [#get_sign_by_solar_date--get_sign_by_lunar_date] **Purpose** Get the zodiac sign. **Zi Wei meaning** The zodiac sign is a Western astrology concept determined solely by the solar date, unrelated to the Zi Wei algorithm. The lunar version converts to solar first, so both give the same result for the same day. **Signature** ```python def get_sign_by_solar_date(solar_date: str, language: LanguageType = "zh-CN") -> str def get_sign_by_lunar_date( lunar_date: str, is_leap_month: bool = False, language: LanguageType = "zh-CN", ) -> str ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------------------- | ------ | -------- | --------- | ------------------------------------------------------ | | `solar_date` / `lunar_date` | `str` | Yes | — | The date in `YYYY-M-D` | | `is_leap_month` | `bool` | No | `False` | Lunar version only: whether that month is a leap month | | `language` | `str` | No | `"zh-CN"` | Output language | There is no `config` parameter — zodiac signs are unaffected by any setting. **Return value** `str`. **Example** ```python print(query.get_sign_by_solar_date("2000-8-16", language="en-US")) print(query.get_sign_by_lunar_date("2000-7-17", language="en-US")) ``` **Output** ```text leo leo ``` *** ## get\_major\_star\_by\_solar\_date / get\_major\_star\_by\_lunar\_date [#get_major_star_by_solar_date--get_major_star_by_lunar_date] **Purpose** Get just the Soul palace's major stars, without charting the whole thing. **Zi Wei meaning** The major stars of the Soul palace are the single most commonly asked item in Zi Wei Dou Shu. When the Soul palace is empty, convention borrows the major stars of the opposite palace, and this function already handles that step. **Signature** ```python def get_major_star_by_solar_date( solar_date: str, time_index: TimeIndexType, *, fix_leap: bool = True, language: LanguageType = "zh-CN", config: ChartConfig | None = None, ) -> str def get_major_star_by_lunar_date( lunar_date: str, time_index: TimeIndexType, *, is_leap_month: bool = False, fix_leap: bool = True, language: LanguageType = "zh-CN", config: ChartConfig | None = None, ) -> str ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------------------- | --------------------- | -------- | --------- | -------------------------------------------------------------------------------------------------------- | | `solar_date` / `lunar_date` | `str` | Yes | — | The date | | `time_index` | `int` | Yes | — | Hour index 0–12; the Soul palace is fixed jointly by month and hour. Everything after it is keyword-only | | `is_leap_month` | `bool` | No | `False` | Lunar version only | | `fix_leap` | `bool` | No | `True` | Whether to correct for leap months | | `language` | `str` | No | `"zh-CN"` | Output language | | `config` | `ChartConfig \| None` | No | `None` | Charting configuration | **Return value** `str` — several major stars separated by commas; the opposite palace's major stars when the Soul palace is empty. **Example** ```python print(query.get_major_star_by_solar_date("2000-8-16", 2, language="en-US")) print(query.get_major_star_by_solar_date("2000-8-16", 2, language="zh-CN")) ``` **Output** ```text emperor 紫微 ``` **Edge cases and pitfalls** The Soul palace is located jointly from the lunar month and the birth hour, so `time_index` is required. Knowing only the date and not the hour, Zi Wei Dou Shu cannot fix a Soul palace. The return value is a translated string that changes with the language. For programmatic checks use `get_major_star_keys_by_solar_date` / `get_major_star_keys_by_lunar_date` below, or chart the whole thing and compare the `key` of the `major_stars`. *** ## get\_major\_star\_keys\_by\_solar\_date / get\_major\_star\_keys\_by\_lunar\_date [#get_major_star_keys_by_solar_date--get_major_star_keys_by_lunar_date] **Purpose** The Soul palace's major stars as language-independent keys — the key form of the two functions above, for programmatic checks. **Signature** ```python def get_major_star_keys_by_solar_date( solar_date: str, time_index: TimeIndexType, *, fix_leap: bool = True, config: ChartConfig | None = None, ) -> list[str] def get_major_star_keys_by_lunar_date( lunar_date: str, time_index: TimeIndexType, *, is_leap_month: bool = False, fix_leap: bool = True, config: ChartConfig | None = None, ) -> list[str] ``` **Return value** `list[str]` — keys from the `MajorStar` enum's domain (e.g. `"ziweiMaj"`); an empty Soul palace borrows its opposite's major stars just the same. Keys are language-independent, so these functions **take no `language`**. **Example** ```python print(query.get_major_star_keys_by_solar_date("2000-8-16", 2)) ``` **Output** ```text ['ziweiMaj'] ``` # Utilities (/en/docs/python/util) Index arithmetic, brightness and mutagen lookups, Soul and body palace derivation, decadal and age scopes, and the four-pillar display string. These functions are the parts the charting algorithm is assembled from. They come in handy when you implement Zi Wei logic yourself or want to double-check a step of the derivation; everyday charting does not call them directly. ```python from x_iztro import utils ``` Every key in the parameters and return values is language-independent and interoperates directly with the `*_key` fields on a chart. Functions that return a structure hand back a **named dataclass** whose fields are read as attributes; functions that return a single key hand back an enum member (a `StrEnum`, directly comparable with the equivalent string). *** ## fix\_index [#fix_index] **Purpose** Constrain any integer to the cyclic range `0..max`. **Zi Wei meaning** The twelve palaces form a ring: one step past the Chou palace (index 11) is back to the Yin palace (index 0). Every "count n forward, count n backward" derivation relies on this wrapping. **Signature** ```python def fix_index(index: int, max: int = 12) -> int ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ----- | -------- | ------- | ----------------------------------- | | `index` | `int` | Yes | — | The index to fix, possibly negative | | `max` | `int` | No | `12` | Cycle length; use 10 for stems | **Return value** `int`, landing in `0..max` — including 0, **excluding `max`** itself. **Example** ```python print(utils.fix_index(-1), utils.fix_index(13)) ``` **Output** ```text 11 1 ``` **Edge cases and pitfalls** Negatives wrap by mathematical modulo (-1 → 11) rather than clamping to 0. A `max` of 0 raises `ZeroDivisionError`; the caller guarantees it is positive — on a chart the usage is fixed at 12 or 10. *** ## earthly\_branch\_to\_palace\_index [#earthly_branch_to_palace_index] **Purpose** Convert an earthly branch to a palace index. **Zi Wei meaning** The twelve palaces start from the **Yin palace** while the natural order of the branches starts from **zi**, putting them two positions apart. This function handles that conversion: yin → 0, mao → 1, …, zi → 10, chou → 11. **Signature** ```python def earthly_branch_to_palace_index(branch: EarthlyBranch | str) -> int ``` **Return value** `int`, 0–11. **Example** ```python from x_iztro import EarthlyBranch print(utils.earthly_branch_to_palace_index(EarthlyBranch.YIN)) print(utils.earthly_branch_to_palace_index(EarthlyBranch.ZI)) ``` **Output** ```text 0 10 ``` *** ## time\_to\_index [#time_to_index] **Purpose** Convert a clock hour to an hour index. **Zi Wei meaning** A day holds twelve double-hours of two hours each, but the Zi hour straddles midnight and splits into the early Zi hour (0) and the late Zi hour (12), giving 13 index values. **Signature** ```python def time_to_index(hour: int) -> int ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ----- | -------- | ------- | -------------------- | | `hour` | `int` | Yes | — | The clock hour, 0–23 | **Return value** `int`, 0–12. **Example** ```python print(utils.time_to_index(0), utils.time_to_index(4), utils.time_to_index(23)) ``` **Output** ```text 0 2 12 ``` Midnight is the early Zi hour, 4 o'clock the Tiger hour, 23 o'clock the late Zi hour. When you are unsure of the hour index while charting, convert with this function. *** ## get\_age\_index [#get_age_index] **Purpose** Get the starting palace index of the age scope from the birth-year branch. **Zi Wei meaning** The age scope starts from a fixed palace and steps forward with the nominal age. The starting palace is set by the trine group of the birth-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. **Signature** ```python def get_age_index(branch: EarthlyBranch | str) -> int ``` **Return value** `int`, 0–11. **Example** ```python print(utils.get_age_index("chenEarthly")) ``` **Output** ```text 8 ``` A chen year belongs to the shen/zi/chen group, so the age scope starts at the Xu palace, whose index is 8. *** ## get\_brightness [#get_brightness] **Purpose** Look up a star's brightness in a given palace. **Signature** ```python def get_brightness( star: str, palace_index: int, config: ChartConfig | None = None, ) -> Brightness | None ``` **Parameters** | Parameter | Type | Required | Default | Description | | -------------- | --------------------- | -------- | ------- | ----------------------------------------------------- | | `star` | `str` | Yes | — | Star key | | `palace_index` | `int` | Yes | — | Palace index; out-of-range values are taken modulo 12 | | `config` | `ChartConfig \| None` | No | `None` | A custom brightness table changes the result | **Return value** A `Brightness` enum member; `None` for stars with no brightness table. It is a `StrEnum`, so `utils.get_brightness("ziweiMaj", 4) == "miao"` holds. An unknown star key raises `IztroError` (with `code` `invalid_argument`). **Example** ```python print(utils.get_brightness("ziweiMaj", 4)) print(utils.get_brightness("lucunMin", 0)) ``` **Output** ```text miao None ``` Ziwei is at miao in the Woo palace (index 4); Lucun has no brightness table. *** ## get\_mutagen / get\_mutagens\_by\_heavenly\_stem [#get_mutagen--get_mutagens_by_heavenly_stem] **Purpose** Look up the mutagens of a heavenly stem. **Zi Wei meaning** Each of the ten stems assigns four fixed stars to lu, quan, ke and ji. `get_mutagen` asks "what does this star take under this stem", while `get_mutagens_by_heavenly_stem` asks "which four stars does this stem transform". **Signature** ```python def get_mutagen(star: str, stem: HeavenlyStem | str, config: ChartConfig | None = None) -> Mutagen | None def get_mutagens_by_heavenly_stem(stem: HeavenlyStem | str, config: ChartConfig | None = None) -> list[str] ``` **Return value** `get_mutagen` returns a `Mutagen` enum member, or `None` when the star is not in that stem's mutagen table. `get_mutagens_by_heavenly_stem` returns a list of four star keys (`list[str]`), in the order **lu, quan, ke, ji**. Both are affected by a custom mutagen table in `config`. **Example** ```python print(utils.get_mutagen("taiyangMaj", "gengHeavenly")) print(utils.get_mutagen("ziweiMaj", "gengHeavenly")) print(utils.get_mutagens_by_heavenly_stem("gengHeavenly")) ``` **Output** ```text sihuaLu None ['taiyangMaj', 'wuquMaj', 'taiyinMaj', 'tiantongMaj'] ``` *** ## get\_soul\_and\_body [#get_soul_and_body] **Purpose** Derive the Soul and body palaces from the lunar month index, the hour and the year stem. **Zi Wei meaning** The Soul palace is the origin of the whole chart: start at the Yin palace for the first month, count forward to the birth month, then count backward from there to the birth hour. The body palace uses the same starting point but counts the hour forward. The Soul palace's stem comes from the year stem via the Five Tigers rule. **Signature** ```python def get_soul_and_body( month_index: int, time_index: int, yearly_stem: HeavenlyStem | str, ) -> SoulAndBody ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------- | ----- | -------- | ------- | ---------------------------------------------------------------------------------- | | `month_index` | `int` | Yes | — | Lunar month index with the first month at 0; obtained from `fix_lunar_month_index` | | `time_index` | `int` | Yes | — | Hour index 0–12 | | `yearly_stem` | `str` | Yes | — | Birth-year stem key | **Return value** `SoulAndBody`: | Field | Type | Description | | ------------------------ | ----- | ------------------------------- | | `soul_index` | `int` | Palace index of the Soul palace | | `body_index` | `int` | Palace index of the body palace | | `heavenly_stem_of_soul` | `str` | Stem key of the Soul palace | | `earthly_branch_of_soul` | `str` | Branch key of the Soul palace | **Example** ```python sb = utils.get_soul_and_body(6, 2, "gengHeavenly") print(sb) print(sb.soul_index, sb.body_index, sb.earthly_branch_of_soul) ``` **Output** ```text SoulAndBody(soul_index=4, body_index=8, heavenly_stem_of_soul='renHeavenly', earthly_branch_of_soul='wuEarthly') 4 8 wuEarthly ``` *** ## get\_five\_elements\_class [#get_five_elements_class] **Purpose** Derive the five elements class from the Soul palace's stem and branch. **Zi Wei meaning** The five elements class (water 2nd, wood 3rd, metal 4th, earth 5th, fire 6th) decides two major things: where Ziwei starts, and the age at which the decadal scope begins. **Signature** ```python def get_five_elements_class(stem: HeavenlyStem | str, branch: EarthlyBranch | str) -> str ``` **Return value** A five elements class key string (from the value set of `FiveElementsClass`). **Example** ```python print(utils.get_five_elements_class("renHeavenly", "wuEarthly")) ``` **Output** ```text wood3rd ``` *** ## get\_palace\_names [#get_palace_names] **Purpose** Derive the twelve palace names from the Soul palace index. **Zi Wei meaning** Once the Soul palace is fixed, the other eleven run counterclockwise in a fixed order: Soul, Siblings, Spouse, Children, Wealth, Health, Surface, Friends, Career, Property, Spirit, Parents. **Signature** ```python def get_palace_names(soul_index: int) -> list[PalaceName] ``` **Return value** A list of twelve `PalaceName` members **indexed by palace index** — item `i` is the palace name of `chart.palaces[i]`. **Example** ```python names = utils.get_palace_names(4) print(names[:4]) print([str(n) for n in names[:4]]) print(names[0] == "wealthPalace") ``` **Output** ```text [, , , ] ['wealthPalace', 'childrenPalace', 'spousePalace', 'siblingsPalace'] True ``` The list elements are `PalaceName` enum members: `repr` shows the enum name while `str` gives the key itself, and being a `StrEnum` they also compare directly with plain strings. The Soul palace is at index 4, so index 0 (the Yin palace) is Wealth. *** ## get\_decadals\_and\_ages [#get_decadals_and_ages] **Purpose** Derive the decadal and age scopes of the twelve palaces from the Soul palace index and the five elements class. **Zi Wei meaning** The starting age of the decadal scope comes from the five elements class (water 2nd at 2, wood 3rd at 3, and so on), with direction from gender polarity and year-branch polarity; the age scope's starting palace comes from the year branch and it steps forward with the nominal age. **Signature** ```python def get_decadals_and_ages( soul_index: int, five_elements_class: str, gender: str, yearly_stem: HeavenlyStem | str, yearly_branch: EarthlyBranch | str, ) -> DecadalsAndAges ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------------- | ----- | -------- | ------- | ------------------------------- | | `soul_index` | `int` | Yes | — | Palace index of the Soul palace | | `five_elements_class` | `str` | Yes | — | Five elements class key | | `gender` | `str` | Yes | — | `"male"` or `"female"` | | `yearly_stem` | `str` | Yes | — | Year stem key | | `yearly_branch` | `str` | Yes | — | Year branch key | **Return value** `DecadalsAndAges`, both fields indexed by palace index: | Field | Type | Description | | ---------- | ----------------- | -------------------------------------------------------- | | `decadals` | `list[Decadal]` | The decadal of each of the twelve palaces | | `ages` | `list[list[int]]` | The age-scope nominal ages of each of the twelve palaces | `Decadal` is the same type as `palace.decadal` on a palace: | Field | Type | Description | | --------------------------------------- | ----------------- | -------------------------------------------------------- | | `range` | `tuple[int, int]` | The decadal's first and last nominal age, both inclusive | | `heavenly_stem` / `heavenly_stem_key` | `str` | Translated stem / key of the decadal | | `earthly_branch` / `earthly_branch_key` | `str` | Translated branch / key of the decadal | **Example** ```python d = utils.get_decadals_and_ages(4, "wood3rd", "female", "gengHeavenly", "chenEarthly") print(d.decadals[0]) print(d.decadals[0].range, d.decadals[0].earthly_branch_key) print(d.ages[0][:3]) ``` **Output** ```text Decadal(range=(43, 52), heavenly_stem='戊', heavenly_stem_key='wuHeavenly', earthly_branch='寅', earthly_branch_key='yinEarthly') (43, 52) yinEarthly [9, 21, 33] ``` `Decadal`'s translated fields are generated in **zh-CN** — this function takes no `language` parameter. For another language, run `heavenly_stem_key` through [`i18n.translate`](/en/docs/python/i18n#translate). **Edge cases and pitfalls** On a fully charted astrolabe every palace already carries `decadal` and `ages` fields with the same contents. This function is for cases where you want the scopes without charting the whole thing. *** ## fix\_lunar\_month\_index / fix\_lunar\_day\_index [#fix_lunar_month_index--fix_lunar_day_index] **Purpose** Compute the corrected lunar month index and day index. **Zi Wei meaning** Where leap-month days belong and where the late Zi hour belongs are two long-disputed boundaries in Zi Wei Dou Shu; these two functions pin the rules down: days after the fifteenth of a leap month count as the next month (can be turned off), and the late Zi hour belongs to the next day. **Signature** ```python def fix_lunar_month_index( lunar_month: int, lunar_day: int, is_leap: bool, time_index: int, fix_leap: bool, ) -> int def fix_lunar_day_index(lunar_day: int, time_index: int) -> int ``` **Return value** The month index is 0-based (the first month is 0); the day index is not decremented in the late Zi hour. `fix_lunar_month_index` carries over only when four conditions hold at once: `is_leap` is true, `fix_leap` is true, `lunar_day` is greater than 15, and `time_index` is not 12. Miss any one of them and the current month is used. **Example** ```python print(utils.fix_lunar_month_index(7, 17, False, 2, True)) print(utils.fix_lunar_day_index(17, 2), utils.fix_lunar_day_index(17, 12)) ``` **Output** ```text 6 16 17 ``` The seventh month is not a leap month, giving index 6; day seventeen decrements to 16 in the Tiger hour, but stays 17 in the late Zi hour because that belongs to the next day. *** ## translate\_chinese\_date [#translate_chinese_date] **Purpose** Assemble the four pillars into a display string. **Signature** ```python def translate_chinese_date( pillars: list[tuple[str, str]], language: str = "zh-CN", ) -> str ``` **Parameters** | Parameter | Type | Required | Default | Description | | ---------- | ----------------------- | -------- | --------- | -------------------------------------------------------------------------- | | `pillars` | `list[tuple[str, str]]` | Yes | — | The four pillar keys \[year, month, day, hour], each a (stem, branch) pair | | `language` | `str` | No | `"zh-CN"` | Output language | **Return value** `str`. When every term is a single character, the pillar's parts run together and the pillars are separated by spaces; when any term is multi-character, the parts within a pillar are separated by spaces and the pillars by `-`. **Example** ```python pillars = [ ("gengHeavenly", "chenEarthly"), ("jiaHeavenly", "shenEarthly"), ("bingHeavenly", "wuEarthly"), ("gengHeavenly", "yinEarthly"), ] print(utils.translate_chinese_date(pillars, "en-US")) # the four-pillar keys can be taken straight from the chart print(utils.translate_chinese_date(chart.raw_dates.chinese_date.pillar_keys(), "en-US")) ``` **Output** ```text geng chen - jia shen - bing woo - geng yin geng chen - jia shen - bing woo - geng yin ``` **Edge cases and pitfalls** Raises `IztroError` (with `code` `invalid_argument`) when there are not four pillars, when a pillar does not have two entries, or when a stem or branch key is invalid. *** ## merge\_stars [#merge_stars] **Purpose** Merge several "twelve palaces of stars" groups into one, palace by palace. **Zi Wei meaning** Star placement happens in batches: major stars, minor stars and adjective stars each produce their own list of twelve palaces. Use this function to fuse them into one complete chart face. **Signature** ```python def merge_stars(*groups: list[list[Star]]) -> list[list[Star]] ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ------------------ | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `groups` | `list[list[Star]]` | Yes | — | Several twelve-palace star lists, each of length 12. Note it is a **varargs** parameter: write `merge_stars(major, minor)`, not one list of lists | **Return value** The merged twelve-palace list, with each palace's stars concatenated in the order the groups were passed. **Example** ```python from x_iztro import star major = star.get_major_star("2000-8-16", 2, "female", language="en-US") minor = star.get_minor_star("2000-8-16", 2, "female", language="en-US") merged = utils.merge_stars(major, minor) print([s.name for s in merged[0]]) ``` **Output** ```text ['general', 'minister', 'horse'] ``` **Edge cases and pitfalls** Raises `ValueError` when a group's length is not 12. This is a pure local implementation and does not go through the binding layer. # Star placement (/en/docs/python/star) Where a group of stars lands given birth data. Use this layer when you do not want a whole chart and only need "which palace does Lucun land in?" or "how are the adjective stars distributed on this chart?". ```python from x_iztro import star ``` Every index is a **palace index**: 0 is the Yin palace, 11 the Chou palace. ## Shared parameters [#shared-parameters] The entry points that take birth data all share one parameter set: | Parameter | Type | Required | Default | Description | | --------------------------- | --------------------- | -------- | --------- | ------------------------------------------------------------------------- | | `solar_date` | `str` | Yes | — | Solar date in `YYYY-M-D` | | `time_index` | `int` | Yes | — | Hour index 0–12 | | `gender` | `str` | No | `"male"` | Gender, which sets the direction of the Changsheng and Boshi gods | | `fix_leap` | `bool` | No | `True` | Whether to correct for leap months | | `language` | `str` | No | `"zh-CN"` | Output language for star names | | `config` | `ChartConfig \| None` | No | `None` | Charting configuration | | `from_stem` / `from_branch` | `str \| None` | No | `None` | The pillar anchoring the five elements class; both must be given together | ```python birth = dict(solar_date="2000-8-16", time_index=2, gender="female", language="en-US") ``` The examples on this page all place stars in `en-US`, so the star names in the output are the English translations. Every entry point returns a **named dataclass** (not a dict), whose fields are read as attributes: `star.get_start_index(**birth).ziwei_index`. Once both are given, the class is derived from that pillar instead, which in turn moves Ziwei and Tianfu and the Changsheng gods. How the other star groups are placed is unaffected. Use it to obtain the placements of the Zhongzhou school's earth and human charts. Only `get_start_index`, `get_major_star` and `get_changsheng12` accept these two parameters. *** ## get\_start\_index [#get_start_index] **Purpose** Find the starting palaces of Ziwei and Tianfu. **Zi Wei meaning** Ziwei is the anchor of the whole chart, located from the five elements class and the lunar day by the Ziwei placement rule; the other thirteen major stars then spread out from Ziwei and Tianfu. Tianfu's position mirrors Ziwei's. **Signature** ```python def get_start_index(solar_date, time_index, gender="male", fix_leap=True, language="zh-CN", config=None, from_stem=None, from_branch=None) -> StartIndex ``` **Return value** `StartIndex`, with the fields `ziwei_index` and `tianfu_index`. **Example** ```python s = star.get_start_index(**birth) print(s) print(s.ziwei_index, s.tianfu_index) ``` **Output** ```text StartIndex(ziwei_index=4, tianfu_index=8) 4 8 ``` *** ## Landing indices per group [#landing-indices-per-group] The following six entry points share a shape: they take birth data and return a dataclass whose fields are all palace indices. | Function | Return type | Fields | Placement rule | | -------------------------- | ------------------ | ---------------------------------------------- | ---------------------------------------------------------------------------------------------- | | `get_lu_yang_tuo_ma_index` | `LuYangTuoMaIndex` | `lu_index` `yang_index` `tuo_index` `ma_index` | The year stem places Lucun, with Qingyang ahead and Tuoluo behind; Tianma from the year branch | | `get_kui_yue_index` | `KuiYueIndex` | `kui_index` `yue_index` | Year stem | | `get_chang_qu_index` | `ChangQuIndex` | `chang_index` `qu_index` | Hour branch | | `get_kong_jie_index` | `KongJieIndex` | `kong_index` `jie_index` | Hour branch | | `get_timely_star_index` | `TimelyStarIndex` | `taifu_index` `fenggao_index` | Hour branch | | `get_luan_xi_index` | `LuanXiIndex` | `hongluan_index` `tianxi_index` | Year branch | **Example** ```python print(star.get_lu_yang_tuo_ma_index(**birth)) print(star.get_chang_qu_index(**birth)) print(star.get_luan_xi_index(**birth)) ``` **Output** ```text LuYangTuoMaIndex(lu_index=6, yang_index=7, tuo_index=5, ma_index=0) ChangQuIndex(chang_index=6, qu_index=4) LuanXiIndex(hongluan_index=9, tianxi_index=3) ``` Qingyang sits one palace ahead of Lucun and Tuoluo one behind — the direct expression of the mnemonic "Qingyang before Lucun, Tuoluo after". *** ## get\_daily\_star\_index / get\_monthly\_star\_index / get\_yearly\_star\_index [#get_daily_star_index--get_monthly_star_index--get_yearly_star_index] **Purpose** Get the landing palaces of the adjective stars placed by day, month and year. **Zi Wei meaning** Adjective stars are grouped by how they are placed: day-based stars count forward from a minor star's position, starting at day one, to the birth day; month-based stars are located from the lunar month; year-based stars are the largest group and start from the year stem or year branch. **Return value** | Function | Return type | Fields | | ------------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `get_daily_star_index` | `DailyStarIndex` | `santai_index` `bazuo_index` `enguang_index` `tiangui_index` | | `get_monthly_star_index` | `MonthlyStarIndex` | `yuejie_index` (Jieshen) `tianyao_index` `tianxing_index` `yinsha_index` `tianyue_index` `tianwu_index` | | `get_yearly_star_index` | `YearlyStarIndex` | 27 fields: `xianchi_index` `huagai_index` `guchen_index` `guasu_index` `tiancai_index` `tianshou_index` `tianchu_index` `posui_index` `feilian_index` `longchi_index` `fengge_index` `tianku_index` `tianxu_index` `tianguan_index` `tianfu_index` `tiande_index` `yuede_index` `tiankong_index` `jielu_index` `kongwang_index` `xunkong_index` `tianshang_index` `tianshi_index` `jiekong_index` `jiesha_adj_index` `nianjie_index` `dahao_adj_index` | Hongluan and Tianxi are year-based too, but they are not in `YearlyStarIndex` — `get_luan_xi_index` supplies them separately. **Example** ```python d = star.get_daily_star_index(**birth) m = star.get_monthly_star_index(**birth) y = star.get_yearly_star_index(**birth) print(d) print(m.yuejie_index, m.tianyao_index, m.tianxing_index) print(y.xianchi_index, y.huagai_index, y.tianshang_index, y.tianshi_index) ``` **Output** ```text DailyStarIndex(santai_index=0, bazuo_index=10, enguang_index=9, tiangui_index=7) 0 5 1 7 2 9 11 ``` **Edge cases and pitfalls** Year-based adjective stars belong to the yearly spirits, so their year branch comes from `horoscope_divide` rather than `year_divide`. When the two settings differ, year-based stars and the major and minor stars can rest on different year branches — a deliberate distinction of school. These three enter the chart face only when `algorithm` is the Zhongzhou school, replacing the default placements of Jielu, Kongwang and Dahao; under the default school they are still computed, they are just not placed into palaces. *** ## get\_major\_star / get\_minor\_star / get\_adjective\_star [#get_major_star--get_minor_star--get_adjective_star] **Purpose** Get the complete distribution of major, minor and adjective stars across the twelve palaces. **Signature** ```python def get_major_star(...) -> list[list[Star]] def get_minor_star(...) -> list[list[Star]] def get_adjective_star(...) -> list[list[Star]] ``` **Return value** A list of twelve, indexed by palace index. Each item is that palace's list of `Star`s, possibly empty. **Example** ```python major = star.get_major_star(**birth) for i, stars in enumerate(major[:5]): print(i, [s.name for s in stars]) ``` **Output** ```text 0 ['general', 'minister'] 1 ['sun', 'sage'] 2 ['marshal'] 3 ['advisor'] 4 ['emperor'] ``` **Edge cases and pitfalls** The returned `Star`s carry brightness and natal mutagen marks and are identical to those from a full chart — they go through the same code. If you want the whole chart, `Astro().by_solar(...)` is simpler. Note the naming across languages: Python and Go use the singular (`get_major_star`, `GetMajorStar`) where Rust uses the plural (`get_major_stars`); the behaviour is the same. *** ## get\_changsheng12 / get\_boshi12 / get\_yearly12 [#get_changsheng12--get_boshi12--get_yearly12] **Purpose** Get how the four groups of twelve gods are arranged across the twelve palaces. **Zi Wei meaning** Each group is twelve marks filling the twelve palaces, exactly one per palace: the Changsheng gods start from the five elements class with direction from gender and year-branch polarity; the Boshi gods start from Lucun with the same direction rule; the Sui-qian gods run forward from the year branch, and the Jiang-qian gods start from the trine group of the year branch. **Signature** ```python def get_changsheng12(...) -> list[str] def get_boshi12(...) -> list[str] def get_yearly12(...) -> dict[str, list[str]] ``` **Return value** `get_changsheng12` and `get_boshi12` return a list of twelve keys, indexed by palace index. `get_yearly12` returns `Yearly12`, whose fields `suiqian12` and `jiangqian12` are each a list of twelve keys. **Example** ```python print(star.get_changsheng12(**birth)[:4]) print(star.get_boshi12(**birth)[:4]) y = star.get_yearly12(**birth) print(y.suiqian12[:4]) print(y.jiangqian12[:4]) ``` **Output** ```text ['jue', 'mu', 'si', 'bing'] ['faylian', 'zhoushu', 'jiangjun', 'xiaohao'] ['diaoke', 'bingfu', 'suijian', 'huiqi'] ['suiyi', 'xiishen', 'huagai', 'jiesha'] ``` These are keys rather than translated names; use `i18n.translate(key)` to display them. *** ## get\_changsheng12\_start\_index / get\_jiangqian12\_start\_index [#get_changsheng12_start_index--get_jiangqian12_start_index] **Purpose** Get just the starting palace of two of the god groups, without laying out the whole cycle. **Zi Wei meaning** The Changsheng starting point is set by the five elements class: water 2nd starts at Shen, wood 3rd at Hai, metal 4th at Si, earth 5th at Shen, fire 6th at Yin. The Jiangxing starting point is set by the trine group of the year branch: yin/woo/xu years at Woo, shen/zi/chen years at Zi, si/you/chou years at You, hai/mao/wei years at Mao. **Signature** ```python def get_changsheng12_start_index(five_elements_class: FiveElementsClass | str) -> int def get_jiangqian12_start_index(branch: EarthlyBranch | str) -> int ``` **Return value** `int`, 0–11. Neither function needs birth data. **Example** ```python print(star.get_changsheng12_start_index("water2nd"), star.get_changsheng12_start_index("fire6th")) print(star.get_jiangqian12_start_index("ziEarthly"), star.get_jiangqian12_start_index("wuEarthly")) ``` **Output** ```text 6 0 10 4 ``` Water 2nd puts Changsheng in Shen (index 6), fire 6th in Yin (index 0). *** ## get\_horoscope\_star [#get_horoscope_star] **Purpose** Get the scope-star distribution of a horoscope layer. **Zi Wei meaning** Scope stars are the ten stars a horoscope produces: Tiankui, Tianyue, Wenchang, Wenqu, Lucun, Qingyang, Tuoluo, Tianma, Hongluan and Tianxi. Where they land is fixed by that layer's stem and branch, and their names change with the layer. The yearly layer carries one extra star, Nianjie. **Signature** ```python def get_horoscope_star( stem: HeavenlyStem | str, branch: EarthlyBranch | str, scope: Scope | str, language: str = "zh-CN", ) -> list[list[Star]] ``` **Parameters** | Parameter | Type | Required | Default | Description | | ---------- | ----- | -------- | --------- | ----------------------------------------------- | | `stem` | `str` | Yes | — | Stem key of that layer | | `branch` | `str` | Yes | — | Branch key of that layer | | `scope` | `str` | Yes | — | The horoscope layer, which fixes the star names | | `language` | `str` | No | `"zh-CN"` | Output language | **Return value** A list of twelve, indexed by palace index. **Star names per layer** | Natal | Decadal | Yearly | Monthly | Daily | Hourly | | -------- | -------- | -------- | -------- | ------- | -------- | | Tiankui | Yunkui | Liukui | Yuekui | Rikui | Shikui | | Tianyue | Yunyue | Liuyue | Yueyue | Riyue | Shiyue | | Wenchang | Yunchang | Liuchang | Yuechang | Richang | Shichang | | Wenqu | Yunqu | Liuqu | Yuequ | Riqu | Shiqu | | Lucun | Yunlu | Liulu | Yuelu | Rilu | Shilu | | Qingyang | Yunyang | Liuyang | Yueyang | Riyang | Shiyang | | Tuoluo | Yuntuo | Liutuo | Yuetuo | Rituo | Shituo | | Tianma | Yunma | Liuma | Yuema | Rima | Shima | | Hongluan | Yunluan | Liuluan | Yueluan | Riluan | Shiluan | | Tianxi | Yunxi | Liuxi | Yuexi | Rixi | Shixi | The keys take the form `yunlu` (decadal Lucun), `liulu` (yearly), `yuelu` (monthly), `rilu` (daily), `shilu` (hourly). **Example** ```python decadal = star.get_horoscope_star("jiaHeavenly", "ziEarthly", "decadal", "en-US") print([[s.name for s in p] for p in decadal[:4]]) origin = star.get_horoscope_star("jiaHeavenly", "ziEarthly", "origin", "en-US") print([[s.name for s in p] for p in origin[:2]]) ``` **Output** ```text [['money(D)', 'horse(D)'], ['driven(D)', 'attractive(D)'], [], ['scholar(D)']] [['money', 'horse'], ['driven', 'attractive']] ``` In `en-US` the layer shows up as the suffix on the name — `(D)` for the decadal layer — rather than as a different word, while the keys stay `yunlu`, `yunma` and so on. **Edge cases and pitfalls** The result for `"yearly"` additionally contains Nianjie, located from the yearly branch and placed ahead of the ten scope stars. No other layer has it. *** ## Low-level placement [#low-level-placement] The functions above all start from birth data, deriving the year pillar, the Soul palace and the corrected lunar month internally before placing anything. This group takes those intermediates directly and is reusable in a pipeline of your own. | Function | Takes | Returns | | ---------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------- | | `get_zuo_you_index(lunar_month)` | The corrected lunar month, 1–12 | `ZuoYouIndex(zuo_index, you_index)` | | `get_huo_ling_index(branch, time_index)` | Year branch, hour | `HuoLingIndex(huo_index, ling_index)` | | `get_huagai_xianchi_index(branch)` | Year branch | `HuagaiXianchiIndex(huagai_index, xianchi_index)` | | `get_gu_gua_index(branch)` | Year branch | `GuGuaIndex(guchen_index, guasu_index)` | | `get_jiesha_adj_index(branch)` | Year branch | `int`, the palace index of Jiesha | | `get_dahao_index(branch)` | Year branch | `int`, the palace index of Dahao | | `get_nianjie_index(branch)` | Year branch | `int`, the palace index of Nianjie | | `get_tianshi_tianshang_index(gender, branch, soul_index, config=None)` | Gender, year branch, Soul palace index | `TianshiTianshangIndex(tianshang_index, tianshi_index)` | | `get_chang_qu_index_by_heavenly_stem(stem)` | Heavenly stem | `ChangQuIndex(chang_index, qu_index)` | **Example** ```python from x_iztro import star chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US") year_branch = chart.raw_dates.chinese_date.yearly_keys[1] print(star.get_huo_ling_index(year_branch, 2)) print(star.get_gu_gua_index(year_branch)) print(star.get_chang_qu_index_by_heavenly_stem("jiaHeavenly")) ``` **Output** ```text HuoLingIndex(huo_index=2, ling_index=10) GuGuaIndex(guchen_index=3, guasu_index=11) ChangQuIndex(chang_index=3, qu_index=7) ``` **Edge cases and pitfalls** `get_zuo_you_index` takes the month after leap-month correction, i.e. `fix_lunar_month_index(...) + 1` — not the raw lunar month. Passing the raw month on a leap-month chart lands in the wrong palace. The result of `get_tianshi_tianshang_index` follows `config.algorithm`: the Zhongzhou school swaps Tianshang and Tianshi for yin men and yang women (where the birth-year branch polarity and the gender polarity differ), while the common school does not. `get_chang_qu_index_by_heavenly_stem` places Wenchang and Wenqu from a heavenly stem and is used for the scope Wenchang and Wenqu of horoscope layers; the natal Wenchang and Wenqu go through `get_chang_qu_index` from the hour branch. # Data tables (/en/docs/python/data) Star information, stem and branch information, ordering constants and all the enums. The input tables of the charting algorithm, plus the enum listings of the language-independent keys. ```python from x_iztro import data ``` All four entry points return **named dataclasses** (the outer container is still a `dict` or `list`), whose fields are read as attributes rather than by subscript. The field names are snake\_case and therefore differ from the camelCase keys of the underlying JSON. *** ## stars\_info [#stars_info] **Purpose** Get the star information table. **Signature** ```python def stars_info() -> dict[str, StarInfo] ``` **Return value** Star key → `StarInfo`. Only twenty stars have an entry: the **fourteen major stars** plus Wenchang, Wenqu, Huoxing, Lingxing, Qingyang and Tuoluo. | Field | Type | Description | | --------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------- | | `brightness` | `list[str]` | Brightness keys across the twelve palaces, index 0 being the Yin palace; an empty string where the palace has no brightness | | `five_elements` | `str \| None` | Five element | | `yin_yang` | `str \| None` | Polarity | **Example** ```python info = data.stars_info() print(len(info)) print(info["ziweiMaj"]) print(info["taiyangMaj"].five_elements) ``` **Output** ```text 20 StarInfo(brightness=['wang', 'wang', 'de', 'wang', 'miao', 'miao', 'wang', 'wang', 'de', 'wang', 'ping', 'miao'], five_elements='土', yin_yang='阴') None ``` **Edge cases and pitfalls** Some stars have no five element or polarity in the table: both are `None` for Taiyang and Qisha, polarity is `None` for Tanlang, Tianxiang, Tianliang and Pojun, and both are `None` for the six minor stars. Note also that `five_elements` and `yin_yang` are never internationalized — they are always the Chinese characters (`土`, `阴` and so on) in every output language. *** ## flow\_star\_counterparts [#flow_star_counterparts] **Purpose** The full table mapping flowing stars to their natal minor-star counterparts (50 entries). **Signature** ```python def flow_star_counterparts() -> dict[str, str] ``` **Return value** Keys are flowing-star keys (the `HoroscopeStar` enum's domain), values natal minor-star keys (the `MinorStar` enum's domain). Flowing stars have no knowledge-pack entries of their own — their readings are looked up via the natal counterpart, and this table is the official mapping. **Example** ```python from x_iztro import data print(data.flow_star_counterparts()["liuchang"]) ``` **Output** ```text wenchangMin ``` *** ## heavenly\_stems [#heavenly_stems] **Purpose** Get the heavenly stem information table. **Zi Wei meaning** The mutagen table of the stems is the root of the whole mutagen system: the birth-year stem determines the natal mutagens, a palace stem determines what that palace flies, and a scope stem determines that layer's mutagens. **Signature** ```python def heavenly_stems() -> dict[str, HeavenlyStemInfo] ``` **Return value** Stem key → `HeavenlyStemInfo`: | Field | Type | Description | | --------------- | ------------- | --------------------------------------------------------- | | `yin_yang` | `str` | Polarity | | `five_elements` | `str` | Five element | | `crash` | `str \| None` | Clashing stem key; wu and ji clash with nothing | | `mutagen` | `list[str]` | The four mutagen star keys, in the order lu, quan, ke, ji | **Example** ```python stems = data.heavenly_stems() print(stems["jiaHeavenly"]) print(stems["wuHeavenly"].crash) ``` **Output** ```text HeavenlyStemInfo(yin_yang='阳', five_elements='木', crash='gengHeavenly', mutagen=['lianzhenMaj', 'pojunMaj', 'wuquMaj', 'taiyangMaj']) None ``` This is the **built-in default table**. A custom mutagen table (`ChartConfig(mutagens=...)`) is not reflected here; for the mutagens actually in effect on a given chart use [`utils.get_mutagens_by_heavenly_stem(stem, config)`](/en/docs/python/util#get_mutagen--get_mutagens_by_heavenly_stem) or a palace's `mutagen_star_keys`. *** ## earthly\_branches [#earthly_branches] **Purpose** Get the earthly branch information table. **Signature** ```python def earthly_branches() -> dict[str, EarthlyBranchInfo] ``` **Return value** Branch key → `EarthlyBranchInfo`: | Field | Type | Description | | --------------- | ----- | ------------------------------------------------------------------------------- | | `yin_yang` | `str` | Polarity, which sets the direction of the decadal scope and the Changsheng gods | | `five_elements` | `str` | Five element | | `crash` | `str` | Clashing branch key | | `soul` | `str` | Soul star key (looked up by the Soul palace branch) | | `body` | `str` | Body star key (looked up by the birth-year branch) | | `inside` | `str` | Corresponding internal organ | | `outside` | `str` | Corresponding body part | | `health_tip` | `str` | Health note | `inside`, `outside` and `health_tip` exist only in Chinese and take no part in internationalization. **Example** ```python zi = data.earthly_branches()["ziEarthly"] print(zi) print(zi.soul, zi.body, zi.crash) ``` **Output** ```text EarthlyBranchInfo(yin_yang='阳', five_elements='水', crash='wuEarthly', soul='tanlangMaj', body='huoxingMin', inside='胆', outside='下体', health_tip='生殖系统、膀胱、尿道之疾病,听觉障碍') tanlangMaj huoxingMin wuEarthly ``` *** ## constants [#constants] **Purpose** Get the ordering constants and derivation rule tables. **Signature** ```python def constants() -> Constants ``` **Return value** `Constants`: | Field | Type | Description | | --------------------- | ---------------- | -------------------------------------------------------------------------- | | `languages` | `list[str]` | Supported language codes | | `heavenly_stems` | `list[str]` | Stem order | | `earthly_branches` | `list[str]` | Branch order | | `zodiac` | `list[str]` | Chinese zodiac keys, in branch order | | `signs` | `list[str]` | Zodiac sign keys, in ecliptic order | | `palaces` | `list[str]` | The twelve palace names, running **counterclockwise** from the Soul palace | | `gender` | `dict[str, str]` | The polarity of each gender | | `chinese_time` | `list[str]` | Hour keys, from the early Zi hour to the late Zi hour | | `time_range` | `list[str]` | The clock range of each hour | | `tiger_rule` | `dict[str, str]` | Five Tigers rule: year stem to first-month stem | | `rat_rule` | `dict[str, str]` | Five Rats rule: day stem to Zi-hour stem | | `mutagen` | `list[str]` | Mutagen order | | `five_elements_class` | `dict[str, int]` | Five elements class key → its number (water 2nd is 2, … fire 6th is 6) | **Example** ```python c = data.constants() print(c.languages) print(c.zodiac[:3], c.chinese_time[12], c.time_range[2]) print(c.gender) print("first-month stem of a jia year:", c.tiger_rule["jiaHeavenly"]) print(c.palaces) print(c.five_elements_class) ``` **Output** ```text ['en-US', 'ja-JP', 'ko-KR', 'zh-CN', 'zh-TW', 'vi-VN'] ['rat', 'ox', 'tiger'] lateRatHour 03:00~05:00 {'female': '阴', 'male': '阳'} first-month stem of a jia year: bingHeavenly ['soulPalace', 'parentsPalace', 'spiritPalace', 'propertyPalace', 'careerPalace', 'friendsPalace', 'surfacePalace', 'healthPalace', 'wealthPalace', 'childrenPalace', 'spousePalace', 'siblingsPalace'] {'earth5th': 5, 'fire6th': 6, 'metal4th': 4, 'water2nd': 2, 'wood3rd': 3} ``` **Edge cases and pitfalls** `palaces` gives the **ordering** of the palace names: Soul, Parents, Spirit, Property, Career, Friends, Surface, Health, Wealth, Children, Spouse, Siblings. That is the sequence in which the twelve palaces spread counterclockwise from the Soul palace; it is not what cell `i` is called on any particular chart. For that, use [`utils.get_palace_names(soul_index)`](/en/docs/python/util#get_palace_names). The order of `languages` is iztro's vocabulary merge order (starting from en-US), not the declaration order of the `Language` enum. The per-language scan order of [`i18n.key_of`](/en/docs/python/i18n#key_of) matches it. `five_elements_class`, `gender`, `tiger_rule` and `rat_rule` are `dict`s rather than `list`s, and their key order is lexicographic (the native extension sorts on conversion) — it carries no meaning as an ordering of classes or stems. The class number is also available straight from the enum: `FiveElementsClass.WOOD_3.number`. *** ## Enum listings [#enum-listings] Every enum in `x_iztro.enums` is a `StrEnum` whose value is the language-independent key. Import them straight from the package root: `from x_iztro import MajorStar, PalaceName`. | Enum | Members | Contents | | -------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Gender` | 2 | `MALE` `FEMALE` | | `Language` | 6 | `ZH_CN` `ZH_TW` `EN_US` `JA_JP` `KO_KR` `VI_VN` | | `HeavenlyStem` | 10 | `JIA` `YI` `BING` `DING` `WU` `JI` `GENG` `XIN` `REN` `GUI` | | `EarthlyBranch` | 12 | `ZI` `CHOU` `YIN` `MAO` `CHEN` `SI` `WU` `WEI` `SHEN` `YOU` `XU` `HAI` | | `PalaceName` | **14** | The twelve palaces `SOUL` `SIBLINGS` `SPOUSE` `CHILDREN` `WEALTH` `HEALTH` `SURFACE` `FRIENDS` `CAREER` `PROPERTY` `SPIRIT` `PARENTS`, plus the two locator marks `BODY` (body palace) and `ORIGINAL` (palace of origin) | | `FiveElementsClass` | 5 | `WATER_2` `WOOD_3` `METAL_4` `EARTH_5` `FIRE_6` | | `Mutagen` | 4 | `LU` `QUAN` `KE` `JI` | | `Brightness` | 7 | `MIAO` `WANG` `DE` `LI` `PING` `BU` `XIAN` | | `StarType` | 8 | `MAJOR` `SOFT` `TOUGH` `ADJECTIVE` `FLOWER` `HELPER` `LUCUN` `TIANMA` | | `Scope` | 6 | `ORIGIN` `DECADAL` `YEARLY` `MONTHLY` `DAILY` `HOURLY` | | `MajorStar` | 14 | The fourteen major stars: `ZIWEI` `TIANJI` `TAIYANG` `WUQU` `TIANTONG` `LIANZHEN` `TIANFU` `TAIYIN` `TANLANG` `JUMEN` `TIANXIANG` `TIANLIANG` `QISHA` `POJUN` | | `MinorStar` | 14 | The fourteen minor stars: `ZUOFU` `YOUBI` `WENCHANG` `WENQU` `LUCUN` `TIANMA` `QINGYANG` `TUOLUO` `HUOXING` `LINGXING` `TIANKUI` `TIANYUE` `DIKONG` `DIJIE` | | `AdjectiveStar` | 43 | Adjective stars, including the Zhongzhou school's `XUNZHONG`; member by member on [Stars](/en/docs/guide/concepts/stars) | | `HoroscopeStar` | 50 | Ten scope stars for each of the five horoscope layers: `YUNLU` `LIULU` `YUELU` `RILU` `SHILU` and so on | | `Changsheng12` | 12 | `CHANGSHENG` `MUYU` `GUANDAI` `LINGUAN` `DIWANG` `SHUAI` `BING` `SI` `MU` `JUE` `TAI` `YANG` | | `Boshi12` | 12 | `BOSHI` `LISHI` `QINGLONG` `XIAOHAO` `JIANGJUN` `ZHOUSHU` `FEILIAN` `XISHEN` `BINGFU` `DAHAO` `FUBING` `GUANFU` | | `Suiqian12` | 13 | `SUIJIAN` `HUIQI` `SANGMEN` `GUANSUO` `GWANFU` `XIAOHAO` `DAHAO` `SUIPO` `LONGDE` `BAIHU` `TIANDE` `DIAOKE` `BINGFU` (`SUIPO` is the Zhongzhou school's Suipo) | | `Jiangqian12` | 12 | `JIANGXING` `PANAN` `SUIYI` `XISHEN` `HUAGAI` `JIESHA` `ZHAISHA` `TIANSHA` `ZHIBEI` `XIANCHI` `YUESHA` `WANGSHEN` | | `Algorithm` | 2 | `DEFAULT` `ZHONGZHOU` | | `AstroType` | 3 | `HEAVEN` `EARTH` `HUMAN` | | `YearDivide` / `HoroscopeDivide` | 2 each | `NORMAL` `EXACT` | | `AgeDivide` | 2 | `NORMAL` `BIRTHDAY` | | `DayDivide` | 2 | `FORWARD` `CURRENT` | A member's name does not always match its value letter for letter — the value of `Boshi12.FEILIAN` is `faylian`, that of `Jiangqian12.XISHEN` is `xiishen`, and that of `Suiqian12.GWANFU` is `gwanfu`. These spellings come from iztro's vocabulary, so **always test against the enum members** rather than writing the strings by hand. Every key with its translations is on [the key reference](/en/docs/guide/guides/keys). ### FiveElementsClass.number [#fiveelementsclassnumber] The five elements class enum carries one extra property, `number`, giving the class number — used both by the starting age of the decadal scope and by the placement of Ziwei: ```python from x_iztro import FiveElementsClass for c in FiveElementsClass: print(c, c.number) ``` **Output** ```text water2nd 2 wood3rd 3 metal4th 4 earth5th 5 fire6th 6 ``` **Example** ```python from x_iztro import MajorStar, PalaceName, Mutagen soul = chart.palace(PalaceName.SOUL) print(soul.major_stars[0].key == MajorStar.ZIWEI) print(MajorStar.ZIWEI, Mutagen.LU, PalaceName.WEALTH) ``` **Output** ```text True ziweiMaj sihuaLu wealthPalace ``` `chart.palace(PalaceName.SOUL)` and `chart.palace("soulPalace")` are exactly equivalent. The enums earn their keep through IDE completion and spell checking, not through type enforcement. When a name comes from external input, use the constructor as a validator: `PalaceName("soulPalce")` raises `ValueError: 'soulPalce' is not a valid PalaceName`, exposing the problem one step earlier than letting a query method silently return `None`. # Translation (/en/docs/python/i18n) Two-way lookup between keys and translations. Every field on a chart already carries both a translation and a `*_key`, so manual translation is usually unnecessary. These functions exist for the cases where you have only a key (or only a translation in some language) and need to convert. ```python from x_iztro import i18n ``` Six languages are supported: `zh-CN`, `zh-TW`, `en-US`, `ja-JP`, `ko-KR`, `vi-VN`. *** ## translate [#translate] **Purpose** Translate any key into a given language. **Signature** ```python def translate(key: str, language: LanguageType = "zh-CN") -> str | None ``` **Parameters** | Parameter | Type | Required | Default | Description | | ---------- | ----- | -------- | --------- | -------------------------- | | `key` | `str` | Yes | — | A language-independent key | | `language` | `str` | No | `"zh-CN"` | Target language | Covering 260 keys across twelve categories: | Category | Count | Examples | | ------------------------------------------------------------ | ----- | ------------------------------------------------------------ | | Stars | 162 | `ziweiMaj`, `changsheng`, `yunlu` | | Palaces (including the body palace and the palace of origin) | 14 | `soulPalace`, `wealthPalace`, `bodyPalace`, `originalPalace` | | Heavenly stems | 10 | `jiaHeavenly` | | Earthly branches | 12 | `ziEarthly` | | Brightness | 7 | `miao`, `wang` | | Mutagens | 4 | `sihuaLu` | | Five elements class | 5 | `water2nd` | | Gender | 2 | `male`, `female` | | Chinese zodiac | 12 | `rat`, `ox` | | Hours | 13 | `earlyRatHour` | | Zodiac signs | 12 | `aries` | | Horoscope scopes | 7 | `decadal`, `turn` | **Return value** The translation; `None` for an unknown key. **Example** ```python print(i18n.translate("ziweiMaj", "en-US")) print(i18n.translate("soulPalace", "ja-JP")) print(i18n.translate("ziweiMaj", "vi-VN")) print(i18n.translate("bodyPalace", "en-US")) print(i18n.translate("nosuch")) ``` **Output** ```text emperor 命宮 Tử Vi body None ``` *** ## key\_of [#key_of] **Purpose** Reverse-look-up a key from a translation in any language. **Signature** ```python def key_of(text: str, key_filter: str | None = None) -> str | None ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------ | ------------- | -------- | ------- | ---------------------------------------------------------------------------------- | | `text` | `str` | Yes | — | A translation in any supported language | | `key_filter` | `str \| None` | No | `None` | A substring the key name must contain, for disambiguating homographic translations | **Return value** The key; `None` when nothing matches. **Example** ```python print(i18n.key_of("紫微")) print(i18n.key_of("emperor")) print(i18n.key_of("자미")) print(i18n.key_of("no such name")) ``` **Output** ```text ziweiMaj ziweiMaj ziweiMaj None ``` Translations in all three languages resolve to the same key. **Edge cases and pitfalls** A few translations are identical across categories: in en-US `horse` is both the zodiac horse and the star Tianma, `dragon` is both the zodiac dragon and Qinglong; in ko-KR `사` is both the branch si and Si among the Changsheng gods. Unfiltered, the scan goes language by language and, within each language, key by key, taking the first hit — in exactly the same order as iztro's `kot` (guarded case by case by golden tests). To pin down a category, pass `key_filter`, which only compares keys containing the substring: ```python print(i18n.key_of("horse")) # horse (the zodiac horse) print(i18n.key_of("horse", "Min")) # tianmaMin (Tianma) print(i18n.key_of("유시")) # hourly (the hourly scope) print(i18n.key_of("유시", "Hour")) # roosterHour (the You hour) print(i18n.key_of("horse", "Palace")) # None ``` Common substrings: `Maj` for the fourteen major stars, `Min` for minor stars, `Heavenly` / `Earthly` for stems and branches, `Palace` for palaces, `Hour` for hours. When the filter matches nothing the result is `None`; it does not fall back to the unfiltered result. `key_of` walks 260 keys × 6 languages. The cost of a single call is negligible, but do not put it in an inner loop over every palace and star — use the `*_key` fields that come with the data there. *** ## all\_keys [#all_keys] **Purpose** Get all 260 translatable keys. **Signature** ```python def all_keys() -> list[str] ``` **Return value** A list of keys, in the order `key_of` scans them: horoscope scopes, Chinese zodiac, hours, zodiac signs, five elements classes, heavenly stems, earthly branches, brightness, mutagens, stars, palaces, gender — matching the merge order of iztro's per-language translation files. **Example** ```python keys = i18n.all_keys() print(len(keys)) print(keys[:4]) print(i18n.translate(keys[0], "en-US")) ``` **Output** ```text 260 ['decadal', 'childhood', 'yearly', 'monthly'] decadal ``` To iterate the keys of one category, the enums in `enums` or `data.constants()` are simpler. *** ## There is no global language switch [#there-is-no-global-language-switch] x-iztro keeps no global "current language" 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 with multiple threads. Passing it explicitly means a call's result depends only on its arguments. To emit several languages within one process, just chart several times; they do not interfere: ```python zh = Astro().by_solar("2000-8-16", 2, "female") en = Astro().by_solar("2000-8-16", 2, "female", language="en-US") print(zh.palace("soulPalace").major_stars[0].name, en.palace("soulPalace").major_stars[0].name) ``` **Output** ```text 紫微 emperor ``` The `*_key` fields of the two charts are identical, so any key-based predicate gives the same answer on both. # Knowledge packs (/en/docs/python/knowledge) KnowledgePack and its entry dataclasses, the bundled default pack, dict/JSON conversion, overlay merging. A knowledge pack is JSON mapping "language-independent key → reading text and school attributes". The core only judges facts; reading texts and the school-specific star attributes live here. For the concept, the format and how to write an overlay, see the [knowledge pack guide](/en/docs/guide/guides/knowledge-pack); the full field reference is [`knowledge/SCHEMA.md`](https://github.com/x-haose/x-iztro/blob/main/knowledge/SCHEMA.md) in the repository. ```python from x_iztro import KnowledgePack from x_iztro.enums import MajorStar pack = KnowledgePack.builtin() intro = pack.star_intro(MajorStar.ZIWEI) ``` `KnowledgePack` is exported from the `x_iztro` top level; the entry dataclasses live in `x_iztro.knowledge`. Every lookup takes a string, and the enums (`MajorStar`, `PatternKey`, `PalaceName`, `Mutagen` …) are `StrEnum`, so you can pass them directly. ## Types [#types] ### KnowledgePack [#knowledgepack] Holds the raw pack object (a dict); the lookups return typed entries. **Metadata (read-only properties)** | Property | Type | Meaning | | ---------- | ------------- | ------------------------------------------------------------------------ | | `schema` | `int` | Format version, currently 1 | | `id` | `str` | Pack identifier; `"iztro-docs"` for the default pack | | `version` | `str` | Pack version; for the default pack, retrieval date + short source commit | | `language` | `str` | Language code of the texts | | `extends` | `str \| None` | The pack this overlay overlays; `None` for a standalone pack | | `source` | `Source` | Origin and licence | **Methods** | Method | Meaning | | ------------------------------------------------------------------------------- | ------------------------------------------------------- | | `KnowledgePack.builtin(language="zh-CN")` | The bundled default pack | | `KnowledgePack.from_dict(d)` | Build from a pack object (holds the reference, no copy) | | `KnowledgePack.from_json(text)` | Build from JSON text | | `to_dict()` | A deep copy of the pack object | | `to_json(**kwargs)` | JSON text; `kwargs` are passed through to `json.dumps` | | `merged(*overlays)` | Layer overlays on, returning a new pack | | `star(key)` / `pattern(key)` / `palace(key)` / `mutagen(key)` / `concept(slug)` | One entry, or `None` | | `stars()` / `patterns()` | All star / pattern entries | | `star_intro(key)` / `pattern_intro(key)` | The reading text directly | ### Entry dataclasses [#entry-dataclasses] `StarEntry`, `PatternEntry`, `TextEntry`, `ConceptEntry`, `StarAttributes` and `Source` are all `frozen=True, slots=True` dataclasses; absent fields are `None`. `StarEntry` | Field | Type | Meaning | | -------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `key` | `str` | Star key | | `name` | `str \| None` | Display name in this pack's language | | `category` | `str \| None` | `"major"` / `"minor"` / `"adjective"` / `"dec"` / `"flow"` (a flowing star, a cross-reference entry pointing at its natal minor-star counterpart) | | `group` | `str \| None` | Grouping: the adjective star's category, the decorative star's group | | `attributes` | `StarAttributes` | School attributes | | `intro` | `str \| None` | Reading (Markdown) | | `combinations` | `dict[str, str]` | Reading for sharing a palace with another major star, keyed by that star | `StarAttributes`: `yin_yang` (`yin` / `yang`), `five_elements` (`wood` / `fire` / `earth` / `metal` / `water`), `stem` (`jia`…`gui`), `five_elements_note`, `dipper`, `chemistry`, `career`, `duty`, `aliases` (`list[str] | None`), `element_color`, `energy_color`. `PatternEntry`: `key`, `name`, `quotes` (`list[str] | None`), `conditions`, `intro`. `TextEntry` (palaces, transformations): `key`, `name`, `intro`. `ConceptEntry` (glossary): `slug`, `title`, `intro`. `Source`: `name`, `url`, `commit`, `license`, `author`, `retrieved_at`, `adapted` (adaptation note). `StarAttributes.five_elements` and `.yin_yang` are what the pack's source says, and may differ from the core star data, which is value-for-value identical to iztro's. The reason is in the [guide](/en/docs/guide/guides/knowledge-pack#why-the-star-attributes-live-here). *** ## builtin [#builtin] **Purpose** Get the bundled default knowledge pack. **Signature** ```python @classmethod def builtin(cls, language: LanguageType = "zh-CN") -> KnowledgePack ``` **Parameters** | Parameter | Type | Required | Default | Meaning | | ---------- | -------------- | -------- | --------- | ------------- | | `language` | `LanguageType` | no | `"zh-CN"` | Text language | **Returns** `KnowledgePack`. **Raises** `IztroError` (with `code` `invalid_argument`) when there is no bundled pack for that language. Only `zh-CN` has one today. **Example** ```python from x_iztro import IztroError, KnowledgePack pack = KnowledgePack.builtin() print(pack) print(pack.schema, pack.id, pack.version, pack.language, pack.extends) print(pack.source.license, pack.source.author) print(len(pack.stars()), len(pack.patterns())) try: KnowledgePack.builtin("en-US") except IztroError as e: print(e.code, e) ``` **Output** ```text KnowledgePack(id='iztro-docs', version='2026-08-19+ec2d58b', language='zh-CN') 1 iztro-docs 2026-08-19+ec2d58b zh-CN None MIT Sylar Long 162 64 invalid_argument no builtin knowledge pack for language 'en-US' ``` *** ## from\_dict / from\_json / to\_dict / to\_json [#from_dict--from_json--to_dict--to_json] **Purpose** Convert between your own pack and x-iztro. **Signature** ```python @classmethod def from_dict(cls, d: dict[str, Any]) -> KnowledgePack @classmethod def from_json(cls, text: str) -> KnowledgePack def to_dict(self) -> dict[str, Any] def to_json(self, **kwargs: Any) -> str ``` **Notes** * `from_dict` holds the dict you pass without copying: mutating that dict afterwards changes the pack. To isolate them, `copy.deepcopy` first or go through `from_json`. * `to_dict` returns a deep copy, so changing it never touches the pack. * `to_json` defaults `ensure_ascii` to `False` (Chinese stays readable); the remaining `kwargs` go straight to `json.dumps`, e.g. `pack.to_json(indent=2)`. * Both constructors **validate the format version**, with the same semantics as the Rust core's parser: a non-object, a missing or zero `schema`, or a `schema` newer than this library supports raises `IztroError` (`invalid_argument`). The structure is not deep-checked: an unrecognised field simply yields nothing. **Example** `my-school.json` is an overlay pack; a complete sample is in the [knowledge pack guide](/en/docs/guide/guides/knowledge-pack#writing-an-overlay-pack): ```python from x_iztro import KnowledgePack overlay = KnowledgePack.from_json(open("my-school.json", encoding="utf-8").read()) print(overlay.id, overlay.extends) raw = overlay.to_dict() raw["stars"]["ziweiMaj"]["intro"] = "changed again" print(overlay.star_intro("ziweiMaj")) # to_dict is a deep copy; the pack is untouched ``` *** ## merged [#merged] **Purpose** Layer overlay packs onto this one and return a new pack. **Signature** ```python def merged(self, *overlays: KnowledgePack | dict[str, Any]) -> KnowledgePack ``` **Parameters** | Parameter | Type | Meaning | | ----------- | ----------------------- | -------------------------------------------------- | | `*overlays` | `KnowledgePack \| dict` | Overlays applied in argument order; later ones win | **Returns** A new `KnowledgePack`; neither this pack nor the overlays change. **Raises** `IztroError` (`invalid_argument`) when a pack does not fit the format, or its `schema` is newer than this library supports. The rules are in the [guide](/en/docs/guide/guides/knowledge-pack#merge-rules): section by section, key by key, an overlay's non-empty fields replace the same-keyed entry's fields, `attributes` and `combinations` merge field by field, array fields are replaced wholesale. The merge itself runs in the Rust core, so all three languages agree. **Example** ```python from x_iztro import IztroError, KnowledgePack, PatternKey from x_iztro.enums import MajorStar pack = KnowledgePack.builtin() overlay = KnowledgePack.from_dict({ "schema": 1, "id": "my-school", "version": "1", "language": "zh-CN", "extends": "iztro-docs", "stars": {"ziweiMaj": {"intro": "我的紫微", "attributes": {"aliases": ["帝座"]}}}, "patterns": {"zi_fu_tong_gong": {"intro": "我的紫府同宫"}}, }) merged = pack.merged(overlay) ziwei = merged.star(MajorStar.ZIWEI) print(merged.id, ziwei.name, ziwei.attributes.aliases, ziwei.attributes.chemistry, ziwei.intro) print(merged.pattern_intro(PatternKey.ZI_FU_TONG_GONG), merged.pattern(PatternKey.ZI_FU_TONG_GONG).quotes) print(pack.star_intro(MajorStar.ZIWEI)[:5]) try: pack.merged({"schema": 99}) except IztroError as e: print(e.code, e) ``` **Output** ```text my-school 紫微 ['帝座'] 尊贵 我的紫微 我的紫府同宫 ['紫府同宫终身福厚。'] 紫微星号称 invalid_argument knowledge pack schema 99 is newer than supported 1 ``` *** ## star / pattern / palace / mutagen / concept [#star--pattern--palace--mutagen--concept] **Purpose** Look up an entry by language-independent key. **Signature** ```python def star(self, key: str) -> StarEntry | None def pattern(self, key: str) -> PatternEntry | None def palace(self, key: str) -> TextEntry | None def mutagen(self, key: str) -> TextEntry | None def concept(self, slug: str) -> ConceptEntry | None ``` **Returns** `None` when the pack has no such entry. **Example** ```python from x_iztro import KnowledgePack, Mutagen, PalaceName from x_iztro.enums import MajorStar pack = KnowledgePack.builtin() ziwei = pack.star(MajorStar.ZIWEI) print(ziwei.key, ziwei.name, ziwei.category, ziwei.attributes.dipper) print(ziwei.attributes.aliases) print(sorted(ziwei.combinations)[:5]) print(pack.palace(PalaceName.SOUL).name, pack.mutagen(Mutagen.LU).name) print(pack.concept("tong-gong").title) print(pack.star("nope")) ``` **Output** ```text ziweiMaj 紫微 major 中天星系 ['帝王星', '老板星', '俸禄星'] ['pojunMaj', 'qishaMaj', 'tanlangMaj', 'tianfuMaj', 'tianxiangMaj'] 命宫 化禄 遇、加、逢、同宫、同度 None ``` *** ## stars / patterns [#stars--patterns] **Purpose** List every star / pattern entry in the pack. **Signature** ```python def stars(self) -> list[StarEntry] def patterns(self) -> list[PatternEntry] ``` **Returns** Sorted by key, each entry carrying its own `key`. The default pack gives 162 and 64. Use these when walking the whole pack — indexing, exporting, feeding an LLM — instead of digging through `to_dict()`. *** ## star\_intro / pattern\_intro [#star_intro--pattern_intro] **Purpose** Get the reading text directly. **Signature** ```python def star_intro(self, key: str) -> str | None def pattern_intro(self, key: str) -> str | None ``` **Returns** `None` both when the entry is missing and when it exists without a reading. **Example** List the natal patterns with their quotations: ```python from x_iztro import Astro, KnowledgePack pack = KnowledgePack.builtin() chart = Astro().by_solar("2000-8-16", 2, "female") for hit in chart.patterns(): print(hit.name, "|", pack.pattern(hit.key).quotes[0]) print(pack.pattern_intro(hit.key)[:10]) ``` **Output** ```text 府相朝垣 | 府相朝垣命必荣 “食禄千锺”的断语使 ``` # Reverse lookup (/en/docs/python/reverse) solar_dates_by_bazi and reverse_chart - the functions and dataclasses for recovering candidate birth dates from BaZi pillars or chart features. Recover candidate birth dates from four BaZi pillars or from chart features. All computation runs in the Rust core (pruned enumeration + full re-charting, zero divergence from forward charting); the Python side is a typed wrapper. Concepts, how pillars follow the Config boundaries, and the multi-solution / truncation semantics are on the [reverse lookup guide](/en/docs/guide/guides/reverse). ```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), ) ``` Everything is defined in `x_iztro.reverse` and re-exported at the package root. Stems, branches, classes and stars all take language-independent keys: members of the `x_iztro.enums` enums or the equivalent strings. ## Types [#types] ### Type alias Pillar [#type-alias-pillar] ```python Pillar = tuple[HeavenlyStem | str, EarthlyBranch | str] ``` One pillar: (stem key, branch key), e.g. `(HeavenlyStem.GENG, EarthlyBranch.CHEN)`. ### BirthCandidate [#birthcandidate] Frozen dataclass. One candidate birth moment, ready to hand to [`Astro.by_solar`](/en/docs/python/astro). | Field | Type | Meaning | | ------------ | ----- | ------------------------------------------------------ | | `solar_date` | `str` | solar date, `YYYY-M-D` | | `time_index` | `int` | hour index 0–12 (0 = early Zi hour, 12 = late Zi hour) | ### StarPosition [#starposition] Frozen dataclass. A star and the branch of the palace it sits in: the atomic condition of a feature lookup. | Field | Type | Meaning | | -------- | ---------------------- | -------------------------------------------------------------------------- | | `star` | `str` | star key (natal chart stars only; horoscope-scope flow stars are rejected) | | `branch` | `EarthlyBranch \| str` | branch key of its palace | ### ReverseCriteria [#reversecriteria] Frozen dataclass. The condition set of a feature lookup; every field is optional, but at least one must be given. | Field | Type | Default | Meaning | | --------------------- | ---------------------------------- | -------------- | -------------------------------------------------------------- | | `soul_branch` | `EarthlyBranch \| str \| None` | `None` | soul palace branch | | `body_branch` | `EarthlyBranch \| str \| None` | `None` | body palace branch | | `five_elements_class` | `FiveElementsClass \| str \| None` | `None` | five elements class | | `stars` | `list[StarPosition]` | `[]` | star placements, all of which must hold | | `mutagens` | 4-tuple of `str \| None` | all `None` | which star carries each birth-year mutagen \[Lu, Quan, Ke, Ji] | | `year_range` | `tuple[int, int]` | `(1900, 2100)` | inclusive solar year range, within 1583–9999 | | `fix_leap` | `bool` | `True` | leap month correction, same meaning as the charting parameter | | `limit` | `int` | `0` | candidate cap; `0` takes the core default (512) | ### ReverseResult [#reverseresult] Frozen dataclass. | Field | Type | Meaning | | ------------ | ---------------------- | ------------------------------------------------------------------------------------------ | | `candidates` | `list[BirthCandidate]` | the birth candidates satisfying every condition | | `truncated` | `bool` | whether the search stopped early at the candidate cap; later solutions were never searched | *** ## solar\_dates\_by\_bazi [#solar_dates_by_bazi] Recover solar birth dates from four BaZi pillars. ```python def solar_dates_by_bazi( yearly: Pillar, monthly: Pillar, daily: Pillar, hourly: Pillar, *, year_range: tuple[int, int] = (1900, 2100), config: ChartConfig | None = None, ) -> list[BirthCandidate] ``` The pillars are interpreted under the boundary readings of `config` (`year_divide` for the year pillar, `horoscope_divide` for the month pillar, `day_divide` for the late Zi hour) — the same semantics as the `raw_dates.chinese_date` a charted astrolabe reports, so reversing any chart's pillars always includes that chart's birth moment. A set of pillars recurs roughly every 60 years within the range; an hour branch of Zi may yield two candidates on adjacent days because of the early/late Zi hour split. **Example** ```python from x_iztro import Astro, solar_dates_by_bazi a = Astro().by_solar("2000-8-16", 2, "female") p = a.raw_dates.chinese_date for c in solar_dates_by_bazi(p.yearly_keys, p.monthly_keys, p.daily_keys, p.hourly_keys): print(c.solar_date, c.time_index) ``` **Output** ```text 1940-8-31 2 2000-8-16 2 2060-8-1 2 ``` Note that these are the key fields (`yearly_keys` and friends), not the display fields (`yearly` and friends) — the latter hold translated text, and passing one raises `unknown heavenly stem key '庚'`. **Raises** `IztroError` with `code` `invalid_argument` for a pillar with mismatched stem/branch polarity (such as 甲丑 Jia-Chou — a yang stem on a yin branch) or a year range that is reversed or outside 1583–9999. See [Error handling](/en/docs/python/errors). *** ## reverse\_chart [#reverse_chart] Recover candidate birth dates from chart features. ```python def reverse_chart( criteria: ReverseCriteria, config: ChartConfig | None = None, ) -> ReverseResult ``` Judgement runs entirely under `config`: the mutagen table, the school and every boundary follow it, so charting a candidate with the same `config` is guaranteed to satisfy every condition. Chart layout does not depend on gender (gender only affects the direction the decadal horoscope advances), so the criteria carry no gender. **Example** ```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), year_range=(1998, 2002), )) print(len(r.candidates), r.truncated) ``` **Output** ```text 39 False ``` **Raises** `IztroError` with `code` `invalid_argument` for empty criteria, a horoscope-scope flow star in `stars`, or an invalid year range. Reaching `limit` stops the search; later solutions never appear in the result. On `truncated = True`, narrow `year_range` or add conditions and query again. # Extending the astrolabe (/en/docs/python/extend) Attaching custom analysis methods to the Astrolabe class with plugins. Zi Wei analysis rules differ from practitioner to practitioner and no library can enumerate them. x-iztro lets you attach your own rules as methods on the astrolabe class — the call syntax matches the built-in methods, and every astrolabe instance gets them. ## The recipe [#the-recipe] A plugin is a function that takes the `Astrolabe` class and attaches methods to it. Write a function taking a `type[Astrolabe]` Define the methods inside it and assign them onto the class Load it with `load_plugin` ```python from x_iztro import Astro, Astrolabe, PalaceName from x_iztro.plugin import load_plugin def my_analysis(cls: type[Astrolabe]) -> None: """Adds two custom analysis methods to the astrolabe.""" def major_star(self) -> str: """Major stars of the Soul palace (borrowing the opposite palace when empty), comma separated""" 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) def five_elements_value(self) -> int: """The number of the five elements class""" return int(self.five_elements_class_key[-3]) cls.major_star = major_star cls.five_elements_value = five_elements_value load_plugin(my_analysis) ``` **Usage** ```python chart = Astro().by_solar("2000-8-16", 2, "female", language="en-US") print(chart.major_star()) # the extension method follows the charting language zh = Astro().by_solar("2000-8-16", 2, "female") print(zh.major_star()) ``` **Output** ```text emperor 紫微 ``` *** ## load\_plugin / load\_plugins [#load_plugin--load_plugins] **Signature** ```python def load_plugin(plugin: Plugin) -> None def load_plugins(plugins: Iterable[Plugin]) -> None ``` `Plugin` is typed as `Callable[[type[Astrolabe]], None]`. **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ------------------ | -------- | ------- | --------------------------------------- | | `plugin` | `Plugin` | Yes | — | A function taking the `Astrolabe` class | | `plugins` | `Iterable[Plugin]` | Yes | — | Several plugins, loaded in order | **Return value** `None`. The methods land directly on the class. **Edge cases and pitfalls** The methods go on the **class**, not on instances, so load order does not matter for existing instances — a chart made earlier can call the new methods once the plugin is loaded. `load_plugin` raises `TypeError` when passed something that is not callable. `load_plugins` loads one at a time and errors on the first non-callable, leaving the remaining plugins unloaded. A plugin modifies the `Astrolabe` class itself, so every chart in the process is affected. A method of the same name is overwritten by a later plugin. *** ## Why this can be attached at all [#why-this-can-be-attached-at-all] `Astrolabe` is a frozen dataclass with `slots=True`, so nothing can be attached to an instance: ```python try: chart.foo = 1 except Exception as e: print(type(e).__name__, e) ``` **Output** ```text FrozenInstanceError cannot assign to field 'foo' ``` Attaching methods to the **class** is unaffected, and that is exactly the granularity a plugin wants: a plugin says "every astrolabe has this method", not "this one chart has an extra field". *** ## Extending other types [#extending-other-types] The same recipe works for palaces and stars — just attach the methods to the corresponding class: ```python from x_iztro.models import Palace def palace_analysis(cls: type[Palace]) -> None: def is_afflicted(self) -> bool: """Whether this palace is "afflicted": holds a malefic and carries ji""" sha = ["qingyangMin", "tuoluoMin", "huoxingMin", "lingxingMin", "dikongMin", "dijieMin"] return self.has_one_of(sha) and self.has_mutagen("sihuaJi") cls.is_afflicted = is_afflicted palace_analysis(Palace) ``` ```python for p in chart.palaces: if p.is_afflicted(): print(p.name, "is afflicted") ``` **Output** ```text health is afflicted ``` `load_plugin` only accepts plugins that act on `Astrolabe`. To attach to another class, call the function directly as above — there is no magic in the plugin mechanism itself. *** ## How to organize this [#how-to-organize-this] Keep `wealth_analysis`, `career_analysis` and `health_analysis` as separate plugins and `load_plugin` what you need. One large plugin forces every consumer to load every method. `s.key == MajorStar.ZIWEI` holds under any output language; `s.name == "紫微"` holds only on a Chinese chart. Use `name` at display time only. Attached methods are invisible to static type checkers, and call sites get flagged as unknown attributes. When you want type friendliness, declare a Protocol for the extended astrolabe or use `cast`. # Error handling (/en/docs/python/errors) The code classification of IztroError, what triggers it, the message format, and handling patterns. Every entry point taking external input raises `IztroError` on invalid arguments. Date format, date existence, the year range and the hour index are validated up front in the core; string values such as gender, language, keys and configuration are validated in the binding layer. ```python from x_iztro import IztroError ``` `IztroError` subclasses `ValueError`, so an existing `except ValueError` still catches it and no code has to change on upgrade. ## code [#code] The exception message is written for humans and its wording may be adjusted between versions; branch on `.code` in your programs: | `code` | Meaning | Typical trigger | | -------------------- | ------------------------------------------- | --------------------------------------------------------- | | `invalid_date` | Illegal date | Bad format, a date that does not exist, outside 1583–9999 | | `invalid_time_index` | Hour index out of range | Not within 0–12 | | `invalid_argument` | Any other illegal argument or configuration | Gender, language, star key, palace name, custom tables | | `internal` | A defect inside the library | Should be reported as a bug | These four values are the same set as Rust's `IztroError::code()` and Go's `iztro.Error.Code`, so cross-language branching logic transfers verbatim. ```python from x_iztro import Astro, IztroError for args in [("2000-2-30", 2, "female"), ("2000-8-16", 13, "female"), ("2000-8-16", 2, "x")]: try: Astro().by_solar(*args) except IztroError as e: print(e.code, "|", e) ``` **Output** ```text invalid_date | invalid solar date '2000-2-30': day is out of range for that month invalid_time_index | time_index must be 0-12, got 13 invalid_argument | invalid gender 'x': expected 'male' or 'female' ``` A panic caused by a defect inside the library is caught in the binding layer and converted into an `IztroError` with the code `internal`; a `pyo3_runtime.PanicException`, which `except Exception` cannot catch, never escapes. *** ## Date-related [#date-related] | Situation | Example | Message | | ---------------------------------- | ------------- | ------------------------------------ | | The format is not `YYYY-M-D` | `"2000/8/16"` | `expected 'YYYY-M-D'` | | Year, month or day is not a number | `"abc-8-16"` | `year is not a number` | | Month out of range | `"2000-13-1"` | `month must be within 1-12` | | That month has no such day | `"2000-2-30"` | `day is out of range for that month` | | Year outside the supported range | `"1500-1-1"` | `year must be within 1583-9999` | Lunar-only (reachable through `by_lunar` and the two `*_by_lunar_date` queries): | Situation | Example | Message | | --------------------------------- | ------------------------------------- | ------------------------------------------ | | That lunar year has no such month | A month missing from the table | `month does not exist in that lunar year` | | That lunar month has no such day | `"2000-7-30"` (a short seventh month) | `day is out of range for that lunar month` | Lunar messages are prefixed `invalid lunar date '': ` and solar ones `invalid solar date '': `, so the message alone tells you which entry point was used. **Example** ```python from x_iztro import Astro for date in ["2000-13-1", "2000-2-30", "1500-1-1"]: try: Astro().by_solar(date, 2, "female") except IztroError as e: print(e) ``` **Output** ```text invalid solar date '2000-13-1': month must be within 1-12 invalid solar date '2000-2-30': day is out of range for that month invalid solar date '1500-1-1': year must be within 1583-9999 ``` The message carries the original input, so batch jobs can pinpoint which record failed. **Edge cases and pitfalls** The Gregorian reform year of 1582 contains a stretch of dates that never existed. The underlying calendar library has no definition for them, so support starts from 1583, after the reform. The upper bound of 9999 is where the lunar data tables end. Both `"2000-8-16"` and `"2000-08-16"` are accepted. The separator must be `-`. `by_lunar` checks whether that month really exists in that lunar year and how many days it has (30 in a long month, 29 in a short one). Passing `True` for `is_leap_month` when that year and month have no leap month is not an error; the parameter is silently ignored. *** ## Hour index [#hour-index] **Trigger** An hour index outside 0–12. **Example** ```python try: Astro().by_solar("2000-8-16", 13, "female") except IztroError as e: print(e) ``` **Output** ```text time_index must be 0-12, got 13 ``` **Edge cases and pitfalls** The Zi hour straddles midnight and splits into the early Zi hour (index 0) and the late Zi hour (index 12), so there are 13 legal values. To convert from a clock hour use [`utils.time_to_index`](/en/docs/python/util#time_to_index), which is guaranteed to land in the legal range. *** ## Gender and language [#gender-and-language] The `code` is `invalid_argument` in both cases. | Parameter | Legal values | Message | | ---------- | ---------------------- | --------------------------------------------------------------------------------- | | `gender` | `"male"` / `"female"` | `invalid gender 'x': expected 'male' or 'female'` | | `language` | The six language codes | `invalid language 'xx': expected one of zh-CN, zh-TW, en-US, ja-JP, ko-KR, vi-VN` | Language codes are case-insensitive; both `"zh-cn"` and `"zh-CN"` are accepted. Passing the `Gender` / `Language` enum members instead lets the IDE stop a typo where it is written. *** ## Key-related [#key-related] The utility and star-placement functions take language-independent keys, and an unknown key is an error: ```python from x_iztro import utils try: utils.get_brightness("nosuchstar", 0) except IztroError as e: print(e.code, "|", e) ``` **Output** ```text invalid_argument | unknown star key 'nosuchstar' ``` These functions recognize keys, not translated names. Passing `"紫微"` gives `unknown star key '紫微'` — convert with [`i18n.key_of`](/en/docs/python/i18n#key_of) first. The **query methods** on a chart (`chart.palace`, `chart.star`, `palace.has`) follow a different rule: they accept both keys and translations in the current language, and on no match they silently return `None` / `False` rather than raising. See [the astrolabe object](/en/docs/python/astrolabe#palace). *** ## Configuration-related [#configuration-related] The six switches and the two custom tables of `ChartConfig` are all validated during charting, and the `code` is `invalid_argument` throughout: | Situation | Message | | ----------------------------------------------- | -------------------------------------------------------------------------------- | | An unknown switch value | `invalid yearDivide 'nope': expected 'normal' or 'exact'` | | An unknown stem key in the mutagen table | `invalid mutagens key 'nope': unknown heavenly stem` | | A stem's mutagens are not four entries | `invalid mutagens for 'jiaHeavenly': expected 4 stars (lu, quan, ke, ji), got 1` | | A star's brightness table is not twelve entries | `invalid brightness for 'ziweiMaj': expected 12 entries, got 1` | The lengths are **strictly** validated: mutagens must be exactly four entries and brightness exactly twelve, and one entry too many or too few is an error. The custom tables take keys only, never translated names. *** ## Handling patterns [#handling-patterns] **Skip bad rows in a batch** ```python rows = [ {"date": "2000-8-16", "ti": 2, "gender": "female"}, {"date": "2000-2-30", "ti": 2, "gender": "female"}, {"date": "1990-3-3", "ti": 13, "gender": "male"}, ] charts, failed = [], [] astro = Astro() for row in rows: try: charts.append(astro.by_solar(row["date"], row["ti"], row["gender"], language="en-US")) except IztroError as e: failed.append((row["date"], e.code)) print(len(charts), failed) ``` **Output** ```text 1 [('2000-2-30', 'invalid_date'), ('1990-3-3', 'invalid_time_index')] ``` **Convert to your own exception type** ```python class ChartError(Exception): def __init__(self, code: str, message: str): super().__init__(message) self.code = code def build(date: str, ti: int, gender: str): try: return Astro().by_solar(date, ti, gender, language="en-US") except IztroError as e: raise ChartError(e.code, f"charting failed: {e}") from e try: build("2000-2-30", 2, "female") except ChartError as e: print(e.code, "|", e) ``` **Output** ```text invalid_date | charting failed: invalid solar date '2000-2-30': day is out of range for that month ``` Once `code` is carried outward, the layer above never has to parse the message text. # Overview (/en/docs/go) The package layout, the type system, and how to read this reference. The Go package embeds a WebAssembly build of the core and calls it through wazero, a runtime written in pure Go — **no cgo required**, with cross-compilation and static linking left intact. This section is the complete Go API reference — every exported function, type and method has its own entry. ## Install [#install] ```bash go get github.com/x-haose/x-iztro/go/iztro ``` ```go import "github.com/x-haose/x-iztro/go/iztro" ``` ## Your first chart [#your-first-chart] ```go chart, err := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) if err != nil { log.Fatal(err) } fmt.Println(chart.SolarDate, chart.LunarDate) // 2000-8-16 二〇〇〇年七月十七 soul := chart.Palace(iztro.PalaceSoul) if len(soul.MajorStars) > 0 { fmt.Println(soul.MajorStars[0].Name) // emperor } else { fmt.Println("the Soul palace is empty, borrowing from the opposite palace:", soul.OppositePalace().MajorStars[0].Name) } ``` `LunarDate` stays in Chinese under every language — it is a lunar date written in Chinese numerals. `二〇〇〇年七月十七` is the 17th day of the 7th lunar month of 2000. A chart usually has two palaces with no major star (empty palaces), and the Soul palace may well be one of them. Writing `soul.MajorStars[0]` straight out panics on such a chart — check the length first, or branch on [`IsEmpty`](/en/docs/go/palace#isempty) and borrow from the opposite palace. ## Package layout [#package-layout] The package is flat; everything exported lives under `iztro`. By topic: | Topic | Main exports | Page in this reference | | ------------------- | ----------------------------------------------------- | -------------------------------------------------------------------- | | Charting | `BySolar`, `ByLunar`, `Rearranged` | [Charting entries](/en/docs/go/astro) | | Data types | `Astrolabe`, `Palace`, `Star`, `Horoscope`, `Config` | The four pages from [Astrolabe object](/en/docs/go/astrolabe) onward | | Key constants | `PalaceSoul`, `StarZiweiMaj`, `MutagenLu` and so on | [Data tables](/en/docs/go/data) | | Lightweight queries | `GetZodiacBySolarDate` and friends | [Lightweight queries](/en/docs/go/query) | | Utilities | `FixIndex`, `GetBrightness` and friends | [Utilities](/en/docs/go/util) | | Star placement | `GetMajorStar`, `GetHoroscopeStar` and friends | [Star placement](/en/docs/go/star) | | Data tables | `StarsInfo`, `HeavenlyStems` and friends | [Data tables](/en/docs/go/data) | | Translation | `Translate`, `KeyOf`, `KeyOfIn` | [Translation](/en/docs/go/i18n) | | Errors | `*Error`, the `Err*` sentinels, the `Code*` constants | [Error handling](/en/docs/go/errors) | | Runtime | `Warmup`, `Close`, `CompilationCacheDir` | Further down this page | ## Constants are the keys [#constants-are-the-keys] The key constants in the package have the language-independent keys as their values, so they compare directly against the `*Key` fields on the data objects: ```go soul := chart.Palace(iztro.PalaceSoul) fmt.Println(soul.MajorStars[0].Key == iztro.StarZiweiMaj) // true ``` They are all untyped string constants, so string literals work just as well — `chart.Palace("soulPalace")` and `chart.Palace(iztro.PalaceSoul)` are equivalent. The constants earn their keep through IDE completion and spell checking. `star.Name` varies with the charting language (`紫微` on a Chinese chart, `emperor` on an English one); `star.Key` is `ziweiMaj` under any language. Every predicate should rest on the `*Key` fields or on the built-in predicate methods. ## Variadic parameters [#variadic-parameters] Methods taking a list of stars or mutagens are variadic throughout, so call sites need not build a slice: ```go soul := chart.Palace(iztro.PalaceSoul) target := chart.Palace(iztro.PalaceWealth) fmt.Println(soul.Has(iztro.StarZiweiMaj, iztro.StarTianxiangMaj)) fmt.Println(soul.FliesTo(target, iztro.MutagenLu, iztro.MutagenJi)) ``` **Output** ```text false false ``` Expand an existing slice with `...`: ```go soul := chart.Palace(iztro.PalaceSoul) stars := []string{iztro.StarZiweiMaj, iztro.StarTianxiangMaj} fmt.Println(soul.Has(stars...)) ``` **Output** ```text false ``` The star parameter of the `HasHoroscopeStars` family is a `[]string` rather than variadic, because two string parameters (palace name and scope) already precede it and a variadic list would make call sites ambiguous. ## Error handling [#error-handling] Entry points that compute something return `(value, error)`; pure query methods (`Palace`, `Star`, `Has` and so on) return no error and give `nil` or a zero value when nothing is found. ```go _, err := iztro.BySolar("2000-13-1", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) fmt.Println(err) fmt.Println(errors.Is(err, iztro.ErrInvalidDate)) var e *iztro.Error if errors.As(err, &e) { fmt.Println(e.Code) } ``` **Output** ```text iztro: invalid solar date '2000-13-1': month must be within 1-12 true invalid_date ``` Every error is an `*iztro.Error` carrying a machine-readable `Code`, and `errors.Is` matches it against the four sentinels. See [Error handling](/en/docs/go/errors). ## Runtime and performance [#runtime-and-performance] The wasm module is **compiled once and instantiated on demand**: each instance owns its linear memory, so concurrent calls take an instance each rather than sharing one behind a lock, and the number of instances is capped at `GOMAXPROCS`. There is no global mutex on the charting hot path — instances are handed out over a channel, and the lock is taken only when the runtime first initializes and on `Close`. Several goroutines charting at once therefore run genuinely in parallel: on the same ten-core machine, 8 goroutines running 800 charts finish more than four times faster than a single goroutine. The compiled machine code is cached on disk under the user cache directory (`CompilationCacheDir` reports it), bucketed by the hash of the wasm contents, so a new wasm naturally lands in a new bucket. Measured magnitudes (Apple M series, 10 cores; the exact figures move with the machine and the size of the wasm): | Stage | Time | | -------------------------------------------------------------------- | ------------------------------- | | First call, compilation cache **miss** (the wasm has to be compiled) | one or two hundred milliseconds | | First call, compilation cache hit | twenty to thirty milliseconds | | Steady-state single chart | around half a millisecond | Most of that steady-state half millisecond goes on serializing the whole chart to JSON on the wasm side and deserializing it back into structs on the Go side, not on the Zi Wei Dou Shu computation itself. When you only need a field or two, a [lightweight query](/en/docs/go/query) (`GetMajorStarBySolarDate` and friends) is far cheaper than charting in full. ### Warmup / Close / CompilationCacheDir [#warmup--close--compilationcachedir] ```go func Warmup(ctx context.Context) error func Close(ctx context.Context) error func CompilationCacheDir(ctx context.Context) (string, error) ``` | Function | Description | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Warmup` | Compiles ahead of time and fills the instance pool, moving the cold-start cost into startup. Skipping it is fine — compilation and instantiation are lazy anyway | | `Close` | Shuts the runtime down and returns all instance memory. Rarely needed; calling any function in this package afterwards re-initializes it automatically | | `CompilationCacheDir` | Returns the compilation cache directory; empty string when the on-disk cache is not enabled | ```go ctx := context.Background() if err := iztro.Warmup(ctx); err != nil { log.Fatal(err) } dir, err := iztro.CompilationCacheDir(ctx) if err != nil { log.Fatal(err) } fmt.Println(dir != "") chart, err := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) if err != nil { log.Fatal(err) } fmt.Println(chart.SolarDate) ``` **Output** ```text true 2000-8-16 ``` A service process that wants its very first request on the hot path only has to call `Warmup` once during startup. ### Context variants [#context-variants] The entry points that cross into wasm — charting, horoscope, rearranging, text projection — each have a `*Context` version taking one extra `context.Context`: | Without ctx | With ctx | | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------- | | `BySolar` / `ByLunar` | `BySolarContext` / `ByLunarContext` | | `Astrolabe.Horoscope` / `HoroscopeNow` | `HoroscopeContext` / `HoroscopeNowContext` | | `Astrolabe.Rearranged` | `RearrangedContext` | | `Astrolabe.ToText` / `Horoscope.ToText` / `PalaceToText` / `SurroundedPalacesToText` | `ToTextContext` / `PalaceToTextContext` / `SurroundedPalacesToTextContext` | | — | `Warmup` / `Close` / `CompilationCacheDir` exist only in the ctx form | ```go ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() chart, err := iztro.BySolarContext(ctx, "2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) if err != nil { log.Fatal(err) } fmt.Println(chart.SolarDate) ``` **Output** ```text 2000-8-16 ``` `ctx` cancels the wait for **a free wasm instance**. Once an instance is in hand the computation on the wasm side cannot be interrupted — a single chart is sub-millisecond to begin with, so there is no long task to break off. On those two architectures wazero uses its optimizing compiler (down to machine code). Everything else falls back to the interpreter, which still produces correct results but is more than an order of magnitude slower. Deploy on amd64 or arm64 in production. ## 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** — 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/go/astro) BySolar, ByLunar, Rearranged and the semantic text projection. Charting is where everything starts: give a birth date, hour and gender, get an `*Astrolabe`. Every entry point returns an `error`. Date format and existence, the solar year range, the hour index, gender, language and configuration are all validated up front in the core. See [Error handling](/en/docs/go/errors). *** ## BySolar [#bysolar] **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 `YearDivide` — 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** ```go func BySolar( solarDate string, timeIndex uint8, gender Gender, fixLeap bool, language Language, config *Config, ) (*Astrolabe, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | ----------- | ---------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `solarDate` | `string` | Yes | — | Solar date in `YYYY-M-D`; month and day need no zero padding. Years 1583–9999 | | `timeIndex` | `uint8` | 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 | — | `GenderMale` or `GenderFemale` (the literals `"male"`/`"female"` also work). Sets the direction of the decadal scope and of the Changsheng and Boshi gods | | `fixLeap` | `bool` | Yes | — | Whether to correct for lunar leap months. When true, the sixteenth of a leap month onward counts as the next month (the late Zi hour excepted, see below) | | `language` | `Language` | Yes | — | Chart language (`LanguageZhCN` and friends); affects every translated field. The `*Key` fields are unaffected | | `config` | `*Config` | Yes | — | Charting configuration; pass `nil` 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. **Example** ```go chart, err := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) if err != nil { log.Fatal(err) } fmt.Println(chart.SolarDate, "|", chart.LunarDate, "|", chart.ChineseDate) fmt.Println(chart.Sign, chart.Zodiac, chart.FiveElementsClass) fmt.Println("soul", chart.Soul, "body", chart.Body) ``` **Output** ```text 2000-8-16 | 二〇〇〇年七月十七 | geng chen - jia shen - bing woo - geng yin leo dragon wood 3rd soul rebel body scholar ``` `LunarDate` is a lunar date in Chinese numerals and stays Chinese under every language; `二〇〇〇年七月十七` is the 17th day of the 7th lunar month of 2000. **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. When unsure of the index, convert with `TimeToIndex(hour)`. The bump requires four conditions at once: that lunar month really is a leap month, `fixLeap` is `true`, the lunar day is greater than 15, and the hour index is not 12 (the late Zi hour). Miss any one of them and the month index stays with the current month. So only for someone born in the second half of a lunar leap month do `true` and `false` give different month indices, which in turn affects Zuofu, Youbi and every month-based star. `&Config{}` and `nil` behave identically — every field carries `omitempty` and an empty value does not override a default. But writing `nil` says "use the defaults" more clearly. *** ## ByLunar [#bylunar] **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 `BySolar` with the corresponding solar date. **Signature** ```go func ByLunar( lunarDate string, timeIndex uint8, gender Gender, leap LeapMonth, language Language, config *Config, ) (*Astrolabe, error) ``` **Parameters** Identical to `BySolar` apart from the following two; `BySolar`'s `fixLeap` is folded into `leap` here. | Parameter | Type | Required | Default | Description | | ----------- | ----------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `lunarDate` | `string` | 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 | — | `NotLeapMonth` — not a leap month; `LeapMonthKeep` — leap month, charted as itself; `LeapMonthFixed` — 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; any other value returns `ErrInvalidArgument` | **Return value** Same as `BySolar`. **Example** ```go a, _ := iztro.ByLunar("2000-7-17", 2, iztro.GenderFemale, iztro.NotLeapMonth, iztro.LanguageEnUS, nil) b, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) fmt.Println(a.SolarDate, a.SolarDate == b.SolarDate) ``` **Output** ```text 2000-8-16 true ``` **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. *** ## Config [#config] The charting configuration. Every field may be omitted, and omitting it takes the default. ```go type Config struct { YearDivide string HoroscopeDivide string AgeDivide string DayDivide string Algorithm string AstroType string Mutagens map[string][]string Brightness map[string][]string } ``` | Field | Values | Default | Description | | ----------------- | ---------------------------------- | ----------- | ------------------------------------------------------------------------------- | | `YearDivide` | `"normal"` / `"exact"` | `"normal"` | Whether the year pillar turns over at lunar New Year or the Beginning of Spring | | `HoroscopeDivide` | `"normal"` / `"exact"` | `"normal"` | Which boundary the yearly spirits take their year branch from | | `AgeDivide` | `"normal"` / `"birthday"` | `"normal"` | Whether the nominal age increments with the lunar year or with the birthday | | `DayDivide` | `"forward"` / `"current"` | `"forward"` | Whether the late Zi hour belongs to the next day or the current one | | `Algorithm` | `"default"` / `"zhongzhou"` | `"default"` | The algorithm school | | `AstroType` | `"heaven"` / `"earth"` / `"human"` | `"heaven"` | The charting perspective | | `Mutagens` | Stem key → four star keys | — | A custom mutagen table, replacing the whole table for a stem | | `Brightness` | Star key → twelve brightness keys | — | A custom brightness table, replacing the whole table for a star | **Example** ```go cfg := &iztro.Config{ Algorithm: iztro.AlgorithmZhongzhou, YearDivide: iztro.YearDivideExact, } chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, cfg) fmt.Println(chart.FiveElementsClass) ``` **Output** ```text wood 3rd ``` Every value set has matching constants, so you need not write the strings by hand: `YearDivideNormal` / `YearDivideExact`, `HoroscopeDivideNormal` / `HoroscopeDivideExact`, `AgeDivideNormal` / `AgeDivideBirthday`, `DayDivideForward` / `DayDivideCurrent`, `AlgorithmDefault` / `AlgorithmZhongzhou`, `AstroHeaven` / `AstroEarth` / `AstroHuman`. **Edge cases and pitfalls** `Mutagens["jiaHeavenly"]` must give all four entries (Lu, Quan, Ke, Ji) and `Brightness["ziweiMaj"]` all twelve; one entry too many or too few and charting fails (an `*Error` whose `Code` is `invalid_argument`). Stems and stars not listed keep the default table. Both tables accept keys and values as keys only, never as translated names. `chart.Config` is reconstructed from the output DTO and holds the six switches only — the two custom tables are charting **input** rather than result and do not enter the DTO (matching the field contract of JS iztro). The chart does keep the originals you passed in internally, so `Rearranged`, `Horoscope` and the the ToText projection still use those tables in their secondary computations; nothing is silently lost. To record the configuration, keep the `*Config` on your own call site. ```go cfg := &iztro.Config{ AstroType: iztro.AstroEarth, Mutagens: map[string][]string{ iztro.StemGeng: {iztro.StarTaiyangMaj, iztro.StarWuquMaj, iztro.StarTianfuMaj, iztro.StarTiantongMaj}, }, } chart, err := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, cfg) if err != nil { log.Fatal(err) } fmt.Println(chart.FiveElementsClass, chart.Config.AstroType) fmt.Println(chart.Config.Mutagens == nil) fmt.Println(chart.Palace(iztro.PalaceSoul).MutagenStarKeys) ``` **Output** ```text earth 5th earth true [tiantongMaj tianjiMaj wenchangMin lianzhenMaj] ``` Every field carries `omitempty`, so empty values never reach the JSON and never override a default. To change one switch, build a `&Config{...}` filling in only that field. *** ## 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. **Signature** ```go func (a *Astrolabe) Rearranged(fromStemKey string, fromBranchKey string) (*Astrolabe, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------- | -------- | -------- | ------- | --------------------------------- | | `fromStemKey` | `string` | Yes | — | Stem key of the new Soul palace | | `fromBranchKey` | `string` | Yes | — | Branch key of the new Soul palace | **Return value** A new `*Astrolabe`. 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, the soul star, 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, the Sui-qian and Jiang-qian gods, and the body star. On the rearranged chart, `Patterns`, horoscope queries and the ToText 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** ```go chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) // anchor on the original chart's Body palace pillar — equivalent to the earth chart var body *iztro.Palace for i := range chart.Palaces { if chart.Palaces[i].IsBodyPalace { body = &chart.Palaces[i] } } earth, _ := chart.Rearranged(body.HeavenlyStemKey, body.EarthlyBranchKey) fmt.Println("heaven", chart.FiveElementsClass, "→ earth", earth.FiveElementsClass) ``` **Output** ```text heaven wood 3rd → earth earth 5th ``` **Edge cases and pitfalls** For the heaven, earth and human charts just chart with `&Config{AstroType: iztro.AstroEarth}`; both charting entry points support it. `Rearranged` exists for anchoring on an arbitrary stem and branch. The body star 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. The soul star is looked up by the Soul palace branch and therefore does update. *** ## Semantic text (ToText) [#semantic-text-totext] **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 the JSON DTO (machine structure) and the translated fields (display), it is the third projection of the same object. **Signature** ```go func (a *Astrolabe) ToText() (string, error) func (h *Horoscope) ToText() (string, error) func (a *Astrolabe) PalaceToText(target PalaceTarget) (string, error) func (a *Astrolabe) SurroundedPalacesToText(target PalaceTarget) (string, error) ``` Each has a `Context` variant (`ToTextContext` etc.); the ctx cancels waiting for the wasm instance. Pattern text is `PatternsToText` — see [Patterns](/en/docs/go/patterns). **Parameters** | Parameter | Type | Required | Default | Description | | --------- | -------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `target` | `PalaceTarget` | Yes | — | Palace addressing: with a non-empty `Key` the palace is located by name key (`PalaceSoul` etc.; `PalaceBody` / `PalaceOriginal` are also accepted), otherwise by `Index` (0–11) | **Return value** `string` — sectioned plain text in the chart's own charting language; 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** ```go chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) text, _ := chart.ToText() fmt.Println(string([]rune(text)[:68])) ``` **Output** ```text === Basic Info === Gender: female Solar Date: 2000-8-16 Lunar Date: ``` **Edge cases and pitfalls** The output language follows the chart's charting language and is not set separately. For an English text, chart in English. # Astrolabe object (/en/docs/go/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. ```go chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) ``` The examples on this page chart with `"en-US"`, so the display values in the output are English. Charting in another language changes only those display strings; the `*Key` fields and the results of every predicate stay the same. ## Fields [#fields] | Field | Type | Description | | --------------------------- | -------- | ------------------------------------ | | `Gender` | `string` | Translated gender | | `SolarDate` | `string` | Solar date, as passed in | | `LunarDate` | `string` | The lunar date written in Chinese | | `ChineseDate` | `string` | Display string of the four pillars | | `Time` | `string` | Hour name | | `TimeRange` | `string` | The clock range of that hour | | `Sign` | `string` | Zodiac sign | | `Zodiac` | `string` | Chinese zodiac animal | | `Soul` | `string` | Translated soul star | | `Body` | `string` | Translated body star | | `FiveElementsClass` | `string` | Translated five elements class | | `EarthlyBranchOfSoulPalace` | `string` | Translated branch of the Soul palace | | `EarthlyBranchOfBodyPalace` | `string` | Translated branch of the Body palace | Display fields follow the charting language. For predicates use the `*Key` fields in the next group. | Field | Type | Description | | ------------------------------ | -------- | ----------------------------------- | | `GenderKey` | `Gender` | `GenderMale` / `GenderFemale` | | `SignKey` | `string` | Zodiac sign key, `aries` … `pisces` | | `ZodiacKey` | `string` | Zodiac animal key, `rat` … `pig` | | `SoulKey` | `string` | Soul star key | | `BodyKey` | `string` | Body star key | | `FiveElementsClassKey` | `string` | Five elements class key | | `EarthlyBranchOfSoulPalaceKey` | `string` | Key of the Soul palace branch | | `EarthlyBranchOfBodyPalaceKey` | `string` | Key of the Body palace branch | The values correspond one to one with the key constants in the package and compare directly with `==`. | Field | Type | Description | | ---------- | ---------- | ----------------------------------------------------------------- | | `Palaces` | `[]Palace` | The twelve palaces; index 0 is the Yin palace, 11 the Chou palace | | `RawDates` | `RawDates` | The structured lunar birth date and the four-pillar keys | 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(iztro.PalaceSoul)`. `RawDates` is the data form of the two display strings `LunarDate` and `ChineseDate`. Use it for date arithmetic or for table lookups by stem and branch, instead of parsing the Chinese strings: ```go type RawDates struct { LunarDate RawLunarDate `json:"lunarDate"` ChineseDate RawChineseDate `json:"chineseDate"` } type RawLunarDate struct { LunarYear int `json:"lunarYear"` // lunar year LunarMonth int `json:"lunarMonth"` // lunar month 1–12; leap or not is IsLeap LunarDay int `json:"lunarDay"` // lunar day 1–30 IsLeap bool `json:"isLeap"` // whether it is a leap month } type RawChineseDate struct { Yearly [2]string `json:"yearly"` // year pillar as [stem, branch] text YearlyKeys [2]string `json:"yearlyKeys"` // year pillar keys Monthly [2]string `json:"monthly"` MonthlyKeys [2]string `json:"monthlyKeys"` Daily [2]string `json:"daily"` DailyKeys [2]string `json:"dailyKeys"` Hourly [2]string `json:"hourly"` HourlyKeys [2]string `json:"hourlyKeys"` } ``` The four `[2]string` name arrays hold the stem and branch as un-localized Chinese text under every output language; the `*Keys` arrays next to them are what you compare against. `RawChineseDate` also has a method `PillarKeys() [4][2]string` giving the keys of all four pillars at once in year, month, day, hour order — exactly the shape [`TranslateChineseDate`](/en/docs/go/util#translatechinesedate) takes as input: ```go rd := chart.RawDates fmt.Println(rd.LunarDate.LunarYear, rd.LunarDate.LunarMonth, rd.LunarDate.LunarDay, rd.LunarDate.IsLeap) fmt.Println(rd.ChineseDate.Yearly, rd.ChineseDate.YearlyKeys) fmt.Println(rd.ChineseDate.PillarKeys()) ``` **Output** ```text 2000 7 17 false [庚 辰] [gengHeavenly chenEarthly] [[gengHeavenly chenEarthly] [jiaHeavenly shenEarthly] [bingHeavenly wuEarthly] [gengHeavenly yinEarthly]] ``` | Field | Type | Description | | ----------- | ---------- | -------------------------------------------------------------------- | | `TimeIndex` | `uint8` | Birth hour index | | `FixLeap` | `bool` | Whether leap-month correction was applied when charting | | `Language` | `Language` | Chart language (`LanguageZhCN` and friends) | | `Config` | `Config` | Charting configuration — the six switches reconstructed from the DTO | Horoscopes, re-anchoring and prompts restart their computation from these four, so the charting parameters need not be supplied again. `chart.Config` is reconstructed from the output DTO and holds the six switches only; the custom mutagen and brightness tables passed in when charting are not in it. The chart does keep the caller's originals internally, so `Rearranged`, `Horoscope` and the prompts still use those two tables in their secondary computations — nothing is silently lost. *** ## Palace / PalaceByIndex [#palace--palacebyindex] **Purpose** Fetch a palace by name, as the Body palace or the palace of origin, or by index. **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 "palace of origin" is the one whose stem matches the birth-year stem, marking where matters originate. **Signature** ```go func (a *Astrolabe) Palace(nameKeyOrName string) *Palace func (a *Astrolabe) PalaceByIndex(index int) *Palace ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------- | -------- | -------- | ------- | ---------------------------------------------------------------------------------- | | `nameKeyOrName` | `string` | Yes | — | A palace-name key, `"bodyPalace"`, `"originalPalace"`, or a translated palace name | | `index` | `int` | Yes | — | Palace index 0–11, where 0 is the Yin palace | **Return value** `*Palace`. `nil` when the name is misspelled or the index is out of range; a palace name such as `"soulPalace"`, plus `"bodyPalace"` and `"originalPalace"`, resolves on every chart as long as it is spelled correctly. **Example** ```go soul := chart.Palace(iztro.PalaceSoul) fmt.Println(soul.Name, soul.HeavenlyStem+soul.EarthlyBranch) fmt.Println("body:", chart.Palace("bodyPalace").Name) fmt.Println("origin:", chart.Palace("originalPalace").Name) fmt.Println("Yin palace:", chart.PalaceByIndex(0).Name) ``` **Output** ```text soul renwoo body: career origin: spouse Yin palace: wealth ``` **Edge cases and pitfalls** The palace of origin requires the palace stem to equal the birth-year stem and the palace not to be Zi or Chou. The stems of the twelve palaces 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 each; Zi and Chou repeat the stems of Yin and Mao — and it is precisely that repetition that gets them excluded. So the birth-year stem is bound to hit exactly once between Yin and You: the palace of origin exists on every chart, and is unique. The Body palace likewise always exists. `nil` can therefore only come from an out-of-range index or a misspelled name. `chart.Palace("soulPalce")` (one `a` short) reports no error and simply returns `nil`; reading a field on it panics a step later, by which point the crash site is some way from the actual typo. The `Palace*` constants in the package let the compiler and the IDE stop it on the spot; when the name comes from outside, validate it through [`KeyOf`](/en/docs/go/i18n#keyof) first. Go has no union types, so this splits into `Palace` (taking a string) and `PalaceByIndex` (taking an int). The surrounded set does the same, with `SurroundedPalaces` and `SurroundedPalacesByIndex`. *** ## Star [#star] **Purpose** Find a star by key and get the palace it sits in at the same time. **Signature** ```go func (a *Astrolabe) Star(keyOrName string) (*Star, *Palace) ``` **Parameters** | Parameter | Type | Required | Default | Description | | ----------- | -------- | -------- | ------- | ------------------------------- | | `keyOrName` | `string` | Yes | — | A star key or a translated name | **Return value** `(*Star, *Palace)`. Both are `nil` when the star is not on this chart. **Example** ```go ziwei, palace := chart.Star(iztro.StarZiweiMaj) fmt.Println(ziwei.Name, "sits in", palace.Name) fmt.Println("its opposite palace is", ziwei.OppositePalace().Name) fmt.Println("brightness", ziwei.Brightness, "mutagen", ziwei.Mutagen) ``` **Output** ```text emperor sits in soul its opposite palace is surface brightness [+3] mutagen ``` An empty mutagen string means this star has no natal mutagen. **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.Changsheng12Key`. *** ## SurroundedPalaces / SurroundedPalacesByIndex [#surroundedpalaces--surroundedpalacesbyindex] **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 (the palace +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** ```go func (a *Astrolabe) SurroundedPalaces(nameKeyOrName string) *SurroundedPalaces func (a *Astrolabe) SurroundedPalacesByIndex(index int) *SurroundedPalaces ``` **Return value** `*SurroundedPalaces`, holding the four `*Palace`s `Target` / `Opposite` / `Wealth` / `Career`. `SurroundedPalaces` returns `nil` on a misspelled name; `SurroundedPalacesByIndex` takes the index modulo 12, so negative indices and indices above 11 wrap correctly and only a zero-valued chart (fewer than twelve palaces) gives `nil`. Its predicates are on [Surrounded palaces](/en/docs/go/surpalaces). **Example** ```go sp := chart.SurroundedPalaces(iztro.PalaceSoul) fmt.Println(sp.Target.Name, sp.Opposite.Name, sp.Wealth.Name, sp.Career.Name) fmt.Println("Ziwei in the surrounded set:", sp.Have(iztro.StarZiweiMaj)) ``` **Output** ```text soul surface wealth career Ziwei in the surrounded set: true ``` *** ## IsSurrounded / IsSurroundedOneOf / NotSurrounded [#issurrounded--issurroundedoneof--notsurrounded] **Purpose** Test the surrounded palaces of a palace straight from the chart, skipping the step of fetching the set first. **Signature** ```go func (a *Astrolabe) IsSurrounded(nameKeyOrName string, stars ...string) bool func (a *Astrolabe) IsSurroundedOneOf(nameKeyOrName string, stars ...string) bool func (a *Astrolabe) NotSurrounded(nameKeyOrName string, stars ...string) bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------- | ----------- | -------- | ------- | -------------------------------------- | | `nameKeyOrName` | `string` | Yes | — | A palace-name key or a translated name | | `stars` | `...string` | Yes | — | Star keys, variadic | **Return value** | Method | Meaning | | ------------------- | ----------------------------------------------------- | | `IsSurrounded` | **Every** listed star is in the surrounded set | | `IsSurroundedOneOf` | **At least one** listed star is in the surrounded set | | `NotSurrounded` | **None** of the listed stars is in the surrounded set | **Example** ```go fmt.Println(chart.IsSurrounded(iztro.PalaceSoul, iztro.StarZiweiMaj, iztro.StarTianxiangMaj)) fmt.Println(chart.IsSurroundedOneOf(iztro.PalaceSoul, iztro.StarQishaMaj, iztro.StarPojunMaj)) fmt.Println(chart.NotSurrounded(iztro.PalaceSoul, iztro.StarHuoxingMin)) ``` **Output** ```text true false true ``` The Soul palace holds only Ziwei, while Tianxiang sits in the Wealth palace, one of the trine — hence the first line is true. Neither Qisha nor Pojun is in any of the four, hence the second is false. **Edge cases and pitfalls** With no stars at all, `IsSurrounded` and `NotSurrounded` return `true` ("all elements satisfy" and "no element fails" both hold vacuously) while `IsSurroundedOneOf` returns `false`. *** ## Horoscope / HoroscopeNow [#horoscope--horoscopenow] **Purpose** Compute the horoscope for a target date, starting from this chart. **Signature** ```go func (a *Astrolabe) Horoscope(targetDate string, targetTimeIndex uint8) (*Horoscope, error) func (a *Astrolabe) HoroscopeNow() (*Horoscope, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | ----------------- | -------- | -------- | ------- | ---------------------------------------------------- | | `targetDate` | `string` | Yes | — | Target solar date in `YYYY-M-D` | | `targetTimeIndex` | `uint8` | Yes | — | Target hour index 0–12, which fixes the hourly scope | `HoroscopeNow` takes the current date and hour from the local clock and has no parameters. **Return value** `*Horoscope` — a horoscope object holding this chart, so palace lookups across the six scopes need not be passed the astrolabe again. Details on [the horoscope object](/en/docs/go/horoscope). **Example** ```go h, _ := chart.Horoscope("2025-6-1", 0) fmt.Println("decadal", h.Decadal.HeavenlyStem+h.Decadal.EarthlyBranch) fmt.Println("yearly ", h.Yearly.HeavenlyStem+h.Yearly.EarthlyBranch) ``` **Output** ```text decadal gengchen yearly yisi ``` *** ## ToText / PalaceToText / SurroundedPalacesToText [#totext--palacetotext--surroundedpalacestotext] **Purpose** Semantic text for the chart, a single palace or the surrounded palaces: a complete description for language models and people. **Signature** ```go func (a *Astrolabe) ToText() (string, error) func (a *Astrolabe) PalaceToText(target PalaceTarget) (string, error) func (a *Astrolabe) SurroundedPalacesToText(target PalaceTarget) (string, error) ``` Each has a `Context` variant. With a non-empty `Key`, `PalaceTarget` locates the palace by name key (`PalaceSoul` etc.; `PalaceBody` / `PalaceOriginal` are also accepted), otherwise by `Index` (0–11). Emits in the charting language; the full format is on [Semantic text](/en/docs/guide/guides/to-text). **Example** ```go text, _ := chart.PalaceToText(iztro.PalaceTarget{Key: iztro.PalaceSoul}) fmt.Println(strings.Split(text, "\n")[0]) ``` **Output** ```text --- soul --- ``` For pattern text see `PatternsToText` on [Patterns](/en/docs/go/patterns). *** ## How it relates to JSON [#how-it-relates-to-json] `Astrolabe` and every type beneath it carry `json` tags whose names match the field contract of JS iztro. `json.Marshal(chart)` is therefore already the DTO you can hand to a frontend or another process: ```go b, err := json.Marshal(chart) if err != nil { log.Fatal(err) } var v map[string]any _ = json.Unmarshal(b, &v) fmt.Println(v["solarDate"], v["genderKey"], v["timeIndex"]) fmt.Println(v["palaces"].([]any)[4].(map[string]any)["nameKey"]) ``` **Output** ```text 2000-8-16 female 2 soulPalace ``` The custom mutagen and brightness tables in `Config` do not enter the JSON — they are charting **input** rather than result, and echoing them back would break the field contract with JS iztro. # Palace object (/en/docs/go/palace) The fields of Palace plus every star predicate, empty-palace check and flying-star method. Palaces are where most Zi Wei analysis happens. `chart.Palace(...)` returns a `*Palace`, which both holds the palace's data and can trace back to its astrolabe, its opposite palace and its surrounded set. ```go soul := chart.Palace(iztro.PalaceSoul) ``` The examples on this page chart with `"en-US"`, so the display values in the output are English. Every method on `*Palace` checks for a nil receiver and returns a zero value rather than panicking; **reading a field** still panics, so the nil check remains yours to make. ## Fields [#fields] | Field | Type | Description | | ------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------- | | `Index` | `int` | Palace index 0–11, where 0 is the Yin palace | | `Name` / `NameKey` | `string` | Translated palace name / its key | | `IsBodyPalace` | `bool` | Whether this is the Body palace | | `IsOriginalPalace` | `bool` | Whether this is the palace of origin (stem equal to the year stem, and not the Zi or Chou palace) | | `HeavenlyStem` / `HeavenlyStemKey` | `string` | Palace stem, which determines the mutagens this palace flies out | | `EarthlyBranch` / `EarthlyBranchKey` | `string` | Palace branch, fixed by the index: 0 is yin, 11 is chou | | `MajorStars` | `[]Star` | Whichever of the fourteen major stars fall here, in placement order | | `MinorStars` | `[]Star` | Whichever of the fourteen minor stars fall here | | `AdjectiveStars` | `[]Star` | Adjective stars | | `Changsheng12` / `Changsheng12Key` | `string` | The Changsheng god of this palace, exactly one per palace | | `Boshi12` / `Boshi12Key` | `string` | The Boshi god | | `Jiangqian12` / `Jiangqian12Key` | `string` | The Jiang-qian god | | `Suiqian12` / `Suiqian12Key` | `string` | The Sui-qian god | | `Decadal` | `Decadal` | The decadal: age range plus stem and branch | | `Ages` | `[]int` | Nominal ages at which the age scope passes through this palace | | `MutagenStarKeys` | `[4]string` | Keys of the four stars transformed by this palace's **own stem**, in the order lu, quan, ke, ji | Major, minor and adjective stars are **slices** — 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 slices. It is computed from the mutagen table **in effect when charting** — a custom table (`Config.Mutagens`) shows up here, and it is exactly what the flying-star methods read. The natal mutagen is a mark on the star's own `MutagenKey` field; the two are not the same thing. ```go soul := chart.Palace(iztro.PalaceSoul) fmt.Println(soul.HeavenlyStem, soul.MutagenStarKeys) ``` **Output** ```text ren [tianliangMaj ziweiMaj zuofuMin wuquMaj] ``` *** ## Has / NotHave / HasOneOf [#has--nothave--hasoneof] **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(StarZiweiMaj, StarTianxiangMaj)`. The search covers all three groups of major, minor and adjective stars. **Signature** ```go func (p *Palace) Has(stars ...string) bool func (p *Palace) NotHave(stars ...string) bool func (p *Palace) HasOneOf(stars ...string) bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ----------- | -------- | ------- | ------------------- | | `stars` | `...string` | Yes | — | Star keys, variadic | **Return value** | Method | Meaning | | ---------- | ------------------------------------------ | | `Has` | Every listed star is in this palace | | `NotHave` | None of the listed stars is in this palace | | `HasOneOf` | At least one listed star is in this palace | **Example** ```go soul := chart.Palace(iztro.PalaceSoul) fmt.Println(soul.Has(iztro.StarZiweiMaj, iztro.StarTianxiangMaj)) fmt.Println(soul.HasOneOf(iztro.StarQishaMaj, iztro.StarZiweiMaj)) fmt.Println(soul.NotHave(iztro.StarHuoxingMin, iztro.StarLingxingMin)) ``` **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** `Has` and `NotHave` return `true`; `HasOneOf` returns `false`. The comparison is against the set of "all keys and translated names of the stars in this palace", and a miss simply means absent — `soul.Has("ziweiMj")` returns `false` without an error, indistinguishable from "the Soul palace has no Ziwei". The `Star*` constants in the package let the compiler and the IDE stop the typo where it is written. The `Has` family compares against both `Key` and `Name`, so `soul.Has("emperor")` also holds on an English chart. But writing it that way breaks the moment the language changes — always test on keys. *** ## HasMutagen / NotHaveMutagen [#hasmutagen--nothavemutagen] **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** ```go func (p *Palace) HasMutagen(mutagenKey string) bool func (p *Palace) NotHaveMutagen(mutagenKey string) bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------ | -------- | -------- | ------- | ------------------------------------------------------- | | `mutagenKey` | `string` | Yes | — | `MutagenLu` / `MutagenQuan` / `MutagenKe` / `MutagenJi` | **Return value** `bool`. **Example** ```go children := chart.Palace(iztro.PalaceChildren) fmt.Println("Children palace has lu:", children.HasMutagen(iztro.MutagenLu)) fmt.Println("Children palace lacks ji:", children.NotHaveMutagen(iztro.MutagenJi)) ``` **Output** ```text Children palace has lu: true Children palace lacks ji: true ``` **Edge cases and pitfalls** `HasMutagen` looks only at the mutagen marks on `MajorStars` and `MinorStars`; an adjective star carrying a mark does not count (replicating iztro's behaviour). Natal mutagens only ever land on the fourteen major stars and a few minor ones, so in practice the two readings rarely differ on a real chart. *** ## IsEmpty [#isempty] **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** ```go func (p *Palace) IsEmpty(excludeStars ...string) bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | -------------- | ----------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------- | | `excludeStars` | `...string` | No | — | 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 `excludeStars` — a hit and it is not empty; only if neither holds is the palace empty. **Example** ```go parents := chart.Palace(iztro.PalaceParents) fmt.Println("Parents palace empty:", parents.IsEmpty()) fmt.Println("Friends palace empty:", chart.Palace(iztro.PalaceFriends).IsEmpty()) // the Parents palace has no major star but does hold Tuoluo — counting Tuoluo makes it non-empty fmt.Println("Parents palace counting Tuoluo:", parents.IsEmpty(iztro.StarTuoluoMin)) ``` **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** `excludeStars` 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. Without `excludeStars` only `MajorStars` is checked. A palace packed with minor and adjective stars but no major star is still empty. *** ## FliesTo / FliesOneOfTo / NotFlyTo [#fliesto--fliesoneofto--notflyto] **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** ```go func (p *Palace) FliesTo(to *Palace, mutagenKeys ...string) bool func (p *Palace) FliesOneOfTo(to *Palace, mutagenKeys ...string) bool func (p *Palace) NotFlyTo(to *Palace, mutagenKeys ...string) bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------- | ----------- | -------- | ------- | ------------------------ | | `to` | `*Palace` | Yes | — | The target palace object | | `mutagenKeys` | `...string` | Yes | — | The mutagens to check | **Return value** | Method | Meaning | | -------------- | ------------------------------------------------------ | | `FliesTo` | **All** the listed mutagens fly into the target palace | | `FliesOneOfTo` | **At least one** of them flies into the target palace | | `NotFlyTo` | **None** of them flies into the target palace | **Example** ```go soul := chart.Palace(iztro.PalaceSoul) fmt.Println("Soul flies lu into Wealth:", soul.FliesTo(chart.Palace(iztro.PalaceWealth), iztro.MutagenLu)) fmt.Println("Soul flies lu or ji into Surface:", soul.FliesOneOfTo(chart.Palace(iztro.PalaceSurface), iztro.MutagenLu, iztro.MutagenJi)) fmt.Println("Soul does not fly quan into Children:", soul.NotFlyTo(chart.Palace(iztro.PalaceChildren), iztro.MutagenQuan)) ``` **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** On the Go side the target is a `*Palace` rather than a string — fetch it with `chart.Palace(...)` first. All three methods return `false` when passed `nil`. Passing no mutagens (or an empty slice) makes `FliesTo` return `false`, while `FliesOneOfTo` and `NotFlyTo` return `true`. That runs against the intuition that a universal statement holds vacuously over the empty set, but it replicates iztro's behaviour: `FliesTo` first works out which stars to look for and calls it false outright when there is not a single one. Once `Config.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. *** ## SelfMutaged / SelfMutagedOneOf / NotSelfMutaged [#selfmutaged--selfmutagedoneof--notselfmutaged] **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** ```go func (p *Palace) SelfMutaged(mutagenKeys ...string) bool func (p *Palace) SelfMutagedOneOf(mutagenKeys ...string) bool func (p *Palace) NotSelfMutaged(mutagenKeys ...string) bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------- | ----------- | -------- | ------- | ------------------------------------------------------------------------------ | | `mutagenKeys` | `...string` | No | — | The mutagens to check; passing none to the latter two methods means "all four" | **Return value** | Method | Meaning | | ------------------ | ------------------------------------------------------------------ | | `SelfMutaged` | All the listed mutagens are self-mutated | | `SelfMutagedOneOf` | At least one of them is self-mutated; passing none checks all four | | `NotSelfMutaged` | None of them is self-mutated; passing none checks all four | **Example** ```go career := chart.Palace(iztro.PalaceCareer) fmt.Println("Career self-mutates lu:", career.SelfMutaged(iztro.MutagenLu)) fmt.Println("Career self-mutates ji:", career.SelfMutaged(iztro.MutagenJi)) fmt.Println("Career has any self-mutagen:", career.SelfMutagedOneOf()) fmt.Println("Career has no self-mutagen:", career.NotSelfMutaged()) ``` **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. *** ## MutagedPlaces / MutagenStars [#mutagedplaces--mutagenstars] **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** ```go func (p *Palace) MutagedPlaces() []*Palace func (p *Palace) MutagenStars(mutagenKeys ...string) []string ``` **Return value** `MutagedPlaces` returns a slice of length 4 in the order **lu, quan, ke, ji**, with `nil` in a slot whose transformed star is not on the chart. `MutagenStars` returns a slice of star keys in the order the mutagens were passed. **Example** ```go soul := chart.Palace(iztro.PalaceSoul) for i, m := range []string{"lu", "quan", "ke", "ji"} { if place := soul.MutagedPlaces()[i]; place != nil { fmt.Printf("%s → %s\n", m, place.Name) } else { fmt.Printf("%s → not on this chart\n", m) } } fmt.Println(soul.MutagenStars(iztro.MutagenLu, iztro.MutagenJi)) ``` **Output** ```text lu → children quan → soul ke → career ji → wealth [tianliangMaj wuquMaj] ``` The Soul palace's stem is ren, and ren sends Lu to Tianliang, Quan to Ziwei, Ke to Zuofu and Ji to Wuqu. *** ## OppositePalace / SurroundedPalaces / Astrolabe [#oppositepalace--surroundedpalaces--astrolabe] **Purpose** Trace from a palace to its opposite palace, its surrounded set and its astrolabe. **Signature** ```go func (p *Palace) OppositePalace() *Palace func (p *Palace) SurroundedPalaces() *SurroundedPalaces func (p *Palace) Astrolabe() *Astrolabe ``` **Return value** A palace constructed on its own, detached from a chart, returns `nil`; a palace obtained from a chart query never does. **Example** ```go soul := chart.Palace(iztro.PalaceSoul) fmt.Println("the opposite of", soul.Name, "is", soul.OppositePalace().Name) fmt.Println("malefics in the surrounded set:", soul.SurroundedPalaces().HaveOneOf(iztro.StarHuoxingMin, iztro.StarLingxingMin)) fmt.Println(soul.Astrolabe().FiveElementsClass) ``` **Output** ```text the opposite of soul is surface malefics in the surrounded set: true wood 3rd ``` *** ## Semantic text [#semantic-text] Single-palace text lives not on `*Palace` but on the astrolabe method [`PalaceToText`](/en/docs/go/astrolabe#totext--palacetotext--surroundedpalacestotext): the Go palace object is pure data, and the text projection recomputes statelessly from the chart's charting context. ```go text, _ := chart.PalaceToText(iztro.PalaceTarget{Index: p.Index}) ``` # Star object (/en/docs/go/star-object) The fields of Star, its brightness and mutagen predicates, and tracing back to its palace. A `Star` is one star sitting in a palace, carrying its type, brightness and mutagen mark, and able to trace back to the palace it sits in. ```go ziwei, palace := chart.Star(iztro.StarZiweiMaj) ``` The examples on this page chart with `"en-US"`, so the display values in the output are English. ## Fields [#fields] | Field | Type | Description | | --------------- | -------- | ----------------------------------------------------------------------------------------- | | `Key` | `string` | Star key, independent of language; use it in predicates | | `Name` | `string` | Star name, translated into the charting language | | `Type` | `string` | Star type, see below | | `Scope` | `string` | Which layer it acts on: `"origin"` for natal stars, the matching scope for scope stars | | `Brightness` | `string` | Translated brightness; an empty string for stars with no brightness table | | `BrightnessKey` | `string` | Brightness key | | `Mutagen` | `string` | Translated natal mutagen; an empty string for stars the birth-year stem did not transform | | `MutagenKey` | `string` | Mutagen key | ### The eight star types [#the-eight-star-types] | Value | Meaning | Typical members | | ----------- | ------------------------ | -------------------------------------------------- | | `major` | The fourteen major stars | Ziwei, Tianfu, Qisha, Pojun | | `soft` | Auspicious stars | Zuofu, Youbi, Wenchang, Wenqu, Tiankui, Tianyue | | `tough` | Malefic stars | Qingyang, Tuoluo, Huoxing, Lingxing, Dikong, Dijie | | `adjective` | Adjective stars | Santai, Bazuo, Tianxing, Tianyao | | `flower` | Peach-blossom stars | Hongluan, Tianxi, Xianchi | | `helper` | Jieshen | Jieshen | | `lucun` | Lucun | Lucun | | `tianma` | Tianma | Tianma | Lucun and Tianma each get a category of their own, because in the traditional division they are neither purely auspicious nor purely malefic and predicates routinely single them out. The Go side uses an empty string for "absent" — an empty `Brightness` means the star has no brightness table, an empty `Mutagen` means the birth-year stem did not transform it. Test with `if star.MutagenKey != ""`. *** ## WithBrightness [#withbrightness] **Purpose** Test whether this star is at one of the given brightness levels. **Zi Wei meaning** Brightness (miao, wang, de, li, ping, bu, xian) describes how strong a star is in its palace. Each star has a fixed value in each of the twelve palaces; at miao or wang its power comes out in full, at xian it is constrained. **Signature** ```go func (s *Star) WithBrightness(brightnessKeys ...string) bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ---------------- | ----------- | -------- | ------- | ---------------------------------- | | `brightnessKeys` | `...string` | Yes | — | Brightness keys; any match is true | **Return value** `bool`. Always false for a star with no brightness. **Example** ```go ziwei, _ := chart.Star(iztro.StarZiweiMaj) fmt.Println(ziwei.WithBrightness(iztro.BrightnessMiao)) fmt.Println(ziwei.WithBrightness(iztro.BrightnessWang, iztro.BrightnessDe)) ``` **Output** ```text true false ``` **Edge cases and pitfalls** The semantics are "any match", not "all match" — a star has exactly one brightness, so passing several just means "any one of these will do". *** ## WithMutagen [#withmutagen] **Purpose** Test whether this star carries a given natal mutagen. **Zi Wei meaning** Natal mutagens are fixed by the birth-year stem: a given year always sends lu, quan, ke and ji to four particular stars. The mark travels with the star, whichever palace it lands in. **Signature** ```go func (s *Star) WithMutagen(mutagenKeys ...string) bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------- | ----------- | -------- | ------- | ------------------------------- | | `mutagenKeys` | `...string` | Yes | — | Mutagen keys; any match is true | **Return value** `bool`. Always false for a star the birth-year stem did not transform. **Example** ```go ziwei, _ := chart.Star(iztro.StarZiweiMaj) taiyang, _ := chart.Star(iztro.StarTaiyangMaj) fmt.Println("Ziwei takes lu:", ziwei.WithMutagen(iztro.MutagenLu)) fmt.Println("Taiyang takes lu:", taiyang.WithMutagen(iztro.MutagenLu)) ``` **Output** ```text Ziwei takes lu: false Taiyang takes lu: true ``` This chart's birth-year stem is geng, and geng sends lu to Taiyang, so the mark lands on Taiyang rather than Ziwei. **Edge cases and pitfalls** `WithMutagen` looks at the mark the **birth-year stem** placed on this star; only four stars on a chart carry one. Mutagens flown by palace stems do not show up here — for those use the palace's [`FliesTo`](/en/docs/go/palace#fliesto--fliesoneofto--notflyto) family. *** ## Palace / OppositePalace / SurroundedPalaces [#palace--oppositepalace--surroundedpalaces] **Purpose** Trace from a star back to its palace, that palace's opposite, and its surrounded set. **Signature** ```go func (s *Star) Palace() *Palace func (s *Star) OppositePalace() *Palace func (s *Star) SurroundedPalaces() *SurroundedPalaces ``` **Return value** A star constructed on its own, detached from a chart, returns `nil`; a star obtained from a chart query never does. **Example** ```go ziwei, _ := chart.Star(iztro.StarZiweiMaj) fmt.Println(ziwei.Palace().Name) fmt.Println(ziwei.OppositePalace().Name) fmt.Println("Tianxiang in the same palace or the trine:", ziwei.SurroundedPalaces().Have(iztro.StarTianxiangMaj)) ``` **Output** ```text soul surface Tianxiang in the same palace or the trine: true ``` **Edge cases and pitfalls** `chart.Star()` already hands back the palace as its second return value, so calling `ziwei.Palace()` again is usually unnecessary. # Surrounded palaces (/en/docs/go/surpalaces) The four palaces of SurroundedPalaces and its five predicates. The surrounded set is the most commonly used reading scope in Zi Wei Dou Shu. A matter cannot be read from its own palace alone: the stars of the opposite palace and the two trine palaces bear on it just as much, and only all four together give the full picture. The examples on this page chart with `"en-US"`, so the display values in the output are English. ## The four palaces [#the-four-palaces] | Field | Offset | Traditional name | Meaning | | ---------- | ------ | ----------------- | --------------------------------------------- | | `Target` | +0 | The palace itself | The matter itself | | `Opposite` | +6 | Opposite palace | The facing side; the most immediate influence | | `Career` | +4 | Career position | One of the trine | | `Wealth` | +8 | Wealth position | One of the trine | All four fields are `*Palace`, so every method of the [palace object](/en/docs/go/palace) is available on them. `Wealth` and `Career` mean "the trine positions relative to this palace", not the two fixed palace names among the twelve. Anchored on the Soul palace they happen to land on the Wealth and Career palaces (+8 and +4), which is where the names come from; anchored elsewhere they are other palaces. ## Four ways to get one [#four-ways-to-get-one] ```go // from the chart, by palace name byName := chart.SurroundedPalaces(iztro.PalaceSoul) // from the chart, by index byIndex := chart.SurroundedPalacesByIndex(4) // from a palace fromPalace := chart.Palace(iztro.PalaceSoul).SurroundedPalaces() // from a star (the surrounded set of the palace it sits in) ziwei, _ := chart.Star(iztro.StarZiweiMaj) fromStar := ziwei.SurroundedPalaces() fmt.Println(byName.Target.Name, byIndex.Target.Name, fromPalace.Target.Name, fromStar.Target.Name) ``` **Output** ```text soul soul soul soul ``` The Soul palace sits at index 4 and Ziwei sits in the Soul palace, so on this chart all four routes give the same surrounded set; pick whichever matches what you already have. `SurroundedPalacesByIndex` takes the index modulo 12, so `-1` and `12` both wrap correctly; but a **zero-valued chart** (an `Astrolabe` built without charting) has no twelve palaces and returns `nil` here. `SurroundedPalaces` takes a name and returns `nil` on a misspelling. Check both for nil before reading fields. *** ## Have / NotHave / HaveOneOf [#have--nothave--haveoneof] **Purpose** Test whether the four palaces together hold the given stars. **Zi Wei meaning** A phrase like "Ziwei is in the surrounded set" asks exactly whether a star appears anywhere among these four palaces, without asking which one. **Signature** ```go func (sp *SurroundedPalaces) Have(stars ...string) bool func (sp *SurroundedPalaces) NotHave(stars ...string) bool func (sp *SurroundedPalaces) HaveOneOf(stars ...string) bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ----------- | -------- | ------- | ------------------- | | `stars` | `...string` | Yes | — | Star keys, variadic | **Return value** | Method | Meaning | | ----------- | ------------------------------------------------------------------------------- | | `Have` | Every listed star appears among the four palaces (not necessarily the same one) | | `NotHave` | None of the listed stars appears | | `HaveOneOf` | At least one listed star appears | **Example** ```go sp := chart.SurroundedPalaces(iztro.PalaceSoul) fmt.Println(sp.Have(iztro.StarZiweiMaj, iztro.StarTianxiangMaj)) fmt.Println(sp.HaveOneOf(iztro.StarQishaMaj, iztro.StarPojunMaj)) fmt.Println(sp.NotHave(iztro.StarHuoxingMin)) ``` **Output** ```text true false true ``` Ziwei is in the Soul palace and Tianxiang in the Wealth palace — different palaces, but both within the four, so `Have` is true. **Edge cases and pitfalls** `Have(A, B)` means "A and B both appear among these four palaces", not that they sit together. For same-palace tests use the palace's [`Has`](/en/docs/go/palace#has--nothave--hasoneof). `Have` and `NotHave` return `true`; `HaveOneOf` returns `false`. *** ## HaveMutagen / NotHaveMutagen [#havemutagen--nothavemutagen] **Purpose** Test whether the four palaces carry a given natal mutagen. **Zi Wei meaning** "Ji is in the surrounded set" means one of these palaces holds a star the birth-year stem sent ji to — a common condition when locating a source of pressure. **Signature** ```go func (sp *SurroundedPalaces) HaveMutagen(mutagenKey string) bool func (sp *SurroundedPalaces) NotHaveMutagen(mutagenKey string) bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------ | -------- | -------- | ------- | ----------------------- | | `mutagenKey` | `string` | Yes | — | One of the mutagen keys | **Return value** `bool`. **Example** ```go sp := chart.SurroundedPalaces(iztro.PalaceSoul) fmt.Println("lu in the surrounded set:", sp.HaveMutagen(iztro.MutagenLu)) fmt.Println("ji in the surrounded set:", sp.HaveMutagen(iztro.MutagenJi)) fmt.Println("no ke in the surrounded set:", sp.NotHaveMutagen(iztro.MutagenKe)) ``` **Output** ```text lu in the surrounded set: false ji in the surrounded set: false no ke in the surrounded set: true ``` This chart's natal mutagens fall in the Children, Surface and Health palaces, none of which is in the Soul palace's surrounded set. **Edge cases and pitfalls** This looks at the **natal mutagen** marks on stars, unrelated to mutagens flown by palace stems. For those, use the palace's flying-star methods. *** ## Semantic text [#semantic-text] Surrounded-palaces text lives not on `*SurroundedPalaces` but on the astrolabe method [`SurroundedPalacesToText`](/en/docs/go/astrolabe#totext--palacetotext--surroundedpalacestotext): ```go text, _ := chart.SurroundedPalacesToText(iztro.PalaceTarget{Key: iztro.PalaceSoul}) ``` # Horoscope object (/en/docs/go/horoscope) The data structures of the six scopes, plus palace lookups that need not be handed the astrolabe again. A horoscope projects the natal chart onto a point in time. The same chart shows a different palace layout in different years — which is exactly what "the decadal scope has moved to that palace" means. ```go h, _ := chart.Horoscope("2025-6-1", 0) ``` `Horoscope` holds the natal chart that produced it, so none of the query methods need the astrolabe passed in again. The examples on this page start from an `"en-US"` natal chart, so the display values in the output are English. ## Fields [#fields] | Field | Type | Span | Description | | ------------------------- | ---------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SolarDate` / `LunarDate` | `string` | — | The solar string and the Chinese lunar spelling of the **target** date. The birth date lives on the natal chart; read it from `h.Astrolabe().SolarDate` | | `Decadal` | `HoroscopeScope` | Ten years | The decadal scope; the childhood scope for the years before it begins | | `Age` | `HoroscopeScope` | One year | The age scope, moving one palace per nominal year | | `Yearly` | `HoroscopeScope` | One year | The yearly scope, its palace fixed by the year's pillar | | `Monthly` | `HoroscopeScope` | One month | The monthly scope | | `Daily` | `HoroscopeScope` | One day | The daily scope | | `Hourly` | `HoroscopeScope` | One double-hour | The hourly scope | Both advance once per year, but they start differently: the age scope starts from the birth-year branch and steps forward with the nominal age, while the yearly scope simply asks which palace that year's pillar falls in. The two lines are independent, and Zi Wei practice usually reads them together. ### HoroscopeScope [#horoscopescope] | Field | Type | Description | | ------------------------------------ | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `Index` | `int` | Which palace this scope lands on (a palace index) | | `Name` | `string` | Display name of the scope, translated into the output language | | `NameKey` | `string` | Scope key: `decadal` / `childhood` (before the decadals begin) / `turn` (age fortune) / `yearly` / `monthly` / `daily` / `hourly`. Predicate on this, never on the translation | | `HeavenlyStem` / `HeavenlyStemKey` | `string` | Stem of the scope, which determines the mutagens it flies | | `EarthlyBranch` / `EarthlyBranchKey` | `string` | Branch of the scope | | `PalaceNames` / `PalaceNameKeys` | `[]string` | The twelve palace names re-derived with this scope's palace as the Soul palace, indexed by palace index | | `Mutagen` / `MutagenStarKeys` | `[]string` | The stars this scope's stem transforms, in the order lu, quan, ke, ji; `MutagenStarKeys` holds the mutated stars' star keys, synonymous with the palace field of the same name | | `Stars` | `[][]Star` | The scope stars of this layer; `nil` for layers that have none | | `NominalAge` | `int` | Age scope only: the nominal age. `0` on every other layer | | `YearlyDecStar` | `*YearlyDecStar` | Yearly scope only: the Sui-qian and Jiang-qian gods. **`nil`** on every other layer | ```go type YearlyDecStar struct { Suiqian12 []string `json:"suiqian12"` // Sui-qian gods, translated, by palace index Suiqian12Keys []string `json:"suiqian12Keys"` // their keys Jiangqian12 []string `json:"jiangqian12"` // Jiang-qian gods, translated Jiangqian12Keys []string `json:"jiangqian12Keys"` // their keys } ``` Rather than giving the age and yearly scopes types of their own, the Go side puts `NominalAge` and `YearlyDecStar` into the shared `HoroscopeScope`, where the other layers leave them at their zero values. Call sites can therefore write generic logic parameterized by scope. `h.Decadal.YearlyDecStar` is `nil`, and reading a field on it panics. Only `h.Yearly.YearlyDecStar` is non-nil — check before you walk the layers. ```go h, _ := chart.Horoscope("2025-6-1", 0) fmt.Println(h.Decadal.YearlyDecStar == nil, h.Yearly.YearlyDecStar != nil) fmt.Println(h.Yearly.YearlyDecStar.Suiqian12[:3]) fmt.Println(h.Yearly.YearlyDecStar.Jiangqian12Keys[:3]) fmt.Println(h.Decadal.NominalAge, h.Age.NominalAge) ``` **Output** ```text true true [blessed sorrowing illness] [jiesha zhaisha tiansha] 0 26 ``` ### PalaceIndexByName [#palaceindexbyname] ```go func (item *HoroscopeScope) PalaceIndexByName(nameKeyOrName string) int ``` Looks a palace index up by palace-name key, or by palace name in the current language, among the twelve palaces as re-derived for that scope; **returns -1 when nothing matches**. ```go h, _ := chart.Horoscope("2025-6-1", 0) fmt.Println(h.Decadal.PalaceIndexByName(iztro.PalaceSoul)) fmt.Println(h.Decadal.PalaceIndexByName(iztro.PalaceWealth)) fmt.Println(h.Decadal.PalaceIndexByName("nosuch")) ``` **Output** ```text 2 10 -1 ``` It returns `-1` rather than `0` — `0` is a legitimate palace index (the Yin palace). Always check for a negative before indexing `chart.Palaces` with it. **Example** ```go h, _ := chart.Horoscope("2025-6-1", 0) for _, item := range []iztro.HoroscopeScope{h.Decadal, h.Monthly, h.Daily, h.Hourly} { fmt.Printf("%s lands on palace %d with pillar %s%s\n", item.Name, item.Index, item.HeavenlyStem, item.EarthlyBranch) } fmt.Println("age scope nominal age", h.Age.NominalAge) fmt.Println("decadal mutagens", h.Decadal.Mutagen) fmt.Println("yearly Sui-qian gods", h.Yearly.YearlyDecStar.Suiqian12[:3]) ``` **Output** ```text decadal lands on palace 2 with pillar gengchen monthly lands on palace 3 with pillar renwoo daily lands on palace 8 with pillar xinchou hourly lands on palace 8 with pillar wuzi age scope nominal age 26 decadal mutagens [sun general moon fortunate] yearly Sui-qian gods [blessed sorrowing illness] ``` *** ## AgePalace [#agepalace] **Purpose** Get the palace the age scope occupies this year. **Zi Wei meaning** The age scope is a line advancing year by year; whichever palace it lands on becomes the focus for that year. **Signature** ```go func (h *Horoscope) AgePalace() *Palace ``` **Return value** `*Palace` — a palace on the natal chart. **Example** ```go h, _ := chart.Horoscope("2025-6-1", 0) fmt.Println(h.AgePalace().Name) ``` **Output** ```text property ``` *** ## Palace [#palace] **Purpose** Get one of the twelve palaces as re-derived under a given horoscope scope. **Zi Wei meaning** Once the decadal scope reaches a palace, the twelve palaces are re-anchored with that palace as the "decadal Soul palace". "The decadal Spouse palace" refers to that re-anchored naming, and it is usually not the same palace as the natal Spouse palace. **Signature** ```go func (h *Horoscope) Palace(nameKeyOrName string, scope string) *Palace ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------- | -------- | -------- | ------- | ----------------------------------------------- | | `nameKeyOrName` | `string` | Yes | — | The palace-name key or translated name to fetch | | `scope` | `string` | Yes | — | Which scope's twelve palaces to search | **Return value** `*Palace` — a palace on the natal chart (the same cell carries different names under different scopes). With `ScopeOrigin`, these are the natal twelve palaces. `nil` when nothing matches. **Example** ```go h, _ := chart.Horoscope("2025-6-1", 0) fmt.Println("the decadal Soul palace is the natal", h.Palace(iztro.PalaceSoul, iztro.ScopeDecadal).Name) fmt.Println("the natal Soul palace is", h.Palace(iztro.PalaceSoul, iztro.ScopeOrigin).Name) ``` **Output** ```text the decadal Soul palace is the natal spouse the natal Soul palace is soul ``` **Edge cases and pitfalls** On the returned palace object, `Name` is still the **natal palace name** (spouse in the example), because it is that cell on the natal chart. To see what the cell is called at the decadal layer, read `h.Decadal.PalaceNames[index]`. *** ## SurroundPalaces [#surroundpalaces] **Purpose** Get the surrounded palaces of a palace under a given horoscope scope. **Signature** ```go func (h *Horoscope) SurroundPalaces(nameKeyOrName string, scope string) *SurroundedPalaces ``` **Parameters** Same as `Palace`. **Return value** `*SurroundedPalaces`; its predicates are on [Surrounded palaces](/en/docs/go/surpalaces). **Example** ```go h, _ := chart.Horoscope("2025-6-1", 0) sp := h.SurroundPalaces(iztro.PalaceWealth, iztro.ScopeYearly) fmt.Println("the surrounded set of the yearly Wealth palace is anchored on the natal", sp.Target.Name) ``` **Output** ```text the surrounded set of the yearly Wealth palace is anchored on the natal health ``` *** ## HasHoroscopeStars / HasOneOfHoroscopeStars / NotHaveHoroscopeStars [#hashoroscopestars--hasoneofhoroscopestars--nothavehoroscopestars] **Purpose** Test whether a palace under a given scope holds the given scope stars. **Zi Wei meaning** Scope stars are a group produced by each horoscope layer: Tiankui, Tianyue, Wenchang, Wenqu, Lucun, Qingyang, Tuoluo, Tianma, Hongluan and Tianxi. They carry different names in different layers — Yunkui and Yunyue at the decadal layer, Liukui and Liuyue at the yearly layer — with the same meaning applied to their own time span. **Signature** ```go func (h *Horoscope) HasHoroscopeStars(nameKeyOrName string, scope string, stars []string) bool func (h *Horoscope) HasOneOfHoroscopeStars(nameKeyOrName string, scope string, stars []string) bool func (h *Horoscope) NotHaveHoroscopeStars(nameKeyOrName string, scope string, stars []string) bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------- | ---------- | -------- | ------- | ------------------------------------------------------------- | | `nameKeyOrName` | `string` | Yes | — | The palace name under that scope | | `scope` | `string` | Yes | — | The horoscope scope | | `stars` | `[]string` | Yes | — | A slice of scope star keys, which must use that layer's names | **Return value** | Method | Meaning | | ------------------------ | ----------------------- | | `HasHoroscopeStars` | All of them are present | | `HasOneOfHoroscopeStars` | At least one is present | | `NotHaveHoroscopeStars` | None is present | **Example** ```go h, _ := chart.Horoscope("2025-6-1", 0) fmt.Println(h.HasHoroscopeStars(iztro.PalaceSoul, iztro.ScopeDecadal, []string{"yunlu"})) fmt.Println(h.HasOneOfHoroscopeStars(iztro.PalaceSoul, iztro.ScopeDecadal, []string{"yunlu", "yunyang"})) fmt.Println(h.NotHaveHoroscopeStars(iztro.PalaceSoul, iztro.ScopeDecadal, []string{"yuntuo"})) ``` **Output** ```text false false true ``` **Edge cases and pitfalls** Two string parameters (palace name and scope) already precede it in these three methods, and a variadic list would make call sites ambiguous, so the star parameter takes a `[]string`. Every other method in the package that takes a list of stars is variadic. All three methods use `scope` plus the palace name to locate one cell on the natal chart, but the set of stars compared against is always the **union of the decadal and yearly scope stars**, regardless of `scope`. So passing `ScopeMonthly` asks "does this cell — the monthly such-and-such palace — hold a decadal or yearly scope star?", not a monthly one: the scope stars of the monthly, daily and hourly layers take no part in the comparison. For the scope-star layout of a given layer read `h.Monthly.Stars`, or use [`GetHoroscopeStar`](/en/docs/go/star#gethoroscopestar). The decadal scope stars are `StarYunlu`, `StarYunyang` and so on, the yearly ones `StarLiulu`, `StarLiuyang` and so on; the two groups have different keys. Since the comparison set is always the union of those two groups, keys from either group can be found under any `scope` — only the palace they land in differs. The per-layer key table is on [Star placement](/en/docs/go/star#gethoroscopestar). *** ## HasHoroscopeMutagen [#hashoroscopemutagen] **Purpose** Test whether a palace under a given scope carries a mutagen flown by that scope's stem. **Zi Wei meaning** Every horoscope layer has a stem of its own, and it transforms four stars just as the birth-year stem does. A question like "does the decadal Lu land in the decadal Wealth palace?" is asking about this. **Signature** ```go func (h *Horoscope) HasHoroscopeMutagen(nameKeyOrName string, scope string, mutagenKey string) bool ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------- | -------- | -------- | ------- | -------------------------------- | | `nameKeyOrName` | `string` | Yes | — | The palace name under that scope | | `scope` | `string` | Yes | — | The horoscope scope | | `mutagenKey` | `string` | Yes | — | A mutagen key | **Return value** `bool`. It checks whether the star transformed by that layer's stem is among the major or minor stars of the target palace (adjective stars are not scanned). **Example** ```go h, _ := chart.Horoscope("2025-6-1", 0) fmt.Println(h.HasHoroscopeMutagen(iztro.PalaceSoul, iztro.ScopeDecadal, iztro.MutagenLu)) fmt.Println(h.Decadal.Mutagen) ``` **Output** ```text false [sun general moon fortunate] ``` The decadal stem is geng, and geng sends Lu to Taiyang, Quan to Wuqu, Ke to Taiyin and Ji to Tiantong. **Edge cases and pitfalls** The natal layer has no "layer stem" — the natal mutagens are already marked on the stars' own `MutagenKey`. `HasHoroscopeMutagen(name, iztro.ScopeOrigin, m)` therefore returns `false` outright, which does not mean the natal chart lacks that mutagen. For natal mutagens use the palace's [`HasMutagen`](/en/docs/go/palace#hasmutagen--nothavemutagen). *** ## ScopeItem / Astrolabe [#scopeitem--astrolabe] **Purpose** Get the `HoroscopeScope` for a scope key, or get back to the natal chart. **Signature** ```go func (h *Horoscope) ScopeItem(scope string) *HoroscopeScope func (h *Horoscope) Astrolabe() *Astrolabe ``` **Return value** `ScopeItem` returns `nil` for `ScopeOrigin` or an unknown scope — the natal chart is not a horoscope layer. **Example** ```go h, _ := chart.Horoscope("2025-6-1", 0) fmt.Println(h.ScopeItem(iztro.ScopeDecadal).Name) fmt.Println(h.ScopeItem(iztro.ScopeOrigin)) fmt.Println(h.Astrolabe().SolarDate) ``` **Output** ```text decadal 2000-8-16 ``` **Edge cases and pitfalls** `ScopeItem` is for writing generic logic parameterized by scope, which is tidier than a chain of `switch scope`. Remember to check for `nil`. *** ## ToText [#totext] **Purpose** The horoscope's semantic text: a complete description for language models and people. **Signature** ```go func (h *Horoscope) ToText() (string, error) func (h *Horoscope) ToTextContext(ctx context.Context) (string, error) ``` **Return value** `string` — sectioned plain text in the chart's charting language; each scope carries a patterns line and flowing-star lines from its own perspective. The full format is on [Semantic text](/en/docs/guide/guides/to-text). **Example** ```go h, _ := chart.Horoscope("2025-1-1", 0) text, _ := h.ToText() fmt.Println(string([]rune(text)[:39])) ``` **Output** ```text === Horoscope === Target Date: 2025-1-1 ``` For the pattern hits as text, see `PatternsToText` on [Patterns](/en/docs/go/patterns). # Patterns (/en/docs/go/patterns) Pattern hits on natal and horoscope charts, the PatternConfig readings, the Pattern constants, and error handling. A pattern (格局) is the recognition of a named star arrangement on a chart. The same 64 rules are judged on natal charts and on horoscope views. For what patterns are and each rule's condition and source, see the [concept page](/en/docs/guide/concepts/patterns). ```go chart, err := iztro.BySolar("1985-5-3", 9, iztro.GenderMale, true, iztro.LanguageEnUS, nil) hits, err := chart.Patterns(nil) ``` The examples on this page all start from an `"en-US"` natal chart, so the display values in the output are the English translations. ## Types [#types] ### PatternHit [#patternhit] | Field | Type | Meaning | | --------------- | --------------- | ------------------------------------------------------------------------------------------------------ | | `Key` | `string` | Language-independent pattern key; its values are the `PatternXxx` constants | | `Name` | `string` | Pattern name, translated to the chart's language | | `Scope` | `string` | The view it was judged in: `ScopeOrigin` for natal, otherwise that level | | `PalaceIndex` | `int` | Slot of the palace where the pattern formed (0-11, Yin palace is 0) | | `PalaceName` | `string` | That palace's name **in this view** | | `PalaceNameKey` | `string` | The palace key; its values are the `PalaceXxx` constants | | `Variant` | `string` | Which reading matched; empty for single-reading patterns | | `Broken` | `bool` | Whether the "spoiled by malefics" condition fired. The hit is reported either way; this is only a flag | | `Stars` | `[]PatternStar` | The stars evidencing the pattern, with their palaces | Three methods: | Method | Meaning | | ------------------------------------- | -------------------------------------------------------------------------------------------------- | | `Is(patternKey string) bool` | Whether this is the given pattern; pass a constant such as `PatternShaPoLang` | | `InPalace(nameKeyOrName string) bool` | Whether the forming palace is the given one; pass a palace key or the name in the chart's language | | `String() string` | `Name(palace)` or `Name(palace,variant)`, for logs and debugging | ### PatternStar [#patternstar] | Field | Type | Meaning | | ------------------------------ | -------- | --------------------------------------------------------------------------------- | | `Key` | `string` | Language-independent star key | | `Name` | `string` | Star name, translated to the chart's language | | `PalaceIndex` | `int` | The slot the star **actually occupies** (when borrowed, not the borrowing palace) | | `Brightness` / `BrightnessKey` | `string` | Brightness display text and key; empty when the star has none | | `Mutagen` / `MutagenKey` | `string` | The mutagen in this view, and its key; empty when there is none | ### PatternConfig [#patternconfig] The reading switches. Anything that is merely a second *form* of the same pattern goes through `PatternHit.Variant`; only data readings that change the **finding of fact itself** live here, which is why there are just three fields. ```go type PatternConfig struct { BrightnessSource string // BrightnessSourceTable (default) or BrightnessSourcePositional Borrow *bool // whether an empty palace borrows the opposite palace's majors; nil takes the core default true FlowStars *bool // whether flowing stars count as their natal counterparts; nil takes the core default true } func Bool(v bool) *bool // convenience: a bool literal's address func DefaultPatternConfig() *PatternConfig // the default reading with every field explicit ``` The two booleans are `*bool`: `nil` means "not stated" and the core takes its default `true`; switch one off explicitly with `iztro.Bool(false)`. `DefaultPatternConfig()` returns `{BrightnessSourceTable, Bool(true), Bool(true)}`, which means the same as passing `nil`. `BrightnessSourceTable` follows the chart's brightness table (Miao and Wang bright, Xian and Bu dim — matching iztro value for value); `BrightnessSourcePositional` follows the traditional placement (Sun bright Yin–Wu, dim You–Chou; Moon bright You–Chou, dim Mao–Wei). The trade-off is explained on the [concept page](/en/docs/guide/concepts/patterns#which-table-decides-sun-and-moon-brightness). All three fields of `&iztro.PatternConfig{}` are zero values (empty string and `nil`), which means exactly what passing `nil` means. To change one thing, write a literal — whatever you leave out keeps the core default: ```go cfg := &iztro.PatternConfig{BrightnessSource: iztro.BrightnessSourcePositional} onlyNatal := &iztro.PatternConfig{FlowStars: iztro.Bool(false)} ``` ### Pattern constants [#pattern-constants] Every one of the 64 pattern keys has a named constant, `Pattern` plus the pinyin in camel case: `PatternShaPoLang`, `PatternFuXiangChaoYuan`, `PatternFengYunJiHui` and so on, valued exactly as `PatternHit.Key`. Always test patterns against the constants, never against `Name` — `Name` follows the chart's language, `Key` does not. *** ## Patterns [#patterns] **Purpose** Every pattern hit on the natal chart. **In Zi Wei terms** Lists every named star arrangement that holds on this chart, together with the palace it formed in and the stars that evidence it. **Signature** ```go func (a *Astrolabe) Patterns(config *PatternConfig) ([]PatternHit, error) func (a *Astrolabe) PatternsContext(ctx context.Context, config *PatternConfig) ([]PatternHit, error) ``` **Parameters** | Parameter | Type | Required | Default | Meaning | | --------- | ----------------- | -------------------- | ------- | --------------------------------------- | | `config` | `*PatternConfig` | yes | — | The reading; pass `nil` for the default | | `ctx` | `context.Context` | for the Context form | — | Cancels the wait for a wasm instance | **Returns** `[]PatternHit` in the source page's entry order; an empty slice when nothing holds. The two transit patterns (禄衰马困 `lu_shuai_ma_kun`, 风云际会 `feng_yun_ji_hui`) never appear on a natal chart. **Errors** | Case | Error | | ------------------------------------------------- | --------------------------------------------------- | | Nil astrolabe | `iztro: patterns: nil astrolabe` | | `BrightnessSource` is neither of the valid values | `iztro: invalid patternConfig: unknown variant ...` | Both are of the `ErrInvalidArgument` class and match with `errors.Is`. **Example** ```go chart, _ := iztro.BySolar("1985-5-3", 9, iztro.GenderMale, true, iztro.LanguageEnUS, nil) hits, _ := chart.Patterns(nil) for _, h := range hits { fmt.Printf("%s %d %s broken=%v\n", h.Name, h.PalaceIndex, h.PalaceName, h.Broken) } ``` **Output** ```text General and Wolf Together 11 surface broken=false Empress and Minister Facing the Palace 5 soul broken=false Marshal, Rebel and Wolf 11 surface broken=false Money and Horse Galloping Together 5 soul broken=false Officer and Helper Flanking Life 5 soul broken=false Literary Nobility and Brilliance 11 surface broken=false Literary Stars Facing Life 5 soul broken=true Literary Stars in Hidden Support 5 soul broken=false Literary Stars in Hidden Support 5 soul broken=false ``` Taking one hit and reading its evidence: ```go for _, h := range hits { if !h.Is(iztro.PatternFuXiangChaoYuan) { continue } fmt.Println(h, h.Variant, h.InPalace(iztro.PalaceSoul)) for _, s := range h.Stars { fmt.Printf(" %s %d %s %s\n", s.Name, s.PalaceIndex, s.Brightness, s.BrightnessKey) } } ``` ```text Empress and Minister Facing the Palace(soul,soul_empty) soul_empty true empress 9 [+1] de minister 1 [-3] xian ``` Judging under an explicit reading: ```go chart, _ := iztro.BySolar("1985-1-5", 11, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) cfg := &iztro.PatternConfig{BrightnessSource: iztro.BrightnessSourcePositional} a, _ := chart.Patterns(nil) b, _ := chart.Patterns(cfg) fmt.Println(names(a)) // names collects each hit's Name fmt.Println(names(b)) ``` ```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] ``` **Edges and traps** Most patterns form at the Soul palace, but "Body-or-Soul" patterns (武贪同行 `wu_tan_tong_xing`, 杀破狼 `sha_po_lang`, 石中隐玉 `shi_zhong_yin_yu` and others) are judged at both, and `PalaceIndex` records whichever matched — if both match, two hits come back. 禄马交驰 `lu_ma_jiao_chi` goes further: it is reported for any palace that qualifies, so one chart may produce several hits. On the chart above the Body palace sits on the Surface palace, which is why three of the hits record it. When an empty palace borrows the opposite palace's majors, `PatternStar.PalaceIndex` records the palace the star actually occupies (the opposite one), not the borrowing palace. For where the pattern formed, read `PatternHit.PalaceIndex`. `Variant`, `Brightness` and `Mutagen` come back empty rather than missing — the Go side decodes from JSON, and optional keys are omitted in the DTO, so they decode to the zero value. Test presence with `h.Variant != ""`. *** ## Horoscope.Patterns [#horoscopepatterns] **Purpose** Pattern hits in the view of one horoscope level. **In Zi Wei terms** Takes that level's palace as the Soul palace, merges in that level's flowing stars and mutagens, and runs every rule again. This is how "if the natal chart has the arrangement and the decadal then arrives at it, its benefit is enjoyed" is computed. **Signature** ```go func (h *Horoscope) Patterns(scope string, config *PatternConfig) ([]PatternHit, error) func (h *Horoscope) PatternsContext(ctx context.Context, scope string, config *PatternConfig) ([]PatternHit, error) ``` **Parameters** | Parameter | Type | Required | Default | Meaning | | --------- | ---------------- | -------- | ------- | ------------------------------------------------------------------------ | | `scope` | `string` | yes | — | The level whose view to judge in; pass a constant such as `ScopeDecadal` | | `config` | `*PatternConfig` | yes | — | The reading; pass `nil` for the default | **Returns** `[]PatternHit`, each carrying the level passed in as its `Scope`. Passing `ScopeOrigin` gives exactly what `Patterns(nil)` on the astrolabe gives. **Errors** A horoscope not created by `Astrolabe.Horoscope` returns `iztro: horoscopePatterns: horoscope must be created by Astrolabe.Horoscope`; an unrecognised `scope` returns `unknown scope`. **Example** ```go chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) 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) } ``` **Output** ```text Marshal, Rebel and Wolf decadal "" Meeting of Wind and Cloud decadal "" 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. **Edges and traps** The Body palace is a natal concept. In a horoscope view, "Body-or-Soul" patterns are judged only at that level's Soul palace. 禄衰马困 `lu_shuai_ma_kun` is judged at whichever level the current view is (decadal view judges the decadal, yearly view the year); when the limit's Soul-palace trine set also holds Qisha (the classical strict reading holds too), `Variant` is `"qisha"`. 风云际会 `feng_yun_ji_hui` compares two limits across levels, so it is judged once, in the `ScopeDecadal` view. Its `Variant` records both the pair of limits and how strictly they "meet" Lu and the Horse: a decadal + minor-limit hit is empty (trine-set meeting) or `"same_palace"` (the strict reading — both limits' Soul palaces hold the stars in-palace); a decadal + annual hit is `"yearly"` or `"yearly_same_palace"`. Each pair reports one hit, two at most. Under the default reading a flowing Lucun reads as Lucun, a flowing Wenchang as Wenchang, and so on. To turn that off, pass `&iztro.PatternConfig{FlowStars: iztro.Bool(false)}`. `Patterns` does not compute incrementally on an existing chart object; it sends the charting context (birth date, hour, gender, language, config) back into the wasm core and starts a fresh judgement. So it neither mutates the chart nor caches — hold onto the returned slice yourself if you are calling it in a loop. *** ## Serialisation [#serialisation] The JSON tags on `PatternHit` and `PatternStar` are the binding DTO's key names, so `encoding/json` produces exactly the structure the Rust and Python sides produce: ```go chart, _ := iztro.BySolar("1985-5-3", 9, iztro.GenderMale, true, iztro.LanguageEnUS, nil) hits, _ := chart.Patterns(nil) for _, hit := range hits { if !hit.Is(iztro.PatternFuXiangChaoYuan) { continue } b, _ := json.MarshalIndent(hit, "", " ") fmt.Println(string(b)) } ``` ```json { "key": "fu_xiang_chao_yuan", "name": "Empress and Minister Facing the Palace", "scope": "origin", "palaceIndex": 5, "palaceName": "soul", "palaceNameKey": "soulPalace", "variant": "soul_empty", "broken": false, "stars": [ { "key": "tianfuMaj", "name": "empress", "palaceIndex": 9, "brightness": "[+1]", "brightnessKey": "de" }, { "key": "tianxiangMaj", "name": "minister", "palaceIndex": 1, "brightness": "[-3]", "brightnessKey": "xian" } ] } ``` *** ## PatternsToText / Horoscope.PatternsToText [#patternstotext--horoscopepatternstotext] **Purpose** The pattern hits as semantic text, one per line: pattern name, landing palace, forming stars, with broken patterns marked `[Broken]`. **Signature** ```go func (a *Astrolabe) PatternsToText(config *PatternConfig) (string, error) func (h *Horoscope) PatternsToText(scope string, config *PatternConfig) (string, error) ``` Each has a `Context` variant. The same judgment as `Patterns` (including re-anchoring context and judging criteria); the horoscope version writes palace names as re-laid out at that scope, and `nil` for `config` takes the default criteria. **Example** ```go text, _ := chart.PatternsToText(nil) fmt.Print(text) ``` **Output** ```text - Empress and Minister Facing the Palace(soul): empress([+3]), minister([+3]) ``` The chart's and the horoscope's `ToText` each already carry a patterns section; the standalone call suits cases that want only the pattern summary. # Lightweight queries (/en/docs/go/query) The Chinese zodiac animal, zodiac sign and Soul palace major stars, without charting the whole thing. Some questions do not need a whole chart. These five functions each run only as far as necessary and return; their results always agree with the corresponding fields of a full chart, because they go through the same core logic. The examples on this page chart with `"en-US"`, so the display values in the output are English. *** ## GetZodiacBySolarDate [#getzodiacbysolardate] **Purpose** Get the Chinese zodiac animal from a solar date. **Zi Wei meaning** The zodiac animal is determined by the **year branch**, and when the year branch turns over is governed by `YearDivide`. For someone born between lunar New Year and the Beginning of Spring, the two settings give different animals — not a defect, a difference of school. **Signature** ```go func GetZodiacBySolarDate(solarDate string, language Language, config *Config) (string, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | ----------- | ---------- | -------- | ------- | ----------------------------------------------------------------- | | `solarDate` | `string` | Yes | — | Solar date in `YYYY-M-D` | | `language` | `Language` | Yes | — | Chart language | | `config` | `*Config` | Yes | — | Pass `nil` for the defaults; only `YearDivide` affects the result | **Return value** The animal name translated into the language. **Example** ```go zodiac, _ := iztro.GetZodiacBySolarDate("2000-8-16", iztro.LanguageEnUS, nil) fmt.Println(zodiac) ``` **Output** ```text dragon ``` **Edge cases and pitfalls** By default the year turns over at lunar New Year. Switch to `&Config{YearDivide: iztro.YearDivideExact}` and it turns over at the Beginning of Spring, so people born from late January to early February can get a different animal. *** ## GetSignBySolarDate / GetSignByLunarDate [#getsignbysolardate--getsignbylunardate] **Purpose** Get the zodiac sign. **Zi Wei meaning** The zodiac sign is a Western astrology concept determined solely by the solar date, unrelated to the Zi Wei algorithm. The lunar version converts to solar first, so both give the same result for the same day. **Signature** ```go func GetSignBySolarDate(solarDate string, language Language) (string, error) func GetSignByLunarDate(lunarDate string, isLeapMonth bool, language Language) (string, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------------------- | ---------- | -------- | ------- | ------------------------------------------------------ | | `solarDate` / `lunarDate` | `string` | Yes | — | The date in `YYYY-M-D` | | `isLeapMonth` | `bool` | Yes | — | Lunar version only: whether that month is a leap month | | `language` | `Language` | Yes | — | Chart language | There is no `config` parameter — zodiac signs are unaffected by any setting. **Return value** The sign name. **Example** ```go s1, _ := iztro.GetSignBySolarDate("2000-8-16", iztro.LanguageEnUS) s2, _ := iztro.GetSignByLunarDate("2000-7-17", false, iztro.LanguageEnUS) fmt.Println(s1, s2) ``` **Output** ```text leo leo ``` *** ## GetMajorStarBySolarDate / GetMajorStarByLunarDate [#getmajorstarbysolardate--getmajorstarbylunardate] **Purpose** Get just the Soul palace's major stars, without charting the whole thing. **Zi Wei meaning** The major stars of the Soul palace are the single most commonly asked item in Zi Wei Dou Shu. When the Soul palace is empty, convention borrows the major stars of the opposite palace, and this function already handles that step. **Signature** ```go func GetMajorStarBySolarDate( solarDate string, timeIndex uint8, fixLeap bool, language Language, config *Config, ) (string, error) func GetMajorStarByLunarDate( lunarDate string, timeIndex uint8, leap LeapMonth, language Language, config *Config, ) (string, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------------------- | ----------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------- | | `solarDate` / `lunarDate` | `string` | Yes | — | The date | | `timeIndex` | `uint8` | Yes | — | Hour index 0–12; the Soul palace is fixed jointly by month and hour | | `fixLeap` | `bool` | Yes | — | Solar version only: whether a solar date falling after the 15th of a leap month is treated as the next month | | `leap` | `LeapMonth` | Yes | — | Lunar version only: `NotLeapMonth` / `LeapMonthKeep` / `LeapMonthFixed`, see [`ByLunar`](/en/docs/go/astro#bylunar) | | `language` | `Language` | Yes | — | Chart language | | `config` | `*Config` | Yes | — | Pass `nil` for the defaults | **Return value** Several major stars separated by commas; the opposite palace's major stars when the Soul palace is empty. **Example** ```go en, _ := iztro.GetMajorStarBySolarDate("2000-8-16", 2, true, iztro.LanguageEnUS, nil) zh, _ := iztro.GetMajorStarBySolarDate("2000-8-16", 2, true, iztro.LanguageZhCN, nil) fmt.Println(en, zh) ``` **Output** ```text emperor 紫微 ``` **Edge cases and pitfalls** The Soul palace is located jointly from the lunar month and the birth hour, so `timeIndex` is required. Knowing only the date and not the hour, Zi Wei Dou Shu cannot fix a Soul palace. The return value is a translated string that changes with the language. For programmatic checks use `MajorStarKeysBySolarDate` / `MajorStarKeysByLunarDate` below, or chart the whole thing and compare the `Key` of the `MajorStars`. *** ## MajorStarKeysBySolarDate / MajorStarKeysByLunarDate [#majorstarkeysbysolardate--majorstarkeysbylunardate] **Purpose** The Soul palace's major stars as language-independent keys — the key form of the two functions above, for programmatic checks. **Signature** ```go func MajorStarKeysBySolarDate( solarDate string, timeIndex uint8, fixLeap bool, config *Config, ) ([]string, error) func MajorStarKeysByLunarDate( lunarDate string, timeIndex uint8, leap LeapMonth, config *Config, ) ([]string, error) ``` **Return value** `[]string` — star key constant values (e.g. `StarZiweiMaj`); an empty Soul palace borrows its opposite's major stars just the same. Keys are language-independent, so these functions **take no `language`**. **Example** ```go keys, _ := iztro.MajorStarKeysBySolarDate("2000-8-16", 2, true, nil) fmt.Println(keys) ``` **Output** ```text [ziweiMaj] ``` # Utilities (/en/docs/go/util) Index arithmetic, brightness and mutagen lookups, Soul and body palace derivation, decadal and age scopes, and the four-pillar display string. These functions are the parts the charting algorithm is assembled from. They come in handy when you implement Zi Wei logic yourself or want to double-check a step of the derivation; everyday charting does not call them directly. Every key in the parameters and return values is language-independent and interoperates directly with the `*Key` fields on a chart. *** ## FixIndex / FixIndex12 [#fixindex--fixindex12] **Purpose** Constrain any integer to a cyclic range. **Zi Wei meaning** The twelve palaces form a ring: one step past the Chou palace (index 11) is back to the Yin palace (index 0). Every "count n forward, count n backward" derivation relies on this wrapping. **Signature** ```go func FixIndex(index int, max int) (int, error) func FixIndex12(index int) int ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ----- | -------- | ------- | ------------------------------------------------------------------------------------------------------ | | `index` | `int` | Yes | — | The index to fix, possibly negative | | `max` | `int` | Yes | — | Cycle length; passing `0` takes the default of 12, and stems use 10. A negative value returns an error | **Return value** An index within `0..max` (0 included, `max` excluded). `FixIndex12` is fixed at modulo 12 and returns no error — use it directly for twelve-palace wrapping. **Example** ```go a, _ := iztro.FixIndex(-1, 0) b, _ := iztro.FixIndex(13, 0) c, _ := iztro.FixIndex(11, 10) fmt.Println(a, b, c) fmt.Println(iztro.FixIndex12(-1), iztro.FixIndex12(13)) _, err := iztro.FixIndex(0, -1) fmt.Println(err) ``` **Output** ```text 11 1 1 11 1 iztro: invalid max '-1': expected a positive integer ``` **Edge cases and pitfalls** This replicates iztro's `fixIndex(index, max = 12)` default parameter and runs against Go's zero-value intuition: `FixIndex(13, 0)` gives 1 rather than an error. For twelve-palace wrapping reach for `FixIndex12` instead and skip both the ambiguity and an `error` that can never occur. Negatives wrap by mathematical modulo (-1 → 11) rather than clamping to 0. Both functions compute on the Go side and do not cross into wasm. *** ## EarthlyBranchToPalaceIndex [#earthlybranchtopalaceindex] **Purpose** Convert an earthly branch to a palace index. **Zi Wei meaning** The twelve palaces start from the **Yin palace** while the natural order of the branches starts from **zi**, putting them two positions apart. This function handles that conversion: yin → 0, mao → 1, …, zi → 10, chou → 11. **Signature** ```go func EarthlyBranchToPalaceIndex(branchKey string) (int, error) ``` **Return value** `int`, 0–11. **Example** ```go yin, _ := iztro.EarthlyBranchToPalaceIndex(iztro.BranchYin) zi, _ := iztro.EarthlyBranchToPalaceIndex(iztro.BranchZi) fmt.Println(yin, zi) ``` **Output** ```text 0 10 ``` *** ## TimeToIndex [#timetoindex] **Purpose** Convert a clock hour to an hour index. **Zi Wei meaning** A day holds twelve double-hours of two hours each, but the Zi hour straddles midnight and splits into the early Zi hour (0) and the late Zi hour (12), giving 13 index values. **Signature** ```go func TimeToIndex(hour uint8) (uint8, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ------- | -------- | ------- | --------------------------------------------------- | | `hour` | `uint8` | Yes | — | The clock hour, 0–23; out of range returns an error | **Return value** `uint8`, 0–12 — exactly the type of the `timeIndex` parameter of the charting entry points, so it can be handed straight over. **Example** ```go a, _ := iztro.TimeToIndex(0) b, _ := iztro.TimeToIndex(4) c, _ := iztro.TimeToIndex(23) fmt.Println(a, b, c) _, err := iztro.TimeToIndex(24) fmt.Println(err) // the result feeds a charting entry point directly chart, err := iztro.BySolar("2000-8-16", b, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) if err != nil { log.Fatal(err) } fmt.Println(chart.Time) ``` **Output** ```text 0 2 12 iztro: invalid hour '24': expected 0-23 Tiger hour ``` Midnight is the early Zi hour, 4 o'clock the Tiger hour, 23 o'clock the late Zi hour. When you are unsure of the hour index while charting, convert with this function. *** ## GetAgeIndex [#getageindex] **Purpose** Get the starting palace index of the age scope from the birth-year branch. **Zi Wei meaning** The age scope starts from a fixed palace and steps forward with the nominal age. The starting palace is set by the trine group of the birth-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. **Signature** ```go func GetAgeIndex(branchKey string) (int, error) ``` **Return value** `int`, 0–11. **Example** ```go idx, _ := iztro.GetAgeIndex(iztro.BranchChen) fmt.Println(idx) ``` **Output** ```text 8 ``` A chen year belongs to the shen/zi/chen group, so the age scope starts at the Xu palace, whose index is 8. *** ## GetBrightness [#getbrightness] **Purpose** Look up a star's brightness in a given palace. **Signature** ```go func GetBrightness(starKey string, palaceIndex int, config *Config) (string, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------- | --------- | -------- | ------- | ----------------------------------------------------- | | `starKey` | `string` | Yes | — | Star key | | `palaceIndex` | `int` | Yes | — | Palace index; out-of-range values are taken modulo 12 | | `config` | `*Config` | Yes | — | A custom brightness table changes the result | **Return value** A brightness key; an empty string for stars with no brightness table. **Example** ```go a, _ := iztro.GetBrightness(iztro.StarZiweiMaj, 4, nil) b, _ := iztro.GetBrightness(iztro.StarLucunMin, 0, nil) fmt.Printf("%q %q\n", a, b) ``` **Output** ```text "miao" "" ``` Ziwei is at miao in the Woo palace (index 4); Lucun has no brightness table. *** ## GetMutagen / GetMutagensByHeavenlyStem [#getmutagen--getmutagensbyheavenlystem] **Purpose** Look up the mutagens of a heavenly stem. **Zi Wei meaning** Each of the ten stems assigns four fixed stars to lu, quan, ke and ji. `GetMutagen` asks "what does this star take under this stem", while `GetMutagensByHeavenlyStem` asks "which four stars does this stem transform". **Signature** ```go func GetMutagen(starKey string, stemKey string, config *Config) (string, error) func GetMutagensByHeavenlyStem(stemKey string, config *Config) ([]string, error) ``` **Return value** `GetMutagen` returns a mutagen key, or an empty string when the star is not in that stem's mutagen table. `GetMutagensByHeavenlyStem` returns a slice of four, in the order **lu, quan, ke, ji**. **Example** ```go a, _ := iztro.GetMutagen(iztro.StarTaiyangMaj, iztro.StemGeng, nil) b, _ := iztro.GetMutagen(iztro.StarZiweiMaj, iztro.StemGeng, nil) c, _ := iztro.GetMutagensByHeavenlyStem(iztro.StemGeng, nil) fmt.Printf("%q %q\n%v\n", a, b, c) ``` **Output** ```text "sihuaLu" "" [taiyangMaj wuquMaj taiyinMaj tiantongMaj] ``` *** ## GetSoulAndBody [#getsoulandbody] **Purpose** Derive the Soul and body palaces from the lunar month index, the hour and the year stem. **Zi Wei meaning** The Soul palace is the origin of the whole chart: start at the Yin palace for the first month, count forward to the birth month, then count backward from there to the birth hour. The body palace uses the same starting point but counts the hour forward. The Soul palace's stem comes from the year stem via the Five Tigers rule. **Signature** ```go func GetSoulAndBody(monthIndex int, timeIndex uint8, yearlyStemKey string) (*SoulAndBody, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------------- | -------- | -------- | ------- | ------------------------------------------------------------------------------- | | `monthIndex` | `int` | Yes | — | Lunar month index with the first month at 0; obtained from `FixLunarMonthIndex` | | `timeIndex` | `uint8` | Yes | — | Hour index 0–12 | | `yearlyStemKey` | `string` | Yes | — | Birth-year stem key | **Return value** `*SoulAndBody`, holding `SoulIndex`, `BodyIndex`, `HeavenlyStemOfSoul` and `EarthlyBranchOfSoul`. **Example** ```go sb, _ := iztro.GetSoulAndBody(6, 2, iztro.StemGeng) fmt.Printf("%+v\n", *sb) ``` **Output** ```text {SoulIndex:4 BodyIndex:8 HeavenlyStemOfSoul:renHeavenly EarthlyBranchOfSoul:wuEarthly} ``` *** ## GetFiveElementsClass [#getfiveelementsclass] **Purpose** Derive the five elements class from the Soul palace's stem and branch. **Zi Wei meaning** The five elements class (water 2nd, wood 3rd, metal 4th, earth 5th, fire 6th) decides two major things: where Ziwei starts, and the age at which the decadal scope begins. **Signature** ```go func GetFiveElementsClass(stemKey string, branchKey string) (string, error) ``` **Return value** A five elements class key. **Example** ```go fe, _ := iztro.GetFiveElementsClass(iztro.StemRen, iztro.BranchWu) fmt.Println(fe) ``` **Output** ```text wood3rd ``` *** ## GetPalaceNames [#getpalacenames] **Purpose** Derive the twelve palace names from the Soul palace index. **Zi Wei meaning** Once the Soul palace is fixed, the other eleven run counterclockwise in a fixed order: Soul, Siblings, Spouse, Children, Wealth, Health, Surface, Friends, Career, Property, Spirit, Parents. **Signature** ```go func GetPalaceNames(soulIndex int) ([]string, error) ``` **Return value** A slice of twelve **keys** (not translated names), **indexed by palace index** — item `i` is the `NameKey` of `chart.Palaces[i]`. Not the same as `GetConstants().Palaces`, which gives the fixed ordering of the palace names, independent of any particular chart. **Example** ```go names, _ := iztro.GetPalaceNames(4) fmt.Println(names[:4]) ``` **Output** ```text [wealthPalace childrenPalace spousePalace siblingsPalace] ``` The Soul palace is at index 4, so index 0 (the Yin palace) is Wealth. *** ## GetDecadalsAndAges [#getdecadalsandages] **Purpose** Derive the decadal and age scopes of the twelve palaces from the Soul palace index and the five elements class. **Zi Wei meaning** The starting age of the decadal scope comes from the five elements class (water 2nd at 2, wood 3rd at 3, and so on), with direction from gender polarity and year-branch polarity; the age scope's starting palace comes from the year branch and it steps forward with the nominal age. **Signature** ```go func GetDecadalsAndAges( soulIndex int, fiveElementsClass string, gender Gender, yearlyStemKey, yearlyBranchKey string, ) (DecadalsAndAges, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | ------------------- | -------- | -------- | ------- | ------------------------------- | | `soulIndex` | `int` | Yes | — | Palace index of the Soul palace | | `fiveElementsClass` | `string` | Yes | — | Five elements class key | | `gender` | `Gender` | Yes | — | `GenderMale` or `GenderFemale` | | `yearlyStemKey` | `string` | Yes | — | Year stem key | | `yearlyBranchKey` | `string` | Yes | — | Year branch key | **Return value** `DecadalsAndAges`, holding `Decadals []Decadal` and `Ages [][]int`, both indexed by palace index. `Decadal` is the same type as `palace.Decadal` on a palace: | Field | Type | Description | | ------------------------------------ | -------- | --------------------------------------------------------- | | `Range` | `[2]int` | First and last nominal age of the decadal, both inclusive | | `HeavenlyStem` / `HeavenlyStemKey` | `string` | Translated stem of the decadal / its key | | `EarthlyBranch` / `EarthlyBranchKey` | `string` | Translated branch of the decadal / its key | **Example** ```go da, _ := iztro.GetDecadalsAndAges(4, "wood3rd", iztro.GenderFemale, iztro.StemGeng, iztro.BranchChen) fmt.Printf("%+v\n", da.Decadals[0]) fmt.Println(da.Ages[0][:3]) ``` **Output** ```text {Range:[43 52] HeavenlyStem:戊 HeavenlyStemKey:wuHeavenly EarthlyBranch:寅 EarthlyBranchKey:yinEarthly} [9 21 33] ``` **Edge cases and pitfalls** On a fully charted astrolabe every palace already carries `Decadal` and `Ages` fields with the same contents, down to the meaning of the translated and key field pairs. This function is for cases where you want the scopes without charting the whole thing. This function takes no `language` parameter, so the translated fields of `Decadal` are always Chinese. For another language take `HeavenlyStemKey` through [`Translate`](/en/docs/go/i18n#translate). *** ## FixLunarMonthIndex / FixLunarDayIndex [#fixlunarmonthindex--fixlunardayindex] **Purpose** Compute the corrected lunar month index and day index. **Zi Wei meaning** Where leap-month days belong and where the late Zi hour belongs are two long-disputed boundaries in Zi Wei Dou Shu; these two functions pin the rules down: days after the fifteenth of a leap month count as the next month (can be turned off), and the late Zi hour belongs to the next day. **Signature** ```go func FixLunarMonthIndex(lunarMonth int, lunarDay int, isLeap bool, timeIndex uint8, fixLeap bool) (int, error) func FixLunarDayIndex(lunarDay int, timeIndex uint8) (int, error) ``` **Return value** The month index is 0-based (the first month is 0); the day index is not decremented in the late Zi hour. **Example** ```go m, _ := iztro.FixLunarMonthIndex(7, 17, false, 2, true) d1, _ := iztro.FixLunarDayIndex(17, 2) d2, _ := iztro.FixLunarDayIndex(17, 12) fmt.Println(m, d1, d2) ``` **Output** ```text 6 16 17 ``` The seventh month is not a leap month, giving index 6; day seventeen decrements to 16 in the Tiger hour, but stays 17 in the late Zi hour because that belongs to the next day. *** ## TranslateChineseDate [#translatechinesedate] **Purpose** Assemble the four pillars into a display string. **Signature** ```go func TranslateChineseDate(pillars [4][2]string, language Language) (string, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | ---------- | -------------- | -------- | ------- | --------------------------------------------------------------------------- | | `pillars` | `[4][2]string` | Yes | — | The four pillar keys \[year, month, day, hour], each a \[stem, branch] pair | | `language` | `Language` | Yes | — | Chart language | **Return value** When every term is a single character, the pillar's parts run together and the pillars are separated by spaces; when any term is multi-character, the parts within a pillar are separated by spaces and the pillars by `-`. **Example** ```go s, _ := iztro.TranslateChineseDate([4][2]string{ {iztro.StemGeng, iztro.BranchChen}, {iztro.StemJia, iztro.BranchShen}, {iztro.StemBing, iztro.BranchWu}, {iztro.StemGeng, iztro.BranchYin}, }, "en-US") fmt.Println(s) // the four-pillar keys can be taken straight from the chart s2, _ := iztro.TranslateChineseDate(chart.RawDates.ChineseDate.PillarKeys(), iztro.LanguageEnUS) fmt.Println(s2) ``` **Output** ```text geng chen - jia shen - bing woo - geng yin geng chen - jia shen - bing woo - geng yin ``` **Edge cases and pitfalls** Returns an error when a stem or branch key is invalid. The fixed-length array guarantees there are exactly four pillars, so no length check is needed. *** ## MergeStars [#mergestars] **Purpose** Merge several "twelve palaces of stars" groups into one, palace by palace. **Zi Wei meaning** Star placement happens in batches: major stars, minor stars and adjective stars each produce their own list of twelve palaces. Use this function to fuse them into one complete chart face. **Signature** ```go func MergeStars(groups ...[][]Star) ([][]Star, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | --------- | ------------- | -------- | ------- | ---------------------------------------------------- | | `groups` | `...[][]Star` | Yes | — | Several twelve-palace star groups, each of length 12 | **Return value** The merged twelve-palace slice, with each palace's stars concatenated in the order the groups were passed. **Example** ```go birth := iztro.StarBirth{SolarDate: "2000-8-16", TimeIndex: 2, Gender: iztro.GenderFemale, FixLeap: true, Language: "en-US"} major, _ := iztro.GetMajorStar(birth) minor, _ := iztro.GetMinorStar(birth) merged, _ := iztro.MergeStars(major, minor) names := []string{} for _, s := range merged[0] { names = append(names, s.Name) } fmt.Println(names) ``` **Output** ```text [general minister horse] ``` **Edge cases and pitfalls** Returns an error when a group's length is not 12. This is a pure local implementation and does not go through wasm. # Star placement (/en/docs/go/star) Where a group of stars lands given birth data. Use this layer when you do not want a whole chart and only need "which palace does Lucun land in?" or "how are the adjective stars distributed on this chart?". Every index is a **palace index**: 0 is the Yin palace, 11 the Chou palace. ## StarBirth [#starbirth] The entry points that take birth data all share this one parameter struct. ```go type StarBirth struct { SolarDate string TimeIndex uint8 Gender string FixLeap bool Language string Config *Config FromStem string FromBranch string } ``` | Field | Type | Description | | ------------------------- | --------- | ------------------------------------------------------------------------- | | `SolarDate` | `string` | Solar date in `YYYY-M-D` | | `TimeIndex` | `uint8` | Hour index 0–12 | | `Gender` | `string` | Gender, which sets the direction of the Changsheng and Boshi gods | | `FixLeap` | `bool` | Whether to correct for leap months | | `Language` | `string` | Output language for star names; empty takes `zh-CN` | | `Config` | `*Config` | Charting configuration; `nil` takes the defaults | | `FromStem` / `FromBranch` | `string` | The pillar anchoring the five elements class; both must be given together | ```go birth := iztro.StarBirth{ SolarDate: "2000-8-16", TimeIndex: 2, Gender: iztro.GenderFemale, FixLeap: true, Language: "en-US", } ``` The examples on this page set `Language` to `"en-US"`, so the star names in the output are English. Leaving the field empty would take the default of `zh-CN`. Once both are given, the class is derived from that pillar instead, which in turn moves Ziwei and Tianfu and the Changsheng gods. How the other star groups are placed is unaffected. Use it to obtain the placements of the Zhongzhou school's earth and human charts. *** ## GetStartIndex [#getstartindex] **Purpose** Find the starting palaces of Ziwei and Tianfu. **Zi Wei meaning** Ziwei is the anchor of the whole chart, located from the five elements class and the lunar day by the Ziwei placement rule; the other thirteen major stars then spread out from Ziwei and Tianfu. Tianfu's position mirrors Ziwei's. **Signature** ```go func GetStartIndex(birth StarBirth) (StartIndex, error) ``` **Return value** `StartIndex{ ZiweiIndex, TianfuIndex int }`. **Example** ```go s, _ := iztro.GetStartIndex(birth) fmt.Printf("%+v\n", s) ``` **Output** ```text {ZiweiIndex:4 TianfuIndex:8} ``` *** ## Landing indices per group [#landing-indices-per-group] The following six entry points share a shape: they take a `StarBirth` and return a struct whose fields are all palace indices. | Function | Return type | Fields | Placement rule | | --------------------- | ------------------ | ------------------------------------------ | ---------------------------------------------------------------------------------------------- | | `GetLuYangTuoMaIndex` | `LuYangTuoMaIndex` | `LuIndex` `YangIndex` `TuoIndex` `MaIndex` | The year stem places Lucun, with Qingyang ahead and Tuoluo behind; Tianma from the year branch | | `GetKuiYueIndex` | `KuiYueIndex` | `KuiIndex` `YueIndex` | Year stem | | `GetChangQuIndex` | `ChangQuIndex` | `ChangIndex` `QuIndex` | Hour branch | | `GetKongJieIndex` | `KongJieIndex` | `KongIndex` `JieIndex` | Hour branch | | `GetTimelyStarIndex` | `TimelyStarIndex` | `TaifuIndex` `FenggaoIndex` | Hour branch | | `GetLuanXiIndex` | `LuanXiIndex` | `HongluanIndex` `TianxiIndex` | Year branch | **Example** ```go l, _ := iztro.GetLuYangTuoMaIndex(birth) c, _ := iztro.GetChangQuIndex(birth) lx, _ := iztro.GetLuanXiIndex(birth) fmt.Printf("%+v\n%+v %+v\n", l, c, lx) ``` **Output** ```text {LuIndex:6 YangIndex:7 TuoIndex:5 MaIndex:0} {ChangIndex:6 QuIndex:4} {HongluanIndex:9 TianxiIndex:3} ``` Qingyang sits one palace ahead of Lucun and Tuoluo one behind — the direct expression of the mnemonic "Qingyang before Lucun, Tuoluo after". *** ## GetDailyStarIndex / GetMonthlyStarIndex / GetYearlyStarIndex [#getdailystarindex--getmonthlystarindex--getyearlystarindex] **Purpose** Get the landing palaces of the adjective stars placed by day, month and year. **Zi Wei meaning** Adjective stars are grouped by how they are placed: day-based stars count forward from a minor star's position, starting at day one, to the birth day; month-based stars are located from the lunar month; year-based stars are the largest group and start from the year stem or year branch. **Return value** | Function | Return type | Fields | | --------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `GetDailyStarIndex` | `DailyStarIndex` | `SantaiIndex` `BazuoIndex` `EnguangIndex` `TianguiIndex` | | `GetMonthlyStarIndex` | `MonthlyStarIndex` | `YuejieIndex` `TianyaoIndex` `TianxingIndex` `YinshaIndex` `TianyueIndex` `TianwuIndex` | | `GetYearlyStarIndex` | `YearlyStarIndex` | 27 fields: `XianchiIndex` `HuagaiIndex` `GuchenIndex` `GuasuIndex` `TiancaiIndex` `TianshouIndex` `TianchuIndex` `PosuiIndex` `FeilianIndex` `LongchiIndex` `FenggeIndex` `TiankuIndex` `TianxuIndex` `TianguanIndex` `TianfuIndex` `TiandeIndex` `YuedeIndex` `TiankongIndex` `JieluIndex` `KongwangIndex` `XunkongIndex` `TianshangIndex` `TianshiIndex` `JiekongIndex` `JieshaAdjIndex` `NianjieIndex` `DahaoAdjIndex` | **Example** ```go d, _ := iztro.GetDailyStarIndex(birth) m, _ := iztro.GetMonthlyStarIndex(birth) fmt.Printf("%+v\n%+v\n", d, m) ``` **Output** ```text {SantaiIndex:0 BazuoIndex:10 EnguangIndex:9 TianguiIndex:7} {YuejieIndex:0 TianyaoIndex:5 TianxingIndex:1 YinshaIndex:0 TianyueIndex:9 TianwuIndex:0} ``` **Edge cases and pitfalls** Year-based adjective stars belong to the yearly spirits, so their year branch comes from `HoroscopeDivide` rather than `YearDivide`. When the two settings differ, year-based stars and the major and minor stars can rest on different year branches — a deliberate distinction of school. They are year-based too, but `GetLuanXiIndex` gives them separately. These three only enter the chart when `Algorithm` is the Zhongzhou school, replacing the default placement of Jielu, Kongwang and Dahao; under the default school they are still computed, just never placed in a palace. *** ## GetMajorStar / GetMinorStar / GetAdjectiveStar [#getmajorstar--getminorstar--getadjectivestar] **Purpose** Get the complete distribution of major, minor and adjective stars across the twelve palaces. **Signature** ```go func GetMajorStar(birth StarBirth) ([][]Star, error) func GetMinorStar(birth StarBirth) ([][]Star, error) func GetAdjectiveStar(birth StarBirth) ([][]Star, error) ``` **Return value** A slice of twelve, indexed by palace index. Each item is that palace's slice of `Star`s, possibly empty. **Example** ```go major, _ := iztro.GetMajorStar(birth) for i := 0; i < 5; i++ { names := []string{} for _, s := range major[i] { names = append(names, s.Name) } fmt.Println(i, names) } ``` **Output** ```text 0 [general minister] 1 [sun sage] 2 [marshal] 3 [advisor] 4 [emperor] ``` **Edge cases and pitfalls** The returned `Star`s carry brightness and natal mutagen marks and are identical to those from a full chart — they go through the same code. If you want the whole chart, `BySolar` is simpler. Note the naming across languages: Go and Python use the singular (`GetMajorStar`, `get_major_star`) where Rust uses the plural (`get_major_stars`); the behaviour is the same. *** ## GetChangsheng12 / GetBoShi12 / GetYearly12 [#getchangsheng12--getboshi12--getyearly12] **Purpose** Get how the four groups of twelve gods are arranged across the twelve palaces. **Zi Wei meaning** Each group is twelve marks filling the twelve palaces, exactly one per palace: the Changsheng gods start from the five elements class with direction from gender and year-branch polarity; the Boshi gods start from Lucun with the same direction rule; the Sui-qian gods run forward from the year branch, and the Jiang-qian gods start from the trine group of the year branch. **Signature** ```go func GetChangsheng12(birth StarBirth) ([]string, error) func GetBoShi12(birth StarBirth) ([]string, error) func GetYearly12(birth StarBirth) (Yearly12, error) ``` **Return value** A slice of twelve keys, indexed by palace index. `GetYearly12` returns `Yearly12{ Suiqian12, Jiangqian12 []string }`. **Example** ```go cs, _ := iztro.GetChangsheng12(birth) bs, _ := iztro.GetBoShi12(birth) y, _ := iztro.GetYearly12(birth) fmt.Println(cs[:4]) fmt.Println(bs[:4]) fmt.Println(y.Suiqian12[:4]) fmt.Println(y.Jiangqian12[:4]) ``` **Output** ```text [jue mu si bing] [faylian zhoushu jiangjun xiaohao] [diaoke bingfu suijian huiqi] [suiyi xiishen huagai jiesha] ``` These are keys rather than translated names; use `Translate(key, language)` to display them. *** ## GetChangsheng12StartIndex / GetJiangqian12StartIndex [#getchangsheng12startindex--getjiangqian12startindex] **Purpose** Get just the starting palace of two of the god groups, without laying out the whole cycle. **Zi Wei meaning** The Changsheng starting point is set by the five elements class: water 2nd starts at Shen, wood 3rd at Hai, metal 4th at Si, earth 5th at Shen, fire 6th at Yin. The Jiangxing starting point is set by the trine group of the year branch: yin/woo/xu years at Woo, shen/zi/chen years at Zi, si/you/chou years at You, hai/mao/wei years at Mao. **Signature** ```go func GetChangsheng12StartIndex(fiveElementsClass string) (int, error) func GetJiangqian12StartIndex(branchKey string) (int, error) ``` **Return value** `int`, 0–11. Neither function needs birth data. **Example** ```go a, _ := iztro.GetChangsheng12StartIndex(iztro.ClassWater2nd) b, _ := iztro.GetChangsheng12StartIndex(iztro.ClassFire6th) c, _ := iztro.GetJiangqian12StartIndex(iztro.BranchZi) d, _ := iztro.GetJiangqian12StartIndex(iztro.BranchWu) fmt.Println(a, b, c, d) ``` **Output** ```text 6 0 10 4 ``` Water 2nd puts Changsheng in Shen (index 6), fire 6th in Yin (index 0). *** ## GetHoroscopeStar [#gethoroscopestar] **Purpose** Get the scope-star distribution of a horoscope layer. **Zi Wei meaning** Scope stars are the ten stars a horoscope produces: Tiankui, Tianyue, Wenchang, Wenqu, Lucun, Qingyang, Tuoluo, Tianma, Hongluan and Tianxi. Where they land is fixed by that layer's stem and branch, and their names change with the layer. The yearly layer carries one extra star, Nianjie. **Signature** ```go func GetHoroscopeStar(stemKey, branchKey, scope string, language Language) ([][]Star, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | ----------- | ---------- | -------- | ------- | ----------------------------------------------- | | `stemKey` | `string` | Yes | — | Stem key of that layer | | `branchKey` | `string` | Yes | — | Branch key of that layer | | `scope` | `string` | Yes | — | The horoscope layer, which fixes the star names | | `language` | `Language` | Yes | — | Chart language | **Return value** A slice of twelve, indexed by palace index. **Star names per layer** | Natal | Decadal | Yearly | Monthly | Daily | Hourly | | -------- | -------- | -------- | -------- | ------- | -------- | | Tiankui | Yunkui | Liukui | Yuekui | Rikui | Shikui | | Tianyue | Yunyue | Liuyue | Yueyue | Riyue | Shiyue | | Wenchang | Yunchang | Liuchang | Yuechang | Richang | Shichang | | Wenqu | Yunqu | Liuqu | Yuequ | Riqu | Shiqu | | Lucun | Yunlu | Liulu | Yuelu | Rilu | Shilu | | Qingyang | Yunyang | Liuyang | Yueyang | Riyang | Shiyang | | Tuoluo | Yuntuo | Liutuo | Yuetuo | Rituo | Shituo | | Tianma | Yunma | Liuma | Yuema | Rima | Shima | | Hongluan | Yunluan | Liuluan | Yueluan | Riluan | Shiluan | | Tianxi | Yunxi | Liuxi | Yuexi | Rixi | Shixi | The keys take the form `yunlu` (decadal Lucun), `liulu` (yearly), `yuelu` (monthly), `rilu` (daily), `shilu` (hourly). **Example** ```go decadal, _ := iztro.GetHoroscopeStar(iztro.StemJia, iztro.BranchZi, iztro.ScopeDecadal, iztro.LanguageEnUS) for i := 0; i < 4; i++ { names := []string{} for _, s := range decadal[i] { names = append(names, s.Name) } fmt.Println(i, names) } ``` **Output** ```text 0 [money(D) horse(D)] 1 [driven(D) attractive(D)] 2 [] 3 [scholar(D)] ``` **Edge cases and pitfalls** The result for `ScopeYearly` additionally contains Nianjie, located from the yearly branch and placed ahead of the ten scope stars. No other layer has it. *** ## Low-level placement [#low-level-placement] The functions above all start from birth data, deriving the year pillar, the Soul palace and the corrected lunar month internally before placing anything. This group takes those intermediates directly and is reusable in a pipeline of your own. | Function | Takes | Returns (every field a palace index `int`) | | ---------------------------------------------------------------- | -------------------------------------- | ----------------------------------------------------- | | `GetZuoYouIndex(lunarMonth)` | The corrected lunar month, 1–12 | `ZuoYouIndex{ZuoIndex, YouIndex}` | | `GetHuoLingIndex(branchKey, timeIndex)` | Year branch, hour | `HuoLingIndex{HuoIndex, LingIndex}` | | `GetHuagaiXianchiIndex(branchKey)` | Year branch | `HuagaiXianchiIndex{HuagaiIndex, XianchiIndex}` | | `GetGuGuaIndex(branchKey)` | Year branch | `GuGuaIndex{GuchenIndex, GuasuIndex}` | | `GetJieshaAdjIndex(branchKey)` | Year branch | `int`, the palace index of Jiesha | | `GetDahaoIndex(branchKey)` | Year branch | `int`, the palace index of Dahao | | `GetNianjieIndex(branchKey)` | Year branch | `int`, the palace index of Nianjie | | `GetTianshiTianshangIndex(gender, branchKey, soulIndex, config)` | Gender, year branch, Soul palace index | `TianshiTianshangIndex{TianshangIndex, TianshiIndex}` | | `GetChangQuIndexByHeavenlyStem(stemKey)` | Heavenly stem | `ChangQuIndex{ChangIndex, QuIndex}` | **Example** ```go chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) yearBranch := chart.RawDates.ChineseDate.YearlyKeys[1] hl, _ := iztro.GetHuoLingIndex(yearBranch, 2) gg, _ := iztro.GetGuGuaIndex(yearBranch) cq, _ := iztro.GetChangQuIndexByHeavenlyStem(iztro.StemJia) fmt.Println(hl, gg, cq) ``` **Output** ```text {2 10} {3 11} {3 7} ``` **Edge cases and pitfalls** `GetZuoYouIndex` takes the month after leap-month correction, i.e. `FixLunarMonthIndex(...) + 1` — not the raw lunar month. Passing the raw month on a leap-month chart lands in the wrong palace. The result of `GetTianshiTianshangIndex` follows `config.Algorithm`: the Zhongzhou school swaps Tianshang and Tianshi for yin men and yang women (where the birth-year branch polarity and the gender polarity differ), while the common school does not. Pass `nil` for `config` to take the defaults. `GetChangQuIndexByHeavenlyStem` places Wenchang and Wenqu from a heavenly stem and is used for the scope Wenchang and Wenqu of horoscope layers; the natal Wenchang and Wenqu go through `GetChangQuIndex` from the hour branch. # Data tables (/en/docs/go/data) Star information, stem and branch information, ordering constants and all the key constants. The input tables of the charting algorithm, plus the language-independent key constants. *** ## StarsInfo [#starsinfo] **Purpose** Get the star information table. **Signature** ```go func StarsInfo() (map[string]StarInfo, error) ``` **Return value** Star key → `StarInfo`. Only twenty stars have an entry: the **fourteen major stars** plus Wenchang, Wenqu, Huoxing, Lingxing, Qingyang and Tuoluo. | Field | Type | Description | | -------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------- | | `Brightness` | `[]string` | Brightness keys across the twelve palaces, index 0 being the Yin palace; an empty string where the palace has no brightness | | `FiveElements` | `string` | Five element; an empty string when the table leaves it blank | | `YinYang` | `string` | Polarity; an empty string when the table leaves it blank | **Example** ```go info, _ := iztro.StarsInfo() fmt.Println(len(info)) fmt.Printf("%+v\n", info[iztro.StarZiweiMaj]) fmt.Printf("%q\n", info[iztro.StarTaiyangMaj].FiveElements) ``` **Output** ```text 20 {Brightness:[wang wang de wang miao miao wang wang de wang ping miao] FiveElements:土 YinYang:阴} "" ``` **Edge cases and pitfalls** Some stars have no five element or polarity in the table: both are empty for Taiyang and Qisha, polarity is empty for Tanlang, Tianxiang, Tianliang and Pojun, and both are empty for the six minor stars. Note also that `FiveElements` and `YinYang` are never internationalized — they are always the Chinese characters (`土`, `阴` and so on) in every output language. *** ## FlowStarCounterparts [#flowstarcounterparts] **Purpose** The full table mapping flowing stars to their natal minor-star counterparts (50 entries). **Signature** ```go func FlowStarCounterparts() (map[string]string, error) ``` **Return value** Keys are flowing-star key constant values (`StarLiuchang` and friends), values natal minor-star keys (`StarWenchangMin` and friends). Flowing stars have no knowledge-pack entries of their own — their readings are looked up via the natal counterpart, and this table is the official mapping. **Example** ```go m, _ := iztro.FlowStarCounterparts() fmt.Println(m["liuchang"]) ``` **Output** ```text wenchangMin ``` *** ## HeavenlyStems [#heavenlystems] **Purpose** Get the heavenly stem information table. **Zi Wei meaning** The mutagen table of the stems is the root of the whole mutagen system: the birth-year stem determines the natal mutagens, a palace stem determines what that palace flies, and a scope stem determines that layer's mutagens. **Signature** ```go func HeavenlyStems() (map[string]HeavenlyStemInfo, error) ``` **Return value** Stem key → `HeavenlyStemInfo`: | Field | Type | Description | | -------------- | ---------- | -------------------------------------------------------------------------- | | `YinYang` | `string` | Polarity | | `FiveElements` | `string` | Five element | | `Crash` | `string` | Clashing stem key; an empty string for wu and ji, which clash with nothing | | `Mutagen` | `[]string` | The four mutagen star keys, in the order lu, quan, ke, ji | **Example** ```go stems, _ := iztro.HeavenlyStems() fmt.Printf("%+v\n", stems[iztro.StemJia]) fmt.Printf("wu clashes with: %q\n", stems[iztro.StemWu].Crash) ``` **Output** ```text {YinYang:阳 FiveElements:木 Crash:gengHeavenly Mutagen:[lianzhenMaj pojunMaj wuquMaj taiyangMaj]} wu clashes with: "" ``` *** ## EarthlyBranches [#earthlybranches] **Purpose** Get the earthly branch information table. **Signature** ```go func EarthlyBranches() (map[string]EarthlyBranchInfo, error) ``` **Return value** Branch key → `EarthlyBranchInfo`: | Field | Type | Description | | -------------- | -------- | ------------------------------------------------------------------------------- | | `YinYang` | `string` | Polarity, which sets the direction of the decadal scope and the Changsheng gods | | `FiveElements` | `string` | Five element | | `Crash` | `string` | Clashing branch key | | `Soul` | `string` | Soul star key (looked up by the Soul palace branch) | | `Body` | `string` | Body star key (looked up by the birth-year branch) | | `Inside` | `string` | Corresponding internal organ | | `Outside` | `string` | Corresponding body part | | `HealthTip` | `string` | Health note | `Inside`, `Outside` and `HealthTip` exist only in Chinese and take no part in internationalization. **Example** ```go branches, _ := iztro.EarthlyBranches() fmt.Printf("%+v\n", branches[iztro.BranchZi]) ``` **Output** ```text {YinYang:阳 FiveElements:水 Crash:wuEarthly Soul:tanlangMaj Body:huoxingMin Inside:胆 Outside:下体 HealthTip:生殖系统、膀胱、尿道之疾病,听觉障碍} ``` *** ## GetConstants [#getconstants] **Purpose** Get the ordering constants and derivation rule tables. **Signature** ```go func GetConstants() (Constants, error) ``` **Return value** `Constants`: | Field | Type | Description | | ------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Languages` | `[]string` | Supported language codes | | `HeavenlyStems` | `[]string` | Stem order | | `EarthlyBranches` | `[]string` | Branch order | | `Zodiac` | `[]string` | Chinese zodiac keys, in branch order | | `Signs` | `[]string` | Zodiac sign keys, in ecliptic order | | `Palaces` | `[]string` | The twelve palace names running **counterclockwise** from the Soul palace: Soul, Parents, Spirit, Property, Career, Friends, Surface, Health, Wealth, Children, Spouse, Siblings | | `Gender` | `map[string]string` | The polarity of each gender | | `ChineseTime` | `[]string` | Hour keys, from the early Zi hour to the late Zi hour | | `TimeRange` | `[]string` | The clock range of each hour | | `TigerRule` | `map[string]string` | Five Tigers rule: year stem to first-month stem | | `RatRule` | `map[string]string` | Five Rats rule: day stem to Zi-hour stem | | `Mutagen` | `[]string` | Mutagen order | | `FiveElementsClass` | `map[string]int` | Five elements class key → its number (water 2nd is 2 … fire 6th is 6) | **Example** ```go c, _ := iztro.GetConstants() fmt.Println(c.Languages) fmt.Println(c.Zodiac[:3], c.ChineseTime[12], c.TimeRange[2]) fmt.Println(c.Gender) fmt.Println("first-month stem of a jia year:", c.TigerRule[iztro.StemJia]) fmt.Println(c.Palaces) fmt.Println(c.FiveElementsClass[iztro.ClassWood3rd], iztro.FiveElementsClassNumber(iztro.ClassFire6th)) ``` **Output** ```text [en-US ja-JP ko-KR zh-CN zh-TW vi-VN] [rat ox tiger] lateRatHour 03:00~05:00 map[female:阴 male:阳] first-month stem of a jia year: bingHeavenly [soulPalace parentsPalace spiritPalace propertyPalace careerPalace friendsPalace surfacePalace healthPalace wealthPalace childrenPalace spousePalace siblingsPalace] 3 6 ``` **Edge cases and pitfalls** `Palaces` gives the **ordering** of the palace names, not what cell `i` is called on a particular chart. For that use [`GetPalaceNames(soulIndex)`](/en/docs/go/util#getpalacenames). `FiveElementsClassNumber(key)` does not need `Constants` first; an unknown key gives 0. The order of `Languages` is the merge order of iztro's vocabularies (starting from en-US), not the declaration order of the constants. [`KeyOf`](/en/docs/go/i18n#keyof) scans the languages in that same order. *** ## Key constants [#key-constants] The key constants in the package have the language-independent keys as their values and compare directly against the `*Key` fields on the data objects. | Prefix | Count | Examples | | ------------- | ------ | ---------------------------------------------------------------------------------------------------------------- | | `Palace*` | 12 + 2 | `PalaceSoul`, `PalaceWealth`, `PalaceBody`, `PalaceOriginal` | | `Star*` | 162 | `StarZiweiMaj`, `StarLucunMin`, `StarYunlu` | | `Stem*` | 10 | `StemJia`, `StemGeng` | | `Branch*` | 12 | `BranchZi`, `BranchWu` | | `Mutagen*` | 4 | `MutagenLu`, `MutagenJi` | | `Brightness*` | 7 | `BrightnessMiao`, `BrightnessWang` | | `Class*` | 5 | `ClassWater2nd`, `ClassWood3rd`, `ClassMetal4th`, `ClassEarth5th`, `ClassFire6th` | | `Scope*` | 6 | `ScopeOrigin`, `ScopeDecadal` | | `StarType*` | 8 | `StarTypeMajor`, `StarTypeTough` | | `Gender*` | 2 | `GenderMale`, `GenderFemale` (type `Gender`) | | `Language*` | 6 | `LanguageZhCN`, `LanguageZhTW`, `LanguageEnUS`, `LanguageJaJP`, `LanguageKoKR`, `LanguageViVN` (type `Language`) | | `*LeapMonth*` | 3 | `NotLeapMonth`, `LeapMonthKeep`, `LeapMonthFixed` (type `LeapMonth`, how `ByLunar` treats the leap month) | `Gender`, `Language` and `LeapMonth` are named string types: as entry-point parameters the compiler rejects any other string passed by mistake, while literals (`"male"`, `"zh-CN"`) still work; `Astrolabe.GenderKey` and `Astrolabe.Language` are of these types too. The remaining constants are untyped string constants that compare directly with the `*Key` fields. The configuration values add six more groups: | Prefix | Values | | ------------------ | ------------------------------------------------ | | `YearDivide*` | `YearDivideNormal` / `YearDivideExact` | | `HoroscopeDivide*` | `HoroscopeDivideNormal` / `HoroscopeDivideExact` | | `AgeDivide*` | `AgeDivideNormal` / `AgeDivideBirthday` | | `DayDivide*` | `DayDivideForward` / `DayDivideCurrent` | | `Algorithm*` | `AlgorithmDefault` / `AlgorithmZhongzhou` | | `Astro*` | `AstroHeaven` / `AstroEarth` / `AstroHuman` | Write `iztro.ClassWood3rd`, not `iztro.FiveElementsWood3rd` — the latter does not exist and will not compile. For the class number use `iztro.FiveElementsClassNumber(key)`. **Example** ```go soul := chart.Palace(iztro.PalaceSoul) fmt.Println(soul.MajorStars[0].Key == iztro.StarZiweiMaj) fmt.Println(iztro.StarZiweiMaj, iztro.MutagenLu, iztro.PalaceWealth) fmt.Println(iztro.ClassWood3rd, iztro.GenderFemale, iztro.LanguageEnUS) // the gender and language parameters of the charting entries take these two groups too en, err := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) if err != nil { log.Fatal(err) } fmt.Println(en.Palace(iztro.PalaceSoul).MajorStars[0].Name) ``` **Output** ```text true ziweiMaj sihuaLu wealthPalace wood3rd female en-US emperor ``` They are all untyped string constants, so `chart.Palace("soulPalace")` and `chart.Palace(iztro.PalaceSoul)` are exactly equivalent. The constants earn their keep through IDE completion and spell checking, not through type enforcement. # Translation (/en/docs/go/i18n) Two-way lookup between keys and translations. Every field on a chart already carries both a translation and a `*Key`, so manual translation is usually unnecessary. These functions exist for the cases where you have only a key (or only a translation in some language) and need to convert. Six languages are supported: `zh-CN`, `zh-TW`, `en-US`, `ja-JP`, `ko-KR`, `vi-VN`. *** ## Translate [#translate] **Purpose** Translate any key into a given language. **Signature** ```go func Translate(key string, language Language) (string, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | ---------- | ---------- | -------- | ------- | -------------------------- | | `key` | `string` | Yes | — | A language-independent key | | `language` | `Language` | Yes | — | Target language | Covering 260 keys across twelve categories: | Category | Count | Examples | | ------------------------------------------------------------ | ----- | ------------------------------------------------------------ | | Stars | 162 | `ziweiMaj`, `changsheng`, `yunlu` | | Palaces (including the Body palace and the palace of origin) | 14 | `soulPalace`, `wealthPalace`, `bodyPalace`, `originalPalace` | | Heavenly stems | 10 | `jiaHeavenly` | | Earthly branches | 12 | `ziEarthly` | | Brightness | 7 | `miao`, `wang` | | Mutagens | 4 | `sihuaLu` | | Five elements class | 5 | `water2nd` | | Gender | 2 | `male`, `female` | | Chinese zodiac | 12 | `rat`, `ox` | | Hours | 13 | `earlyRatHour` | | Zodiac signs | 12 | `aries` | | Horoscope scopes | 7 | `decadal`, `turn` | **Return value** The translation; an unknown key returns an empty string, not an error. **Example** ```go a, _ := iztro.Translate(iztro.StarZiweiMaj, iztro.LanguageEnUS) b, _ := iztro.Translate(iztro.PalaceSoul, iztro.LanguageJaJP) c, _ := iztro.Translate("nosuch", iztro.LanguageZhCN) fmt.Printf("%q %q %q\n", a, b, c) ``` **Output** ```text "emperor" "命宮" "" ``` **Edge cases and pitfalls** A key that cannot be found is not an exception; it returns an empty string. To tell "the translation happens to be empty" from "the key does not exist", confirm the key belongs to one of the categories in the table above. *** ## KeyOf [#keyof] **Purpose** Reverse-look-up a key from a translation in any language. **Signature** ```go func KeyOf(text string) (string, error) func KeyOfIn(text, keyFilter string) (string, error) ``` **Parameters** | Parameter | Type | Required | Default | Description | | ----------- | -------- | -------- | ------- | ---------------------------------------------------------------------------------- | | `text` | `string` | Yes | — | A translation in any supported language | | `keyFilter` | `string` | Yes | — | A substring the key name must contain, for disambiguating homographic translations | **Return value** The key; an empty string when nothing matches. **Example** ```go a, _ := iztro.KeyOf("紫微") b, _ := iztro.KeyOf("emperor") c, _ := iztro.KeyOf("자미") d, _ := iztro.KeyOf("no such name") fmt.Printf("%q %q %q %q\n", a, b, c, d) ``` **Output** ```text "ziweiMaj" "ziweiMaj" "ziweiMaj" "" ``` Translations in all three languages resolve to the same key. **Edge cases and pitfalls** A few translations are identical across categories: in en-US `horse` is both the zodiac horse and the star Tianma, `dragon` is both the zodiac dragon and Qinglong; in ko-KR `사` is both the branch si and Si among the Changsheng gods. `KeyOf` scans language by language, and within each language key by key, taking the first hit — in exactly the same order as iztro's `kot` (guarded case by case by golden tests). To pin down a category, use `KeyOfIn`, which only compares keys containing the substring: ```go a, _ := iztro.KeyOf("horse") // "horse" (the zodiac horse) b, _ := iztro.KeyOfIn("horse", "Min") // "tianmaMin" (Tianma) c, _ := iztro.KeyOf("유시") // "hourly" (the hourly scope) d, _ := iztro.KeyOfIn("유시", "Hour") // "roosterHour" (the You hour) e, _ := iztro.KeyOfIn("horse", "Palace") // "" ``` Common substrings: `Maj` for the fourteen major stars, `Min` for minor stars, `Heavenly` / `Earthly` for stems and branches, `Palace` for palaces, `Hour` for hours. When the filter matches nothing the result is an empty string; it does not fall back to the unfiltered result. `KeyOf` walks 260 keys × 6 languages and makes one wasm round trip. Do not put it in an inner loop over every palace and star — use the `*Key` fields that come with the data there. *** ## AllKeys [#allkeys] **Purpose** Get all 260 translatable keys. **Signature** ```go func AllKeys() ([]string, error) ``` **Return value** A slice of keys, in the order `KeyOf` scans them: horoscope scopes, Chinese zodiac, hours, zodiac signs, five elements classes, heavenly stems, earthly branches, brightness, mutagens, stars, palaces, gender — matching the merge order of iztro's per-language translation files. **Example** ```go keys, _ := iztro.AllKeys() first, _ := iztro.Translate(keys[0], iztro.LanguageEnUS) fmt.Println(len(keys), keys[:4], first) ``` **Output** ```text 260 [decadal childhood yearly monthly] decadal ``` To iterate the keys of one category, the constants in `keys.go` or `GetConstants()` are simpler. *** ## There is no global language switch [#there-is-no-global-language-switch] x-iztro keeps no global "current language" 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. Passing it explicitly means a call's result depends only on its arguments. To emit several languages within one process, just chart several times; they do not interfere: ```go zh, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageZhCN, nil) en, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) fmt.Println(zh.Palace(iztro.PalaceSoul).MajorStars[0].Name, en.Palace(iztro.PalaceSoul).MajorStars[0].Name) ``` **Output** ```text 紫微 emperor ``` The `*Key` fields of the two charts are identical, so any key-based predicate gives the same answer on both. # Knowledge packs (/en/docs/go/knowledge) KnowledgePack and its entry structs, the bundled default pack, JSON parsing, overlay merging and error handling. A knowledge pack is JSON mapping "language-independent key → reading text and school attributes". The core only judges facts; reading texts and the school-specific star attributes live here. For the concept, the format and how to write an overlay, see the [knowledge pack guide](/en/docs/guide/guides/knowledge-pack); the full field reference is [`knowledge/SCHEMA.md`](https://github.com/x-haose/x-iztro/blob/main/knowledge/SCHEMA.md) in the repository. ```go pack, err := iztro.BuiltinKnowledgePack(iztro.LanguageZhCN) intro := pack.StarIntro(iztro.StarZiweiMaj) ``` Every lookup takes a `string`; pass the `StarXxx` / `PatternXxx` / `PalaceXxx` / `MutagenXxx` constants. The default pack and the merge both live in the embedded wasm core; this package only encodes and decodes JSON. ## Types [#types] ### KnowledgePack [#knowledgepack] All fields are exported and tagged, so `encoding/json` works on them directly. | Field | Type | Meaning | | ---------- | ------------------------- | ------------------------------------------------------------------------ | | `Schema` | `int` | Format version, currently 1 | | `ID` | `string` | Pack identifier; `"iztro-docs"` for the default pack | | `Version` | `string` | Pack version; for the default pack, retrieval date + short source commit | | `Language` | `string` | Language code of the texts, e.g. `LanguageZhCN` | | `Extends` | `string` | The pack this overlay overlays; empty for a standalone pack | | `Source` | `KnowledgeSource` | Origin and licence | | `Stars` | `map[string]StarEntry` | Star entries, keyed by star key | | `Patterns` | `map[string]PatternEntry` | Pattern entries, keyed by pattern key | | `Palaces` | `map[string]TextEntry` | Palace entries, keyed by palace key | | `Mutagens` | `map[string]TextEntry` | Transformation entries, keyed by transformation key | | `Concepts` | `map[string]ConceptEntry` | Glossary entries, keyed by slug | ### KnowledgeSource [#knowledgesource] `Name`, `URL`, `Commit`, `License`, `Author`, `RetrievedAt`, `Adapted` (adaptation note), all `string`, empty when absent. ### StarEntry [#starentry] | Field | Type | Meaning | | -------------- | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `Name` | `string` | Display name in this pack's language | | `Category` | `string` | `"major"` / `"minor"` / `"adjective"` / `"dec"` / `"flow"` (a flowing star, a cross-reference entry pointing at its natal minor-star counterpart) | | `Group` | `string` | Grouping: the adjective star's category, the decorative star's group | | `Attributes` | `StarAttributes` | School attributes | | `Intro` | `string` | Reading (Markdown) | | `Combinations` | `map[string]string` | Reading for sharing a palace with another major star, keyed by that star | ### StarAttributes [#starattributes] `YinYang` (`yin` / `yang`), `FiveElements` (`wood` / `fire` / `earth` / `metal` / `water`), `Stem` (`jia`…`gui`), `FiveElementsNote`, `Dipper`, `Chemistry`, `Career`, `Duty`, `Aliases` (`[]string`), `ElementColor`, `EnergyColor`. `FiveElements` and `YinYang` are what the pack's source says, and may differ from the core star data, which is value-for-value identical to iztro's. The reason is in the [guide](/en/docs/guide/guides/knowledge-pack#why-the-star-attributes-live-here). ### PatternEntry [#patternentry] `Name`, `Quotes` (`[]string`), `Conditions`, `Intro`. ### TextEntry / ConceptEntry [#textentry--conceptentry] `TextEntry` (palaces, transformations) has `Name` and `Intro`; `ConceptEntry` (glossary) has `Title` and `Intro`. The Go side does not use pointers to separate "not written" from "written as empty". An absent field is the empty string or nil, so compare against the empty string to tell whether an entry carries text. *** ## BuiltinKnowledgePack [#builtinknowledgepack] **Purpose** Get the bundled default knowledge pack. **Signature** ```go func BuiltinKnowledgePack(language Language) (*KnowledgePack, error) func BuiltinKnowledgePackContext(ctx context.Context, language Language) (*KnowledgePack, error) ``` **Parameters** | Parameter | Type | Meaning | | ---------- | ----------------- | --------------------------------------------------------- | | `ctx` | `context.Context` | Context variant only; cancels waiting for a wasm instance | | `language` | `Language` | Text language | **Returns** `(*KnowledgePack, error)`. Languages without a bundled pack return an error, matchable with `errors.Is(err, iztro.ErrInvalidArgument)`. Only `LanguageZhCN` has one today. **Example** ```go pack, err := iztro.BuiltinKnowledgePack(iztro.LanguageZhCN) if err != nil { log.Fatal(err) } fmt.Println(pack.ID, pack.Version, pack.Language, pack.Source.License) fmt.Println(len(pack.Stars), len(pack.Patterns), len(pack.Palaces), len(pack.Mutagens), len(pack.Concepts)) _, err = iztro.BuiltinKnowledgePack(iztro.LanguageEnUS) fmt.Println(err, errors.Is(err, iztro.ErrInvalidArgument)) ``` **Output** ```text iztro-docs 2026-08-19+ec2d58b zh-CN MIT 162 64 12 4 49 iztro: no builtin knowledge pack for language 'en-US' true ``` *** ## ParseKnowledgePack [#parseknowledgepack] **Purpose** Parse a pack from JSON text. **Signature** ```go func ParseKnowledgePack(data []byte) (*KnowledgePack, error) ``` **Returns** `(*KnowledgePack, error)`. Invalid JSON, a missing or zero `schema`, and a `schema` newer than this library supports all return an `ErrInvalidArgument`-class error — the same semantics as the Rust core's parser (`KnowledgePack::from_json`). Serializing is plain `encoding/json`: ```go data, err := json.Marshal(pack) ``` **Example** ```go overlay, err := iztro.ParseKnowledgePack([]byte(`{"schema":1,"id":"my-school","version":"1", "language":"zh-CN","extends":"iztro-docs", "stars":{"ziweiMaj":{"intro":"我的紫微","attributes":{"aliases":["帝座"]}}}, "patterns":{"zi_fu_tong_gong":{"intro":"我的紫府同宫"}}}`)) if err != nil { log.Fatal(err) } fmt.Println(overlay.ID, overlay.Extends) _, err = iztro.ParseKnowledgePack([]byte("nope")) fmt.Println(err) _, err = iztro.ParseKnowledgePack([]byte(`{"schema":99}`)) fmt.Println(err) ``` **Output** ```text my-school iztro-docs iztro: invalid knowledge pack: invalid character 'o' in literal null (expecting 'u') iztro: knowledge pack schema 99 is newer than supported 1 ``` *** ## Merged [#merged] **Purpose** Layer overlay packs onto this one and return a new pack. **Signature** ```go func (p *KnowledgePack) Merged(overlays ...*KnowledgePack) (*KnowledgePack, error) func (p *KnowledgePack) MergedContext(ctx context.Context, overlays ...*KnowledgePack) (*KnowledgePack, error) ``` **Parameters** | Parameter | Type | Meaning | | ---------- | ------------------- | --------------------------------------------------------- | | `ctx` | `context.Context` | Context variant only; cancels waiting for a wasm instance | | `overlays` | `...*KnowledgePack` | Overlays applied in argument order; later ones win | **Returns** A new `*KnowledgePack`; neither this pack nor the overlays change. A nil receiver, a nil overlay, or a pack whose `schema` is invalid (hand-built structs are validated by the core here) all return an `ErrInvalidArgument`-class error. The rules are in the [guide](/en/docs/guide/guides/knowledge-pack#merge-rules): section by section, key by key, an overlay's non-empty fields replace the same-keyed entry's fields, `Attributes` and `Combinations` merge field by field, array fields are replaced wholesale. The merge itself runs in the wasm core, so all three languages agree. **Example** ```go pack, _ := iztro.BuiltinKnowledgePack(iztro.LanguageZhCN) merged, err := pack.Merged(overlay) if err != nil { log.Fatal(err) } zi := merged.Star(iztro.StarZiweiMaj) fmt.Println(merged.ID, zi.Name, zi.Attributes.Aliases, zi.Attributes.Chemistry, zi.Intro) fmt.Println(merged.PatternIntro(iztro.PatternZiFuTongGong), merged.Pattern(iztro.PatternZiFuTongGong).Quotes) fmt.Println(string([]rune(pack.StarIntro(iztro.StarZiweiMaj))[:5])) _, err = pack.Merged(nil) fmt.Println(err) ``` **Output** ```text my-school 紫微 [帝座] 尊贵 我的紫微 我的紫府同宫 [紫府同宫终身福厚。] 紫微星号称 iztro: mergeKnowledgePacks: nil overlay pack ``` *** ## Star / Pattern / Palace / Mutagen / Concept [#star--pattern--palace--mutagen--concept] **Purpose** Look up an entry by language-independent key. **Signature** ```go func (p *KnowledgePack) Star(starKey string) *StarEntry func (p *KnowledgePack) Pattern(patternKey string) *PatternEntry func (p *KnowledgePack) Palace(palaceKey string) *TextEntry func (p *KnowledgePack) Mutagen(mutagenKey string) *TextEntry func (p *KnowledgePack) Concept(slug string) *ConceptEntry ``` **Returns** `nil` when the pack has no such entry, and `nil` for a nil receiver rather than a panic. The pointer is to a copy of the entry, so writing through it does not change the pack. **Example** ```go pack, _ := iztro.BuiltinKnowledgePack(iztro.LanguageZhCN) zi := pack.Star(iztro.StarZiweiMaj) fmt.Println(zi.Name, zi.Category, zi.Attributes.Dipper, zi.Attributes.Aliases) fmt.Println(zi.Combinations[iztro.StarTianfuMaj] != "") fmt.Println(pack.Palace(iztro.PalaceSoul).Name, pack.Mutagen(iztro.MutagenLu).Name) fmt.Println(pack.Concept("tong-gong").Title) fmt.Println(pack.Star("nope") == nil) ``` **Output** ```text 紫微 major 中天星系 [帝王星 老板星 俸禄星] true 命宫 化禄 遇、加、逢、同宫、同度 true ``` *** ## StarIntro / PatternIntro [#starintro--patternintro] **Purpose** Get the reading text directly. **Signature** ```go func (p *KnowledgePack) StarIntro(starKey string) string func (p *KnowledgePack) PatternIntro(patternKey string) string ``` **Returns** The empty string both when the entry is missing and when it exists without a reading. **Example** List the natal patterns with their quotations: ```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 { fmt.Println(hit.Name, "|", pack.Pattern(hit.Key).Quotes[0]) fmt.Println(string([]rune(pack.PatternIntro(hit.Key))[:10])) } ``` **Output** ```text 府相朝垣 | 府相朝垣命必荣 “食禄千锺”的断语使 ``` # Reverse lookup (/en/docs/go/reverse) SolarDatesByBazi and ReverseChart - the functions and types for recovering candidate birth dates from BaZi pillars or chart features. Recover candidate birth dates from four BaZi pillars or from chart features. All computation runs in the wasm core (pruned enumeration + full re-charting, zero divergence from forward charting); the Go side is a typed wrapper. Concepts, how pillars follow the Config boundaries, and the multi-solution / truncation semantics are on the [reverse lookup guide](/en/docs/guide/guides/reverse). ```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) ``` Stems, branches, classes and stars all take language-independent keys (the `StemGeng`, `BranchChen`, `ClassWood3rd`, `StarZiweiMaj` constants and family). Both entry points have `Context` variants; `ctx` cancels the wait for a wasm instance. ## Types [#types] ### Pillar [#pillar] ```go type Pillar [2]string ``` One pillar: \[stem key, branch key]. The `RawDates.ChineseDate.YearlyKeys` family on an astrolabe converts directly: `iztro.Pillar(cd.YearlyKeys)`. ### BirthCandidate [#birthcandidate] One candidate birth moment, ready to hand to [`BySolar`](/en/docs/go/astro). | Field | Type | Meaning | | ----------- | -------- | ------------------------------------------------------ | | `SolarDate` | `string` | solar date, `YYYY-M-D` | | `TimeIndex` | `uint8` | hour index 0–12 (0 = early Zi hour, 12 = late Zi hour) | ### StarPosition [#starposition] A star and the branch of the palace it sits in: the atomic condition of a feature lookup. | Field | Type | Meaning | | -------- | -------- | -------------------------------------------------------------------------- | | `Star` | `string` | star key (natal chart stars only; horoscope-scope flow stars are rejected) | | `Branch` | `string` | branch key of its palace | ### ReverseCriteria [#reversecriteria] The condition set of a feature lookup. Zero values mean "not stated": an empty string leaves that condition unset, a zero `YearRange` takes `[1900, 2100]`, a `Limit` of 0 takes the core default (512), and a `nil` `FixLeap` takes the core default `true` (it is a `*bool`; switch it off explicitly with `iztro.Bool(false)`). Every condition is optional, but at least one must be given. | Field | Type | Meaning | | ------------------- | ---------------- | -------------------------------------------------------------------------------------------------- | | `SoulBranch` | `string` | soul palace branch key, empty = unconstrained | | `BodyBranch` | `string` | body palace branch key, empty = unconstrained | | `FiveElementsClass` | `string` | five elements class key, empty = unconstrained | | `Stars` | `[]StarPosition` | star placements, all of which must hold | | `Mutagens` | `[4]string` | star key carrying each birth-year mutagen \[Lu, Quan, Ke, Ji]; empty = unconstrained | | `YearRange` | `[2]int` | inclusive solar year range, within 1583–9999 | | `FixLeap` | `*bool` | leap month correction, same meaning as the charting parameter; `nil` takes the core default `true` | | `Limit` | `int` | candidate cap | ### ReverseResult [#reverseresult] | Field | Type | Meaning | | ------------ | ------------------ | ------------------------------------------------------------------------------------------ | | `Candidates` | `[]BirthCandidate` | the birth candidates satisfying every condition | | `Truncated` | `bool` | whether the search stopped early at the candidate cap; later solutions were never searched | *** ## SolarDatesByBazi [#solardatesbybazi] Recover solar birth dates from four BaZi pillars. ```go func SolarDatesByBazi(yearly, monthly, daily, hourly Pillar, startYear, endYear int, config *Config) ([]BirthCandidate, error) func SolarDatesByBaziContext(ctx context.Context, yearly, monthly, daily, hourly Pillar, startYear, endYear int, config *Config) ([]BirthCandidate, error) ``` The pillars are interpreted under the boundary readings of `config` (`YearDivide` for the year pillar, `HoroscopeDivide` for the month pillar, `DayDivide` for the late Zi hour) — the same semantics as the `RawDates.ChineseDate` a charted astrolabe reports, so reversing any chart's pillars always includes that chart's birth moment. A set of pillars recurs roughly every 60 years within the range; an hour branch of Zi may yield two candidates on adjacent days because of the early/late Zi hour split. **Example** ```go a, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) cd := a.RawDates.ChineseDate cands, err := iztro.SolarDatesByBazi( iztro.Pillar(cd.YearlyKeys), iztro.Pillar(cd.MonthlyKeys), iztro.Pillar(cd.DailyKeys), iztro.Pillar(cd.HourlyKeys), 1900, 2100, nil) if err != nil { log.Fatal(err) } for _, c := range cands { fmt.Println(c.SolarDate, c.TimeIndex) } ``` **Output** ```text 1940-8-31 2 2000-8-16 2 2060-8-1 2 ``` **Errors** A pillar with mismatched stem/branch polarity (such as 甲丑 Jia-Chou — a yang stem on a yin branch), or a year range that is reversed or outside 1583–9999, returns an `ErrInvalidArgument`-class error (matchable with `errors.Is`). See [Error handling](/en/docs/go/errors). *** ## ReverseChart [#reversechart] Recover candidate birth dates from chart features. ```go func ReverseChart(criteria *ReverseCriteria, config *Config) (*ReverseResult, error) func ReverseChartContext(ctx context.Context, criteria *ReverseCriteria, config *Config) (*ReverseResult, error) ``` Judgement runs entirely under `config`: the mutagen table, the school and every boundary follow it, so charting a candidate with the same `config` is guaranteed to satisfy every condition. Chart layout does not depend on gender (gender only affects the direction the decadal horoscope advances), so the criteria carry no gender. **Example** ```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, "", "", ""}, YearRange: [2]int{1998, 2002}, }, nil) if err != nil { log.Fatal(err) } fmt.Println(len(r.Candidates), r.Truncated) ``` **Output** ```text 39 false ``` **Errors** A `nil` criteria, empty criteria, a horoscope-scope flow star in `Stars`, or an invalid year range returns an `ErrInvalidArgument`-class error. Reaching `Limit` stops the search; later solutions never appear in the result. On `Truncated = true`, narrow `YearRange` or add conditions and query again. # Extending the astrolabe (/en/docs/go/extend) Adding custom analysis methods to a chart through struct embedding. Zi Wei analysis rules differ from practitioner to practitioner and no library can enumerate them. Go forbids adding methods to a type from another package, so the extension point is **embedding**: put `*Astrolabe` inside a struct of your own, and the new methods are called with a dot just like the built-in ones — and checked at compile time. ## The recipe [#the-recipe] Define a struct embedding `*iztro.Astrolabe` Add methods to that struct Construct it from a charted astrolabe ```go package main import ( "strings" "github.com/x-haose/x-iztro/go/iztro" ) // MyChart embeds the astrolabe and adds analysis methods of its own. type MyChart struct { *iztro.Astrolabe } // MajorStar returns the major stars of the Soul palace (borrowing the opposite // palace when empty), comma separated. func (c MyChart) MajorStar() string { soul := c.Palace(iztro.PalaceSoul) source := soul if soul.IsEmpty() { source = soul.OppositePalace() } names := make([]string, 0, len(source.MajorStars)) for _, s := range source.MajorStars { if s.Type == iztro.StarTypeMajor { names = append(names, s.Name) } } return strings.Join(names, ",") } // FiveElementsValue returns the number of the five elements class. func (c MyChart) FiveElementsValue() int { return map[string]int{ iztro.ClassWater2nd: 2, iztro.ClassWood3rd: 3, iztro.ClassMetal4th: 4, iztro.ClassEarth5th: 5, iztro.ClassFire6th: 6, }[c.FiveElementsClassKey] } ``` **Usage** ```go chart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) my := MyChart{chart} fmt.Println(my.MajorStar()) fmt.Println(my.FiveElementsValue()) // the built-in fields and methods remain available fmt.Println(my.SolarDate) fmt.Println(my.Palace(iztro.PalaceSoul).Name) // the extension method follows the charting language zhChart, _ := iztro.BySolar("2000-8-16", 2, iztro.GenderFemale, true, iztro.LanguageZhCN, nil) fmt.Println(MyChart{zhChart}.MajorStar()) ``` **Output** ```text emperor 3 2000-8-16 soul 紫微 ``` Write `*iztro.Astrolabe`, not `Astrolabe *iztro.Astrolabe` — the former promotes the built-in fields and methods to the outer type so `my.SolarDate` works, while the latter forces `my.Astrolabe.SolarDate` every time. *** ## Extending other types [#extending-other-types] The same recipe works for palaces and stars: ```go type MyPalace struct { *iztro.Palace } // IsAfflicted reports whether this palace holds a malefic and carries ji. func (p MyPalace) IsAfflicted() bool { return p.HasOneOf( iztro.StarQingyangMin, iztro.StarTuoluoMin, iztro.StarHuoxingMin, iztro.StarLingxingMin, iztro.StarDikongMin, iztro.StarDijieMin, ) && p.HasMutagen(iztro.MutagenJi) } ``` ```go for i := range chart.Palaces { p := MyPalace{&chart.Palaces[i]} if p.IsAfflicted() { fmt.Println(p.Name, "is afflicted") } } ``` **Output** ```text health is afflicted ``` *** ## Constraining extensions with an interface [#constraining-extensions-with-an-interface] When several chart types must provide the same set of analysis capabilities, use an interface: ```go type WealthAnalyzer interface { WealthScore() int HasWealthPattern() bool } func report(a WealthAnalyzer) { fmt.Println(a.WealthScore(), a.HasWealthPattern()) } ``` Any type implementing both methods can be passed in, checked at compile time. *** ## How to organize this [#how-to-organize-this] Have `WealthChart`, `CareerChart` and `HealthChart` each embed the astrolabe and let callers construct what they need. One large type forces every call site to carry every method. `s.Key == iztro.StarZiweiMaj` holds under any output language; `s.Name == "紫微"` holds only on a Chinese chart. Use `Name` at display time only. Extension methods are usually read-only, so a value receiver is fine — what is embedded is a pointer, and copying the outer struct does not copy the chart. A pointer receiver is only needed when the method has to modify the outer struct's own fields. *** ## Compared to runtime injection [#compared-to-runtime-injection] Embedding happens at compile time. Against attaching functions to objects at runtime: | | Embedding | Runtime injection | | ------------------------- | -------------------------- | ------------------------ | | Whether the method exists | Known at compile time | Known only at runtime | | Type checking | Yes | No | | Call cost | Same as a built-in method | One extra dynamic lookup | | When errors appear | Compilation fails | Fails at runtime | | Scope | Affects only your own type | Global or per instance | The price is that extension methods must be written at compile time and cannot be decided by a config file or user input. When you need that flexibility, dispatch yourself through a `map[string]func(*iztro.Astrolabe) bool`. # Error handling (/en/docs/go/errors) The Code categories of *Error, the four sentinels, what triggers them, and handling patterns. Entry points that compute something return `(value, error)`. Date format, date existence, the year range and the hour index are validated up front in the core; the string values of gender, language, keys and configuration are validated in the binding layer. Pure query methods (`Palace`, `Star`, `Has` and so on) return no error and give `nil` or a zero value when nothing is found. ## \*Error [#error] Every failure in the package comes back as the same concrete type: ```go type Error struct { Code string // machine-readable category, one of the Code* constants Message string // the error text itself (English) } func (e *Error) Error() string // returns "iztro: " + Message func (e *Error) Unwrap() error // returns the sentinel of this category, for errors.Is ``` ### The four categories [#the-four-categories] | Sentinel | `Code` constant | Value | Meaning | | --------------------- | ---------------------- | -------------------- | ------------------------------------------------------------------------------------------------------- | | `ErrInvalidDate` | `CodeInvalidDate` | `invalid_date` | Malformed date, non-existent date, or a solar year outside 1583–9999 | | `ErrInvalidTimeIndex` | `CodeInvalidTimeIndex` | `invalid_time_index` | Hour index out of range (0–12 are legal) | | `ErrInvalidArgument` | `CodeInvalidArgument` | `invalid_argument` | Any other illegal argument or configuration: gender, language, star keys, custom tables | | `ErrInternal` | `CodeInternal` | `internal` | A defect inside the library or a Go-side runtime failure, such as wasm instantiation or result decoding | The four values of `Code` are the same set as Rust's `IztroError::code()` and Python's `IztroError.code`, so cross-language branching logic transfers verbatim. ### Two ways to test [#two-ways-to-test] ```go _, err := iztro.BySolar("2000-2-30", 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) // branch by category: errors.Is against the sentinels fmt.Println(errors.Is(err, iztro.ErrInvalidDate)) fmt.Println(errors.Is(err, iztro.ErrInvalidTimeIndex)) // for the Code and the raw text: errors.As for the concrete type var e *iztro.Error if errors.As(err, &e) { fmt.Println(e.Code, "|", e.Message) } fmt.Println(err) ``` **Output** ```text true false invalid_date | invalid solar date '2000-2-30': day is out of range for that month iztro: invalid solar date '2000-2-30': day is out of range for that month ``` `Error()` prepends `iztro: ` to `Message`, keeping these errors easy to tell apart from the caller's own; the `Message` field itself has no prefix. The body of the message comes from the core and is identical across the three language bindings, but **the wording may change between versions** — branch on `errors.Is` or `Code`, never by parsing the text. *** ## Date-related [#date-related] | Situation | Example | Message body | | ---------------------------------- | ------------- | ------------------------------------ | | The format is not `YYYY-M-D` | `"2000/8/16"` | `expected 'YYYY-M-D'` | | Year, month or day is not a number | `"abc-8-16"` | `year is not a number` | | Month out of range | `"2000-13-1"` | `month must be within 1-12` | | That month has no such day | `"2000-2-30"` | `day is out of range for that month` | | Year outside the supported range | `"1500-1-1"` | `year must be within 1583-9999` | `Code` is `invalid_date`. Lunar-only cases (reachable through `ByLunar` and the two `*ByLunarDate` queries): | Situation | Example | Message body | | --------------------------------- | ----------------------------- | ------------------------------------------ | | That lunar year has no such month | month missing from the table | `month does not exist in that lunar year` | | That lunar month has no such day | `"2000-7-30"` (a short month) | `day is out of range for that lunar month` | Lunar messages are prefixed `invalid lunar date '': ` and solar ones `invalid solar date '': `, so the message alone tells you which entry point was used. **Example** ```go for _, date := range []string{"2000-13-1", "2000-2-30", "1500-1-1"} { if _, err := iztro.BySolar(date, 2, iztro.GenderFemale, true, iztro.LanguageEnUS, nil); err != nil { fmt.Println(err) } } ``` **Output** ```text iztro: invalid solar date '2000-13-1': month must be within 1-12 iztro: invalid solar date '2000-2-30': day is out of range for that month iztro: invalid solar date '1500-1-1': year must be within 1583-9999 ``` The message carries the original input, so batch jobs can pinpoint which record failed. **Edge cases and pitfalls** The Gregorian reform year of 1582 contains a stretch of dates that never existed. The underlying calendar library has no definition for them, so support starts from 1583, after the reform. The upper bound of 9999 is where the lunar data tables end. Both `"2000-8-16"` and `"2000-08-16"` are accepted. The separator must be `-`. `ByLunar` checks whether that month really exists in that lunar year and how many days it has (30 in a long month, 29 in a short one). Flagging `leap` as a leap month when that year and month have none is not an error — the ordinary month is used; a `leap` value outside the three `LeapMonth` constants returns `ErrInvalidArgument`. *** ## Hour index [#hour-index] **Trigger** An hour index greater than 12. **Example** ```go _, err := iztro.BySolar("2000-8-16", 13, iztro.GenderFemale, true, iztro.LanguageEnUS, nil) fmt.Println(err) ``` **Output** ```text iztro: time_index must be 0-12, got 13 ``` **Edge cases and pitfalls** The Zi hour straddles midnight and splits into the early Zi hour (index 0) and the late Zi hour (index 12), so there are 13 legal values. To convert from a clock hour use [`TimeToIndex`](/en/docs/go/util#timetoindex), which is guaranteed to land in the legal range. *** ## Gender and language [#gender-and-language] `Code` is `invalid_argument` for both. | Parameter | Legal values | Message body | | ---------- | ------------------------------------------------------------------- | --------------------------------------------------------------------------------- | | `gender` | `"male"` / `"female"` (the `GenderMale` / `GenderFemale` constants) | `invalid gender 'x': expected 'male' or 'female'` | | `language` | The six language codes (the `Language*` constants) | `invalid language 'xx': expected one of zh-CN, zh-TW, en-US, ja-JP, ko-KR, vi-VN` | Language codes are case-insensitive, and hyphen and underscore are interchangeable (`"zh-cn"` and `"zh_cn"` are both accepted). `gender` and `language` are the named types `Gender` / `Language`, so passing some other string variable by mistake is rejected at compile time; a misspelled literal lands in this error class at run time. **Example** ```go _, err := iztro.BySolar("2000-8-16", 2, "x", true, iztro.LanguageEnUS, nil) fmt.Println(err) fmt.Println(errors.Is(err, iztro.ErrInvalidArgument)) ``` **Output** ```text iztro: invalid gender 'x': expected 'male' or 'female' true ``` *** ## Key-related [#key-related] The utility and star-placement functions take language-independent keys, and an unknown key is an error: ```go _, err := iztro.GetBrightness("nosuch", 0, nil) fmt.Println(err) ``` **Output** ```text iztro: unknown star key 'nosuch' ``` These functions recognize keys, not translated names. Passing `"紫微"` gives `unknown star key '紫微'` — convert with [`KeyOf`](/en/docs/go/i18n#keyof) first. The exception is the **query methods on a chart**, such as `chart.Palace()` and `chart.Star()`, which accept both a key and a translated name in the chart's current language. *** ## Configuration-related [#configuration-related] The six switches and the two custom tables on `Config` are all validated during charting, with `Code` being `invalid_argument` throughout: | Situation | Message body | | ----------------------------------------------- | -------------------------------------------------------------------------------- | | Unknown value for a switch | `invalid yearDivide 'nope': expected 'normal' or 'exact'` | | Unknown stem key in the mutagen table | `invalid mutagens key 'nope': unknown heavenly stem` | | A stem's mutagens are not four entries | `invalid mutagens for 'jiaHeavenly': expected 4 stars (lu, quan, ke, ji), got 1` | | A star's brightness table is not twelve entries | `invalid brightness for 'ziweiMaj': expected 12 entries, got 1` | The lengths are checked **strictly**: exactly four mutagens and exactly twelve brightness entries, one too many or too few being an error alike. The custom tables take keys only, never translated names. *** ## Handling patterns [#handling-patterns] **Skip bad rows in a batch** ```go rows := []struct { Date string TimeIndex uint8 Gender iztro.Gender }{ {"2000-8-16", 2, "female"}, {"2000-2-30", 2, "female"}, {"1990-3-3", 13, "male"}, } var charts []*iztro.Astrolabe var failed []string for _, r := range rows { chart, err := iztro.BySolar(r.Date, r.TimeIndex, r.Gender, true, iztro.LanguageEnUS, nil) if err != nil { var e *iztro.Error errors.As(err, &e) failed = append(failed, r.Date+" -> "+e.Code) continue } charts = append(charts, chart) } fmt.Println(len(charts), failed) ``` **Output** ```text 1 [2000-2-30 -> invalid_date 1990-3-3 -> invalid_time_index] ``` **Wrap into your own error** ```go build := func(date string, ti uint8, gender iztro.Gender) (*iztro.Astrolabe, error) { chart, err := iztro.BySolar(date, ti, gender, true, iztro.LanguageEnUS, nil) if err != nil { return nil, fmt.Errorf("charting failed: %w", err) } return chart, nil } _, err := build("2000-2-30", 2, "female") fmt.Println(err) fmt.Println(errors.Is(err, iztro.ErrInvalidDate)) ``` **Output** ```text charting failed: iztro: invalid solar date '2000-2-30': day is out of range for that month true ``` `%w` preserves the original error, so callers can keep narrowing it down with `errors.Is` / `errors.As`. *** ## About nil [#about-nil] Query methods return `nil` rather than an error when nothing is found — not finding something is a normal result, not an exception: ```go p := chart.Palace("nosuchPalace") fmt.Println(p == nil) s, sp := chart.Star("nosuchStar") fmt.Println(s == nil, sp == nil) fmt.Println(chart.Palace(iztro.PalaceSoul).Has("ziweiMj")) ``` **Output** ```text true true true false ``` `chart.Palace(...)`, `chart.Star(...)` and `h.ScopeItem(...)` can all return `nil`. Reading a field straight off will panic. All three lines above are misspellings rather than "not on this chart": one letter short in a palace name gives `nil`, one letter short in a star name makes `Has` return `false` — indistinguishable from genuinely lacking that star. The `Palace*` and `Star*` constants in the package let the compiler and the IDE stop the typo where it is written. When the name comes from outside, run it through [`KeyOf`](/en/docs/go/i18n#keyof) first: it returns an empty string for text it cannot recognize, which is a solid basis for rejecting bad input. `chart.Palace("bodyPalace")` and `chart.Palace("originalPalace")` are non-`nil` on every chart: the palace of origin requires the palace stem to equal the birth-year stem and the palace not to be Zi or Chou, and the ten palaces from Yin to You walk the ten stems exactly once each, so the birth-year stem is bound to hit exactly once. If you do get `nil`, the name is misspelled.