Skip to content

Sanmeigaku (算命学)

A Sanmeigaku engine that derives a body star chart (人体星図) from the year, month, and day pillars of the sexagenary calendar (the hour pillar is not used). It reuses the Four Pillars solar-term astronomy and Ten-God logic, renaming the results into the Sanmeigaku star families: five main stars (十大主星) and three subordinate stars (十二大従星).

Star positions are recorded by their source stem or branch. The geographic 人体星図 arrangement (北/南/東/西/中央 and the shoulder/foot cells) and the 初年/中年/晩年 life-stage labelling vary between teachers and are left to an interpretation layer. The principal hidden stem (元命) defaults to each branch's primary qi (本気); see HiddenStemRule.

from fortune_telling_core import Querent, ReadingRequest
from fortune_telling_core.traditions.sanmeigaku import (
    SANMEIGAKU_DECK,
    SANMEIGAKU_SPREAD,
    build_engine,
)

request = ReadingRequest(
    deck_id=SANMEIGAKU_DECK.id,
    spread_id=SANMEIGAKU_SPREAD.id,
    querent=Querent(
        id="sample",
        display_name="Sample",
        attributes={"birth_datetime": "1984-02-02T12:00:00+09:00"},
    ),
)
reading = build_engine().cast(request)

Time-varying fortunes (年運 / 大運)

Beyond the natal chart, the summary reports the period stars for a as_of moment:

  • 年運 (annual stars) — the calendar year's 干支 mapped to a 主星 / 従星 for the day master. Always rendered; the year defaults to as_of (or requested_at) and can be pinned with the target_year attribute.
  • 大運 (luck cycles) — ten-year columns advancing (順行) or retreating (逆行) from the month pillar. These are direction-dependent, so they appear only when the request supplies a gender attribute (male / female): 陽年男・陰年女 → 順行, 陰年男・陽年女 → 逆行. Each column carries its 干支 and 主星 / 従星; the start age uses the same 節入りまでの日数 ÷ 3 convention as the Four Pillars engine (schools differ on the exact 起運 rule). build_engine(luck_count=...) sets how many columns are rendered.

sanmeigaku

SANMEIGAKU_DECK module-attribute

SANMEIGAKU_DECK = Deck(
    id="sanmeigaku.deck.stars.v1",
    symbols=tuple(
        (
            Symbol(
                id=f"sanmeigaku.main.{star.slug}",
                name=f"{star.cjk} {star.name}",
                attributes={
                    "kind": "main_star",
                    "cjk": star.cjk,
                },
            )
        )
        for star in (MAIN_STARS.values())
    )
    + tuple(
        (
            Symbol(
                id=f"sanmeigaku.subordinate.{star.slug}",
                name=f"{star.cjk} {star.name}",
                attributes={
                    "kind": "subordinate_star",
                    "cjk": star.cjk,
                },
            )
        )
        for star in (SUBORDINATE_STARS.values())
    ),
)

SANMEIGAKU_SPREAD module-attribute

SANMEIGAKU_SPREAD = Spread(
    id="sanmeigaku.spread.jintai.v1",
    name="Sanmeigaku Body Star Chart",
    positions=(
        Position(
            "year_stem",
            "Year Stem Star",
            "主星 from the year heavenly stem (年干).",
        ),
        Position(
            "month_stem",
            "Month Stem Star",
            "主星 from the month heavenly stem (月干).",
        ),
        Position(
            "year_branch",
            "Year Branch Star",
            "主星 from the year branch principal hidden stem (年支元命).",
        ),
        Position(
            "month_branch",
            "Month Branch Star",
            "主星 from the month branch principal hidden stem (月支元命).",
        ),
        Position(
            "day_branch",
            "Day Branch Star",
            "主星 from the day branch principal hidden stem (日支元命); the chart center.",
        ),
        Position(
            "year_branch_subordinate",
            "Year Branch Subordinate Star",
            "従星 of the day stem at the year branch (年支).",
        ),
        Position(
            "month_branch_subordinate",
            "Month Branch Subordinate Star",
            "従星 of the day stem at the month branch (月支).",
        ),
        Position(
            "day_branch_subordinate",
            "Day Branch Subordinate Star",
            "従星 of the day stem at the day branch (日支).",
        ),
    ),
)

DayBoundary

Bases: StrEnum

MIDNIGHT class-attribute instance-attribute

MIDNIGHT = 'midnight'

LATE_ZISHI class-attribute instance-attribute

LATE_ZISHI = 'late_zishi'

HiddenStemRule

Bases: StrEnum

Principal hidden-stem (元命) selection rule.

PRIMARY selects each branch's principal qi (本気) — the first hidden stem recorded for the branch. This is a deterministic, reproducible default.

The classical 月律分野 rule selects the 元命 from the days elapsed since the sectional solar term (節入り), choosing 余気 / 中気 / 本気 by documented day thresholds. Those thresholds diverge between schools and no single authoritative public table was available when this engine was written, so the day-threshold rule is intentionally not bundled. Register a future value here once a sourced table is available.

PRIMARY class-attribute instance-attribute

PRIMARY = 'primary'

TimeModel

Bases: StrEnum

Civil-time adjustment models used by date-based traditions.

CLOCK keeps the supplied aware datetime unchanged. LOCAL_MEAN_TIME shifts by longitude relative to the timezone offset. TRUE_SOLAR also applies the equation of time.

CLOCK class-attribute instance-attribute

CLOCK = 'clock'

LOCAL_MEAN_TIME class-attribute instance-attribute

LOCAL_MEAN_TIME = 'lmt'

TRUE_SOLAR class-attribute instance-attribute

TRUE_SOLAR = 'true_solar'

SanmeigakuEngine

SanmeigakuEngine(
    ephemeris: Ephemeris | None = None,
    *,
    time_model: TimeModel = TimeModel.CLOCK,
    day_boundary: DayBoundary = DayBoundary.MIDNIGHT,
    hidden_stem_rule: HiddenStemRule = HiddenStemRule.PRIMARY,
    luck_count: int = 8,
)

Bases: AbstractEngine

Sanmeigaku body star chart engine.

Parameters:

Name Type Description Default
ephemeris Ephemeris | None

Optional backend implementing the shared ephemeris protocol. Defaults to BuiltinEphemeris.

None
time_model TimeModel

Default time-adjustment model used when the request does not specify one.

CLOCK
day_boundary DayBoundary

Default day-boundary rule used when the request does not specify one.

MIDNIGHT
hidden_stem_rule HiddenStemRule

Default principal hidden-stem (元命) selection rule.

PRIMARY
luck_count int

Number of 大運 (luck-cycle) columns to render in the summary when a gender is supplied.

8

Time-varying fortunes: the 年運 (annual stars) for request.as_of (falling back to requested_at) are always rendered; the 大運 luck cycles are added when the request supplies gender (male/female), which fixes their 順行 / 逆行 direction.

Source code in src/fortune_telling_core/traditions/sanmeigaku/engine.py
def __init__(
    self,
    ephemeris: Ephemeris | None = None,
    *,
    time_model: TimeModel = TimeModel.CLOCK,
    day_boundary: DayBoundary = DayBoundary.MIDNIGHT,
    hidden_stem_rule: HiddenStemRule = HiddenStemRule.PRIMARY,
    luck_count: int = 8,
) -> None:
    self.ephemeris = ephemeris or BuiltinEphemeris()
    self.time_model = time_model
    self.day_boundary = day_boundary
    self.hidden_stem_rule = hidden_stem_rule
    self.luck_count = luck_count

id class-attribute instance-attribute

id = 'sanmeigaku.engine'

version class-attribute instance-attribute

version = '0.1.0'

ephemeris instance-attribute

ephemeris = ephemeris or BuiltinEphemeris()

time_model instance-attribute

time_model = time_model

day_boundary instance-attribute

day_boundary = day_boundary

hidden_stem_rule instance-attribute

hidden_stem_rule = hidden_stem_rule

luck_count instance-attribute

luck_count = luck_count

deck

deck(request: ReadingRequest) -> Deck

Return the Sanmeigaku star deck.

Source code in src/fortune_telling_core/traditions/sanmeigaku/engine.py
def deck(self, request: ReadingRequest) -> Deck:
    """Return the Sanmeigaku star deck."""

    if request.deck_id != SANMEIGAKU_DECK.id:
        raise ValidationError(f"unsupported Sanmeigaku deck: {request.deck_id}")
    return SANMEIGAKU_DECK

spread

spread(request: ReadingRequest) -> Spread

Return the Sanmeigaku body star chart spread.

Source code in src/fortune_telling_core/traditions/sanmeigaku/engine.py
def spread(self, request: ReadingRequest) -> Spread:
    """Return the Sanmeigaku body star chart spread."""

    if request.spread_id != SANMEIGAKU_SPREAD.id:
        raise ValidationError(f"unsupported Sanmeigaku spread: {request.spread_id}")
    return SANMEIGAKU_SPREAD

draw

draw(request: ReadingRequest, rng: Rng) -> Draw

Compute the Sanmeigaku chart as a deterministic draw.

Source code in src/fortune_telling_core/traditions/sanmeigaku/engine.py
def draw(self, request: ReadingRequest, rng: Rng) -> Draw:
    """Compute the Sanmeigaku chart as a deterministic draw."""

    del rng
    birth = self._birth(request)
    effective = effective_datetime(
        birth.birth_datetime, birth.longitude, birth.time_model, self.ephemeris
    )
    pillars = compute_pillars(
        birth.birth_datetime,
        effective,
        self.ephemeris,
        birth.gender or _LUCK_GENDER,
        birth.day_boundary,
    )
    return draw_from_pillars(
        pillars,
        target_year=birth.target_year,
        gender=birth.gender,
        luck_count=self.luck_count,
    )

cast

cast(request: ReadingRequest) -> Reading

Compute a Sanmeigaku reading without a caller RNG.

Source code in src/fortune_telling_core/traditions/sanmeigaku/engine.py
def cast(self, request: ReadingRequest) -> Reading:
    """Compute a Sanmeigaku reading without a caller RNG."""

    draw = self.draw(request, _NULL_RNG)
    return self._interpret(request, draw, rng=None)

build_engine

build_engine(
    ephemeris: Ephemeris | None = None,
    *,
    time_model: TimeModel = TimeModel.CLOCK,
    day_boundary: DayBoundary = DayBoundary.MIDNIGHT,
    hidden_stem_rule: HiddenStemRule = HiddenStemRule.PRIMARY,
    luck_count: int = 8,
) -> SanmeigakuEngine

Create a Sanmeigaku engine.

Parameters:

Name Type Description Default
ephemeris Ephemeris | None

Optional ephemeris backend. Defaults to the built-in backend.

None
time_model TimeModel

Default time-adjustment model.

CLOCK
day_boundary DayBoundary

Default day-boundary rule.

MIDNIGHT
hidden_stem_rule HiddenStemRule

Default principal hidden-stem (元命) rule.

PRIMARY
luck_count int

Number of 大運 columns to render when gender is supplied.

8
Source code in src/fortune_telling_core/traditions/sanmeigaku/engine.py
def build_engine(
    ephemeris: Ephemeris | None = None,
    *,
    time_model: TimeModel = TimeModel.CLOCK,
    day_boundary: DayBoundary = DayBoundary.MIDNIGHT,
    hidden_stem_rule: HiddenStemRule = HiddenStemRule.PRIMARY,
    luck_count: int = 8,
) -> SanmeigakuEngine:
    """Create a Sanmeigaku engine.

    Args:
        ephemeris: Optional ephemeris backend. Defaults to the built-in backend.
        time_model: Default time-adjustment model.
        day_boundary: Default day-boundary rule.
        hidden_stem_rule: Default principal hidden-stem (元命) rule.
        luck_count: Number of 大運 columns to render when ``gender`` is supplied.
    """

    return SanmeigakuEngine(
        ephemeris=ephemeris,
        time_model=time_model,
        day_boundary=day_boundary,
        hidden_stem_rule=hidden_stem_rule,
        luck_count=luck_count,
    )