fix(avastha): keep simultaneous conditions and use the PVR activity formula

Upstream e628118d, 7d545039, f75a6e77, and ac565836. Dignity follows the sign, Deeptadi keeps every condition that applies, and Shayanadi hours come from the previous Hindu sunrise. BUG-1389.

Co-Authored-By: Grok (agent loop)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jesse-ux
2026-10-18 16:57:45 +08:00
co-authored by Grok (agent loop) Cursor
parent 0bd254cf18
commit 83c97a8976
9 changed files with 365 additions and 49 deletions
+64
View File
@@ -0,0 +1,64 @@
"""Classical age/parity and dignity rules, independent of house placement."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts'))
from avastha_calculator import AvasthaCalculator
def test_deeptadi_keeps_exaltation_and_combustion_together():
row = AvasthaCalculator().calculate_all_avasthas(
'Venus', 'Pisces', 27, 1, is_combust=True)['avasthas']['Deeptadi']
assert row['state'] == 'Deepta'
assert 'Deepta' in row['applicable_states']
assert 'Kopa' in row['applicable_states']
assert row['conditions']['combust'] is True
assert row['legacy_single_label_note'] == '无经典出处'
assert row['uncomputed_label'] == '未计算'
@pytest.mark.parametrize('sign,degree,state,strength', [
('Aries', 0, 'Infant', .25), ('Aries', 6, 'Young', .5),
('Aries', 12, 'Youth', 1), ('Aries', 18, 'Old', None),
('Aries', 24, 'Dead', 0), ('Taurus', 0, 'Dead', 0),
('Taurus', 6, 'Old', None), ('Taurus', 12, 'Youth', 1),
('Taurus', 18, 'Young', .5), ('Taurus', 24, 'Infant', .25),
])
def test_age_sign_parity_and_exact_boundaries(sign, degree, state, strength):
row = AvasthaCalculator().calculate_all_avasthas('Sun', sign, degree, 9)['avasthas']['Bala']
assert row['state'] == state and row['strength'] == strength
@pytest.mark.parametrize('planet,sign,expected', [
('Sun', 'Aries', 'Awake'), ('Venus', 'Pisces', 'Awake'),
('Mars', 'Cancer', 'Deep Sleep'), ('Saturn', 'Aquarius', 'Awake'),
('Sun', 'Capricorn', 'Deep Sleep'), ('Sun', 'Sagittarius', 'Dreaming'),
('Sun', 'Gemini', 'Dreaming'), ('Rahu', 'Aries', 'Not classified'),
])
def test_jagrat_follows_dignity_not_house(planet, sign, expected):
calculator = AvasthaCalculator()
for house in (1, 2, 9):
row = calculator.calculate_all_avasthas(planet, sign, 4.95, house)['avasthas']['Jagrat']
assert row['state'] == expected
def test_simplified_states_cannot_masquerade_as_full_classical_methods():
states = AvasthaCalculator().calculate_all_avasthas('Sun', 'Aries', 4.95, 9)['avasthas']
for name in ('Deeptadi', 'Lajjitadi', 'Shayanadi'):
assert states[name]['method_status'] == 'simplified_heuristic'
assert states['Deeptadi']['state'] == 'Deepta'
@pytest.mark.parametrize('planet,sign,combust,expected', [
('Sun', 'Sagittarius', False, 'Shanta'),
('Sun', 'Gemini', False, 'Dina'),
('Sun', 'Capricorn', False, 'Duhkhita'),
('Mars', 'Cancer', False, 'Khala'),
('Venus', 'Gemini', True, 'Kopa'),
])
def test_deeptadi_names_follow_declared_source_not_mislabelled_conditions(planet, sign, combust, expected):
row = AvasthaCalculator().calculate_all_avasthas(
planet, sign, 4.0, 1, is_combust=combust)['avasthas']['Deeptadi']
assert row['state'] == expected
assert row['label_profile'] == 'science_of_light_deeptadi_natural_relationship_subset'
assert row['method_status'] == 'simplified_heuristic'
+39
View File
@@ -0,0 +1,39 @@
"""Integration contract for the full-reading Avastha producer."""
import ast
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
def test_full_reading_wires_hindu_sunrise_into_activity_formula(monkeypatch):
repo = Path(__file__).resolve().parents[1]
monkeypatch.syspath_prepend(str(repo / 'scripts'))
import saham_daynight
seen = {}
def fake_daytime(*args, **kwargs):
seen['rising'] = kwargs.get('rising')
return {'julian_day_ut': 2451545.0, 'sunrise_jd_ut': 2451545.0 - 17 / 24}
monkeypatch.setattr(saham_daynight, 'determine_daytime', fake_daytime)
tree = ast.parse((repo / 'scripts/jyotish_engine.py').read_text(encoding='utf-8'))
blocks = [node for node in ast.walk(tree) if isinstance(node, ast.Try)
and any(isinstance(item, ast.ImportFrom) and item.module == 'avastha_calculator'
for item in node.body)]
assert len(blocks) == 1
report = {'modules': {}, 'errors': []}
env = dict(
report=report,
args=SimpleNamespace(lat=0, lon=0, tz=0),
asc_deg=0,
planet_lons={'Sun': 0, 'Moon': 0},
planets={'Sun': {'degree': 0, 'degree_raw': 0, 'sign': 'Aries', 'house': 1}},
_birth_datetime_from_args=lambda args: datetime(2000, 1, 1),
)
exec(compile(ast.Module(body=blocks, type_ignores=[]), '<avastha-main>', 'exec'), env)
assert report['errors'] == []
assert seen['rising'] == 'hindu'
actual = report['modules']['avasthas']['Sun']['avasthas']['Shayanadi']
assert actual['method_status'] == 'pvr_15_4_4_running_ghati'
assert actual['components']['running_ghati'] == 43
assert actual['state_index'] == 10
+43
View File
@@ -0,0 +1,43 @@
import math
import pytest
from scripts.avastha_calculator import calculate_pvr_shayanadi, AvasthaCalculator
def test_pvr_footnotes_navamsa_part_and_running_ghati():
# Printed Mercury 22Ge14 is the seventh *part*, not its D9 sign.
# Sunrise 06:00, birth 23:00: 42.5 elapsed ghatis, 43rd running.
row = calculate_pvr_shayanadi('Mercury', 82 + 14 / 60, 0, 0, 17)
assert row['components'] == dict(
planet_nakshatra=7, planet_index=4,
navamsa_part=7, moon_nakshatra=1, running_ghati=43,
lagna_sign_number=1, formula_sum=241,
)
assert row['state'] == 'Shayana'
def test_zero_remainder_is_twelfth_state():
# 1*1*1 + 1 + 9 + 1 = 12.
assert calculate_pvr_shayanadi('Sun', 0, 0, 0, 3.2)['state'] == 'Nidra'
def test_exact_navamsa_and_ghati_boundaries():
edge = 10 / 3
assert calculate_pvr_shayanadi('Sun', math.nextafter(edge, -math.inf), 0, 0, 0)['components']['navamsa_part'] == 1
assert calculate_pvr_shayanadi('Sun', edge, 0, 0, 0)['components']['navamsa_part'] == 2
assert calculate_pvr_shayanadi('Sun', 0, 0, 0, .4)['components']['running_ghati'] == 2
def test_actual_activity_context_replaces_element_heuristic():
row = AvasthaCalculator().calculate_all_avasthas(
'Sun', 'Aries', 0, 1,
activity_context=dict(moon_longitude=0, asc_longitude=0, hours_since_sunrise=3.2),
)
assert row['avasthas']['Shayanadi']['state'] == 'Nidra'
assert row['avasthas']['Shayanadi']['method_status'] == 'pvr_15_4_4_running_ghati'
assert row['avasthas']['Shayanadi']['activity_intensity_status'] == 'requires_name_first_sound_index'
@pytest.mark.parametrize('elapsed', [-1, float('nan'), float('inf'), True])
def test_invalid_sunrise_elapsed_is_not_silently_defaulted(elapsed):
with pytest.raises(ValueError):
calculate_pvr_shayanadi('Sun', 0, 0, 0, elapsed)