Source code for stellium.chinese.bazi.models

"""Bazi (Four Pillars) chart data models.

A Bazi chart consists of four pillars:
- Year Pillar (年柱) - represents ancestors, early childhood
- Month Pillar (月柱) - represents parents, career
- Day Pillar (日柱) - represents self, spouse
- Hour Pillar (时柱) - represents children, later life

Each pillar has a Heavenly Stem and an Earthly Branch.

Implements the ChineseChart protocol for interoperability with other systems.
"""

from __future__ import annotations

from collections import Counter
from dataclasses import dataclass
from datetime import datetime
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
    from stellium.chinese.bazi.strength import StrengthAnalysis

from stellium.chinese.core import (
    EarthlyBranch,
    Element,
    HeavenlyStem,
    Polarity,
)


[docs] @dataclass(frozen=True) class Pillar: """A single pillar (柱) consisting of a Stem and Branch.""" stem: HeavenlyStem branch: EarthlyBranch @property def hanzi(self) -> str: """The two-character Chinese representation.""" return f"{self.stem.hanzi}{self.branch.hanzi}" @property def pinyin(self) -> str: """Pinyin romanization.""" return f"{self.stem.pinyin} {self.branch.pinyin}" @property def stem_element(self) -> Element: """The element of the stem (primary element of the pillar).""" return self.stem.element @property def branch_element(self) -> Element: """The element of the branch.""" return self.branch.element @property def animal(self) -> str: """The zodiac animal of the branch.""" return self.branch.animal @property def hidden_stems(self) -> list[HeavenlyStem]: """The hidden stems (藏干) within the branch.""" return self.branch.get_hidden_stem_objects()
[docs] def to_dict(self) -> dict[str, Any]: """Export pillar data as a dictionary.""" return { "hanzi": self.hanzi, "pinyin": self.pinyin, "stem": { "name": self.stem.name, "hanzi": self.stem.hanzi, "pinyin": self.stem.pinyin, "element": self.stem.element.english, "polarity": self.stem.polarity.value, }, "branch": { "name": self.branch.name, "hanzi": self.branch.hanzi, "pinyin": self.branch.pinyin, "animal": self.branch.animal, "element": self.branch.element.english, "polarity": self.branch.polarity.value, "hidden_stems": [ {"name": s.name, "hanzi": s.hanzi, "element": s.element.english} for s in self.hidden_stems ], }, }
def __str__(self) -> str: return f"{self.hanzi} ({self.stem.pinyin} {self.branch.animal})" def __repr__(self) -> str: return f"Pillar({self.hanzi}, {self.stem.element.english} {self.branch.animal})"
[docs] @dataclass(frozen=True) class BaZiChart: """A complete Four Pillars (Bazi / 八字) chart. The Day Stem represents the "Day Master" (日主), which is the self. Implements the ChineseChart protocol. """ year: Pillar month: Pillar day: Pillar hour: Pillar birth_datetime: datetime # ========================================================================= # ChineseChart Protocol Implementation # ========================================================================= @property def system_name(self) -> str: """The name of this system.""" return "Bazi"
[docs] def element_counts(self, include_hidden: bool = False) -> dict[Element, int]: """Count occurrences of each element across stems and branches. Args: include_hidden: If True, includes hidden stems in the count. Hidden stems are weighted: main=1.0, middle=0.5, residual=0.3 Note: For weighted hidden stem analysis, use element_strength() instead. """ elements: list[Element] = [] # Count stem elements for stem in self.all_stems: elements.append(stem.element) # Count branch elements for branch in self.all_branches: elements.append(branch.element) if include_hidden: # Add hidden stem elements for pillar in self.pillars: for hidden_stem in pillar.hidden_stems: elements.append(hidden_stem.element) return dict(Counter(elements))
[docs] def to_dict(self) -> dict[str, Any]: """Export chart data as a dictionary (JSON-serializable).""" return { "system": self.system_name, "birth_datetime": self.birth_datetime.isoformat(), "eight_characters": self.hanzi, "day_master": { "stem": self.day_master.name, "hanzi": self.day_master.hanzi, "element": self.day_master_element.english, }, "pillars": { "year": self.year.to_dict(), "month": self.month.to_dict(), "day": self.day.to_dict(), "hour": self.hour.to_dict(), }, "element_counts": { elem.english: count for elem, count in self.element_counts().items() }, "polarity_counts": { pol.value: count for pol, count in self.polarity_counts().items() }, }
[docs] def display(self) -> str: """Human-readable prose display of the chart.""" dm = self.day_master lines = [ f"Bazi Chart: {self.hanzi}", f"Day Master: {dm.hanzi} ({dm.pinyin}) - {dm.element.english} {dm.polarity.value}", "", "Four Pillars:", f" Year: {self.year.hanzi} ({self.year.stem.element.english} {self.year.branch.animal})", f" Month: {self.month.hanzi} ({self.month.stem.element.english} {self.month.branch.animal})", f" Day: {self.day.hanzi} ({self.day.stem.element.english} {self.day.branch.animal})", f" Hour: {self.hour.hanzi} ({self.hour.stem.element.english} {self.hour.branch.animal})", ] return "\n".join(lines)
# ========================================================================= # Bazi-Specific Properties and Methods # ========================================================================= @property def day_master(self) -> HeavenlyStem: """The Day Master (日主) - the stem that represents the self.""" return self.day.stem @property def day_master_element(self) -> Element: """The element of the Day Master.""" return self.day_master.element @property def pillars(self) -> tuple[Pillar, Pillar, Pillar, Pillar]: """All four pillars in order: year, month, day, hour.""" return (self.year, self.month, self.day, self.hour) @property def all_stems(self) -> tuple[HeavenlyStem, ...]: """All four heavenly stems.""" return tuple(p.stem for p in self.pillars) @property def all_branches(self) -> tuple[EarthlyBranch, ...]: """All four earthly branches.""" return tuple(p.branch for p in self.pillars)
[docs] def polarity_counts(self) -> dict[Polarity, int]: """Count Yin vs Yang across all stems and branches.""" polarities: list[Polarity] = [] for stem in self.all_stems: polarities.append(stem.polarity) for branch in self.all_branches: polarities.append(branch.polarity) return dict(Counter(polarities))
@property def hanzi(self) -> str: """The eight characters (八字) in Chinese.""" return "".join(p.hanzi for p in self.pillars) @property def all_hidden_stems(self) -> list[HeavenlyStem]: """All hidden stems across all four pillars.""" stems = [] for pillar in self.pillars: stems.extend(pillar.hidden_stems) return stems
[docs] def ten_gods(self, include_hidden: bool = True): """Analyze Ten Gods (十神) relationships in the chart. Args: include_hidden: Whether to include hidden stems in analysis Returns: List of TenGodRelation objects """ from stellium.chinese.bazi.analysis import analyze_ten_gods return analyze_ten_gods(self, include_hidden=include_hidden)
[docs] def display_detailed(self) -> str: """Detailed prose display including hidden stems and Ten Gods.""" from stellium.chinese.bazi.analysis import calculate_ten_god dm = self.day_master pillar_names = ["Year", "Month", "Day", "Hour"] hidden_labels = ["main", "middle", "residual"] lines = [ f"Bazi Chart: {self.hanzi}", f"Day Master: {dm.hanzi} ({dm.pinyin}) - {dm.element.english} {dm.polarity.value}", "", "Four Pillars with Ten Gods:", ] for name, pillar in zip(pillar_names, self.pillars, strict=True): god = calculate_ten_god(dm, pillar.stem) god_label = "Self" if god.hanzi == "我" else f"{god.english} ({god.hanzi})" lines.append( f" {name}: {pillar.hanzi} - {pillar.stem.element.english} {pillar.branch.animal} " f"[{god_label}]" ) # Hidden stems for this pillar if pillar.hidden_stems: hidden_parts = [] for i, hs in enumerate(pillar.hidden_stems): hs_god = calculate_ten_god(dm, hs) label = ( hidden_labels[i] if i < len(hidden_labels) else f"hidden{i + 1}" ) hidden_parts.append(f"{hs.hanzi} {hs_god.hanzi} ({label})") lines.append(f" Hidden: {', '.join(hidden_parts)}") return "\n".join(lines)
[docs] def strength(self) -> StrengthAnalysis: """Analyze the Day Master's strength. Returns a StrengthAnalysis with classification (Strong/Weak/etc.), component scores, and favorable/unfavorable elements. Example:: bazi = ChartBuilder.from_details("1994-01-06 11:47", "Palo Alto, CA").bazi() result = bazi.strength() print(result.strength.english) # "Strong", "Weak", etc. print(result.favorable_elements) """ from stellium.chinese.bazi.strength import analyze_strength return analyze_strength(self)
def __str__(self) -> str: return self.hanzi