Source code for stellium.chinese.bazi.analysis

"""Ten Gods (十神) analysis for Bazi charts.

The Ten Gods represent the relationship between the Day Master (日主) and other
stems in the chart. Each relationship has both productive and challenging aspects.

The Ten Gods are determined by two factors:
1. Element relationship (same, produces, produced-by, controls, controlled-by)
2. Polarity match (same polarity = "indirect/偏", different polarity = "direct/正")

Note: The Day Master stem always has relationship "Self" (日主/我) with itself.
"""

from dataclasses import dataclass
from enum import Enum
from typing import TYPE_CHECKING

from stellium.chinese.core import Element, HeavenlyStem

if TYPE_CHECKING:
    from stellium.chinese.bazi.models import BaZiChart


[docs] class TenGod(Enum): """The Ten Gods (十神) relationship types. Each value contains: (english_name, chinese_name, hanzi, short_code) """ # Self (日主) SELF = ("Self", "日主", "我", "DM") # Same element relationships (比劫) BI_JIAN = ("Friend", "比肩", "比", "BJ") # Same polarity JIE_CAI = ("Rob Wealth", "劫财", "劫", "JC") # Different polarity # Day Master produces (食伤) - "Output" stars SHI_SHEN = ("Eating God", "食神", "食", "SS") # Same polarity SHANG_GUAN = ("Hurting Officer", "伤官", "伤", "SG") # Different polarity # Day Master controls (财星) - "Wealth" stars PIAN_CAI = ("Indirect Wealth", "偏财", "偏财", "PC") # Same polarity ZHENG_CAI = ("Direct Wealth", "正财", "正财", "ZC") # Different polarity # Controls Day Master (官杀) - "Power" stars QI_SHA = ("Seven Killings", "七杀", "杀", "QS") # Same polarity ZHENG_GUAN = ("Direct Officer", "正官", "官", "ZG") # Different polarity # Produces Day Master (印星) - "Resource" stars PIAN_YIN = ("Indirect Seal", "偏印", "枭", "PY") # Same polarity ZHENG_YIN = ("Direct Seal", "正印", "印", "ZY") # Different polarity def __init__(self, english: str, chinese: str, hanzi: str, short_code: str): self.english = english self.chinese = chinese self.hanzi = hanzi self.short_code = short_code @property def category(self) -> str: """The category this god belongs to.""" categories = { TenGod.SELF: "Self", TenGod.BI_JIAN: "Companion", TenGod.JIE_CAI: "Companion", TenGod.SHI_SHEN: "Output", TenGod.SHANG_GUAN: "Output", TenGod.PIAN_CAI: "Wealth", TenGod.ZHENG_CAI: "Wealth", TenGod.QI_SHA: "Power", TenGod.ZHENG_GUAN: "Power", TenGod.PIAN_YIN: "Resource", TenGod.ZHENG_YIN: "Resource", } return categories[self] @property def is_direct(self) -> bool: """Whether this is a 'direct/正' relationship (different polarities).""" return self in ( TenGod.JIE_CAI, TenGod.SHANG_GUAN, TenGod.ZHENG_CAI, TenGod.ZHENG_GUAN, TenGod.ZHENG_YIN, )
[docs] @dataclass(frozen=True) class TenGodRelation: """A Ten God relationship for a specific stem in the chart.""" stem: HeavenlyStem ten_god: TenGod pillar_name: str # "year", "month", "day", "hour", or "hidden_X" is_hidden: bool = False # True if this is a hidden stem @property def display(self) -> str: """Human-readable display.""" loc = f"({self.pillar_name})" if not self.is_hidden else f"[{self.pillar_name}]" return f"{self.stem.hanzi} {self.ten_god.hanzi} {loc}"
[docs] def get_element_relationship( day_master_element: Element, other_element: Element ) -> str: """Determine the Wu Xing relationship between Day Master and another element. Returns one of: "same", "produces", "produced_by", "controls", "controlled_by" """ if day_master_element == other_element: return "same" elif day_master_element.produces == other_element: return "produces" elif other_element.produces == day_master_element: return "produced_by" elif day_master_element.controls == other_element: return "controls" elif other_element.controls == day_master_element: return "controlled_by" else: # This shouldn't happen with the five elements raise ValueError( f"Unknown relationship: {day_master_element} -> {other_element}" )
[docs] def calculate_ten_god(day_master: HeavenlyStem, other_stem: HeavenlyStem) -> TenGod: """Calculate the Ten God relationship between Day Master and another stem. Args: day_master: The Day Master (日主) stem other_stem: The stem to analyze Returns: The TenGod relationship """ # Self check if day_master == other_stem: return TenGod.SELF dm_element = day_master.element other_element = other_stem.element same_polarity = day_master.polarity == other_stem.polarity relationship = get_element_relationship(dm_element, other_element) # Map relationship + polarity to Ten God mapping = { ("same", True): TenGod.BI_JIAN, ("same", False): TenGod.JIE_CAI, ("produces", True): TenGod.SHI_SHEN, ("produces", False): TenGod.SHANG_GUAN, ("controls", True): TenGod.PIAN_CAI, ("controls", False): TenGod.ZHENG_CAI, ("controlled_by", True): TenGod.QI_SHA, ("controlled_by", False): TenGod.ZHENG_GUAN, ("produced_by", True): TenGod.PIAN_YIN, ("produced_by", False): TenGod.ZHENG_YIN, } return mapping[(relationship, same_polarity)]
[docs] def analyze_ten_gods( chart: "BaZiChart", include_hidden: bool = True ) -> list[TenGodRelation]: """Analyze all Ten God relationships in a Bazi chart. Args: chart: The BaZiChart to analyze include_hidden: Whether to include hidden stems in the analysis Returns: List of TenGodRelation objects for all stems in the chart """ day_master = chart.day_master relations: list[TenGodRelation] = [] pillar_names = ["year", "month", "day", "hour"] # Analyze main stems for pillar, name in zip(chart.pillars, pillar_names, strict=False): ten_god = calculate_ten_god(day_master, pillar.stem) relations.append( TenGodRelation( stem=pillar.stem, ten_god=ten_god, pillar_name=name, is_hidden=False, ) ) # Analyze hidden stems if requested if include_hidden: for pillar, name in zip(chart.pillars, pillar_names, strict=False): hidden_stems = pillar.branch.get_hidden_stem_objects() for i, hidden_stem in enumerate(hidden_stems): ten_god = calculate_ten_god(day_master, hidden_stem) # Label hidden stems by position: main (本气), middle (中气), residual (余气) position_labels = ["main", "middle", "residual"] position = position_labels[i] if i < 3 else f"hidden_{i}" relations.append( TenGodRelation( stem=hidden_stem, ten_god=ten_god, pillar_name=f"{name}_{position}", is_hidden=True, ) ) return relations
[docs] def count_ten_gods(relations: list[TenGodRelation]) -> dict[TenGod, int]: """Count occurrences of each Ten God in the chart. Args: relations: List of TenGodRelation from analyze_ten_gods() Returns: Dictionary mapping TenGod to count """ from collections import Counter return dict(Counter(r.ten_god for r in relations))
[docs] def count_ten_god_categories(relations: list[TenGodRelation]) -> dict[str, int]: """Count Ten Gods by category (Companion, Output, Wealth, Power, Resource). Args: relations: List of TenGodRelation from analyze_ten_gods() Returns: Dictionary mapping category name to count """ from collections import Counter return dict(Counter(r.ten_god.category for r in relations))
[docs] def get_ten_gods_for_pillar( relations: list[TenGodRelation], pillar_name: str ) -> list[TenGodRelation]: """Get all Ten God relations for a specific pillar (including hidden stems). Args: relations: List of TenGodRelation from analyze_ten_gods() pillar_name: One of "year", "month", "day", "hour" Returns: List of TenGodRelation for that pillar """ return [r for r in relations if r.pillar_name.startswith(pillar_name)]