feat(dasha): Yogini 小运按比例单列,大运输出不变
完整解读仍只看大运。小运从该大运自己的 Yogini 起按年数比例排,出生前的段丢掉。 Co-Authored-By: Grok (agent loop) Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Grok (agent loop)
Cursor
parent
28dd8df333
commit
6eeb98c774
@@ -61,10 +61,63 @@ def calculate_yogini_dasha(birth_info: dict) -> dict:
|
||||
moon_longitude: sidereal degrees, required
|
||||
as_of / today: observation instant; report callers should pass it
|
||||
dasha_year_days: fixed-day year, default 365.25
|
||||
|
||||
Major rows stay free of sub-periods. Callers that need antardashas use
|
||||
``yogini_antardashas`` on a copy.
|
||||
"""
|
||||
return _calculate_moon_balanced_yogini(birth_info)
|
||||
|
||||
|
||||
def _as_datetime(value):
|
||||
if isinstance(value, datetime):
|
||||
return value
|
||||
return datetime.fromisoformat(str(value))
|
||||
|
||||
|
||||
def yogini_antardashas(major_row: dict, *, birth_dt, year_days: float) -> list:
|
||||
"""Proportional antardashas inside one Yogini mahadasha.
|
||||
|
||||
Eight sub-periods start at this mahadasha's own Yogini and follow Goel
|
||||
Table I. Each length is ``full_years * sub_years / 36``. They are laid out
|
||||
from ``unclipped_start_date``. Sub-periods that end at or before birth are
|
||||
dropped. The one that crosses birth keeps ``unclipped_start_date`` and
|
||||
starts at birth. The last sub-period ends on the mahadasha ``end_date``.
|
||||
"""
|
||||
birth = _as_datetime(birth_dt)
|
||||
year_length = float(year_days)
|
||||
if year_length <= 0:
|
||||
raise ValueError('dasha_year_days must be positive')
|
||||
start_idx = next(
|
||||
index for index, item in enumerate(YOGINI_SEQUENCE) if item['name'] == major_row['yogini']
|
||||
)
|
||||
full_years = float(major_row['full_years'])
|
||||
cursor = _as_datetime(major_row['unclipped_start_date'])
|
||||
major_end = _as_datetime(major_row['end_date'])
|
||||
rows = []
|
||||
for offset in range(len(YOGINI_SEQUENCE)):
|
||||
item = YOGINI_SEQUENCE[(start_idx + offset) % len(YOGINI_SEQUENCE)]
|
||||
span_days = full_years * item['years'] / TOTAL_CYCLE * year_length
|
||||
end = major_end if offset == len(YOGINI_SEQUENCE) - 1 else cursor + timedelta(days=span_days)
|
||||
if end <= birth:
|
||||
cursor = end
|
||||
continue
|
||||
visible_start = birth if cursor < birth else cursor
|
||||
row = {
|
||||
'level': 'antardasha',
|
||||
'planet': item['planet'],
|
||||
'yogini': item['name'],
|
||||
'lord': item['planet'],
|
||||
'years': (end - visible_start).total_seconds() / (86400 * year_length),
|
||||
'start_date': visible_start.isoformat(timespec='seconds'),
|
||||
'end_date': end.isoformat(timespec='seconds'),
|
||||
}
|
||||
if cursor < birth:
|
||||
row['unclipped_start_date'] = cursor.isoformat(timespec='seconds')
|
||||
rows.append(row)
|
||||
cursor = end
|
||||
return rows
|
||||
|
||||
|
||||
def _calculate_moon_balanced_yogini(birth_info):
|
||||
"""Goel Table I order, angular birth balance, explicit fixed-day year."""
|
||||
birth_dt = birth_info.get("birth_datetime")
|
||||
|
||||
@@ -2,7 +2,7 @@ from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.yogini_dasha import YOGINI_SEQUENCE, calculate_yogini_dasha
|
||||
from scripts.yogini_dasha import YOGINI_SEQUENCE, TOTAL_CYCLE, calculate_yogini_dasha, yogini_antardashas
|
||||
|
||||
|
||||
def test_yogini_classical_sequence_and_balance():
|
||||
@@ -57,3 +57,77 @@ def test_yogini_explicit_reference_controls_current_cycle():
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user