完整解读仍只看大运。小运从该大运自己的 Yogini 起按年数比例排,出生前的段丢掉。 Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
134 lines
6.0 KiB
Python
134 lines
6.0 KiB
Python
from datetime import datetime, timedelta
|
||
|
||
import pytest
|
||
|
||
from scripts.yogini_dasha import YOGINI_SEQUENCE, TOTAL_CYCLE, calculate_yogini_dasha, yogini_antardashas
|
||
|
||
|
||
def test_yogini_classical_sequence_and_balance():
|
||
# Goel Classical chapter Table I: lord and years, not the older Maya/Jaya/Rati list.
|
||
assert [(row['name'], row['planet'], row['years']) for row in YOGINI_SEQUENCE] == [
|
||
('Mangala', 'Moon', 1),
|
||
('Pingala', 'Sun', 2),
|
||
('Dhanya', 'Jupiter', 3),
|
||
('Bhramari', 'Mars', 4),
|
||
('Bhadrika', 'Mercury', 5),
|
||
('Ulka', 'Saturn', 6),
|
||
('Siddha', 'Venus', 7),
|
||
('Sankata', 'Rahu', 8),
|
||
]
|
||
birth = datetime(2000, 1, 1, 12)
|
||
# Mid-Pushya (eighth nakshatra): remainder 3 is Dhanya, half of 3 years left.
|
||
result = calculate_yogini_dasha({
|
||
'birth_datetime': birth,
|
||
'moon_longitude': 100.0,
|
||
'as_of': '2000-01-02',
|
||
'dasha_year_days': 360,
|
||
})
|
||
assert result['starting_planet'] == 'Jupiter'
|
||
assert result['starting_yogini'] == 'Dhanya'
|
||
assert result['birth_balance_years'] == pytest.approx(1.5)
|
||
assert datetime.fromisoformat(result['major'][0]['end_date']) == birth + timedelta(days=540)
|
||
assert result['major'][0]['years'] == pytest.approx(1.5)
|
||
assert result['current']['planet'] == 'Jupiter'
|
||
assert result['major'][1]['planet'] == 'Mars'
|
||
assert all(a['end_date'] == b['start_date'] for a, b in zip(result['major'], result['major'][1:]))
|
||
|
||
|
||
def test_yogini_no_invented_lagna_fallback_or_missing_balance():
|
||
# 原值: lagna-only input started at Maya via legacy_lagna_rashi_compatibility
|
||
# 新值: status blocked,major 为空
|
||
# 原因: BUG-1262,没有月亮黄经就不能编一段从出生起算的满周期
|
||
result = calculate_yogini_dasha({
|
||
'lagna_rashi_index': 0,
|
||
'moon_nakshatra_index': 0,
|
||
'birth_datetime': datetime(2000, 1, 1),
|
||
})
|
||
assert result['status'] == 'blocked'
|
||
assert result['major'] == []
|
||
|
||
|
||
def test_yogini_explicit_reference_controls_current_cycle():
|
||
args = {'birth_datetime': '2000-01-01', 'moon_longitude': 0.0, 'dasha_year_days': 360}
|
||
first = calculate_yogini_dasha(dict(args, as_of='2000-01-02'))
|
||
later = calculate_yogini_dasha(dict(args, as_of='2040-01-01'))
|
||
assert first['current']['cycle_number'] == 1
|
||
assert later['current']['cycle_number'] == 2
|
||
# Ashwini start: (0 + 3) % 8 is Bhramari / Mars, and the balance is the whole period.
|
||
assert first['starting_planet'] == 'Mars'
|
||
assert first['birth_balance_years'] == pytest.approx(4.0)
|
||
|
||
|
||
def _sub_names(start_name):
|
||
start = next(index for index, item in enumerate(YOGINI_SEQUENCE) if item['name'] == start_name)
|
||
return [YOGINI_SEQUENCE[(start + offset) % 8]['name'] for offset in range(8)]
|
||
|
||
|
||
def test_yogini_antardasha_proportional_on_unclipped_seed():
|
||
# Moon at Ashwini 0°: Bhramari opens on the birth instant, so nothing is clipped.
|
||
birth = datetime(2000, 1, 1)
|
||
year_days = 360
|
||
result = calculate_yogini_dasha({
|
||
'birth_datetime': birth,
|
||
'moon_longitude': 0.0,
|
||
'as_of': '2000-01-02',
|
||
'dasha_year_days': year_days,
|
||
})
|
||
assert all('antardasha' not in row for row in result['major'])
|
||
major = result['major'][0]
|
||
assert major['unclipped_start_date'] == major['start_date']
|
||
subs = yogini_antardashas(major, birth_dt=birth, year_days=year_days)
|
||
assert [row['yogini'] for row in subs] == _sub_names('Bhramari')
|
||
assert all('unclipped_start_date' not in row for row in subs)
|
||
assert subs[0]['start_date'] == major['start_date']
|
||
assert subs[-1]['end_date'] == major['end_date']
|
||
assert all(left['end_date'] == right['start_date'] for left, right in zip(subs, subs[1:]))
|
||
for row, item in zip(subs, YOGINI_SEQUENCE[3:] + YOGINI_SEQUENCE[:3]):
|
||
assert row['level'] == 'antardasha'
|
||
assert row['planet'] == row['lord'] == item['planet']
|
||
assert row['years'] == pytest.approx(major['full_years'] * item['years'] / TOTAL_CYCLE)
|
||
visible = sum(
|
||
(datetime.fromisoformat(row['end_date']) - datetime.fromisoformat(row['start_date'])).total_seconds()
|
||
for row in subs
|
||
)
|
||
major_span = (
|
||
datetime.fromisoformat(major['end_date']) - datetime.fromisoformat(major['start_date'])
|
||
).total_seconds()
|
||
assert abs(visible - major_span) <= 86400
|
||
|
||
|
||
def test_yogini_antardasha_clips_the_birth_crossing_subperiod():
|
||
# Mid-Pushya is the same seed as test_yogini_classical_sequence_and_balance,
|
||
# shifted so the elapsed balance falls inside Siddha rather than on a boundary.
|
||
birth = datetime(2000, 1, 1, 12)
|
||
year_days = 360
|
||
moon = (7 + 0.6) * (360 / 27)
|
||
result = calculate_yogini_dasha({
|
||
'birth_datetime': birth,
|
||
'moon_longitude': moon,
|
||
'as_of': '2000-01-02',
|
||
'dasha_year_days': year_days,
|
||
})
|
||
first = result['major'][0]
|
||
later = result['major'][1]
|
||
first_subs = yogini_antardashas(first, birth_dt=birth, year_days=year_days)
|
||
later_subs = yogini_antardashas(later, birth_dt=birth, year_days=year_days)
|
||
assert first_subs[0]['yogini'] == 'Siddha'
|
||
assert first_subs[0]['start_date'] == first['start_date'] == birth.isoformat(timespec='seconds')
|
||
assert datetime.fromisoformat(first_subs[0]['unclipped_start_date']) < birth
|
||
assert [row['yogini'] for row in first_subs] == _sub_names('Dhanya')[-4:]
|
||
assert later_subs[0]['start_date'] == later['start_date']
|
||
assert [row['yogini'] for row in later_subs] == _sub_names(later['yogini'])
|
||
assert 'unclipped_start_date' not in later_subs[0]
|
||
for major, subs in ((first, first_subs), (later, later_subs)):
|
||
assert all(left['end_date'] == right['start_date'] for left, right in zip(subs, subs[1:]))
|
||
assert subs[-1]['end_date'] == major['end_date']
|
||
visible = sum(
|
||
(datetime.fromisoformat(row['end_date']) - datetime.fromisoformat(row['start_date'])).total_seconds()
|
||
for row in subs
|
||
)
|
||
major_span = (
|
||
datetime.fromisoformat(major['end_date']) - datetime.fromisoformat(major['start_date'])
|
||
).total_seconds()
|
||
assert abs(visible - major_span) == 0
|