Source code for stellium.chinese.bazi.strength

"""Day Master Strength Analysis (日主强弱) for BaZi charts.

Determines whether the Day Master (日主) is strong (旺) or weak (弱)
by analyzing four traditional factors:

1. **Seasonal strength (令)** — Is the Day Master's element in season?
   The month branch determines which element is prosperous, resting,
   imprisoned, or dead.

2. **Root strength (根/禄)** — Does the Day Master have roots in
   the branches? Hidden stems matching the Day Master's element
   provide grounding and stability.

3. **Support count** — How many chart elements support the Day Master?
   Companion (比劫) and Resource (印星) elements help.

4. **Drain count** — How many chart elements drain or control the
   Day Master? Output (食伤), Wealth (财星), and Power (官杀)
   elements weaken it.

The weighted score determines strength classification:
- Very Strong (极旺): dominant element, seasonal support, many roots
- Strong (旺): favorable balance of support vs drain
- Moderate (中和): roughly balanced
- Weak (弱): unfavorable balance
- Very Weak (极弱): no seasonal support, no roots, heavy drain

References:
- Traditional seasonal strength tables (十二长生 / Twelve Growth Stages)
- Joey Yap, "BaZi — The Destiny Code"
- Lily Chung, "The Path to Good Fortune"
"""

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

from stellium.chinese.bazi.analysis import (
    analyze_ten_gods,
    count_ten_god_categories,
)
from stellium.chinese.core import EarthlyBranch, Element, HeavenlyStem

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


[docs] class DayMasterStrength(Enum): """Classification of Day Master strength.""" VERY_STRONG = ("Very Strong", "极旺", "jí wàng") STRONG = ("Strong", "旺", "wàng") MODERATE = ("Moderate", "中和", "zhōng hé") WEAK = ("Weak", "弱", "ruò") VERY_WEAK = ("Very Weak", "极弱", "jí ruò") def __init__(self, english: str, hanzi: str, pinyin: str): self.english = english self.hanzi = hanzi self.pinyin = pinyin
# ── Seasonal Strength Tables ──────────────────────────────────────────── # # Each element has a seasonal relationship with each month branch. # The month branch represents the dominant energy of the season. # # Stages (traditional 十二长生 simplified to 4 levels): # PROSPEROUS (旺) = element is in season, strongest (+3) # STRONG (相) = element is supported by the season (+1) # RESTING (休) = element is neutral, resting (0) # IMPRISONED (囚) = element is weakened (-1) # DEAD (死) = element is at its weakest (-2) # Map: month branch → which element is prosperous (旺) in that month # Spring (Yin/Mao/Chen) → Wood prospers # Summer (Si/Wu/Wei) → Fire prospers # Late Summer (transition months Chen/Wei/Xu/Chou) → Earth prospers # Autumn (Shen/You/Xu) → Metal prospers # Winter (Hai/Zi/Chou) → Water prospers MONTH_PROSPEROUS_ELEMENT: dict[EarthlyBranch, Element] = { EarthlyBranch.YIN: Element.WOOD, # Early spring EarthlyBranch.MAO: Element.WOOD, # Mid spring EarthlyBranch.CHEN: Element.EARTH, # Late spring (Earth transition) EarthlyBranch.SI: Element.FIRE, # Early summer EarthlyBranch.WU_BRANCH: Element.FIRE, # Mid summer (午 Horse) EarthlyBranch.WEI: Element.EARTH, # Late summer (Earth transition) EarthlyBranch.SHEN: Element.METAL, # Early autumn EarthlyBranch.YOU: Element.METAL, # Mid autumn EarthlyBranch.XU: Element.EARTH, # Late autumn (Earth transition) EarthlyBranch.HAI: Element.WATER, # Early winter EarthlyBranch.ZI: Element.WATER, # Mid winter EarthlyBranch.CHOU: Element.EARTH, # Late winter (Earth transition) } def _get_seasonal_score(element: Element, month_branch: EarthlyBranch) -> int: """Get the seasonal strength score for an element in a given month. Uses the Wu Xing cycle relationships: - Same as prosperous element → PROSPEROUS (+3) - Produced by prosperous element → STRONG (+1) - Produces prosperous element (drained) → RESTING (0) - Controlled by prosperous element → IMPRISONED (-1) - Controls prosperous element (exhausting) → DEAD (-2) Args: element: The element to evaluate month_branch: The month branch (determines the season) Returns: Score from -2 to +3 """ prosperous = MONTH_PROSPEROUS_ELEMENT[month_branch] if element == prosperous: return 3 # PROSPEROUS — in season elif prosperous.produces == element: return 1 # STRONG — produced by season elif element.produces == prosperous: return 0 # RESTING — drained by season elif prosperous.controls == element: return -1 # IMPRISONED — controlled by season elif element.controls == prosperous: return -2 # DEAD — exhausting to fight the season # Should never reach here with 5 elements return 0 def _count_roots(chart: "BaZiChart", day_master_element: Element) -> float: """Calculate weighted root strength from branch hidden stems. A "root" (根) is when the Day Master's element appears as a hidden stem in any of the four branches. Root position matters: - Day branch (坐下, "sitting beneath"): weight 1.5 — YOUR branch, the strongest possible root - Month branch (月支, "command"): weight 1.2 — seasonal authority - Hour branch (时支): weight 0.8 — children/future pillar - Year branch (年支): weight 0.6 — ancestors/distant pillar Within each branch, the hidden stem position also matters: - Main qi (本气): full weight — primary energy - Middle qi (中气): 60% weight — secondary - Residual qi (余气): 30% weight — trace energy Args: chart: The BaZi chart day_master_element: The Day Master's element Returns: Weighted root score (typically 0-6) """ # Pillar weights by position (Day is most significant) pillar_weights = [0.6, 1.2, 1.5, 0.8] # year, month, day, hour # Hidden stem position weights (main > middle > residual) position_weights = [1.0, 0.6, 0.3] total = 0.0 for pillar, p_weight in zip(chart.pillars, pillar_weights, strict=True): hidden_stems = pillar.branch.get_hidden_stem_objects() for i, hidden_stem in enumerate(hidden_stems): if hidden_stem.element == day_master_element: h_weight = position_weights[i] if i < len(position_weights) else 0.2 total += p_weight * h_weight return total def _count_support_drain( chart: "BaZiChart", ) -> tuple[int, int]: """Count supporting vs draining elements in the chart. Supporting categories (help the Day Master): - Self/Companion (比劫) — same element - Resource (印星) — produces the Day Master Draining categories (weaken the Day Master): - Output (食伤) — Day Master produces (energy flows out) - Wealth (财星) — Day Master controls (effort to control) - Power (官杀) — controls Day Master (pressure) Args: chart: The BaZi chart Returns: Tuple of (support_count, drain_count) from Ten Gods analysis """ relations = analyze_ten_gods(chart, include_hidden=True) categories = count_ten_god_categories(relations) support = ( categories.get("Self", 0) + categories.get("Companion", 0) + categories.get("Resource", 0) ) drain = ( categories.get("Output", 0) + categories.get("Wealth", 0) + categories.get("Power", 0) ) return support, drain
[docs] @dataclass(frozen=True) class StrengthAnalysis: """Complete Day Master strength analysis result.""" day_master: HeavenlyStem day_master_element: Element strength: DayMasterStrength score: float # Component scores seasonal_score: int root_count: float support_count: int drain_count: int month_branch: EarthlyBranch @property def is_strong(self) -> bool: """Whether the Day Master is considered strong (旺 or 极旺).""" return self.strength in ( DayMasterStrength.VERY_STRONG, DayMasterStrength.STRONG, ) @property def is_weak(self) -> bool: """Whether the Day Master is considered weak (弱 or 极弱).""" return self.strength in (DayMasterStrength.VERY_WEAK, DayMasterStrength.WEAK) @property def favorable_elements(self) -> list[Element]: """Elements that are favorable for this Day Master. If strong: needs Output, Wealth, Power to drain excess If weak: needs Companion, Resource to build strength """ dm = self.day_master_element if self.is_strong: # Strong DM benefits from being drained/controlled return [dm.produces, dm.controls, dm.controlled_by] else: # Weak DM benefits from support return [dm, dm.produced_by] @property def unfavorable_elements(self) -> list[Element]: """Elements that are unfavorable for this Day Master. Opposite of favorable. """ dm = self.day_master_element if self.is_strong: return [dm, dm.produced_by] else: return [dm.produces, dm.controls, dm.controlled_by]
[docs] def to_dict(self) -> dict: """Export to JSON-serializable dictionary.""" return { "day_master": { "stem": self.day_master.hanzi, "element": self.day_master_element.english, "pinyin": self.day_master.pinyin, }, "strength": { "classification": self.strength.english, "hanzi": self.strength.hanzi, "score": round(self.score, 2), }, "factors": { "seasonal_score": self.seasonal_score, "root_count": round(self.root_count, 2), "support_count": self.support_count, "drain_count": self.drain_count, }, "is_strong": self.is_strong, "favorable_elements": [e.english for e in self.favorable_elements], "unfavorable_elements": [e.english for e in self.unfavorable_elements], }
[docs] def display(self) -> str: """Human-readable summary.""" lines = [ f"Day Master: {self.day_master.hanzi} ({self.day_master_element.english} " f"{self.day_master.polarity.value})", f"Strength: {self.strength.hanzi} ({self.strength.english}) " f"[score: {self.score:.1f}]", "", "Factors:", f" Seasonal: {self.seasonal_score:+d} " f"({self.day_master_element.english} in " f"{self.month_branch.pinyin} month)", f" Roots: {self.root_count:.1f} " f"(weighted hidden stems matching {self.day_master_element.english})", f" Support: {self.support_count} (Self + Companion + Resource)", f" Drain: {self.drain_count} (Output + Wealth + Power)", "", f"Favorable: {', '.join(e.english for e in self.favorable_elements)}", f"Unfavorable: {', '.join(e.english for e in self.unfavorable_elements)}", ] return "\n".join(lines)
[docs] def analyze_strength(chart: "BaZiChart") -> StrengthAnalysis: """Analyze the Day Master's strength in the chart. This is the main entry point for strength analysis. Args: chart: A calculated BaZiChart Returns: Complete StrengthAnalysis with classification and component scores Example: >>> from stellium.chinese import BaZiEngine >>> engine = BaZiEngine(timezone_offset_hours=8) >>> chart = engine.calculate(datetime(1990, 5, 15, 10, 30)) >>> result = analyze_strength(chart) >>> print(result.display()) """ day_master = chart.day_master dm_element = day_master.element month_branch = chart.month.branch # Factor 1: Seasonal strength seasonal = _get_seasonal_score(dm_element, month_branch) # Factor 2: Root count roots = _count_roots(chart, dm_element) # Factor 3 & 4: Support vs drain support, drain = _count_support_drain(chart) # Calculate weighted score # Seasonal is most important (traditional weighting) # Roots provide stability # Net support/drain is the balance score = ( seasonal * 2.0 # Season is weighted double + roots * 1.5 # Each root is significant + (support - drain) * 0.5 # Net balance ) # Classify if score >= 6.0: strength = DayMasterStrength.VERY_STRONG elif score >= 2.0: strength = DayMasterStrength.STRONG elif score >= -2.0: strength = DayMasterStrength.MODERATE elif score >= -6.0: strength = DayMasterStrength.WEAK else: strength = DayMasterStrength.VERY_WEAK return StrengthAnalysis( day_master=day_master, day_master_element=dm_element, strength=strength, score=score, seasonal_score=seasonal, root_count=roots, support_count=support, drain_count=drain, month_branch=month_branch, )